Przeglądaj źródła

fix(runner): 保留团队工具原始错误信息

本轮运行结束后 assertActive 改为抛出带状态码的 HttpError,工具桥与 Runner 的兜底 catch 不再吞掉普通 Error 的 message,改为保留并截断,便于区分运行结束与真实通道故障。补充回归测试。
Samien 1 miesiąc temu
rodzic
commit
cd23427010

+ 1 - 1
src/runner/agent.ts

@@ -278,7 +278,7 @@ export class RunnerAgent {
     const pending=Promise.resolve().then(async()=>{
       if(command.expiresAt<=Date.now()||!(await this.api('admit',{id:command.id})).allowed||command.expiresAt<=Date.now())throw new HttpError(403,'命令授权已经失效,未执行','runner_command_not_admitted')
       return this.execute(command)
-    }).then(result=>({result}),error=>({error:{message:error instanceof HttpError||error instanceof ComputerError?error.message:'执行节点操作失败',code:error instanceof HttpError||error instanceof ComputerError?error.code:'runner_error'}}))
+    }).then(result=>({result}),error=>({error:{message:(error instanceof Error&&error.message?error.message:'执行节点操作失败').slice(0,1000),code:error instanceof HttpError||error instanceof ComputerError?error.code:'runner_error'}}))
       .then(reply=>{this.db.prepare("UPDATE commands SET state='complete',result=? WHERE id=?").run(JSON.stringify(reply),command.id);return reply})
       .finally(()=>this.inflight.delete(command.id))
     this.inflight.set(command.id,pending);return pending

+ 1 - 1
src/server/workspaceRuntime.ts

@@ -702,7 +702,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
           if(desktop)await this.desktopEnvironments!.requireAvailable(owner,agent,target)
           if(cloud)await this.cloud!.requireAvailable(owner,agent,target)
           const assertActive=()=>{
-            if(settled||this.closing||toolController.signal.aborted)throw new Error('运行已停止')
+            if(settled||this.closing||toolController.signal.aborted)throw new HttpError(410,'本轮运行已结束,请发起新一轮对话后重试。','run_finished')
             this.requireAuthorization(owner,run.runId);this.nodes.requireSource(owner,this.store.require<Agent>(owner,'agent',agent.id))
             if(team)this.teamTools.assertTurn(owner,run.id)
             if(plugins){const current=this.getWork(owner,run.id),latest=this.store.require<Agent>(owner,'agent',agent.id);if(current.cancelRequested||this.store.require<Run>(owner,'run',run.runId).stopRequested||latest.archived||!['running','waiting'].includes(current.status))throw new HttpError(403,'本轮插件授权已结束','plugin_grant_revoked')}

+ 3 - 2
src/server/workspaceToolLease.ts

@@ -70,7 +70,7 @@ export async function createWorkspaceToolLease(input: LeaseInput): Promise<Works
   const opaqueId = (id: string) => `team_${createHash('sha256').update(`${token}:${id}`).digest('hex').slice(0,32)}`
   const server = createServer((req, res) => { void handle(req, res).catch(error => {
     json(res, error instanceof HttpError ? error.status : 500, {
-      error: error instanceof HttpError ? error.message : '团队工具执行失败',
+      error: error instanceof Error && error.message ? error.message.slice(0, 1000) : '团队工具执行失败',
       code: error instanceof HttpError ? error.code : 'team_tool_failed',
     })
   }) })
@@ -134,7 +134,8 @@ export async function createWorkspaceToolLease(input: LeaseInput): Promise<Works
           if(value&&typeof value==='object'&&Array.isArray((value as any).content))return value
           return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value }
         } catch (error) {
-          return { content: [{ type: 'text', text: JSON.stringify({ error: error instanceof HttpError ? error.message : '团队操作失败,请检查当前状态后再重试。', code: error instanceof HttpError ? error.code : 'team_tool_failed' }) }], isError: true }
+          const message = error instanceof Error && error.message ? error.message.slice(0, 1000) : '团队操作失败,请检查当前状态后再重试。'
+          return { content: [{ type: 'text', text: JSON.stringify({ error: message, code: error instanceof HttpError ? error.code : 'team_tool_failed' }) }], isError: true }
         }
       })
       previous = { fingerprint, result }; calls.set(call.callId, previous)

+ 29 - 0
tests/server/workspaceTeamTools.test.ts

@@ -242,6 +242,35 @@ describe('loopback tool lease', () => {
     expect((await http('/tools/list',{})).status).toBe(403)
   })
 
+  it('reports the underlying error message instead of masking tool failures', async () => {
+    const lease = await createWorkspaceToolLease({
+      target, profile:manager.profile, workId:work.id,
+      session:()=>({runtimeId:'runtime',storedId:'stored'}),signal:new AbortController().signal,
+      assertActive:()=>{},catalog:()=>[{id:'workspace_probe',name:'workspace_probe',description:'probe',inputSchema:{}}],
+      call:async()=>{throw new Error('虚拟机磁盘已满')},onFailure:()=>{},
+    })
+    leases.push(lease); await lease.bind()
+    const catalog = await (await http('/tools/list',{})).json()
+    const result = await (await http('/tools/call',{toolId:catalog.tools[0].id,arguments:{},callId:'probe'})).json()
+    expect(result.isError).toBe(true)
+    expect(JSON.parse(result.content[0].text)).toMatchObject({error:'虚拟机磁盘已满',code:'team_tool_failed'})
+  })
+
+  it('keeps plain authorization stop messages readable', async () => {
+    let active = true
+    const lease = await createWorkspaceToolLease({
+      target, profile:manager.profile, workId:work.id,
+      session:()=>({runtimeId:'runtime',storedId:'stored'}),signal:new AbortController().signal,
+      assertActive:()=>{if(!active)throw new Error('运行已停止')},
+      catalog:()=>[],call:async()=>({}),onFailure:()=>{},
+    })
+    leases.push(lease); await lease.bind()
+    active = false
+    const response = await http('/tools/list',{})
+    expect(response.status).toBe(500)
+    expect(await response.json()).toMatchObject({error:'运行已停止',code:'team_tool_failed'})
+  })
+
   it('checks readiness per Profile and rejects unavailable or remote bridges', async () => {
     await requireTeamToolBridge(target,'writer')
     expect(target.session.request).toHaveBeenCalledWith(expect.stringContaining('/capabilities'),expect.objectContaining({search:new URLSearchParams({profile:'writer'})}))