Przeglądaj źródła

补齐聊天电脑面板中的临时助手展示与生命周期保护

LSamien 1 miesiąc temu
rodzic
commit
c5a8086cb9

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

@@ -46,18 +46,18 @@ onBeforeUnmount(()=>{closed=true;revision++;clearTimeout(timer)})
      <span v-if="frame" class="open-badge"><AppIcon name="external" :size="12"/>打开桌面</span>
     </button>
     <p v-if="error||state?.problem" class="problem" role="alert">{{error||state?.problem}}</p>
-    <label class="computer-select">此 Agent 使用的电脑<select aria-label="此 Agent 使用的电脑" :value="state?.enabled?'vm':'off'" :disabled="busy||state?.inUse||!!agent.remoteAgentId" @change="choose(($event.target as HTMLSelectElement).value)"><option value="off">不使用电脑</option><option value="vm">本地虚拟机</option></select></label>
+    <label class="computer-select">此 Agent 使用的电脑<select aria-label="此 Agent 使用的电脑" :value="state?.enabled?'vm':'off'" :disabled="busy||state?.inUse||!!agent.remoteAgentId||!!agent.temporaryGoalId" @change="choose(($event.target as HTMLSelectElement).value)"><option value="off">不使用电脑</option><option value="vm">本地虚拟机</option></select></label>
     <template v-if="state?.enabled">
-     <p>{{state.mode==='shared'?'共享虚拟机 · 与其他成员共用桌面和工作文件':'此 Agent 的独立虚拟机'}}</p>
+     <p>{{agent.temporaryGoalId?'临时助手的电脑由当前任务管理':state.mode==='shared'?'共享虚拟机 · 与其他成员共用桌面和工作文件':'此 Agent 的独立虚拟机'}}</p>
      <button v-if="!state.image" class="primary" :disabled="!isAdmin" @click="emit('settings')">设置本地虚拟机</button>
-     <button v-else-if="state.container==='missing'" class="primary" :disabled="busy" @click="action('create')">创建 {{agent.name}} 的虚拟机</button>
-     <button v-else-if="state.container==='stopped'" class="primary" :disabled="busy" @click="action('start')">启动虚拟机</button>
-     <template v-else>
+     <button v-else-if="state.container==='missing'&&!agent.temporaryGoalId" class="primary" :disabled="busy" @click="action('create')">创建 {{agent.name}} 的虚拟机</button>
+     <button v-else-if="state.container==='stopped'&&!agent.temporaryGoalId" class="primary" :disabled="busy" @click="action('start')">启动虚拟机</button>
+     <template v-else-if="state.container==='running'">
       <button class="primary" :disabled="!state.ready||busy" @click="emit('desktop',agent)">打开桌面</button>
       <button v-if="state.mode==='per-bot'" :disabled="busy" @click="emit('workspace',agent)"><AppIcon name="panel"/>打开双桌面</button>
       <p class="control-state">{{state.controlMode==='human'?'你或其他操作者正在控制电脑':state.inUse?'Agent 正在操作':'仅查看 · 虚拟机空闲'}}</p>
      </template>
-     <div v-if="state.container!=='missing'" class="vm-actions"><button :disabled="busy||state.inUse" @click="action('stop')">停止虚拟机</button><button :disabled="busy||state.inUse" @click="action('recreate')">重建虚拟机</button></div>
+     <div v-if="state.container!=='missing'&&!agent.temporaryGoalId" class="vm-actions"><button :disabled="busy||state.inUse" @click="action('stop')">停止虚拟机</button><button :disabled="busy||state.inUse" @click="action('recreate')">重建虚拟机</button></div>
      <p class="hint">重建会保留工作目录和浏览器资料。虚拟机空闲 5 分钟后自动停止。</p>
      <button v-if="isAdmin" class="settings-link" @click="emit('settings')"><AppIcon name="settings" :size="14"/>本地虚拟机设置</button>
     </template>

+ 2 - 2
src/client/views/ConversationsView.vue

@@ -195,7 +195,7 @@ async function leaveComputer(){
   return true
 }
 onBeforeRouteUpdate(leaveComputer);onBeforeRouteLeave(leaveComputer)
-const computerAgents=computed(()=>agents.value.filter(agent=>agent.execution==='computer'&&!agent.archived&&(active.value?.memberIds.includes(agent.id)||(!!agent.temporaryGoalId&&agent.temporaryGoalId===selectedTask.value))))
+const computerCandidates=computed(()=>agents.value.filter(agent=>!agent.archived&&(active.value?.memberIds.includes(agent.id)||(!!agent.temporaryGoalId&&agent.temporaryGoalId===activeTask.value?.goal?.id))))
 const members = computed(() => agents.value.filter((a) => active.value?.memberIds.includes(a.id)))
 const isAgentDialog = computed(() => dialog.value === 'agent' || dialog.value === 'editAgent')
 const body = (v: unknown) => v as JsonValue
@@ -698,7 +698,7 @@ onBeforeUnmount(() => {
         :mention-options="active.kind === 'group' ? members.map(a => ({id:a.id,label:a.name,insertText:`@${a.name} `})) : []"
         @send="sendFromComposer" @stop="control('stop')" @tool-trace-toggle="showThinking = !showThinking" @clear-reference="quoted = null" @error="error = $event" />
     </section>
-    <LocalVmChatPanel v-if="computerDockOpen" :active="!twoDesktops" :agents="members" :is-admin="auth.user?.role === 'admin'" @close="computerDockOpen=false" @changed="refresh()" @settings="shell?.openLocalVm()" @desktop="desktopAgent=$event;computerOpen=true" @workspace="desktopAgent=$event;twoDesktops=true" />
+    <LocalVmChatPanel v-if="computerDockOpen" :active="!twoDesktops" :agents="computerCandidates" :is-admin="auth.user?.role === 'admin'" @close="computerDockOpen=false" @changed="refresh()" @settings="shell?.openLocalVm()" @desktop="desktopAgent=$event;computerOpen=true" @workspace="desktopAgent=$event;twoDesktops=true" />
     </div>
     <ComputerPanel ref="desktopViewer" v-if="computerOpen&&desktopAgent" :agents="[desktopAgent]" auto-take @changed="load()" @close="computerOpen=false" />
     <LocalVmWorkspace ref="vmWorkspace" v-if="twoDesktops&&desktopAgent" :agents="agents.filter(a=>a.execution==='computer'&&!a.archived&&!a.computerEnvironmentId)" :primary="desktopAgent.id" @close="twoDesktops=false" />

+ 1 - 0
src/server/localVm.ts

@@ -160,6 +160,7 @@ export class LocalVmService {
     router.post('/api/app/agents/:id/local-vm/:action',async ctx=>{
       const owner=this.auth.require(ctx).id,action=parse(z.enum(['create','start','stop','recreate','remove']),ctx.params.action)
       const agent=this.store.require<WorkspaceAgent>(owner,'agent',ctx.params.id)
+      if(agent.temporaryGoalId)throw new HttpError(409,'临时助手的虚拟机由当前任务管理','helper_task_bound')
       if(agent.execution!=='computer'||agent.archived||agent.remoteAgentId)throw new HttpError(409,'此 Agent 未使用本地虚拟机','local_vm_disabled')
       this.shared.assertLocalVmIdle(owner,agent.computerEnvironmentId?this.store.list<WorkspaceAgent>(owner,'agent').filter(a=>a.computerEnvironmentId===agent.computerEnvironmentId).map(a=>a.id):[agent.id])
       ctx.body=await this.hub.computer(owner,agent,'lifecycle',{action},()=>this.nodes.requireSource(owner,agent))

+ 2 - 0
tests/server/localVmRoutes.test.ts

@@ -43,6 +43,8 @@ it('protects preparation, removes the old catalog API and scopes sharing to the
   expect(store.require<any>('bob','agent',other.id).computerEnvironmentId).toBeUndefined()
   await request(app.callback()).put(base+'/policy').set('x-admin','yes').send({requestId:randomUUID(),mode:'per-bot',maxInstances:2}).expect(200)
   expect(store.require<any>('alice','agent',first.id).computerEnvironmentId).toBeUndefined()
+  const helper=store.createAgent('alice',{name:'临时任务助手',profile:'default',execution:'computer'},{createdByAgentId:first.id,createdFromRunId:randomUUID(),temporaryGoalId:randomUUID()})
+  await request(app.callback()).post(`/api/app/agents/${helper.id}/local-vm/create`).send({}).expect(409)
   active=false;expect(service.allowed(id,runner.id)).toBe(false)
  }finally{store.close();rmSync(home,{recursive:true,force:true})}
 })