Browse Source

fix(bot): 服务端虚拟机只在机内操作,不再提示允许本机环境

旧的本机协作提示和 host 回退已经不是现在的用法。虚拟机会话不再打开服务器本机,云端规则也不再提这句勾选。
Samien 1 month ago
parent
commit
ba3569e

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

@@ -190,7 +190,7 @@ 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" :class="{selected:envs.vm}"><input type="checkbox" aria-label="本地虚拟机" :checked="envs.vm" :disabled="computerChoiceDisabled" @change="toggleEnv('vm',($event.target as HTMLInputElement).checked)"><span><b><AppIcon name="monitor" :size="14"/>本地虚拟机</b><small>隔离桌面;节点离线时自动改本机</small></span></label>
+      <label class="env-check" :class="{selected:envs.vm}"><input type="checkbox" aria-label="本地虚拟机" :checked="envs.vm" :disabled="computerChoiceDisabled" @change="toggleEnv('vm',($event.target as HTMLInputElement).checked)"><span><b><AppIcon name="monitor" :size="14"/>本地虚拟机</b><small>隔离桌面,操作只在虚拟机内</small></span></label>
       <label class="env-check" :class="{selected:envs.cloud}"><input type="checkbox" aria-label="云端 · Grok Bot" :checked="envs.cloud" :disabled="computerChoiceDisabled" @change="toggleEnv('cloud',($event.target as HTMLInputElement).checked)"><span><b><AppIcon name="globe" :size="14"/>云端 · Grok Bot</b><small>共享云端电脑,持续保存工作</small></span></label>
       <label class="env-check" :class="{selected:envs.desktop}"><input type="checkbox" aria-label="本机控制" :checked="envs.desktop" :disabled="computerChoiceDisabled" @change="toggleEnv('desktop',($event.target as HTMLInputElement).checked)"><span><b><AppIcon name="monitor" :size="14"/>本机控制</b><small>{{native?.hosts?.length?`${native.hosts.filter((h:any)=>h.online).length} 台 Mac 在线,可按名选择`:'暂无已连接的 Mac'}}</small></span></label>
       <label class="env-check" :class="{selected:envs.browser}"><input type="checkbox" aria-label="仅浏览器" :checked="envs.browser" :disabled="computerChoiceDisabled" @change="toggleEnv('browser',($event.target as HTMLInputElement).checked)"><span><b><AppIcon name="globe" :size="14"/>仅浏览器</b><small>连接 Mac 上的机器人独立浏览器</small></span></label>

+ 3 - 2
src/runner/worker/gateway.ts

@@ -37,7 +37,8 @@ const TOOLS:WorkerTool[]=[
 const pathField=z.string().min(1).max(4096).refine(value=>!value.includes('\0'))
 const PROFILE_COMPUTER_RULES='当前使用本机协作模式:沿用基础 Profile 的本机工具、配置和上下文,本地虚拟机是独立的 Linux 电脑。computer_shell/read_file/write_file/desktop_state/action/export 只操作虚拟机;本机文件、终端和程序使用 Profile 已授权的原生工具。computer_copy_file 在两个环境间显式复制文件。调用前明确目标环境,文件路径和浏览器登录互不通用;需要回传虚拟机文件时使用 computer_export。人工接管期间停止操作,交还后先检查当前状态。'
 const HERMES_COMPUTER_RULES='本轮由 Hermes 管理模型、配置、识图、技能和授权资源。使用 Hermes 原生 skills_list、skill_view 和 skill_manage 发现、读取和维护技能;遵循当前 Profile 的技能规则。技能依赖本机登录态、环境变量或授权文件时,使用 Hermes 原生工具完成相应步骤;适合 Linux 的脚本与输入文件可经 computer_copy_file 传入虚拟机,再用 computer_shell 执行。路径、软件和登录态在本机与虚拟机之间不通用。不要自行复制 Profile 配置或凭据到虚拟机。'
-const ISOLATED_COMPUTER_RULES='当前使用隔离虚拟机:命令、文件处理和浏览器操作默认通过 computer_* 工具在指定的 Linux 电脑执行。需要使用本机文件或运行本机技能时,遵循 Hermes 返回的授权请求;拒绝后停止该操作。人工接管期间停止操作,交还后先检查当前状态。'
+const ISOLATED_COMPUTER_RULES='当前使用隔离虚拟机:命令、文件、浏览器和桌面操作都通过 computer_* 在这台 Linux 电脑内部完成。不要使用服务器本机的终端、文件或桌面,也不要在节点离线时改去本机。需要真实 Mac 时,那是单独勾选的本机环境,不从这台虚拟机里打开。人工接管期间停止操作,交还后先检查当前状态。'
+const ISOLATED_HERMES_RULES='本轮由 Hermes 管理模型、配置和这次会话。使用 Hermes 原生 skills_list、skill_view 和 skill_manage 发现、读取和维护技能。技能和 computer_* 都留在这台虚拟机里;不要把 Profile 的本机登录、文件或凭据复制进来。'
 export interface ProxyHandle {readonly proxy:ComputerPublicProxy;release():Promise<void>}
 export class ComputerRuntime {
   retainDesktops=false
@@ -191,7 +192,7 @@ export class ComputerGateway {
   private profileSession?:ProfileComputerSession
   private skillSession?:ProfileSkillSession
   private get hermesManaged(){return this.meta.hermesRuntime===true}
-  private get rules(){const sharing=this.meta.environmentId!==this.meta.agentId?'多个机器人共用这台虚拟机:命令和文件操作并行执行,桌面操作交替进行;动手前注意其他机器人可能正在使用同一桌面,避免覆盖彼此的工作。':'';return this.hermesManaged?`${sharing}${this.meta.profileSession?PROFILE_COMPUTER_RULES:ISOLATED_COMPUTER_RULES}\n${HERMES_COMPUTER_RULES}`:`${sharing}${PROFILE_COMPUTER_RULES}\n${SKILL_RULES}`}
+  private get rules(){const sharing=this.meta.environmentId!==this.meta.agentId?'多个机器人共用这台虚拟机:命令和文件操作并行执行,桌面操作交替进行;动手前注意其他机器人可能正在使用同一桌面,避免覆盖彼此的工作。':'';if(this.meta.profileSession)return `${sharing}${PROFILE_COMPUTER_RULES}\n${this.hermesManaged?HERMES_COMPUTER_RULES:SKILL_RULES}`;return `${sharing}${ISOLATED_COMPUTER_RULES}\n${this.hermesManaged?ISOLATED_HERMES_RULES:SKILL_RULES}`}
   private team?:{id:string;catalog:WorkerTool[];call(name:string,args:Record<string,unknown>,callId:string):Promise<unknown>;workspaceMemory:boolean}
   constructor(readonly runtime:ComputerRuntime,readonly meta:ComputerTarget,readonly workId:string,readonly check:()=>Promise<void>,readonly publish:(body:Record<string,unknown>)=>Promise<any>,readonly profileTarget?:GatewayTarget){ }
   async connect(){this.runtime.assertTarget(this.meta);await this.runtime.ready;await this.authorized();if(this.hermesManaged&&!this.profileTarget)throw new HttpError(409,'缺少 Hermes 会话连接,不能回退到独立 Worker。','computer_profile_unavailable')}

+ 2 - 4
src/server/grokCloud.ts

@@ -28,10 +28,8 @@ export const GROK_COMPUTER_TOOLS=[
   {id:'cloud_computer_action',name:'cloud_computer_action',description:'操作 Grok Bot 云端电脑。先查看桌面,再使用 click、text、key 或 scroll。',inputSchema:schema({kind:{enum:['click','text','key','scroll']},x:{type:'integer'},y:{type:'integer'},text:field,key:field,direction:{enum:['up','down','left','right']}},['kind'])},
 ]
 export const GROK_COMPUTER_RULES='当前电脑为 Grok Bot 云端共享虚拟机,工作目录 /workspace。电脑操作、文件读写和命令必须使用 cloud_computer_* 工具。本机路径和本机终端不属于这台电脑。多个机器人共享工作文件和浏览器登录,避免覆盖其他机器人的工作;人工接管时等待交还。'
-export function grokComputerRules(allowHostEnvironment:boolean){
-  return allowHostEnvironment
-    ? '当前电脑为 Grok Bot 云端共享虚拟机,工作目录 /workspace;云端操作必须使用 cloud_computer_* 工具。用户同时勾选了“允许本机环境”,可按任务需要使用基础 Profile 已授权的本机文件和终端工具,以及另行授权的本机桌面工具。调用前明确目标环境,本机与云端的路径、程序、文件和浏览器资料互不通用。多个机器人共享云端工作文件和浏览器登录,避免覆盖其他机器人的工作;人工接管时等待交还。'
-    : GROK_COMPUTER_RULES+' 用户未勾选“允许本机环境”:本轮不要使用本机终端、本机文件或本机桌面完成电脑操作。'
+export function grokComputerRules(_allowHostEnvironment = false){
+  return GROK_COMPUTER_RULES
 }
 
 /** Credentials and network tokens stay on the Web server. Every robot of an

+ 1 - 1
src/server/runnerHub.ts

@@ -158,7 +158,7 @@ export class RunnerHub {
               scope?.authorize()
               if(computer&&!scope?.cleanupOnly){
                 const agent=this.store.get<import('../shared/workspace.js').WorkspaceAgent>(owner,'agent',computer.agentId)
-                if(!agent||(agent.vmExecution==='profile')!==(computer.profileSession===true))return false
+                if(!agent)return false
               }
               if(computer?.hostAccess){
                 const agent=this.store.get<import('../shared/workspace.js').WorkspaceAgent>(owner,'agent',computer.agentId)

+ 1 - 1
src/server/workspaceGateway.ts

@@ -111,7 +111,7 @@ export class WorkspaceNodes {
   targetForAgent(owner: string, agent: { id?:string;nodeId: string; remoteAgentId?: string;execution?:string;computer?:WorkspaceAgent['computer'];allowHostEnvironment?:boolean;vmExecution?:WorkspaceAgent['vmExecution'];temporaryGoalId?:string;helperRunnerId?:string;computerEnvironmentId?:string }): GatewayTarget {
     if(agent.execution==='computer'){
       if(!agent.id||agent.remoteAgentId)throw new HttpError(409,'隔离电脑需要当前服务器管理的机器人','computer_agent_required')
-      const target=this.runnerTarget?.(owner,agent.nodeId,{environmentId:agent.computerEnvironmentId??agent.id,agentId:agent.id,ownerKey:createHash('sha256').update(owner).digest('hex'),...(agent.vmExecution==='profile'&&!agent.temporaryGoalId?{profileSession:true}:{}),...(agent.allowHostEnvironment===true&&(agent.computer==='vm'||!agent.computer)&&!agent.temporaryGoalId?{hostAccess:true}:{})})
+      const target=this.runnerTarget?.(owner,agent.nodeId,{environmentId:agent.computerEnvironmentId??agent.id,agentId:agent.id,ownerKey:createHash('sha256').update(owner).digest('hex')})
       if(agent.computerEnvironmentId&&this.store.require<import('./sharedComputers.js').SharedComputer>(owner,'shared-computer',agent.computerEnvironmentId).runnerId!==target?.runner?.id)throw new HttpError(409,'共享电脑的原执行节点已变化,请先恢复原节点','shared_runner_changed')
       if(agent.helperRunnerId&&target?.runner?.id!==agent.helperRunnerId)throw new HttpError(409,'临时助手的原执行节点已变化,请创建新助手','helper_runner_changed')
       if(!target)throw new HttpError(409,'隔离电脑需要连接执行节点','computer_runner_required')

+ 20 - 10
src/server/workspaceRuntime.ts

@@ -597,15 +597,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
       const cwd = recovering || target.pairedToken ? undefined
         : await configuredWorkingDirectory((path, options) => target.session.request(path, options), agent.profile)
       this.nodes.requireSource(owner, agent)
-      const opened = binding?.storedId
-        ? await gateway.rpc('session.resume', {
-            profile: agent.profile,
-            session_id: binding.storedId,
-            omit_messages: true,
-            ...(recovering&&target.runner?.computer?{recoverOnly:true}:{}),
-            close_on_disconnect: false,
-          })
-        : await gateway.rpc('session.create', {
+      const createSession = () => gateway.rpc('session.create', {
             profile: agent.profile,
             title: `Yaoyao ${c.id}${run.conversationTaskId ? ` / ${run.conversationTaskId}` : ''}`,
             source: 'yaoyao_workspace',
@@ -614,6 +606,24 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
             room_plumbing: true,
             close_on_disconnect: false,
           })
+      let opened
+      if (binding?.storedId) {
+        try {
+          opened = await gateway.rpc('session.resume', {
+            profile: agent.profile,
+            session_id: binding.storedId,
+            omit_messages: true,
+            ...(recovering&&target.runner?.computer?{recoverOnly:true}:{}),
+            close_on_disconnect: false,
+          })
+        } catch (error) {
+          const code = error instanceof HttpError ? error.code : ''
+          const message = error instanceof Error ? error.message : ''
+          if (recovering || (code !== 'computer_session_mode_changed' && !message.includes('执行方式已改变'))) throw error
+          binding = undefined
+          opened = await createSession()
+        }
+      } else opened = await createSession()
       runtimeId = String(opened.session_id ?? '')
       const storedId = String(
         opened.stored_session_id ?? opened.session_key ?? opened.resumed ?? binding?.storedId ?? '',
@@ -774,7 +784,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
           `本次来源消息 ID:${run.messageId};当前会话 ID:${c.id}。`,
           plugins ? '本轮已挂载用户为当前 Bot 授权的插件工具,工具名以 plugin_ 开头,说明中包含实际服务和操作。仅按用户当前任务使用;连接或重新授权应用请让用户打开 Bot 模式的工具 → 已连接应用。不要索取 API Key 或在回复中展示凭据。' : '',
           desktop ? DESKTOP_ENVIRONMENT_RULES+(this.desktopEnvironments?.onlineHostsLine(owner)??'') : '',
-          cloud ? grokComputerRules(agent.computer==='cloud'&&agent.allowHostEnvironment===true) : '',
+          cloud ? grokComputerRules() : '',
           `[yaoyao-run:${run.runId}:${resultMessage.id}]`,
         ]
           .filter(Boolean)

+ 6 - 4
tests/server/workspacePanels.test.ts

@@ -119,7 +119,7 @@ it('persists the separate host option, checks the Runner grant and refuses chang
  const path=`/api/app/agents/${agent.id}/computer-selection`
  const response=await request(app.callback()).put(path).send({computer:'vm',allowHostEnvironment:true}).expect(200)
  expect(response.body.agent).toMatchObject({computer:'vm',execution:'computer',allowHostEnvironment:true})
- expect(target).toHaveBeenCalledWith('owner','local',expect.objectContaining({agentId:agent.id,hostAccess:true}))
+ const granted=target.mock.calls.at(-1)?.[2];expect(granted).toMatchObject({agentId:agent.id});expect(granted).not.toHaveProperty('hostAccess')
  await request(app.callback()).put(path).set('x-user','other').send({computer:'vm',allowHostEnvironment:true}).expect(404)
  store.put('owner','turn','active-hybrid',{agentId:agent.id,status:'running'})
  await request(app.callback()).put(path).send({computer:'vm',allowHostEnvironment:false}).expect(409)
@@ -135,7 +135,9 @@ it('persists the separate Profile execution mode, checks its Runner capability a
  const path=`/api/app/agents/${agent.id}/computer-selection`
  const changed=await request(app.callback()).put(path).send({computer:'vm',vmExecution:'profile'}).expect(200)
  expect(changed.body.agent).toMatchObject({computer:'vm',execution:'computer',vmExecution:'profile',allowHostEnvironment:true})
- expect(target).toHaveBeenCalledWith('owner','local',expect.objectContaining({profileSession:true,hostAccess:true}))
+ const opened=target.mock.calls.at(-1)?.[2]
+ expect(opened).not.toHaveProperty('profileSession')
+ expect(opened).not.toHaveProperty('hostAccess')
  store.put('owner','turn','active-profile',{agentId:agent.id,status:'running'})
  await request(app.callback()).put(path).send({computer:'vm',vmExecution:'worker'}).expect(409)
  expect(()=>store.updateAgent('owner',agent.id,{vmExecution:'worker'})).toThrow('先停止当前任务')
@@ -173,8 +175,8 @@ it('allows the host option for cloud and VM only and clears it when selecting an
   expect(returned.body.agent.allowHostEnvironment).toBe(false)
  }
  await request(app.callback()).put(path).send({computer:'hybrid'}).expect(400)
- expect(grokComputerRules(true)).toContain('基础 Profile 已授权的本机文件和终端工具')
- expect(grokComputerRules(false)).toContain('本轮不要使用本机终端')
+ expect(grokComputerRules(true)).toContain('cloud_computer_*')
+ expect(grokComputerRules(false)).not.toContain('允许本机环境')
 })
 it('clears inapplicable host grants through generic updates and migrates the previous combined choice once',()=>{
  const agent=store.createAgent('owner',{name:'权限迁移',profile:'default',computer:'vm',execution:'computer',allowHostEnvironment:true})