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

feat(desktop): Bot 默认操作服务器本机,取消人工绑定电脑选择器改为逐台授权列表

Samien 1 месяц назад
Родитель
Сommit
feae1ae53d

Разница между файлами не показана из-за своего большого размера
+ 16 - 20
src/client/components/workspace/LocalVmChatPanel.vue


+ 8 - 3
src/server/desktopEnvironments.ts

@@ -84,11 +84,16 @@ export class DesktopEnvironments {
  private localSession(){let s=this.sessions.get('local');if(!s){s=newHostState('local',false);this.sessions.set('local',s)}return s}
  private localSession(){let s=this.sessions.get('local');if(!s){s=newHostState('local',false);this.sessions.set('local',s)}return s}
  /** Agent binding first; otherwise the loopback host, otherwise the single
  /** Agent binding first; otherwise the loopback host, otherwise the single
   *  connected remote host. Ambiguous remote sets require an explicit binding. */
   *  connected remote host. Ambiguous remote sets require an explicit binding. */
+ /** Default host for bot work: the server's own desktop (loopback App)
+  * while it is online — a legacy explicit binding only takes over when the
+  * server desktop is away, and a single online remote is the last resort. */
  private sessionFor(agent:WorkspaceAgent):HostState|undefined{
  private sessionFor(agent:WorkspaceAgent):HostState|undefined{
+  const local=this.sessions.get('local')
+  if(local&&this.hostOnline(local))return local
   if(agent.desktopHost)return this.sessions.get(agent.desktopHost)
   if(agent.desktopHost)return this.sessions.get(agent.desktopHost)
-  if(this.sessions.has('local'))return this.sessions.get('local')
-  const remote=[...this.sessions.keys()].filter(key=>key!=='local')
-  return remote.length===1?this.sessions.get(remote[0]):undefined
+  if(local)return local
+  const remote=[...this.sessions.values()].filter(h=>h.key!=='local'&&this.hostOnline(h))
+  return remote.length===1?remote[0]:undefined
  }
  }
  selected(owner:string,agent:WorkspaceAgent):Mode|undefined {if(agent.archived||agent.remoteAgentId||agent.temporaryGoalId)return undefined;const envs=agentEnvs(agent);if(agent.envs)return envs.desktop?'local':envs.browser?'browser':undefined;if(agent.computer==='local'||agent.computer==='browser')return agent.computer;if(agent.computer==='auto'&&agent.execution!=='computer'){const s=this.sessionFor(agent);if(s&&this.ready(s,owner))return 'local'}return undefined}
  selected(owner:string,agent:WorkspaceAgent):Mode|undefined {if(agent.archived||agent.remoteAgentId||agent.temporaryGoalId)return undefined;const envs=agentEnvs(agent);if(agent.envs)return envs.desktop?'local':envs.browser?'browser':undefined;if(agent.computer==='local'||agent.computer==='browser')return agent.computer;if(agent.computer==='auto'&&agent.execution!=='computer'){const s=this.sessionFor(agent);if(s&&this.ready(s,owner))return 'local'}return undefined}
  toolMode(owner:string,agent:WorkspaceAgent):Mode|undefined {if(allowsHostEnvironment(agent)){const s=this.sessionFor(agent);if(s&&this.ready(s,owner))return 'local'}return this.selected(owner,agent)}
  toolMode(owner:string,agent:WorkspaceAgent):Mode|undefined {if(allowsHostEnvironment(agent)){const s=this.sessionFor(agent);if(s&&this.ready(s,owner))return 'local'}return this.selected(owner,agent)}

+ 8 - 6
tests/e2e/desktop-hosts.spec.ts

@@ -69,12 +69,14 @@ test('binds an agent to a connected desktop host from its chat panel', async ({
   const desktopBox = dock.locator('.env-check', { hasText: '本机控制' }).getByRole('checkbox')
   const desktopBox = dock.locator('.env-check', { hasText: '本机控制' }).getByRole('checkbox')
   await desktopBox.check()
   await desktopBox.check()
   await expect(desktopBox).toBeChecked()
   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)
-  await selector.scrollIntoViewIfNeeded()
+  await expect(dock.getByRole('combobox', { name: '操作的电脑', exact: true })).toHaveCount(0)
+  const hostCard = dock.locator('.host-permission', { hasText: '客厅 Mac mini' })
+  await expect(hostCard).toBeVisible()
+  await expect(hostCard).toContainText('在线')
+  await hostCard.scrollIntoViewIfNeeded()
   await page.screenshot({ path: testInfo.outputPath('desktop-host-binding.png') })
   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
   const agents = (await (await page.request.get('/api/app/agents')).json()).agents
-  expect(agents.find((a: any) => a.id === seed.agentId)).toMatchObject({ envs: { vm: true, desktop: true }, desktopHost: configuration.hostId })
+  const updated = agents.find((a: any) => a.id === seed.agentId)
+  expect(updated).toMatchObject({ envs: { vm: true, desktop: true } })
+  expect(updated.desktopHost).toBeUndefined()
 })
 })

+ 15 - 6
tests/server/desktopEnvironments.test.ts

@@ -33,20 +33,29 @@ it('adds an authorized native desktop to hybrid tools while leaving the default
 function drive(handle:(c:any)=>unknown=c=>c.operation==='view'?{data:Buffer.from('fixture frame').toString('base64'),width:1280,height:800}:{ok:true}){let results:any[]=[];service.exchange({host,results});const p=setInterval(()=>{const value=service.exchange({host,results});results=value.commands.map(c=>({id:c.id,value:handle(c)}))},5);pollers.add(p);poller=p}
 function drive(handle:(c:any)=>unknown=c=>c.operation==='view'?{data:Buffer.from('fixture frame').toString('base64'),width:1280,height:800}:{ok:true}){let results:any[]=[];service.exchange({host,results});const p=setInterval(()=>{const value=service.exchange({host,results});results=value.commands.map(c=>({id:c.id,value:handle(c)}))},5);pollers.add(p);poller=p}
 const HOST_A='11111111-aaaa-4bbb-8ccc-111111111111',HOST_B='22222222-aaaa-4bbb-8ccc-222222222222'
 const HOST_A='11111111-aaaa-4bbb-8ccc-111111111111',HOST_B='22222222-aaaa-4bbb-8ccc-222222222222'
 function driveRemote(id:string,hostInfo:any,handle:(c:any)=>unknown=c=>c.operation==='view'?{data:Buffer.from('remote frame').toString('base64'),width:1280,height:800}:{ok:true}){let results:any[]=[];service.remoteExchange(id,{host:hostInfo,results});const p=setInterval(()=>{const value=service.remoteExchange(id,{host:hostInfo,results});results=value.commands.map(c=>({id:c.id,value:handle(c)}))},5);pollers.add(p);poller=p}
 function driveRemote(id:string,hostInfo:any,handle:(c:any)=>unknown=c=>c.operation==='view'?{data:Buffer.from('remote frame').toString('base64'),width:1280,height:800}:{ok:true}){let results:any[]=[];service.remoteExchange(id,{host:hostInfo,results});const p=setInterval(()=>{const value=service.remoteExchange(id,{host:hostInfo,results});results=value.commands.map(c=>({id:c.id,value:handle(c)}))},5);pollers.add(p);poller=p}
-it('routes a bound agent to its remote desktop host and keeps hosts isolated',async()=>{
+it('defaults bot work to the server desktop and falls back to a legacy binding only when it is away',async()=>{
  agent=store.updateAgent('owner',agent.id,{computer:'local',desktopHost:HOST_A})
  agent=store.updateAgent('owner',agent.id,{computer:'local',desktopHost:HOST_A})
  const remoteHost={...host,name:'客厅 Mac Pro'}
  const remoteHost={...host,name:'客厅 Mac Pro'}
  driveRemote(HOST_A,remoteHost)
  driveRemote(HOST_A,remoteHost)
- service.exchange({host,results:[]})
+ drive()
+ const state=(await request(app()).get(base()+'/desktop-environment')).body
+ expect(state.hostName??state.host?.name).toBe('测试 Mac')
+ const named=(await request(app()).get(base()+'/computer?backend=desktop&host='+encodeURIComponent('客厅 Mac Pro'))).body
+ expect(named.hostName).toBe('客厅 Mac Pro')
+ vi.useFakeTimers()
+ clearInterval(poller)
+ vi.advanceTimersByTime(9000)
+ const fallback=(await request(app()).get(base()+'/desktop-environment')).body
+ expect(fallback.hostName??fallback.host?.name).toBe('客厅 Mac Pro')
  const epoch=service.hostEpoch('owner',agent)
  const epoch=service.hostEpoch('owner',agent)
  expect(epoch).toBe(remoteHost.id)
  expect(epoch).toBe(remoteHost.id)
  const result=await service.call('owner',agent.id,'desktop_environment_view',{},new AbortController().signal,()=>{},'local',epoch)
  const result=await service.call('owner',agent.id,'desktop_environment_view',{},new AbortController().signal,()=>{},'local',epoch)
  expect(result.content[0].type).toBe('image')
  expect(result.content[0].type).toBe('image')
  expect(JSON.parse(result.content[1].text).host).toBe('客厅 Mac Pro')
  expect(JSON.parse(result.content[1].text).host).toBe('客厅 Mac Pro')
- const state=(await request(app()).get(base()+'/desktop-environment')).body
- expect(state.hostName??state.host?.name).toBe('客厅 Mac Pro')
- expect(state.hosts.map((h:any)=>h.id)).toEqual(['local',HOST_A])
- expect(state.hosts.find((h:any)=>h.id===HOST_A)).toMatchObject({name:'客厅 Mac Pro',online:true})
+ vi.useRealTimers()
+ const hosts=(await request(app()).get(base()+'/desktop-environment')).body
+ expect(hosts.hosts.map((h:any)=>h.id)).toEqual(['local',HOST_A])
+ expect(hosts.hosts.find((h:any)=>h.id===HOST_A)).toMatchObject({name:'客厅 Mac Pro',online:true})
 })
 })
 it('requires an explicit binding when several remote hosts are connected',async()=>{
 it('requires an explicit binding when several remote hosts are connected',async()=>{
  agent=store.updateAgent('owner',agent.id,{computer:'local'})
  agent=store.updateAgent('owner',agent.id,{computer:'local'})

Некоторые файлы не были показаны из-за большого количества измененных файлов