Просмотр исходного кода

fix(envs): 并列勾选面板对所有机器人直接显示

此前仅在 bot 已有 envs 字段时显示勾选列表,而旧单选界面永远不会
写入 envs,导致并列开关不可见。现改为:勾选列表对全部机器人直接
渲染(Compose 固定桌面保留旧页签),旧 computer 值按迁移规则显示
为勾选状态,首次变更即固化 envs;修复空环境时状态不加载导致复选
框禁用、隐式 VM(computer 未设+隔离执行)派生遗漏、ComputerPanel
backend 查询串拼接错位三个问题。同步 e2e 与面板单测到新语义。
Samien 1 месяц назад
Родитель
Сommit
bfab499df3

+ 3 - 3
src/client/components/workspace/ComputerPanel.vue

@@ -24,9 +24,9 @@ const query=()=>props.backend?'?backend='+encodeURIComponent(props.backend):''
 async function post(path:string,body:unknown){return apiRequest<any>(endpoint()+path+query(),{method:'POST',body:body as JsonValue})}
 async function refresh(){
   if(polling||busy.value||closed||!selected.value||document.hidden)return
-  polling=true;const current=++generation,base=endpoint()+query()
+  polling=true;const current=++generation,base=endpoint()
   try{
-    const status=await apiRequest<ComputerControlStatus>(base)
+    const status=await apiRequest<ComputerControlStatus>(base+query())
     if(current!==generation||closed)return
     state.value=status
     connectionOK.value=true
@@ -34,7 +34,7 @@ async function refresh(){
     if(mine.value&&Date.now()-lastRenew>8000){await post('/renew',ticket.value);lastRenew=Date.now()}
     if(status.backend==='browser'){browserState.value=await apiRequest<BotBrowserState>(`/api/app/agents/${encodeURIComponent(selected.value)}/browser`)}else browserState.value=undefined
     if(status.mode!=='off'&&(status.backend!=='browser'||browserState.value?.open)){
-      const image=await apiRequest<ComputerFrame>(base+'/frame')
+      const image=await apiRequest<ComputerFrame>(base+'/frame'+query())
       if(current===generation&&!closed)frame.value=image
     }else frame.value=undefined
     error.value=''

+ 10 - 10
src/client/components/workspace/LocalVmChatPanel.vue

@@ -13,7 +13,7 @@ const selected=ref(''),state=ref<LocalVmInstance&{enabled:boolean;controlMode?:s
 const agent=computed(()=>props.agents.find(a=>a.id===selected.value))
 const native=ref<DesktopEnvironmentState>()
 const nativeSelected=computed(()=>native.value?.selected)
-const modern=computed(()=>!!agent.value?.envs)
+const modern=computed(()=>!state.value?.fixedCapacity)
 const envs=computed(()=>agent.value?agentEnvs(agent.value):{vm:false,cloud:false,desktop:false,browser:false})
 function toggleEnv(key:'vm'|'cloud'|'desktop'|'browser',enabled:boolean){
  if(computerChoiceDisabled.value)return
@@ -31,7 +31,7 @@ const hostSelectable=computed(()=>desktopHosts.value.some(host=>host.id!=='local
 const boundHost=computed(()=>agent.value?.desktopHost||'')
 function chooseDesktopHost(value:string){
  if(computerChoiceDisabled.value||!nativeSelected.value||value===boundHost.value)return
- void run(()=>apiRequest(`/api/app/agents/${selected.value}/computer-selection`,{method:'PUT',body:{computer:nativeSelected.value as string,desktopHost:value||null}}))
+ void run(()=>apiRequest(`/api/app/agents/${selected.value}/computer-selection`,{method:'PUT',body:modern.value?{envs:{...envs.value},desktopHost:value||null}:{computer:nativeSelected.value as string,desktopHost:value||null}}))
 }
 const authorizeBody=():Record<string,string>=>boundHost.value?{hostId:boundHost.value}:{}
 const computerChoices=computed(()=>[
@@ -75,7 +75,7 @@ async function refresh(){
    if(state.value.ready&&!nativeSelected.value){const image=await apiRequest<ComputerFrame>(`/api/app/agents/${selected.value}/computer/frame`);if(version===revision&&!closed)frame.value=image}else if(!nativeSelected.value)frame.value=undefined
    error.value=''
   }
-  const wantVm=modern.value?envs.value.vm:(!nativeSelected.value&&!cloudSelected.value)
+  const wantVm=modern.value?(envs.value.vm||state.value===undefined):(!nativeSelected.value&&!cloudSelected.value)
   if(wantVm){
    const result=await apiRequest<typeof state.value>(base());if(version!==revision||closed)return;state.value=result
    if(result?.container==='running'&&result.ready&&!nativeSelected.value&&!showCloud.value){const image=await apiRequest<ComputerFrame>(`/api/app/agents/${selected.value}/computer/frame`);if(version===revision&&!closed)frame.value=image}
@@ -129,10 +129,10 @@ onBeforeUnmount(()=>{closed=true;revision++;clearTimeout(timer)})
     <div class="computer-select">
      <span>此机器人使用的电脑</span>
      <div v-if="modern" class="env-checks" role="group" aria-label="此机器人使用的电脑(可多选)">
-      <label class="env-check"><input type="checkbox" :checked="envs.vm" :disabled="computerChoiceDisabled" @change="toggleEnv('vm',($event.target as HTMLInputElement).checked)"><span>本地虚拟机<small>在隔离的本地桌面中工作;执行节点离线时自动改用本机会话</small></span></label>
-      <label class="env-check"><input type="checkbox" :checked="envs.cloud" :disabled="computerChoiceDisabled" @change="toggleEnv('cloud',($event.target as HTMLInputElement).checked)"><span>云端 · Grok Bot<small>共享云端电脑,持续保存工作</small></span></label>
-      <label class="env-check"><input type="checkbox" :checked="envs.desktop" :disabled="computerChoiceDisabled" @change="toggleEnv('desktop',($event.target as HTMLInputElement).checked)"><span>本机控制<small>{{native?.hosts?.length?`已连接 ${native.hosts.filter((h:any)=>h.online).length} 台 Mac,工具可按名称选择`:'暂无已连接的 Mac'}}</small></span></label>
-      <label class="env-check"><input type="checkbox" :checked="envs.browser" :disabled="computerChoiceDisabled" @change="toggleEnv('browser',($event.target as HTMLInputElement).checked)"><span>仅浏览器<small>在连接的 Mac 上使用机器人独立浏览器</small></span></label>
+      <label class="env-check"><input type="checkbox" aria-label="本地虚拟机" :checked="envs.vm" :disabled="computerChoiceDisabled" @change="toggleEnv('vm',($event.target as HTMLInputElement).checked)"><span>本地虚拟机<small>在隔离的本地桌面中工作;执行节点离线时自动改用本机会话</small></span></label>
+      <label class="env-check"><input type="checkbox" aria-label="云端 · Grok Bot" :checked="envs.cloud" :disabled="computerChoiceDisabled" @change="toggleEnv('cloud',($event.target as HTMLInputElement).checked)"><span>云端 · Grok Bot<small>共享云端电脑,持续保存工作</small></span></label>
+      <label class="env-check"><input type="checkbox" aria-label="本机控制" :checked="envs.desktop" :disabled="computerChoiceDisabled" @change="toggleEnv('desktop',($event.target as HTMLInputElement).checked)"><span>本机控制<small>{{native?.hosts?.length?`已连接 ${native.hosts.filter((h:any)=>h.online).length} 台 Mac,工具可按名称选择`:'暂无已连接的 Mac'}}</small></span></label>
+      <label class="env-check"><input type="checkbox" aria-label="仅浏览器" :checked="envs.browser" :disabled="computerChoiceDisabled" @change="toggleEnv('browser',($event.target as HTMLInputElement).checked)"><span>仅浏览器<small>在连接的 Mac 上使用机器人独立浏览器</small></span></label>
       <p class="hint">勾选的环境并列可用;全部取消即不使用任何电脑环境。</p>
      </div>
      <div v-else ref="computerTabs" class="computer-tabs" role="tablist" aria-label="此机器人使用的电脑" @keydown="moveComputerTab">
@@ -141,14 +141,14 @@ onBeforeUnmount(()=>{closed=true;revision++;clearTimeout(timer)})
     </div>
     <div :id="`computer-options-${selected}`" class="computer-options" role="tabpanel" :aria-labelledby="`computer-tab-${selected}-${computerChoice}`">
     <p v-if="computerChoice==='auto'" class="hint">按已有可用环境自动选择。查看此面板不会创建或唤醒云端电脑。</p>
-    <label v-if="localVmSelected" class="vm-execution-option">执行方式
+    <label v-if="localVmSelected&&!modern" class="vm-execution-option">执行方式
      <select aria-label="本地虚拟机执行方式" :value="agent.vmExecution??'worker'" :disabled="computerChoiceDisabled" @change="chooseVmExecution(($event.target as HTMLSelectElement).value)">
       <option value="worker">虚拟机模式</option><option value="profile">本机协作模式</option>
      </select>
      <span class="hint">{{agent.vmExecution==='profile'?'使用基础机器人的完整本机环境,同时操作这台虚拟机。文件和程序仍属于各自的电脑。':'使用独立的虚拟机执行环境,可按需允许本机文件和命令。'}}</span>
     </label>
-    <label v-if="canAllowHost&&(!localVmSelected||agent.vmExecution!=='profile')" class="host-environment-option"><input type="checkbox" :checked="agent.allowHostEnvironment===true" :disabled="computerChoiceDisabled" @change="allowHostEnvironment(($event.target as HTMLInputElement).checked)"> 允许本机环境</label>
-    <template v-if="canAllowHost&&agent.allowHostEnvironment">
+    <label v-if="!modern&&canAllowHost&&(!localVmSelected||agent.vmExecution!=='profile')" class="host-environment-option"><input type="checkbox" :checked="agent.allowHostEnvironment===true" :disabled="computerChoiceDisabled" @change="allowHostEnvironment(($event.target as HTMLInputElement).checked)"> 允许本机环境</label>
+    <template v-if="!modern&&canAllowHost&&agent.allowHostEnvironment">
      <p class="hint">允许 Bot 同时使用 Hermes 所在电脑的文件和命令。{{cloudSelected?'云端虚拟机仍使用独立的云端工具和文件。':'虚拟机工具仍在隔离 Linux 桌面内执行。'}}上方显示虚拟机画面。</p>
      <p v-if="native?.local.ready" class="hint">已连接 {{native.host?.name}},Bot 也可操作本机桌面。</p>
      <button v-else-if="native?.local.supported" :disabled="busy||!isAdmin" @click="run(()=>apiRequest(`/api/app/agents/${selected}/desktop-environment/authorize`,{method:'POST',body:authorizeBody(),timeoutMs:125000}))">授权本机桌面操作</button>

+ 2 - 1
src/shared/workspace.ts

@@ -42,8 +42,9 @@ export interface WorkspaceAgent {
 }
 export interface AgentEnvs { vm:boolean; cloud:boolean; desktop:boolean; browser:boolean }
 /** Normalized parallel environments; legacy single-choice agents derive here. */
-export function agentEnvs(agent: Pick<WorkspaceAgent,'computer'|'envs'>): AgentEnvs {
+export function agentEnvs(agent: Pick<WorkspaceAgent,'computer'|'envs'> & {execution?:'profile'|'computer'}): AgentEnvs {
   if(agent.envs)return {vm:agent.envs.vm===true,cloud:agent.envs.cloud===true,desktop:agent.envs.desktop===true,browser:agent.envs.browser===true}
+  if(agent.computer===undefined&&agent.execution==='computer')return {vm:true,cloud:false,desktop:false,browser:false}
   switch(agent.computer){
     case 'vm':return {vm:true,cloud:false,desktop:false,browser:false}
     case 'cloud':return {vm:false,cloud:true,desktop:false,browser:false}

+ 17 - 13
tests/client/localVmImages.test.ts

@@ -7,7 +7,7 @@ vi.mock('../../src/client/api/client',()=>({apiRequest:vi.fn()}))
 const images=[{key:'standard',name:'标准桌面',description:'XFCE',ready:true,imageId:'sha256:a'},{key:'cursor',name:'Cursor Universal',description:'Debian',ready:true,imageId:'sha256:b'}]
 const agent={id:'image-fixture',name:'镜像测试',computer:'vm',execution:'computer',nodeId:'local',profile:'default'} as any
 const vm={configured:true,runtime:'docker',daemonUp:true,image:true,mode:'per-bot',maxInstances:2,busy:false,images}
-it('selects Profile collaboration without changing the VM and locks the mode while it is in use',async()=>{
+it('replaces the legacy execution-mode select with parallel toggles and locks them while in use',async()=>{
  vi.spyOn(document,'hidden','get').mockReturnValue(false)
  let inUse=false
  vi.mocked(apiRequest).mockImplementation(async path=>{
@@ -17,13 +17,13 @@ it('selects Profile collaboration without changing the VM and locks the mode whi
  })
  const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  try{
-  await flushPromises();await wrapper.get('[aria-label="本地虚拟机执行方式"]').setValue('profile');await flushPromises()
-  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{computer:'vm',vmExecution:'profile',allowHostEnvironment:true}})
-  await wrapper.setProps({agents:[{...agent,vmExecution:'profile',allowHostEnvironment:true}]});await flushPromises()
-  expect(wrapper.text()).toContain('使用基础机器人的完整本机环境')
-  expect(wrapper.find('.host-environment-option').exists()).toBe(false)
+  await flushPromises()
+  expect(wrapper.find('[aria-label="本地虚拟机执行方式"]').exists()).toBe(false)
+  expect((wrapper.get('[aria-label="本地虚拟机"]').element as HTMLInputElement).checked).toBe(true)
+  await wrapper.get('[aria-label="本机控制"]').setValue(true);await flushPromises()
+  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:true,cloud:false,desktop:true,browser:false}}})
   inUse=true;await wrapper.setProps({active:false});await wrapper.setProps({active:true});await flushPromises()
-  expect((wrapper.get('[aria-label="本地虚拟机执行方式"]').element as HTMLSelectElement).disabled).toBe(true)
+  expect((wrapper.get('[aria-label="本机控制"]').element as HTMLInputElement).disabled).toBe(true)
  }finally{wrapper.unmount()}
 })
 afterEach(()=>{vi.clearAllMocks();vi.restoreAllMocks()})
@@ -41,7 +41,7 @@ it('saves never-stop and restores the selected value after refresh',async()=>{
   expect(wrapper.text()).toContain('空闲时保持运行')
  }finally{wrapper.unmount()}
 })
-it('offers a host environment checkbox without a combined environment tab',async()=>{
+it('shows parallel toggles without a combined environment tab',async()=>{
  vi.spyOn(document,'hidden','get').mockReturnValue(false)
  vi.mocked(apiRequest).mockImplementation(async path=>{
   if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
@@ -50,11 +50,13 @@ it('offers a host environment checkbox without a combined environment tab',async
  })
  const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  try{
-  await flushPromises();expect(wrapper.find('[role="tab"][aria-label="本机 + 虚拟机"]').exists()).toBe(false);await wrapper.get('input[type="checkbox"]').setValue(true);await flushPromises()
-  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{computer:'vm',allowHostEnvironment:true}})
+  await flushPromises()
+  expect(wrapper.find('[role="tab"][aria-label="本机 + 虚拟机"]').exists()).toBe(false)
+  await wrapper.get('[aria-label="云端 · Grok Bot"]').setValue(true);await flushPromises()
+  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:true,cloud:true,desktop:false,browser:false}}})
  }finally{wrapper.unmount()}
 })
-it('offers the checkbox for cloud selection and hides it for other environments',async()=>{
+it('checks desktop alongside cloud and never shows the legacy host option',async()=>{
  vi.spyOn(document,'hidden','get').mockReturnValue(false)
  vi.mocked(apiRequest).mockImplementation(async path=>{
   if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
@@ -64,8 +66,10 @@ it('offers the checkbox for cloud selection and hides it for other environments'
  const cloudAgent={...agent,computer:'cloud',execution:'profile'}
  const wrapper=mount(Panel,{props:{agents:[cloudAgent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  try{
-  await flushPromises();await wrapper.get('input[type="checkbox"]').setValue(true);await flushPromises()
-  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{computer:'cloud',allowHostEnvironment:true}})
+  await flushPromises()
+  expect((wrapper.get('[aria-label="云端 · Grok Bot"]').element as HTMLInputElement).checked).toBe(true)
+  await wrapper.get('[aria-label="本机控制"]').setValue(true);await flushPromises()
+  expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:false,cloud:true,desktop:true,browser:false}}})
   for(const computer of ['auto','off','local','browser']){
    await wrapper.setProps({agents:[{...cloudAgent,computer}]});await flushPromises()
    expect(wrapper.find('.host-environment-option').exists()).toBe(false)

+ 4 - 4
tests/e2e/desktop-hosts.spec.ts

@@ -66,9 +66,9 @@ test('binds an agent to a connected desktop host from its chat panel', async ({
   await page.goto(`/conversations/${seed.conversationId}`)
   await page.getByRole('button', { name: '电脑与定时任务', exact: true }).click()
   const dock = page.getByRole('complementary', { name: '机器人电脑面板' })
-  const localTab = dock.getByRole('tab', { name: '本机', exact: true })
-  await localTab.click()
-  await expect(localTab).toHaveAttribute('aria-selected', 'true')
+  const desktopBox = dock.locator('.env-check', { hasText: '本机控制' }).getByRole('checkbox')
+  await desktopBox.check()
+  await expect(desktopBox).toBeChecked()
   const selector = dock.getByRole('combobox', { name: '操作的电脑', exact: true })
   await expect(selector).toBeVisible()
   await expect(selector.locator('option', { hasText: '客厅 Mac mini' })).toHaveCount(1)
@@ -76,5 +76,5 @@ test('binds an agent to a connected desktop host from its chat panel', async ({
   await page.screenshot({ path: testInfo.outputPath('desktop-host-binding.png') })
   await selector.selectOption(configuration.hostId)
   const agents = (await (await page.request.get('/api/app/agents')).json()).agents
-  expect(agents.find((a: any) => a.id === seed.agentId)).toMatchObject({ computer: 'local', desktopHost: configuration.hostId })
+  expect(agents.find((a: any) => a.id === seed.agentId)).toMatchObject({ envs: { vm: true, desktop: true }, desktopHost: configuration.hostId })
 })

+ 43 - 35
tests/e2e/team-tools.spec.ts

@@ -249,13 +249,18 @@ test('configures the Agent computer in its chat panel and keeps preparation in a
  await editor.getByRole('button',{name:'保存',exact:true}).click()
  await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
- const vmTab=dock.getByRole('tab',{name:'本地虚拟机',exact:true})
- await vmTab.click()
- await expect(vmTab).toHaveAttribute('aria-selected','true')
+ const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox')
+ await vmBox.check()
+ await expect(vmBox).toBeChecked()
  await expect(dock.getByRole('button',{name:'设置本地虚拟机',exact:true})).toBeVisible()
  await page.screenshot({path:testInfo.outputPath('local-vm-unconfigured-agent.png')})
 })
 
+async function envCheck(page:any,label:string){
+ const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
+ const box=dock.locator('.env-check',{hasText:label}).getByRole('checkbox')
+ return {box,dock}
+}
 async function openVmSettings(page:any){
  await page.locator('.desktop-sidebar').getByRole('button',{name:'工具',exact:true}).click()
  await page.getByRole('menuitem',{name:'Bot 设置',exact:true}).click()
@@ -269,7 +274,7 @@ async function login(page:any,ready=false){
  await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  await page.request.post('/__test/local-vm'+(ready?'?ready=1':''),{data:{}})
 }
-test('persists idle-stop settings and the host environment checkbox on desktop and mobile',async({page},testInfo)=>{
+test('persists idle-stop settings and parallel environment toggles on desktop and mobile',async({page},testInfo)=>{
  await login(page,true)
  const settings=await openVmSettings(page)
  const timeout=settings.getByRole('combobox',{name:'虚拟机空闲停止时间',exact:true})
@@ -281,50 +286,53 @@ test('persists idle-stop settings and the host environment checkbox on desktop a
  await page.request.post('/__test/hybrid-runner',{data:{}})
  const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
- const dock=page.getByRole('complementary',{name:'机器人电脑面板'}),hostOption=dock.getByRole('checkbox',{name:'允许本机环境',exact:true})
- await expect(dock.getByRole('tab',{name:'本机 + 虚拟机',exact:true})).toHaveCount(0)
- await hostOption.check();await expect(hostOption).toBeChecked()
- expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({computer:'vm',execution:'computer',allowHostEnvironment:true})
+ const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
+ const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox'),hostBox=dock.locator('.env-check',{hasText:'本机控制'}).getByRole('checkbox')
+ await vmBox.check();await expect(vmBox).toBeChecked()
+ await hostBox.check();await expect(hostBox).toBeChecked()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true,desktop:true},computer:'vm',execution:'computer'})
  await expect(dock).toContainText('永不自动停止')
- await hostOption.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-desktop.png')})
- await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(hostOption).toBeChecked()
- await page.setViewportSize({width:390,height:844});await hostOption.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-mobile.png')})
+ await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-desktop.png')})
+ await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(vmBox).toBeChecked();await expect(hostBox).toBeChecked()
+ await page.setViewportSize({width:390,height:844});await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-mobile.png')})
  expect(await page.evaluate(()=>document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
- await dock.getByRole('tab',{name:'不使用电脑',exact:true}).click();await expect(hostOption).toHaveCount(0)
- await dock.getByRole('tab',{name:'本地虚拟机',exact:true}).click();await expect(hostOption).not.toBeChecked()
- expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({computer:'vm',execution:'computer',allowHostEnvironment:false})
+ await hostBox.uncheck();await expect(dock.getByRole('checkbox',{name:'允许本机环境',exact:true})).toHaveCount(0)
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true,desktop:false},computer:'vm',allowHostEnvironment:false})
 })
-test('selects local VM Profile collaboration and preserves it after reload on desktop and phone',async({page},testInfo)=>{
+test('keeps the parallel VM toggle through reload on desktop and phone',async({page},testInfo)=>{
  await login(page,true)
  await page.request.post('/__test/hybrid-runner',{data:{}})
  const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
- const dock=page.getByRole('complementary',{name:'机器人电脑面板'}),mode=dock.getByRole('combobox',{name:'本地虚拟机执行方式',exact:true})
- await mode.selectOption('profile');await expect(mode).toHaveValue('profile')
- await expect(dock.getByRole('tab',{name:'本地虚拟机',exact:true})).toHaveAttribute('aria-selected','true')
- await expect(dock).toContainText('使用基础机器人的完整本机环境')
- await expect(dock.getByRole('checkbox',{name:'允许本机环境',exact:true})).toHaveCount(0)
- expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({computer:'vm',execution:'computer',vmExecution:'profile',allowHostEnvironment:true})
- await mode.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-desktop.png')})
- await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(mode).toHaveValue('profile')
+ const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
+ const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox')
+ await expect(dock.getByRole('combobox',{name:'本地虚拟机执行方式',exact:true})).toHaveCount(0)
+ await expect(vmBox).toBeChecked()
+ await vmBox.uncheck()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:false},computer:'off',execution:'profile'})
+ await vmBox.check();await expect(vmBox).toBeChecked()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true},computer:'vm',execution:'computer'})
+ await vmBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-desktop.png')})
+ await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(vmBox).toBeChecked()
  await page.setViewportSize({width:390,height:844});await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
- await mode.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-phone.png')})
+ await vmBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-phone.png')})
  expect(await page.evaluate(()=>document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
- await mode.selectOption('worker');await expect(dock.getByRole('checkbox',{name:'允许本机环境',exact:true})).not.toBeChecked()
+ await vmBox.uncheck()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:false},computer:'off',execution:'profile'})
 })
-test('cloud VM keeps its own selection while the host option can be checked and clears on auto',async({page},testInfo)=>{
+test('cloud stays selected while desktop runs in parallel from the checkbox panel',async({page},testInfo)=>{
  await login(page,true)
  const seed=await(await page.request.post('/__test/cloud-host-option',{data:{}})).json()
  await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
- const dock=page.getByRole('complementary',{name:'机器人电脑面板'}),option=dock.getByRole('checkbox',{name:'允许本机环境',exact:true}),cloud=dock.getByRole('tab',{name:'云端 · Grok Bot',exact:true})
- await expect(cloud).toHaveAttribute('aria-selected','true');await expect(option).not.toBeChecked();await option.check()
- await expect(option).toBeChecked();await expect(cloud).toHaveAttribute('aria-selected','true')
- expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({computer:'cloud',execution:'profile',allowHostEnvironment:true})
- await option.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('cloud-host-option.png')})
- await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(option).toBeChecked()
- await dock.getByRole('tab',{name:'自动',exact:true}).click();await expect(option).toHaveCount(0)
- expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId).allowHostEnvironment).toBe(false)
- await cloud.click();await expect(option).not.toBeChecked()
+ const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
+ const cloudBox=dock.locator('.env-check',{hasText:'云端 · Grok Bot'}).getByRole('checkbox'),hostBox=dock.locator('.env-check',{hasText:'本机控制'}).getByRole('checkbox')
+ await expect(cloudBox).toBeChecked()
+ await hostBox.check();await expect(cloudBox).toBeChecked();await expect(hostBox).toBeChecked()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{cloud:true,desktop:true},computer:'cloud',execution:'profile'})
+ await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('cloud-host-option.png')})
+ await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(cloudBox).toBeChecked();await expect(hostBox).toBeChecked()
+ await hostBox.uncheck()
+ expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{cloud:true,desktop:false},allowHostEnvironment:false})
 })
 
 test('Local VM settings replace the image catalog and prepare the managed desktop',async({page},testInfo)=>{