@@ -117,6 +117,7 @@
. status :: before { content : "" ; width : 7 px ; height : 7 px ; border-radius : 50 % ; background : currentColor ; }
. status . running { color : var ( - - success ) ; background : var ( - - success - soft ) ; }
. status . starting { color : var ( - - accent - strong ) ; background : var ( - - accent - soft ) ; }
. status . associated { color : var ( - - warning ) ; background : var ( - - warning - soft ) ; }
. status . occupied { color : var ( - - warning ) ; background : var ( - - warning - soft ) ; }
. status . exited { color : var ( - - muted ) ; background : var ( - - surface -3 ) ; }
. row-actions { display : flex ; justify-content : flex-end ; gap : 6 px ; opacity : .7 ; }
@@ -237,8 +238,8 @@
< / section >
< section class = "workspace" aria-labelledby = "listTitle" >
< div class = "toolbar" > < div class = "search" > < span aria-hidden = "true" > ⌕< / span > < input id = "search" type = "search" placeholder = "搜索实例、路径或 PID" aria-label = "搜索实例、路径或 PID" > < / div > < select id = "browserFilter" aria-label = "筛选浏览器" > < option value = "all" > 全部浏览器< / option > < option value = "chrome" > Chrome< / option > < option value = "edge" > Edge< / option > < / select > < select id = "statusFilter" aria-label = "筛选状态" > < option value = "all" > 全部状态< / option > < option value = "running" > 运行中< / option > < option value = "starting" > 启动中< / option > < option value = "occupied" > 被占用< / option > < option value = "exited" > 已退出< / option > < / select > < div class = "toolbar-spacer" > < / div > < button class = "secondary" id = "refresh" > ↻ 刷新状态< / button > < / div >
< div class = "table-wrap" > < table > < caption id = "listTitle" class = "sr-only" > 浏览器实例列表< / caption > < thead > < tr > < th scope = "col" > 实例名称< / th > < th scope = "col" > 浏览器类型< / th > < th scope = "col" > 用户数据目录< / th > < th scope = "col" > 状态< / th > < th scope = "col" > 启动 < / th > < / tr > < / thead > < tbody id = "instanceRows" > < / tbody > < / table > < div class = "empty-state" id = "emptyState" > < strong > 没有匹配的实例< / strong > < span > 尝试清除筛选,或新建一个浏览器实例。< / span > < / div > < / div >
< div class = "toolbar" > < div class = "search" > < span aria-hidden = "true" > ⌕< / span > < input id = "search" type = "search" placeholder = "搜索实例、路径、 PID 或端口 " aria-label = "搜索实例、路径、 PID 或端口 " > < / div > < select id = "browserFilter" aria-label = "筛选浏览器" > < option value = "all" > 全部浏览器< / option > < option value = "chrome" > Chrome< / option > < option value = "edge" > Edge< / option > < / select > < select id = "statusFilter" aria-label = "筛选状态" > < option value = "all" > 全部状态< / option > < option value = "running" > 运行中< / option > < option value = "starting" > 启动中< / option > < option value = "associated" > 外部已关联 < / option > < option value = "occupied" > 被占用< / option > < option value = "exited" > 已退出< / option > < / select > < div class = "toolbar-spacer" > < / div > < button class = "secondary" id = "refresh" > ↻ 刷新状态< / button > < / div >
< div class = "table-wrap" > < table > < caption id = "listTitle" class = "sr-only" > 浏览器实例列表< / caption > < thead > < tr > < th scope = "col" > 实例名称< / th > < th scope = "col" > 浏览器类型< / th > < th scope = "col" > 用户数据目录< / th > < th scope = "col" > 调试端口 < / th > < th scope = "col" > 状态< / th > < th scope = "col" > 操作 < / th > < / tr > < / thead > < tbody id = "instanceRows" > < / tbody > < / table > < div class = "empty-state" id = "emptyState" > < strong > 没有匹配的实例< / strong > < span > 尝试清除筛选,或新建一个浏览器实例。< / span > < / div > < / div >
< div class = "table-footer" > < span id = "resultCount" > 显示 4 个实例< / span > < span > 最后检查:刚刚< / span > < / div >
< / section >
< / section >
@@ -256,7 +257,7 @@
< / div >
< div class = "settings-actions" > < button type = "button" class = "secondary" id = "discardSettings" > 取消< / button > < button type = "submit" class = "primary" > 保存设置< / button > < / div >
< / form >
< aside class = "settings-card" > < div > < h2 > 运行行为< / h2 > < p > 影响后续启动和应用关闭时的默认行为。< / p > < / div > < div class = "setting-list" > < label class = "check-line" > < input type = "checkbox" id = "closeOnExit" > < span > < b > 退出 Chub 时关闭本工具启动的实例< / b > < br > < small class = "help" > 默认关闭。不会影响外部浏览器。< / small > < / span > < / label > < label class = "check-line " > < input type = "checkbox" id = "enableCdp" > < span > < b > 允许本地 CDP < / b > < br > < small class = "help" > 仅绑定 loopback; MVP 默认关闭。 < / small > < / span > < / label > < div class = "settings-status" id = "settingsStatus" > 设置已保存 · 最后更新:刚刚< / div > < / div > < / aside >
< aside class = "settings-card" > < div > < h2 > 运行行为< / h2 > < p > 影响后续启动和应用关闭时的默认行为。< / p > < / div > < div class = "setting-list" > < label class = "check-line" > < input type = "checkbox" id = "closeOnExit" > < span > < b > 退出 Chub 时关闭本工具启动的实例< / b > < br > < small class = "help" > 默认关闭。不会影响外部浏览器。< / small > < / span > < / label > < div class = "field " > < label for = "remoteDebugPort" > 远程调试起始端口 < / label > < input id = "remoteDebugPort" type = "number" min = "1024" max = "65535" value = "9666" inputmode = "numeric" > < small > 留空时使用 9666。启动实例仅绑定 127.0.0.1;端口被占用时顺序尝试下一个可用端口。 < / small > < / div > < div class = "settings-status" id = "settingsStatus" > 设置已保存 · 最后更新:刚刚< / div > < / div > < / aside >
< / div >
< / section >
< / main >
@@ -269,10 +270,10 @@
< script >
const instances = [
{ id : 'inst-chrome-shop' , name : 'Shopee · SG 主店' , kind : 'chrome' , browser : 'Chrome' , status : 'running' , statusText : '运行中' , pid : '18420' , dir : 'D:\\Chub\\profiles\\shopee-sg' , started : '今天 09:42' , url : 'https://seller.shopee.sg/' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' } ,
{ id : 'inst-edge-review' , name : '素材审核 · Edge' , kind : 'edge' , browser : 'Edge' , status : 'starting' , statusText : '启动中' , pid : '—' , dir : 'D:\\Chub\\profiles\\asset-review' , started : '正在启动' , url : 'https://example.com/review' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' } ,
{ id : 'inst-chrome-ph' , name : 'Shopee · PH 备用' , kind : 'chrome' , browser : 'Chrome' , status : 'occupi ed' , statusText : '被占用 ' , pid : '16108' , dir : 'D:\\Chub\\profiles\\shopee-ph' , started : '昨天 18:16' , url : 'https://seller.shopee.ph/' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' } ,
{ id : 'inst-edge-test' , name : '测试环境 · Edge' , kind : 'edge' , browser : 'Edge' , status : 'exited' , statusText : '已退出' , pid : '—' , dir : 'D:\\Chub\\profiles\\edge-test' , started : '昨天 16:04' , url : 'https://localhost:3000' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' , exit : '退出码 0' }
{ id : 'inst-chrome-shop' , name : 'Shopee · SG 主店' , kind : 'chrome' , browser : 'Chrome' , status : 'running' , statusText : '运行中(托管) ' , pid : '18420' , port : '9666' , dir : 'D:\\Chub\\profiles\\shopee-sg' , started : '今天 09:42' , url : 'https://seller.shopee.sg/' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' } ,
{ id : 'inst-edge-review' , name : '素材审核 · Edge' , kind : 'edge' , browser : 'Edge' , status : 'starting' , statusText : '启动中' , pid : '—' , port : '检测中…' , dir : 'D:\\Chub\\profiles\\asset-review' , started : '正在启动' , url : 'https://example.com/review' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' } ,
{ id : 'inst-chrome-ph' , name : 'Shopee · PH 备用' , kind : 'chrome' , browser : 'Chrome' , status : 'associat ed' , statusText : '外部已关联 ' , pid : '16108' , port : '9668' , dir : 'D:\\Chub\\profiles\\shopee-ph' , started : '昨天 18:16' , url : 'https://seller.shopee.ph/' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' } ,
{ id : 'inst-edge-test' , name : '测试环境 · Edge' , kind : 'edge' , browser : 'Edge' , status : 'exited' , statusText : '已退出' , pid : '—' , port : '—' , dir : 'D:\\Chub\\profiles\\edge-test' , started : '昨天 16:04' , url : 'https://localhost:3000' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' , exit : '退出码 0' }
] ;
let selectedId = 'inst-chrome-shop' ;
let modalAction = 'stop' ;
@@ -280,14 +281,14 @@
const statusClass = ( s ) => s ;
function filtered ( ) {
const q = $ ( 'search' ) . value . trim ( ) . toLowerCase ( ) , browser = $ ( 'browserFilter' ) . value , status = $ ( 'statusFilter' ) . value ;
return instances . filter ( x => ( ! q || [ x . name , x . browser , x . pid , x . dir ] . join ( ' ' ) . toLowerCase ( ) . includes ( q ) ) && ( browser === 'all' || x . kind === browser ) && ( status === 'all' || x . status === status ) ) ;
return instances . filter ( x => ( ! q || [ x . name , x . browser , x . pid , x . port , x . dir ] . join ( ' ' ) . toLowerCase ( ) . includes ( q ) ) && ( browser === 'all' || x . kind === browser ) && ( status === 'all' || x . status === status ) ) ;
}
function renderRows ( ) {
const rows = filtered ( ) , body = $ ( 'instanceRows' ) ; body . innerHTML = '' ;
rows . forEach ( x => { const tr = document . createElement ( 'tr' ) ; tr . className = x . id === selectedId ? 'selected' : '' ; tr . tabIndex = 0 ; tr . dataset . id = x . id ; tr . innerHTML = `
<td><div class="instance-name"><span class="browser-logo ${ x . kind } " aria-hidden="true"> ${ x . kind === 'chrome' ? 'C' : 'E' } </span><div><strong> ${ x . name } </strong><small> ${ x . id } </small></div></div></td>
<td> ${ x . browser } </td><td class="mono"> ${ x . dir } </td><td><span class="status ${ statusClass ( x . status ) } "> ${ x . statusText } </span></td>
<td><div class="row-actions"><button data-action="start" aria-label="启动 ${ x . name } ">启动 </button></div></td>` ;
<td> ${ x . browser } </td><td class="mono"> ${ x . dir } </td><td class="mono"> ${ x . port || '—' } </td><td ><span class="status ${ statusClass ( x . status ) } "> ${ x . statusText } </span></td>
<td><div class="row-actions"><button data-action="start" aria-label=" ${ x . status === 'associated' ? '重新检测' : '启动' } ${ x . name } "> ${ x . status === 'associated' ? '重新检测' : '启动' } </button></div></td> ` ;
tr . addEventListener ( 'click' , e => { if ( ! e . target . closest ( 'button' ) ) openDetails ( x . id ) ; } ) ;
tr . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; openDetails ( x . id ) ; } } ) ;
tr . querySelectorAll ( 'button' ) . forEach ( b => b . addEventListener ( 'click' , e => { e . stopPropagation ( ) ; selectedId = x . id ; if ( b . dataset . action === 'details' ) openDetails ( x . id ) ; else if ( b . dataset . action === 'stop' ) openConfirm ( x . id , false ) ; else openConfirm ( x . id , 'start' ) ; } ) ) ;
@@ -295,7 +296,7 @@
} ) ;
$ ( 'emptyState' ) . style . display = rows . length ? 'none' : 'block' ; $ ( 'resultCount' ) . textContent = ` 显示 ${ rows . length } 个实例 ` ;
}
function openDetails ( id ) { selectedId = id ; const x = instances . find ( i => i . id === id ) ; $ ( 'drawer' ) . innerHTML = ` <div class="drawer-head"><div><h2> ${ x . name } </h2><p> ${ x . browser } · ${ x . id } </p></div><button class="icon-button" id="closeDrawer" aria-label="关闭详情">× </button></div><div class="drawer-body"><div><span class="status ${ x . status } "> ${ x . statusText } </span></div><div class="detail-card"><div class="detail-line"><span>进程 PID</span><span class="mono"> ${ x . pid } </span></div><div class="detail-line"><span>浏览器</span><span> ${ x . browser } </span></div><div class="detail-line"><span>user data dir</span><span class="mono"> ${ x . dir } </span></div><div class="detail-line"><span>启动地址</span><span class="mono"> ${ x . url } </span></div><div class="detail-line"><span>可执行文件</span><span class="mono"> ${ x . executable } </span></div> ${ x . exit ? ` <div class="detail-line"><span>退出结果</span><span> ${ x . exit } </span></div> ` : '' } </div><div class="form-section"><h3>实例操作</h3><div class="drawer-actions" style="border-top:0;padding-top:0;justify-content:flex-start"><button class="secondary" id="restartBtn" ${ x . status === 'starting' ? 'disabled' : '' } >↻ 重启</button><button class="danger-button" id="stopBtn" ${ x . status === 'exited' ? 'disabled' : '' } >关闭实例</button></div></div><div class="form-section"><h3>最近活动</h3><div class="activity"><div class="activity-item"><i></i><span>状态已同步 · ${ x . started } </span></div><div class="activity-item"><i></i><span> ${ x . status === 'occupied' ? '检测到外部进程占用该 user data dir' : '实例配置已通过身份校验' } </span></div></div></div></div> ` ; showDrawer ( ) ; $ ( 'closeDrawer' ) . onclick = hideDrawer ; $ ( 'stopBtn' ) . onclick = ( ) => openConfirm ( x . id , false ) ; $ ( 'restartBtn' ) . onclick = ( ) => openConfirm ( x . id , 'restart' ) ; }
function openDetails ( id ) { selectedId = id ; const x = instances . find ( i => i . id === id ) , externallyAssociated = x . status === 'associated' ; $ ( 'drawer' ) . innerHTML = ` <div class="drawer-head"><div><h2> ${ x . name } </h2><p> ${ x . browser } · ${ x . id } </p></div><button class="icon-button" id="closeDrawer" aria-label="关闭详情">× </button></div><div class="drawer-body"><div><span class="status ${ x . status } "> ${ x . statusText } </span></div><div class="detail-card"><div class="detail-line"><span>进程 PID</span><span class="mono"> ${ x . pid } </span></div><div class="detail-line"><span>调试端口</span><span class="mono"> ${ x . port || '—' } </span></div><div class="detail-line"><span>浏览器</span><span>${ x . browser } </span></div><div class="detail-line"><span>user data dir</span><span class="mono"> ${ x . dir } </span></div><div class="detail-line"><span>启动地址</span><span class="mono"> ${ x . url } </span></div><div class="detail-line"><span>可执行文件</span><span class="mono"> ${ x . executable } </span></div> ${ x . exit ? ` <div class="detail-line"><span>退出结果</span><span> ${ x . exit } </span></div> ` : '' } </div><div class="form-section"><h3>实例操作</h3><div class="drawer-actions" style="border-top:0;padding-top:0;justify-content:flex-start"><button class="secondary" id="restartBtn" ${ x . status === 'starting' || externallyAssociated ? 'disabled' : '' } >↻ 重启</button><button class="danger-button" id="stopBtn" ${ x . status === 'exited' || externallyAssociated ? 'disabled' : '' } >关闭实例</button></div> ${ externallyAssociated ? '<small class="help">外部关联实例仅供观察,Chub 不会接管其生命周期。</small>' : '' } </div><div class="form-section"><h3>最近活动</h3><div class="activity"><div class="activity-item"><i></i><span>状态已同步 · ${ x . started } </span></div><div class="activity-item"><i></i><span> ${ externallyAssociated ? '已验证本地 CDP 端点;未接管外部浏览器。' : x . status === 'occupied' ? '检测到外部进程占用该 user data dir' : '实例配置已通过身份校验' } </span></div></div></div></div> ` ; showDrawer ( ) ; $ ( 'closeDrawer' ) . onclick = hideDrawer ; $ ( 'stopBtn' ) . onclick = ( ) => openConfirm ( x . id , false ) ; $ ( 'restartBtn' ) . onclick = ( ) => openConfirm ( x . id , 'restart' ) ; }
function openLaunch ( ) { $ ( 'drawer' ) . innerHTML = ` <div class="drawer-head"><div><h2>新建浏览器实例</h2><p>配置完成后,Chub 会在本机启动一个隔离环境。</p></div><button class="icon-button" id="closeDrawer" aria-label="关闭新建实例">× </button></div><form class="drawer-body" id="launchForm"><div class="form-section"><h3>浏览器配置</h3><div class="field"><span>浏览器类型 <span>*</span></span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="kind" value="chrome" checked> Chrome</label><label><input type="radio" name="kind" value="edge"> Edge</label></div></div><div class="field"><label for="exe">可执行文件</label><input id="exe" value="自动发现" aria-describedby="exeHelp"><small id="exeHelp">留空时按浏览器类型搜索标准安装路径。</small></div></div><div class="form-section"><h3>运行环境</h3><div class="field"><label for="dataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="dataDir" placeholder="例如:D: \\ Chub \\ profiles \\ new-instance" required><button type="button" class="secondary" id="chooseInstanceDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="profile">profile-directory</label><input id="profile" value="Default"></div><div class="field"><label for="url">启动 URL(可选)</label><input id="url" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="proxy">代理服务器</label><input id="proxy" placeholder="可选,例如 http://127.0.0.1:8080"><small>仅支持无认证代理,认证信息不会被保存。</small></div></div><div class="form-section"><h3>启动选项</h3><label class="check-line"><input type="checkbox" id="headless"> <span><b>无头模式</b><br><small class="help">启动后台浏览器,不显示窗口。</small></span></label><label class="check-line"><input type="checkbox" id="remember" checked> <span><b>保存此配置</b><br><small class="help">只保存路径和非敏感参数。</small></span></label></div><div class="drawer-actions"><button type="button" class="secondary" id="cancelLaunch">取消</button><button type="submit" class="primary">启动实例</button></div></form> ` ; showDrawer ( ) ; $ ( 'closeDrawer' ) . onclick = hideDrawer ; $ ( 'cancelLaunch' ) . onclick = hideDrawer ; $ ( 'chooseInstanceDir' ) . onclick = ( ) => showToast ( '原型演示:将打开目录选择器。' ) ; $ ( 'launchForm' ) . onsubmit = e => { e . preventDefault ( ) ; const data = $ ( 'dataDir' ) ; if ( ! data . value . trim ( ) ) { data . focus ( ) ; data . parentElement . classList . add ( 'error' ) ; const msg = document . createElement ( 'div' ) ; msg . className = 'field-error' ; msg . textContent = '请填写 User Data Dir。' ; data . parentElement . appendChild ( msg ) ; return ; } hideDrawer ( ) ; setTimeout ( ( ) => showToast ( '启动请求已提交,实例进入 STARTING 状态。' ) , 80 ) ; } ; }
function showDrawer ( ) { $ ( 'drawer' ) . classList . add ( 'open' ) ; $ ( 'backdrop' ) . classList . add ( 'open' ) ; $ ( 'drawer' ) . setAttribute ( 'aria-hidden' , 'false' ) ; setTimeout ( ( ) => $ ( 'drawer' ) . querySelector ( 'button, input, select' ) ? . focus ( ) , 60 ) ; }
function hideDrawer ( ) { $ ( 'drawer' ) . classList . remove ( 'open' ) ; $ ( 'backdrop' ) . classList . remove ( 'open' ) ; $ ( 'drawer' ) . setAttribute ( 'aria-hidden' , 'true' ) ; }