aws-runtime-bridge 1.9.124 → 1.9.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +23 -2
  2. package/dist/adapter/AcodeSdkAdapter.js +1 -1
  3. package/dist/adapter/types.js +1 -1
  4. package/dist/config.d.ts +22 -2
  5. package/dist/config.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/routes/dashboard.js +58 -3
  8. package/dist/routes/git.d.ts +28 -0
  9. package/dist/routes/git.js +32 -31
  10. package/dist/routes/terminal.js +24 -24
  11. package/dist/services/auto-register.d.ts +2 -2
  12. package/dist/services/auto-register.js +1 -1
  13. package/dist/services/aws-client-agent-mcp.d.ts +2 -1
  14. package/dist/services/aws-client-agent-mcp.js +2 -2
  15. package/dist/services/cc-switch-sdk.js +1 -1
  16. package/dist/services/cli-commands.js +5 -5
  17. package/dist/services/fileSnapshotStore.js +1 -1
  18. package/dist/services/instance-ai-config-service.js +1 -1
  19. package/dist/services/instance-features.d.ts +2 -0
  20. package/dist/services/instance-features.js +1 -1
  21. package/dist/services/panel-auth.d.ts +2 -0
  22. package/dist/services/panel-auth.js +1 -1
  23. package/dist/services/startup-config-wizard.js +6 -6
  24. package/dist/services/tunnel-client.js +1 -1
  25. package/dist/services/update-notifier.js +2 -2
  26. package/package/acode/dist/ask-user-question-tool.js +2 -2
  27. package/package/acode/dist/mcpToolNames.d.ts +2 -0
  28. package/package/acode/dist/mcpToolNames.js +1 -1
  29. package/package/acode/dist/meeting-debug-log.d.ts +1 -0
  30. package/package/acode/dist/meeting-debug-log.js +3 -0
  31. package/package/acode/dist/runtime.d.ts +2 -0
  32. package/package/acode/dist/runtime.js +12 -12
  33. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +12 -0
  34. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  35. package/package/aws-client-agent-mcp/dist/agent-client.test.js +179 -1
  36. package/package/aws-client-agent-mcp/dist/config.js +1 -1
  37. package/package/aws-client-agent-mcp/dist/http-client.d.ts +2 -0
  38. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  39. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +6 -2
  40. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
  41. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -28
  42. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +604 -1
  43. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +10 -0
  44. package/package/aws-client-agent-mcp/dist/mcp-tools.js +12 -10
  45. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +38 -0
  46. package/package/aws-client-agent-mcp/dist/meeting-debug-log.d.ts +13 -0
  47. package/package/aws-client-agent-mcp/dist/meeting-debug-log.js +3 -0
  48. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +2 -0
  49. package/package/aws-client-agent-mcp/dist/message-buffer.js +1 -1
  50. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +60 -0
  51. package/package/aws-client-agent-mcp/dist/types.d.ts +11 -1
  52. package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
  53. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- import{configManager as g}from"./config.js";import{MESSAGE_POLL_INTERVAL_MS as m}from"./constants.js";import{HttpClient as f}from"./http-client.js";import{logger as s,setMcpNodeLogSink as C,updateStartupLogIdentity as u}from"./logger.js";import{claimRuntimeLaunchBinding as I}from"./runtime-launch-binding.js";import{StatusReporter as y}from"./status-reporter.js";import{WebSocketClient as p}from"./websocket-client.js";class b{wsClient;httpClient;agentConfig;serverConfig;agentId=null;displayName=null;projectId=null;isRunning=!1;isDegradedMode=!1;messageHandler=null;profileHandler=null;configHandler=null;pollTimer=null;statusReporter=null;newCharacterHandler=null;runtimeTokenRefreshTimer=null;cachedProfile=null;constructor(){g.loadConfig(),this.agentConfig=g.getAgentConfig(),this.serverConfig=g.getServerConfig(),this.wsClient=new p(this.serverConfig),this.httpClient=new f(this.serverConfig),this.setupWebSocketListeners()}async initialize(){const e=await I(this.agentConfig,this.serverConfig);if(!e){this.serverConfig.runtimeBridgeBaseUrl=void 0;return}g.applyRuntimeLaunchBinding(e),this.agentConfig=g.getAgentConfig(),this.serverConfig=g.getServerConfig(),this.wsClient=new p(this.serverConfig),this.httpClient=new f(this.serverConfig),this.setupWebSocketListeners()}setupWebSocketListeners(){this.wsClient.on("directMessage",e=>{this.handleIncomingDm(e)}),this.wsClient.on("groupMessage",e=>{this.handleIncomingGroup(e)}),this.wsClient.on("profileInfo",e=>{s.info(`[AgentClient] WebSocket profileInfo \u4E8B\u4EF6\u89E6\u53D1\uFF0CdisplayName: ${e.displayName}`),this.handleProfileInfo(e)}),this.wsClient.on("agentConfig",e=>{s.info("[AgentClient] WebSocket agentConfig \u4E8B\u4EF6\u89E6\u53D1"),this.configHandler&&this.configHandler(e??{})}),this.wsClient.on("agentStatus",e=>{s.info(`[AgentClient] WebSocket agentStatus \u4E8B\u4EF6\u89E6\u53D1: ${e.status}`)}),this.wsClient.on("disconnected",(e,t)=>{const n=t?.toString()||"";s.warn("[AgentClient] WebSocket \u65AD\u5F00\u8FDE\u63A5: code=%s, reason=%s, agentId=%s, \u6B63\u5728\u81EA\u52A8\u91CD\u8FDE",e,n,this.agentId)}),this.wsClient.on("error",e=>{s.error("[AgentClient] WebSocket \u9519\u8BEF: %s, stack=%s",e.message,e.stack)}),this.wsClient.on("reconnectFailed",()=>{s.error("[AgentClient] \u91CD\u8FDE\u5931\u8D25\uFF08\u5DF2\u8FBE\u91CD\u8BD5\u4E0A\u9650\uFF09\uFF0C\u505C\u6B62\u6D88\u606F\u5FAA\u73AF, agentId=%s",this.agentId),this.stop()}),this.wsClient.on("tokenRejected",()=>{s.error("[AgentClient] runtime token \u88AB 1008 \u62D2\u7EDD\u4E14\u5237\u65B0\u65E0\u53D8\u5316\uFF0C\u8FDB\u5165\u6162\u901F\u91CD\u8BD5\u7B49\u5F85\u65B0 token \u7B7E\u53D1: agentId=%s",this.agentId)}),this.wsClient.on("tokenRefreshFailed",e=>{s.error("[AgentClient] runtime token \u5237\u65B0\u8FDE\u7EED\u5931\u8D25 %s \u6B21\uFF0C\u8FDB\u5165\u6162\u901F\u91CD\u8BD5\u7B49\u5F85 bridge \u6062\u590D: agentId=%s",e,this.agentId)})}async register(){return s.info("[AgentClient] \u6B63\u5728\u6CE8\u518C Agent..."),this.hasPresetAgentId()?await this.registerWithExistingAgentId():await this.registerAsNewAgent()}hasPresetAgentId(){return!!(this.agentConfig.agentId&&this.agentConfig.agentId.trim().length>0)}async registerWithExistingAgentId(){const e=this.agentConfig.agentId;if(!e)throw new Error("Preset agentId is missing");this.agentId=e.trim();let t;try{t=await this.getProfileByAgentId(this.agentId)}catch(i){throw s.error("[AgentClient] getProfileByAgentId \u5931\u8D25 (presetAgentId=%s): %s",this.agentId,i instanceof Error?i.message:String(i)),i}this.displayName=t.displayName,this.projectId=t.projectId||null,s.info(`[AgentClient] \u4F7F\u7528\u9884\u8BBE AgentID \u7ED1\u5B9A\u5B9E\u4F8B: ${this.displayName} (${this.agentId})`),s.info(`[AgentClient] \u6240\u5C5E\u9879\u76EE: ${this.projectId||"(\u672A\u8BBE\u7F6E)"}`);try{await this.wsClient.connect(this.agentId)}catch(i){throw s.error("[AgentClient] WebSocket \u8FDE\u63A5\u5931\u8D25 (agentId=%s): %s",this.agentId,i instanceof Error?i.message:String(i)),i}s.info("[AgentClient] WebSocket \u8FDE\u63A5\u5DF2\u5EFA\u7ACB\uFF0CAgent \u73B0\u5728\u5DF2\u4E0A\u7EBF"),this.initializeStatusReporter();const n=await this.getProfileByAgentId(this.agentId);return this.displayName=n.displayName,this.projectId=n.projectId||null,u(n.roleName||"unknown-role",n.instanceName||this.agentId),{agentId:this.agentId,displayName:this.displayName,status:n.isOnline?"online":"offline",projectId:n.projectId,workspacePath:n.workspacePath,runtimeStatus:n.runtimeStatus,runtimeSessionId:n.runtimeSessionId}}async registerAsNewAgent(){const e=await this.httpClient.callTool("register",{roleName:this.agentConfig.roleName,projectId:this.agentConfig.projectId,workspacePath:this.agentConfig.workspacePath,prompt:this.agentConfig.prompt,runtimeBridgeBaseUrl:this.serverConfig.runtimeBridgeBaseUrl}),t=e.agentId;if(!t)throw new Error("Registration failed: agentId was not returned");return this.agentId=t,this.displayName=e.displayName,this.projectId=e.projectId||null,u(this.agentConfig.roleName||"unknown-role",e.instanceName||t),s.info(`[AgentClient] \u6CE8\u518C\u6210\u529F: ${this.displayName} (${this.agentId})`),s.info(`[AgentClient] \u72B6\u6001: ${e.status}`),s.info(`[AgentClient] \u6240\u5C5E\u9879\u76EE: ${this.projectId||"(\u672A\u8BBE\u7F6E)"}`),await this.wsClient.connect(this.agentId),s.info("[AgentClient] WebSocket \u8FDE\u63A5\u5DF2\u5EFA\u7ACB\uFF0CAgent \u73B0\u5728\u5DF2\u4E0A\u7EBF"),this.initializeStatusReporter(),e.isExisting===!1&&this.newCharacterHandler&&this.newCharacterHandler(),e}async unregister(){s.info("[AgentClient] \u6B63\u5728\u6CE8\u9500 Agent..."),this.agentId&&await this.httpClient.callTool("unregister",{agentId:this.agentId}),this.stopRuntimeTokenRefresh(),this.wsClient.disconnect(),this.agentId=null,this.displayName=null,this.projectId=null,this.isRunning=!1,s.info("[AgentClient] \u5DF2\u6CE8\u9500")}async discoverColleagues(){return await this.httpClient.callTool("discover_colleague",{agentId:this.agentId})}async discoverGroupRooms(){return this.ensureRegistered(),await this.httpClient.callTool("discover_group_rooms",{agentId:this.agentId})}async hireColleague(e){return this.ensureRegistered(),await this.httpClient.callTool("hire_colleague",{agentId:this.agentId,...e})}async hireColleaguesBatch(e){return this.ensureRegistered(),await this.httpClient.callTool("hire_colleagues_batch",{agentId:this.agentId,...e})}async callAgentScopedTool(e,t){return this.ensureRegistered(),await this.httpClient.callTool(e,{agentId:this.agentId,...t})}async reportMemoryStats(e){await this.callAgentScopedTool("report_memory_stats",{memoryStats:e})}async sendGroupMessage(e,t,n){this.ensureRegistered();const i=t||this.projectId||this.agentConfig.projectId;return await this.httpClient.callTool("send_group_message",{agentId:this.agentId,projectId:i,roomId:n,content:e})}async sendDirectMessage(e,t,n=!1,i,r){this.ensureRegistered();const a={agentId:this.agentId,targetId:e,content:t,requireReply:n,waitReply:n,replyToCallId:i};if(typeof r=="number"&&r>0&&(a.timeoutMs=r),n){const h=typeof r=="number"&&r>0?r+3e4:0;return await this.httpClient.callToolOnce("send_dm",a,h)}return await this.httpClient.callTool("send_dm",a)}async sendCallAndWaitReply(e,t,n=6e4){const i=await this.sendDirectMessage(e,t,!0,void 0,n);if(i.reply)return i.reply;throw new Error("No reply received for call-style direct message before timeout")}async replyToCall(e,t,n){return await this.sendDirectMessage(t,n,!1,e)}async getDmHistory(e,t=50,n){this.ensureRegistered();const i={agentId:this.agentId,targetUserId:e,limit:t};return n&&(i.since=n),await this.httpClient.callTool("get_dm_history",i)}async getGroupHistory(e=50,t,n){if(this.ensureRegistered(),t){const o={projectId:this.projectId||this.agentConfig.projectId,roomId:t,limit:e};return n&&(o.since=n),await this.httpClient.callTool("get_group_history",o)}const i=await this.discoverGroupRooms(),r=this.projectId||this.agentConfig.projectId,a=this.collectRoomIds(i,r);if(a.length===0)return{messages:[],count:0};const c=(await Promise.all(a.map(async o=>{const l={projectId:r,roomId:o,limit:e};return n&&(l.since=n),await this.httpClient.callTool("get_group_history",l)}))).flatMap(o=>o.messages).sort((o,l)=>new Date(o.timestamp).getTime()-new Date(l.timestamp).getTime());return{messages:c,count:c.length}}async fetchUnreadMessages(e={}){this.ensureRegistered();const t=this.projectId||this.agentConfig.projectId,n={agentId:this.agentId,blockIfEmpty:e.blockIfEmpty??!1};e.since&&(n.since=e.since),e.blockTimeoutMs&&e.blockTimeoutMs>0&&(n.blockTimeoutMs=e.blockTimeoutMs);const i=e.includePipelineTasks?"poll_message":"get_dm_messages",r=await this.fetchDirectMessagesWithRecovery(i,n),a=await this.discoverGroupRooms(),h=this.collectRoomIds(a,t),c=await Promise.all(h.map(async o=>{const l={agentId:this.agentId,projectId:t,roomId:o};e.since&&(l.since=e.since);try{return await this.httpClient.callTool("get_group_messages",l)}catch(d){return s.warn("[AgentClient] \u8DF3\u8FC7\u7FA4\u623F\u95F4\u672A\u8BFB\u8865\u62C9\u5931\u8D25: roomId=%s, error=%s",o,d instanceof Error?d.message:String(d)),{messages:[],currentReadPos:0,hasMore:!1}}}));return{directMessages:Array.isArray(r.messages)?r.messages:[],groupMessages:c.flatMap(o=>this.normalizeGroupMessages(o)).sort((o,l)=>new Date(o.timestamp).getTime()-new Date(l.timestamp).getTime())}}async markDmRead(e){if(e.length===0)return new Set;this.ensureRegistered();const t=await this.httpClient.callTool("mark_dm_read",{agentId:this.agentId,msgIds:e});return new Set(Array.isArray(t?.deliverableMsgIds)?t.deliverableMsgIds:[])}async markGroupRead(e,t){if(t.length===0)return new Set;this.ensureRegistered();const n=await this.httpClient.callTool("mark_group_read",{agentId:this.agentId,roomId:e,msgIds:t});return new Set(Array.isArray(n?.deliverableMsgIds)?n.deliverableMsgIds:[])}async fetchBrowserListenerEvents(){const e=this.serverConfig.runtimeBridgeBaseUrl,t=this.agentId;if(!e||!t)return s.info("[AgentClient] fetchBrowserListenerEvents \u8DF3\u8FC7: bridgeBaseUrl=%s, agentId=%s",e||"(\u7A7A)",t||"(\u7A7A)"),[];try{const n=`${e}/runtime/browser-listeners/${encodeURIComponent(t)}/events`,i=this.serverConfig.runtimeAccessToken||"";s.debug("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6: url=%s, tokenLen=%d",n,i.length);const r=await fetch(n,{method:"GET",headers:{"X-Runtime-Token":i},signal:AbortSignal.timeout(3e3)});if(!r.ok)return s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u5931\u8D25: HTTP %s, url=%s",r.status,n),[];const a=await r.json();return!a.ok||!Array.isArray(a.events)?(s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u54CD\u5E94\u5F02\u5E38: ok=%s, eventsType=%s",a.ok,Array.isArray(a.events)?"array":typeof a.events),[]):(s.debug("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u6210\u529F: %d \u6761\u4E8B\u4EF6",a.events.length),a.events)}catch(n){return s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u5F02\u5E38: %s",n instanceof Error?n.message:String(n)),[]}}async notifyBridgeForceMessage(){const e=this.serverConfig.runtimeBridgeBaseUrl,t=this.agentId;if(!(!e||!t))try{const n=`${e}/runtime/sessions/${encodeURIComponent(t)}/notify-force-message`,i=this.serverConfig.runtimeAccessToken||"",r=await fetch(n,{method:"POST",headers:{"X-Runtime-Token":i},signal:AbortSignal.timeout(3e3)});r.ok||s.warn("[AgentClient] \u901A\u77E5 bridge \u5F3A\u5236\u6D88\u606F\u5931\u8D25: HTTP %s, url=%s",r.status,n)}catch(n){s.warn("[AgentClient] \u901A\u77E5 bridge \u5F3A\u5236\u6D88\u606F\u5F02\u5E38: %s",n instanceof Error?n.message:String(n))}}async fetchDirectMessagesWithRecovery(e,t){if(e==="poll_message")try{return await this.httpClient.callToolOnce(e,t)}catch(n){if(this.isRecoverableLongPollError(n))return s.warn("[AgentClient] poll_message \u5355\u6B21\u957F\u8F6E\u8BE2\u8D85\u65F6\uFF0C\u6309\u7A7A\u7ED3\u679C\u9000\u907F\u540E\u7EE7\u7EED\u7B49\u5F85:",n),{messages:[]};throw n}return await this.httpClient.callTool(e,t)}isRecoverableLongPollError(e){return e instanceof Error?/MCP tool poll_message call failed: 503(?:\s|$)/.test(e.message)||/MCP tool poll_message timed out after \d+ms/.test(e.message):!1}normalizeGroupMessages(e){return Array.isArray(e.messages)?e.messages.filter(t=>!!(t&&typeof t=="object"&&typeof t.msgId=="number"&&typeof t.senderId=="string"&&typeof t.content=="string"&&typeof t.timestamp=="string")):[]}collectRoomIds(e,t){const n=e.groups.filter(i=>i.projectId===t).map(i=>i.id);return Array.from(new Set(n))}async getProfile(){this.ensureRegistered();const e=this.agentId;if(!e)throw new Error("Agent is not registered");return{...await this.getProfileByAgentId(e),describe:this.agentConfig.describe}}async getProfileByAgentId(e){return await this.httpClient.callTool("get_profile",{agentId:e})}async getMyTasks(){return this.ensureRegistered(),await this.httpClient.callTool("my_task",{agentId:this.agentId})}async createTaskInstanceComment(e){this.ensureRegistered();const t={agentId:this.agentId,...e};return await this.httpClient.callTool("create_task_instance_comment",t)}async submitTaskResult(e,t,n){this.ensureRegistered();const i={agentId:this.agentId,nodeId:e,outputPayload:t};return n&&Array.isArray(n)&&n.length>0&&(i.changeLog=n),await this.httpClient.callTool("submit_task_result",i)}async startTaskRollback(e){return this.ensureRegistered(),await this.httpClient.callTool("start_task_rollback",{agentId:this.agentId,...e})}async finishTaskRollback(e){return this.ensureRegistered(),await this.httpClient.callTool("finish_task_rollback",{agentId:this.agentId,...e})}async rejectTask(e,t){return this.ensureRegistered(),await this.httpClient.callTool("reject_task",{agentId:this.agentId,nodeId:e,reason:t})}async terminateTask(e,t){return this.ensureRegistered(),await this.httpClient.callTool("terminate_task",{agentId:this.agentId,taskInstanceId:e,reason:t})}async getDebugLogConfig(){return await this.httpClient.callTool("get_debug_log_config",{agentId:this.agentId})}async queryDebugLogs(e){return this.ensureRegistered(),await this.httpClient.callTool("query_debug_logs",{...e})}async queryFileChangesHistory(e){return this.ensureRegistered(),await this.httpClient.callTool("query_file_changes_history",{...e})}async getRuntimeFeatures(){return await this.httpClient.fetchRuntimeFeatures()}async listLaunchConfigs(){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_list",{})}async createLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_create",{...e})}async updateLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_update",{...e})}async deleteLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_delete",{configId:e})}async startLaunchConfig(e){this.ensureRegistered();const t=Math.max(0,Math.min(3e5,Number(e.wait_timeout_ms)||0));return t>0?await this.httpClient.callToolOnce("launch_config_start",e,t+6e4):await this.httpClient.callTool("launch_config_start",{...e})}async restartLaunchConfig(e){this.ensureRegistered();const t=Math.max(0,Math.min(3e5,Number(e.wait_timeout_ms)||0));return t>0?await this.httpClient.callToolOnce("launch_config_restart",e,t+6e4):await this.httpClient.callTool("launch_config_restart",{...e})}async stopLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_stop",{configId:e})}async getLaunchLogs(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_logs",{...e})}async listLaunchRuns(){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_status",{})}start(e,t){if(this.isRunning){s.warn("[AgentClient] \u6D88\u606F\u5FAA\u73AF\u5DF2\u5728\u8FD0\u884C\u4E2D");return}this.isDegradedMode||this.ensureRegistered(),this.messageHandler=e,this.profileHandler=t?.profileHandler||null,this.configHandler=t?.configHandler||null,this.isRunning=!0,s.info(`[AgentClient] start() \u88AB\u8C03\u7528\uFF0CcachedProfile: ${this.cachedProfile?"\u5B58\u5728":"\u4E0D\u5B58\u5728"}, profileHandler: ${this.profileHandler?"\u5DF2\u8BBE\u7F6E":"\u672A\u8BBE\u7F6E"}, \u964D\u7EA7\u6A21\u5F0F: ${this.isDegradedMode?"\u662F":"\u5426"}`),this.cachedProfile&&this.profileHandler&&(s.info(`[AgentClient] \u5904\u7406\u7F13\u5B58\u7684 Profile \u4FE1\u606F: ${this.cachedProfile.displayName}`),this.profileHandler(this.cachedProfile),this.cachedProfile=null),t?.pollMode??!1?(s.warn("[AgentClient] \u542F\u52A8\u6D88\u606F\u4E3B\u5FAA\u73AF\uFF08HTTP\u8F6E\u8BE2\u6A21\u5F0F - \u4E0D\u63A8\u8350\uFF09"),this.messageLoop()):s.info("[AgentClient] \u542F\u52A8\u6D88\u606F\u4E3B\u5FAA\u73AF\uFF08WebSocket\u63A8\u9001\u6A21\u5F0F - \u63A8\u8350\uFF09")}stop(){this.isRunning=!1,this.pollTimer&&(clearTimeout(this.pollTimer),this.pollTimer=null),this.stopRuntimeTokenRefresh(),s.info("[AgentClient] \u6D88\u606F\u4E3B\u5FAA\u73AF\u5DF2\u505C\u6B62")}async messageLoop(){for(;this.isRunning;)try{await this.delay(m)}catch(e){s.error("[AgentClient] \u6D88\u606F\u5FAA\u73AF\u9519\u8BEF:",e),await this.delay(5e3)}}handleIncomingDm(e){e.senderId!==this.agentId&&this.processMessage(e)}handleIncomingGroup(e){e.senderId!==this.agentId&&this.processMessage(e)}handleProfileInfo(e){s.info(`[AgentClient] \u6536\u5230 Profile \u4FE1\u606F: ${e.displayName} (${e.roleName})`),e.projectId&&(this.projectId=e.projectId,s.info(`[AgentClient] \u66F4\u65B0\u6240\u5C5E\u9879\u76EE: ${this.projectId}`)),e.roleName&&e.roleName!==this.agentConfig.roleName&&(s.info(`[AgentClient] \u540C\u6B65\u89D2\u8272\u540D: "${this.agentConfig.roleName||"(\u7A7A)"}" -> "${e.roleName}"`),this.agentConfig.roleName=e.roleName),e.roleName&&u(e.roleName,e.instanceName||this.agentId||""),this.profileHandler?this.profileHandler(e):(s.info("[AgentClient] profileHandler \u672A\u8BBE\u7F6E\uFF0C\u7F13\u5B58 Profile \u4FE1\u606F"),this.cachedProfile=e)}async processMessage(e){if(!this.messageHandler)return;const t=typeof e.msgId=="string";s.info(`[AgentClient] \u6536\u5230${t?"\u79C1\u4FE1":"\u7FA4\u6D88\u606F"}\u6765\u81EA ${e.senderName}: ${e.content.substring(0,50)}...`);try{const n=await this.messageHandler(e);if(t){const i=e;i.requireReply&&i.callId&&await this.replyToCall(i.callId,i.senderId,n||"\u5DF2\u6536\u5230\u6D88\u606F\uFF0C\u6B63\u5728\u5904\u7406\u4E2D...")}}catch(n){const i=typeof e.msgId=="string";if(s.error("[AgentClient] \u5904\u7406\u6D88\u606F\u5931\u8D25: type=%s, senderId=%s, senderName=%s, msgId=%s, error=%s",i?"\u79C1\u4FE1":"\u7FA4\u6D88\u606F",e.senderId,e.senderName,e.msgId,n instanceof Error?n.message:String(n)),i){const r=e;r.requireReply&&r.callId&&await this.replyToCall(r.callId,r.senderId,`\u5904\u7406\u5931\u8D25: ${n}`)}}}ensureRegistered(){if(!this.agentId)throw new Error("Agent is not registered")}delay(e){return new Promise(t=>{this.pollTimer=setTimeout(t,e)})}onNewCharacter(e){this.newCharacterHandler=e}getState(){return{agentId:this.agentId,displayName:this.displayName,projectId:this.projectId,isConnected:this.wsClient.isConnected(),isRunning:this.isRunning}}getAgentConfig(){return this.agentConfig}getServerConfig(){return this.serverConfig}reloadConfig(){g.loadConfig(),this.agentConfig=g.getAgentConfig(),s.info(`[AgentClient] \u914D\u7F6E\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF0C\u89D2\u8272: ${this.agentConfig.roleName}`)}enterDegradedMode(){this.isDegradedMode=!0,s.warn("[AgentClient] \u8FDB\u5165\u964D\u7EA7\u6A21\u5F0F\uFF0C\u5C06\u5728\u540E\u53F0\u6301\u7EED\u5C1D\u8BD5\u91CD\u8FDE")}exitDegradedMode(){this.isDegradedMode=!1,s.info("[AgentClient] \u9000\u51FA\u964D\u7EA7\u6A21\u5F0F")}isInDegradedMode(){return this.isDegradedMode}async retryRegistrationInBackground(e,t){let n=0;(async()=>{for(;this.isDegradedMode;)try{s.info(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u5C1D\u8BD5\u91CD\u65B0\u6CE8\u518C (${n+1}/${t===-1?"\u65E0\u9650":t})`);const r=await this.register();s.info(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u6CE8\u518C\u6210\u529F - ${r.displayName} (${r.agentId})`),this.exitDegradedMode(),this.messageHandler&&this.start(this.messageHandler);return}catch(r){n++;const a=r instanceof Error?r.message:String(r);if(s.warn(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u6CE8\u518C\u5931\u8D25 (${n}/${t===-1?"\u65E0\u9650":t}): ${a}`),t>0&&n>=t){s.error("[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF0C\u505C\u6B62\u91CD\u8FDE"),this.isDegradedMode=!1;return}await new Promise(h=>setTimeout(h,e))}})().catch(r=>{s.error("[AgentClient] \u964D\u7EA7\u6A21\u5F0F\u540E\u53F0\u91CD\u8BD5\u5FAA\u73AF\u5F02\u5E38\u9000\u51FA: error=%s, stack=%s",r instanceof Error?r.message:String(r),r instanceof Error?r.stack:"(no stack)")})}initializeStatusReporter(){if(!this.agentId){s.warn("[AgentClient] \u65E0\u6CD5\u521D\u59CB\u5316\u72B6\u6001\u62A5\u544A\u5668\uFF1AagentId \u4E3A\u7A7A");return}this.statusReporter=new y({agentId:this.agentId,sendStatusUpdate:e=>{this.wsClient.send({type:"AGENT_STATUS",data:e,timestamp:new Date().toISOString()})}}),C(e=>this.reportMcpNodeLog(e)),this.startRuntimeTokenRefresh(),s.info("[AgentClient] \u72B6\u6001\u62A5\u544A\u5668\u5DF2\u521D\u59CB\u5316")}startRuntimeTokenRefresh(){this.stopRuntimeTokenRefresh();const e=this.serverConfig.runtimeTokenRefreshIntervalMs;if(!e||e<=0){s.warn("[AgentClient] runtimeTokenRefreshIntervalMs \u672A\u914D\u7F6E\u6216\u65E0\u6548\uFF0C\u8DF3\u8FC7\u4E3B\u52A8\u7EED\u671F");return}const t=this.agentId;if(!t){s.warn("[AgentClient] agentId \u4E3A\u7A7A\uFF0C\u8DF3\u8FC7\u4E3B\u52A8\u7EED\u671F");return}this.runtimeTokenRefreshTimer=setInterval(async()=>{try{await this.httpClient.refreshRuntimeAccessToken(t)&&s.info("[AgentClient] runtimeAccessToken \u5DF2\u4E3B\u52A8\u7EED\u671F")}catch(n){s.warn("[AgentClient] \u4E3B\u52A8\u7EED\u671F runtimeAccessToken \u5931\u8D25: %s",n instanceof Error?n.message:String(n)),n instanceof Error&&n.stack&&s.warn("[AgentClient] \u4E3B\u52A8\u7EED\u671F\u5F02\u5E38\u5806\u6808:",n.stack)}},e),this.runtimeTokenRefreshTimer.unref?.(),s.info(`[AgentClient] runtime token \u4E3B\u52A8\u7EED\u671F\u5DF2\u542F\u52A8\uFF0C\u95F4\u9694 ${e}ms`)}stopRuntimeTokenRefresh(){this.runtimeTokenRefreshTimer&&(clearInterval(this.runtimeTokenRefreshTimer),this.runtimeTokenRefreshTimer=null)}reportMcpNodeLog(e){const t=this.wsClient;!this.agentId||!this.wsClient.isConnected()||typeof t.sendMcpNodeLog!="function"||t.sendMcpNodeLog(e)}getStatusReporter(){return this.statusReporter}}export{b as AgentClient};
1
+ import{configManager as g}from"./config.js";import{MESSAGE_POLL_INTERVAL_MS as I}from"./constants.js";import{HttpClient as f}from"./http-client.js";import{logger as s,setMcpNodeLogSink as y,updateStartupLogIdentity as u}from"./logger.js";import{claimRuntimeLaunchBinding as w}from"./runtime-launch-binding.js";import{StatusReporter as A}from"./status-reporter.js";import{WebSocketClient as m}from"./websocket-client.js";class T extends Error{constructor(e){super(e),this.name="PollLongPollTimeoutError"}}const p=27e4;class v{wsClient;httpClient;agentConfig;serverConfig;agentId=null;displayName=null;projectId=null;isRunning=!1;isDegradedMode=!1;messageHandler=null;profileHandler=null;configHandler=null;systemNotifyHandler=null;pollTimer=null;statusReporter=null;newCharacterHandler=null;runtimeTokenRefreshTimer=null;cachedProfile=null;constructor(){g.loadConfig(),this.agentConfig=g.getAgentConfig(),this.serverConfig=g.getServerConfig(),this.wsClient=new m(this.serverConfig),this.httpClient=new f(this.serverConfig),this.setupWebSocketListeners()}async initialize(){const e=await w(this.agentConfig,this.serverConfig);if(!e){this.serverConfig.runtimeBridgeBaseUrl=void 0;return}g.applyRuntimeLaunchBinding(e),this.agentConfig=g.getAgentConfig(),this.serverConfig=g.getServerConfig(),this.wsClient=new m(this.serverConfig),this.httpClient=new f(this.serverConfig),this.setupWebSocketListeners()}setupWebSocketListeners(){this.wsClient.on("directMessage",e=>{this.handleIncomingDm(e)}),this.wsClient.on("groupMessage",e=>{this.handleIncomingGroup(e)}),this.wsClient.on("meetingMessage",e=>{this.handleIncomingMeeting(e)}),this.wsClient.on("systemNotify",e=>{this.systemNotifyHandler?.(e)}),this.wsClient.on("profileInfo",e=>{s.info(`[AgentClient] WebSocket profileInfo \u4E8B\u4EF6\u89E6\u53D1\uFF0CdisplayName: ${e.displayName}`),this.handleProfileInfo(e)}),this.wsClient.on("agentConfig",e=>{s.info("[AgentClient] WebSocket agentConfig \u4E8B\u4EF6\u89E6\u53D1"),this.configHandler&&this.configHandler(e??{})}),this.wsClient.on("agentStatus",e=>{s.info(`[AgentClient] WebSocket agentStatus \u4E8B\u4EF6\u89E6\u53D1: ${e.status}`)}),this.wsClient.on("disconnected",(e,t)=>{const n=t?.toString()||"";s.warn("[AgentClient] WebSocket \u65AD\u5F00\u8FDE\u63A5: code=%s, reason=%s, agentId=%s, \u6B63\u5728\u81EA\u52A8\u91CD\u8FDE",e,n,this.agentId)}),this.wsClient.on("error",e=>{s.error("[AgentClient] WebSocket \u9519\u8BEF: %s, stack=%s",e.message,e.stack)}),this.wsClient.on("reconnectFailed",()=>{s.error("[AgentClient] \u91CD\u8FDE\u5931\u8D25\uFF08\u5DF2\u8FBE\u91CD\u8BD5\u4E0A\u9650\uFF09\uFF0C\u505C\u6B62\u6D88\u606F\u5FAA\u73AF, agentId=%s",this.agentId),this.stop()}),this.wsClient.on("tokenRejected",()=>{s.error("[AgentClient] runtime token \u88AB 1008 \u62D2\u7EDD\u4E14\u5237\u65B0\u65E0\u53D8\u5316\uFF0C\u8FDB\u5165\u6162\u901F\u91CD\u8BD5\u7B49\u5F85\u65B0 token \u7B7E\u53D1: agentId=%s",this.agentId)}),this.wsClient.on("tokenRefreshFailed",e=>{s.error("[AgentClient] runtime token \u5237\u65B0\u8FDE\u7EED\u5931\u8D25 %s \u6B21\uFF0C\u8FDB\u5165\u6162\u901F\u91CD\u8BD5\u7B49\u5F85 bridge \u6062\u590D: agentId=%s",e,this.agentId)})}async register(){return s.info("[AgentClient] \u6B63\u5728\u6CE8\u518C Agent..."),this.hasPresetAgentId()?await this.registerWithExistingAgentId():await this.registerAsNewAgent()}hasPresetAgentId(){return!!(this.agentConfig.agentId&&this.agentConfig.agentId.trim().length>0)}async registerWithExistingAgentId(){const e=this.agentConfig.agentId;if(!e)throw new Error("Preset agentId is missing");this.agentId=e.trim();let t;try{t=await this.getProfileByAgentId(this.agentId)}catch(r){throw s.error("[AgentClient] getProfileByAgentId \u5931\u8D25 (presetAgentId=%s): %s",this.agentId,r instanceof Error?r.message:String(r)),r}this.displayName=t.displayName,this.projectId=t.projectId||null,s.info(`[AgentClient] \u4F7F\u7528\u9884\u8BBE AgentID \u7ED1\u5B9A\u5B9E\u4F8B: ${this.displayName} (${this.agentId})`),s.info(`[AgentClient] \u6240\u5C5E\u9879\u76EE: ${this.projectId||"(\u672A\u8BBE\u7F6E)"}`);try{await this.wsClient.connect(this.agentId)}catch(r){throw s.error("[AgentClient] WebSocket \u8FDE\u63A5\u5931\u8D25 (agentId=%s): %s",this.agentId,r instanceof Error?r.message:String(r)),r}s.info("[AgentClient] WebSocket \u8FDE\u63A5\u5DF2\u5EFA\u7ACB\uFF0CAgent \u73B0\u5728\u5DF2\u4E0A\u7EBF"),this.initializeStatusReporter();const n=await this.getProfileByAgentId(this.agentId);return this.displayName=n.displayName,this.projectId=n.projectId||null,u(n.roleName||"unknown-role",n.instanceName||this.agentId),{agentId:this.agentId,displayName:this.displayName,status:n.isOnline?"online":"offline",projectId:n.projectId,workspacePath:n.workspacePath,runtimeStatus:n.runtimeStatus,runtimeSessionId:n.runtimeSessionId}}async registerAsNewAgent(){const e=await this.httpClient.callTool("register",{roleName:this.agentConfig.roleName,projectId:this.agentConfig.projectId,workspacePath:this.agentConfig.workspacePath,prompt:this.agentConfig.prompt,runtimeBridgeBaseUrl:this.serverConfig.runtimeBridgeBaseUrl}),t=e.agentId;if(!t)throw new Error("Registration failed: agentId was not returned");return this.agentId=t,this.displayName=e.displayName,this.projectId=e.projectId||null,u(this.agentConfig.roleName||"unknown-role",e.instanceName||t),s.info(`[AgentClient] \u6CE8\u518C\u6210\u529F: ${this.displayName} (${this.agentId})`),s.info(`[AgentClient] \u72B6\u6001: ${e.status}`),s.info(`[AgentClient] \u6240\u5C5E\u9879\u76EE: ${this.projectId||"(\u672A\u8BBE\u7F6E)"}`),await this.wsClient.connect(this.agentId),s.info("[AgentClient] WebSocket \u8FDE\u63A5\u5DF2\u5EFA\u7ACB\uFF0CAgent \u73B0\u5728\u5DF2\u4E0A\u7EBF"),this.initializeStatusReporter(),e.isExisting===!1&&this.newCharacterHandler&&this.newCharacterHandler(),e}async unregister(){s.info("[AgentClient] \u6B63\u5728\u6CE8\u9500 Agent..."),this.agentId&&await this.httpClient.callTool("unregister",{agentId:this.agentId}),this.stopRuntimeTokenRefresh(),this.wsClient.disconnect(),this.agentId=null,this.displayName=null,this.projectId=null,this.isRunning=!1,s.info("[AgentClient] \u5DF2\u6CE8\u9500")}async discoverColleagues(){return await this.httpClient.callTool("discover_colleague",{agentId:this.agentId})}async discoverGroupRooms(){return this.ensureRegistered(),await this.httpClient.callTool("discover_group_rooms",{agentId:this.agentId})}async hireColleague(e){return this.ensureRegistered(),await this.httpClient.callTool("hire_colleague",{...e,agentId:this.agentId})}async hireColleaguesBatch(e){return this.ensureRegistered(),await this.httpClient.callTool("hire_colleagues_batch",{...e,agentId:this.agentId})}async callAgentScopedTool(e,t){this.ensureRegistered();const n={...t,agentId:this.agentId};if(e==="create_meeting"){const r=typeof t.waitTimeoutMs=="number"?t.waitTimeoutMs:Number.NaN,i=Number.isFinite(r)&&r>0?Math.min(r,p):p;return await this.httpClient.callToolOnce(e,{...n,waitTimeoutMs:i},0)}return await this.httpClient.callTool(e,n)}async reportMemoryStats(e){await this.callAgentScopedTool("report_memory_stats",{memoryStats:e})}async sendGroupMessage(e,t,n){this.ensureRegistered();const r=t||this.projectId||this.agentConfig.projectId;return await this.httpClient.callTool("send_group_message",{agentId:this.agentId,projectId:r,roomId:n,content:e})}async sendDirectMessage(e,t,n=!1,r,i){this.ensureRegistered();const a={agentId:this.agentId,targetId:e,content:t,requireReply:n,waitReply:n,replyToCallId:r};if(typeof i=="number"&&i>0&&(a.timeoutMs=i),n){const h=typeof i=="number"&&i>0?i+3e4:0;return await this.httpClient.callToolOnce("send_dm",a,h)}return await this.httpClient.callTool("send_dm",a)}async sendCallAndWaitReply(e,t,n=6e4){const r=await this.sendDirectMessage(e,t,!0,void 0,n);if(r.reply)return r.reply;throw new Error("No reply received for call-style direct message before timeout")}async replyToCall(e,t,n){return await this.sendDirectMessage(t,n,!1,e)}async getDmHistory(e,t=50,n){this.ensureRegistered();const r={agentId:this.agentId,targetUserId:e,limit:t};return n&&(r.since=n),await this.httpClient.callTool("get_dm_history",r)}async getGroupHistory(e=50,t,n){if(this.ensureRegistered(),t){const o={projectId:this.projectId||this.agentConfig.projectId,roomId:t,limit:e};return n&&(o.since=n),await this.httpClient.callTool("get_group_history",o)}const r=await this.discoverGroupRooms(),i=this.projectId||this.agentConfig.projectId,a=this.collectRoomIds(r,i);if(a.length===0)return{messages:[],count:0};const c=(await Promise.all(a.map(async o=>{const l={projectId:i,roomId:o,limit:e};return n&&(l.since=n),await this.httpClient.callTool("get_group_history",l)}))).flatMap(o=>o.messages).sort((o,l)=>new Date(o.timestamp).getTime()-new Date(l.timestamp).getTime());return{messages:c,count:c.length}}async fetchUnreadMessages(e={}){this.ensureRegistered();const t=this.projectId||this.agentConfig.projectId,n={agentId:this.agentId,blockIfEmpty:e.blockIfEmpty??!1};e.since&&(n.since=e.since),e.blockTimeoutMs&&e.blockTimeoutMs>0&&(n.blockTimeoutMs=e.blockTimeoutMs),e.preemptiveJoinWaitMs!==void 0&&e.preemptiveJoinWaitMs!==null&&e.preemptiveJoinWaitMs>=0&&(n.preemptiveJoinWaitMs=e.preemptiveJoinWaitMs);const r=e.includePipelineTasks?"poll_message":"get_dm_messages";s.info("[AgentClient] fetchUnreadMessages \u5165\u53E3: tool=%s, blockIfEmpty=%s, blockTimeoutMs=%s, pollMessageTimeoutMs=%s, skipGroupMessages=%s, preemptiveJoinWaitMs=%s, agentId=%s",r,String(e.blockIfEmpty??!1),String(e.blockTimeoutMs??"-"),String(e.pollMessageTimeoutMs??"-"),String(e.skipGroupMessages??!1),String(e.preemptiveJoinWaitMs??"-"),this.agentId);const i=await this.fetchDirectMessagesWithRecovery(r,n,e.pollMessageTimeoutMs,e.propagatePollTimeout===!0);if(s.info("[AgentClient] fetchUnreadMessages \u79C1\u4FE1\u9636\u6BB5\u8FD4\u56DE: %s \u6761",Array.isArray(i.messages)?i.messages.length:0),e.skipGroupMessages)return s.info("[AgentClient] \u4F1A\u8BAE\u5FEB\u901F hydrate \u5DF2\u53D6\u5F97\u79C1\u4FE1\u54CD\u5E94\uFF0C\u8DF3\u8FC7\u7FA4\u804A\u53D1\u73B0\u5E76\u7ACB\u5373\u8FD4\u56DE: directMessages=%s",Array.isArray(i.messages)?i.messages.length:0),{directMessages:Array.isArray(i.messages)?i.messages:[],groupMessages:[]};let a;try{a=await this.discoverGroupRooms()}catch(o){s.warn("[AgentClient] \u53D1\u73B0\u7FA4\u623F\u95F4\u5931\u8D25\uFF0C\u672C\u6B21\u6309\u65E0\u7FA4\u6D88\u606F\u5904\u7406\uFF08\u5DF2\u53D6\u5230\u7684\u79C1\u4FE1\u4E0D\u53D7\u5F71\u54CD\uFF09: %s",o instanceof Error?o.message:String(o)),a={groups:[]}}const h=this.collectRoomIds(a,t),c=await Promise.all(h.map(async o=>{const l={agentId:this.agentId,projectId:t,roomId:o};e.since&&(l.since=e.since);try{return await this.httpClient.callTool("get_group_messages",l)}catch(d){return s.warn("[AgentClient] \u8DF3\u8FC7\u7FA4\u623F\u95F4\u672A\u8BFB\u8865\u62C9\u5931\u8D25: roomId=%s, error=%s",o,d instanceof Error?d.message:String(d)),{messages:[],currentReadPos:0,hasMore:!1}}}));return{directMessages:Array.isArray(i.messages)?i.messages:[],groupMessages:c.flatMap(o=>this.normalizeGroupMessages(o)).sort((o,l)=>new Date(o.timestamp).getTime()-new Date(l.timestamp).getTime())}}async markDmRead(e){if(e.length===0)return new Set;this.ensureRegistered();try{const t=await this.httpClient.callTool("mark_dm_read",{agentId:this.agentId,msgIds:e}),n=new Set(Array.isArray(t?.deliverableMsgIds)?t.deliverableMsgIds:[]),r=e.filter(i=>!n.has(i));return r.length>0?s.warn("[AgentClient] mark_dm_read \u786E\u8BA4\u540E\u5C06\u6709 %d \u6761\u6D88\u606F\u88AB\u4E22\u5F03\u4E0D\u4EA4\u4ED8: \u8BF7\u6C42=%s, \u53EF\u4EA4\u4ED8=%s, \u88AB\u4E22\u5F03=%s",r.length,JSON.stringify(e),JSON.stringify([...n]),JSON.stringify(r)):s.info("[AgentClient] mark_dm_read \u786E\u8BA4\u5B8C\u6210: %d \u6761\u5168\u90E8\u53EF\u4EA4\u4ED8",e.length),n}catch(t){throw s.warn("[AgentClient] mark_dm_read \u8BF7\u6C42\u5931\u8D25\uFF08\u5C06\u964D\u7EA7\u4E3A\u76F4\u63A5\u4EA4\u4ED8\uFF09: %s",t instanceof Error?t.message:String(t)),t}}async markGroupRead(e,t){if(t.length===0)return new Set;this.ensureRegistered();const n=await this.httpClient.callTool("mark_group_read",{agentId:this.agentId,roomId:e,msgIds:t});return new Set(Array.isArray(n?.deliverableMsgIds)?n.deliverableMsgIds:[])}async markNotificationRead(e){if(e.length!==0){this.ensureRegistered();try{await this.httpClient.callTool("mark_notification_read",{agentId:this.agentId,msgIds:e})}catch{}}}async markMeetingRead(e){if(e.length!==0){this.ensureRegistered();try{await this.httpClient.callTool("mark_meeting_read",{agentId:this.agentId,msgIds:e})}catch{}}}async fetchBrowserListenerEvents(){const e=this.serverConfig.runtimeBridgeBaseUrl,t=this.agentId;if(!e||!t)return s.info("[AgentClient] fetchBrowserListenerEvents \u8DF3\u8FC7: bridgeBaseUrl=%s, agentId=%s",e||"(\u7A7A)",t||"(\u7A7A)"),[];try{const n=`${e}/runtime/browser-listeners/${encodeURIComponent(t)}/events`,r=this.serverConfig.runtimeAccessToken||"";s.debug("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6: url=%s, tokenLen=%d",n,r.length);const i=await fetch(n,{method:"GET",headers:{"X-Runtime-Token":r},signal:AbortSignal.timeout(3e3)});if(!i.ok)return s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u5931\u8D25: HTTP %s, url=%s",i.status,n),[];const a=await i.json();return!a.ok||!Array.isArray(a.events)?(s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u54CD\u5E94\u5F02\u5E38: ok=%s, eventsType=%s",a.ok,Array.isArray(a.events)?"array":typeof a.events),[]):(s.debug("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u6210\u529F: %d \u6761\u4E8B\u4EF6",a.events.length),a.events)}catch(n){return s.warn("[AgentClient] \u62C9\u53D6\u76D1\u542C\u5668\u4E8B\u4EF6\u5F02\u5E38: %s",n instanceof Error?n.message:String(n)),[]}}async notifyBridgeForceMessage(){const e=this.serverConfig.runtimeBridgeBaseUrl,t=this.agentId;if(!(!e||!t))try{const n=`${e}/runtime/sessions/${encodeURIComponent(t)}/notify-force-message`,r=this.serverConfig.runtimeAccessToken||"",i=await fetch(n,{method:"POST",headers:{"X-Runtime-Token":r},signal:AbortSignal.timeout(3e3)});i.ok||s.warn("[AgentClient] \u901A\u77E5 bridge \u5F3A\u5236\u6D88\u606F\u5931\u8D25: HTTP %s, url=%s",i.status,n)}catch(n){s.warn("[AgentClient] \u901A\u77E5 bridge \u5F3A\u5236\u6D88\u606F\u5F02\u5E38: %s",n instanceof Error?n.message:String(n))}}async fetchDirectMessagesWithRecovery(e,t,n,r=!1){if(e==="poll_message"){s.info("[AgentClient] fetchDirectMessagesWithRecovery \u5165\u53E3: tool=poll_message, pollMessageTimeoutMs=%s, agentId=%s",String(n??"(\u9ED8\u8BA4)"),String(t?.agentId??""));try{const i=await this.httpClient.callToolOnce(e,t,n);return s.info("[AgentClient] fetchDirectMessagesWithRecovery poll_message \u6210\u529F\u8FD4\u56DE: %s \u6761",Array.isArray(i?.messages)?i.messages.length:0),i}catch(i){if(this.isRecoverableLongPollError(i)){if(r)throw new T(i instanceof Error?i.message:String(i));return s.warn("[AgentClient] poll_message \u5355\u6B21\u957F\u8F6E\u8BE2\u8D85\u65F6\uFF0C\u6309\u7A7A\u7ED3\u679C\u9000\u907F\u540E\u7EE7\u7EED\u7B49\u5F85:",i),{messages:[]}}throw i}}return await this.httpClient.callTool(e,t)}isRecoverableLongPollError(e){return e instanceof Error?/MCP tool poll_message call failed: 503(?:\s|$)/.test(e.message)||/MCP tool poll_message timed out after \d+ms/.test(e.message):!1}normalizeGroupMessages(e){return Array.isArray(e.messages)?e.messages.filter(t=>!!(t&&typeof t=="object"&&typeof t.msgId=="number"&&typeof t.senderId=="string"&&typeof t.content=="string"&&typeof t.timestamp=="string")):[]}collectRoomIds(e,t){const n=e.groups.filter(r=>r.projectId===t).map(r=>r.id);return Array.from(new Set(n))}async getProfile(){this.ensureRegistered();const e=this.agentId;if(!e)throw new Error("Agent is not registered");return{...await this.getProfileByAgentId(e),describe:this.agentConfig.describe}}async getProfileByAgentId(e){return await this.httpClient.callTool("get_profile",{agentId:e})}async getMyTasks(){return this.ensureRegistered(),await this.httpClient.callTool("my_task",{agentId:this.agentId})}async createTaskInstanceComment(e){this.ensureRegistered();const t={agentId:this.agentId,...e};return await this.httpClient.callTool("create_task_instance_comment",t)}async submitTaskResult(e,t,n){this.ensureRegistered();const r={agentId:this.agentId,nodeId:e,outputPayload:t};return n&&Array.isArray(n)&&n.length>0&&(r.changeLog=n),await this.httpClient.callTool("submit_task_result",r)}async startTaskRollback(e){return this.ensureRegistered(),await this.httpClient.callTool("start_task_rollback",{agentId:this.agentId,...e})}async finishTaskRollback(e){return this.ensureRegistered(),await this.httpClient.callTool("finish_task_rollback",{agentId:this.agentId,...e})}async rejectTask(e,t){return this.ensureRegistered(),await this.httpClient.callTool("reject_task",{agentId:this.agentId,nodeId:e,reason:t})}async terminateTask(e,t){return this.ensureRegistered(),await this.httpClient.callTool("terminate_task",{agentId:this.agentId,taskInstanceId:e,reason:t})}async getDebugLogConfig(){return await this.httpClient.callTool("get_debug_log_config",{agentId:this.agentId})}async queryDebugLogs(e){return this.ensureRegistered(),await this.httpClient.callTool("query_debug_logs",{...e})}async queryFileChangesHistory(e){return this.ensureRegistered(),await this.httpClient.callTool("query_file_changes_history",{...e})}async getRuntimeFeatures(){return await this.httpClient.fetchRuntimeFeatures()}async listLaunchConfigs(){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_list",{})}async createLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_create",{...e})}async updateLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_update",{...e})}async deleteLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_delete",{configId:e})}async startLaunchConfig(e){this.ensureRegistered();const t=Math.max(0,Math.min(3e5,Number(e.wait_timeout_ms)||0));return t>0?await this.httpClient.callToolOnce("launch_config_start",e,t+6e4):await this.httpClient.callTool("launch_config_start",{...e})}async restartLaunchConfig(e){this.ensureRegistered();const t=Math.max(0,Math.min(3e5,Number(e.wait_timeout_ms)||0));return t>0?await this.httpClient.callToolOnce("launch_config_restart",e,t+6e4):await this.httpClient.callTool("launch_config_restart",{...e})}async stopLaunchConfig(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_stop",{configId:e})}async getLaunchLogs(e){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_logs",{...e})}async listLaunchRuns(){return this.ensureRegistered(),await this.httpClient.callTool("launch_config_status",{})}start(e,t){if(this.isRunning){s.warn("[AgentClient] \u6D88\u606F\u5FAA\u73AF\u5DF2\u5728\u8FD0\u884C\u4E2D");return}this.isDegradedMode||this.ensureRegistered(),this.messageHandler=e,this.profileHandler=t?.profileHandler||null,this.configHandler=t?.configHandler||null,this.systemNotifyHandler=t?.systemNotifyHandler||null,this.isRunning=!0,s.info(`[AgentClient] start() \u88AB\u8C03\u7528\uFF0CcachedProfile: ${this.cachedProfile?"\u5B58\u5728":"\u4E0D\u5B58\u5728"}, profileHandler: ${this.profileHandler?"\u5DF2\u8BBE\u7F6E":"\u672A\u8BBE\u7F6E"}, \u964D\u7EA7\u6A21\u5F0F: ${this.isDegradedMode?"\u662F":"\u5426"}`),this.cachedProfile&&this.profileHandler&&(s.info(`[AgentClient] \u5904\u7406\u7F13\u5B58\u7684 Profile \u4FE1\u606F: ${this.cachedProfile.displayName}`),this.profileHandler(this.cachedProfile),this.cachedProfile=null),t?.pollMode??!1?(s.warn("[AgentClient] \u542F\u52A8\u6D88\u606F\u4E3B\u5FAA\u73AF\uFF08HTTP\u8F6E\u8BE2\u6A21\u5F0F - \u4E0D\u63A8\u8350\uFF09"),this.messageLoop()):s.info("[AgentClient] \u542F\u52A8\u6D88\u606F\u4E3B\u5FAA\u73AF\uFF08WebSocket\u63A8\u9001\u6A21\u5F0F - \u63A8\u8350\uFF09")}stop(){this.isRunning=!1,this.pollTimer&&(clearTimeout(this.pollTimer),this.pollTimer=null),this.stopRuntimeTokenRefresh(),s.info("[AgentClient] \u6D88\u606F\u4E3B\u5FAA\u73AF\u5DF2\u505C\u6B62")}async messageLoop(){for(;this.isRunning;)try{await this.delay(I)}catch(e){s.error("[AgentClient] \u6D88\u606F\u5FAA\u73AF\u9519\u8BEF:",e),await this.delay(5e3)}}handleIncomingDm(e){e.senderId!==this.agentId&&this.processMessage(e)}handleIncomingGroup(e){e.senderId!==this.agentId&&this.processMessage(e)}handleIncomingMeeting(e){!e||e.senderId===this.agentId||this.processMessage(e)}handleProfileInfo(e){s.info(`[AgentClient] \u6536\u5230 Profile \u4FE1\u606F: ${e.displayName} (${e.roleName})`),e.projectId&&(this.projectId=e.projectId,s.info(`[AgentClient] \u66F4\u65B0\u6240\u5C5E\u9879\u76EE: ${this.projectId}`)),e.roleName&&e.roleName!==this.agentConfig.roleName&&(s.info(`[AgentClient] \u540C\u6B65\u89D2\u8272\u540D: "${this.agentConfig.roleName||"(\u7A7A)"}" -> "${e.roleName}"`),this.agentConfig.roleName=e.roleName),e.roleName&&u(e.roleName,e.instanceName||this.agentId||""),this.profileHandler?this.profileHandler(e):(s.info("[AgentClient] profileHandler \u672A\u8BBE\u7F6E\uFF0C\u7F13\u5B58 Profile \u4FE1\u606F"),this.cachedProfile=e)}async processMessage(e){if(!this.messageHandler)return;const t=typeof e.msgId=="string";s.info(`[AgentClient] \u6536\u5230${t?"\u79C1\u4FE1":"\u7FA4\u6D88\u606F"}\u6765\u81EA ${e.senderName}: ${e.content.substring(0,50)}...`);try{const n=await this.messageHandler(e);if(t){const r=e;r.requireReply&&r.callId&&await this.replyToCall(r.callId,r.senderId,n||"\u5DF2\u6536\u5230\u6D88\u606F\uFF0C\u6B63\u5728\u5904\u7406\u4E2D...")}}catch(n){const r=typeof e.msgId=="string";if(s.error("[AgentClient] \u5904\u7406\u6D88\u606F\u5931\u8D25: type=%s, senderId=%s, senderName=%s, msgId=%s, error=%s",r?"\u79C1\u4FE1":"\u7FA4\u6D88\u606F",e.senderId,e.senderName,e.msgId,n instanceof Error?n.message:String(n)),r){const i=e;i.requireReply&&i.callId&&await this.replyToCall(i.callId,i.senderId,`\u5904\u7406\u5931\u8D25: ${n}`)}}}ensureRegistered(){if(!this.agentId)throw new Error("Agent is not registered")}delay(e){return new Promise(t=>{this.pollTimer=setTimeout(t,e)})}onNewCharacter(e){this.newCharacterHandler=e}getState(){return{agentId:this.agentId,displayName:this.displayName,projectId:this.projectId,isConnected:this.wsClient.isConnected(),isRunning:this.isRunning}}getAgentConfig(){return this.agentConfig}getServerConfig(){return this.serverConfig}reloadConfig(){g.loadConfig(),this.agentConfig=g.getAgentConfig(),s.info(`[AgentClient] \u914D\u7F6E\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF0C\u89D2\u8272: ${this.agentConfig.roleName}`)}enterDegradedMode(){this.isDegradedMode=!0,s.warn("[AgentClient] \u8FDB\u5165\u964D\u7EA7\u6A21\u5F0F\uFF0C\u5C06\u5728\u540E\u53F0\u6301\u7EED\u5C1D\u8BD5\u91CD\u8FDE")}exitDegradedMode(){this.isDegradedMode=!1,s.info("[AgentClient] \u9000\u51FA\u964D\u7EA7\u6A21\u5F0F")}isInDegradedMode(){return this.isDegradedMode}async retryRegistrationInBackground(e,t){let n=0;(async()=>{for(;this.isDegradedMode;)try{s.info(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u5C1D\u8BD5\u91CD\u65B0\u6CE8\u518C (${n+1}/${t===-1?"\u65E0\u9650":t})`);const i=await this.register();s.info(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u6CE8\u518C\u6210\u529F - ${i.displayName} (${i.agentId})`),this.exitDegradedMode(),this.messageHandler&&this.start(this.messageHandler);return}catch(i){n++;const a=i instanceof Error?i.message:String(i);if(s.warn(`[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u6CE8\u518C\u5931\u8D25 (${n}/${t===-1?"\u65E0\u9650":t}): ${a}`),t>0&&n>=t){s.error("[AgentClient] \u964D\u7EA7\u6A21\u5F0F\uFF1A\u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF0C\u505C\u6B62\u91CD\u8FDE"),this.isDegradedMode=!1;return}await new Promise(h=>setTimeout(h,e))}})().catch(i=>{s.error("[AgentClient] \u964D\u7EA7\u6A21\u5F0F\u540E\u53F0\u91CD\u8BD5\u5FAA\u73AF\u5F02\u5E38\u9000\u51FA: error=%s, stack=%s",i instanceof Error?i.message:String(i),i instanceof Error?i.stack:"(no stack)")})}initializeStatusReporter(){if(!this.agentId){s.warn("[AgentClient] \u65E0\u6CD5\u521D\u59CB\u5316\u72B6\u6001\u62A5\u544A\u5668\uFF1AagentId \u4E3A\u7A7A");return}this.statusReporter=new A({agentId:this.agentId,sendStatusUpdate:e=>{this.wsClient.send({type:"AGENT_STATUS",data:e,timestamp:new Date().toISOString()})}}),y(e=>this.reportMcpNodeLog(e)),this.startRuntimeTokenRefresh(),s.info("[AgentClient] \u72B6\u6001\u62A5\u544A\u5668\u5DF2\u521D\u59CB\u5316")}startRuntimeTokenRefresh(){this.stopRuntimeTokenRefresh();const e=this.serverConfig.runtimeTokenRefreshIntervalMs;if(!e||e<=0){s.warn("[AgentClient] runtimeTokenRefreshIntervalMs \u672A\u914D\u7F6E\u6216\u65E0\u6548\uFF0C\u8DF3\u8FC7\u4E3B\u52A8\u7EED\u671F");return}const t=this.agentId;if(!t){s.warn("[AgentClient] agentId \u4E3A\u7A7A\uFF0C\u8DF3\u8FC7\u4E3B\u52A8\u7EED\u671F");return}this.runtimeTokenRefreshTimer=setInterval(async()=>{try{await this.httpClient.refreshRuntimeAccessToken(t)&&s.info("[AgentClient] runtimeAccessToken \u5DF2\u4E3B\u52A8\u7EED\u671F")}catch(n){s.warn("[AgentClient] \u4E3B\u52A8\u7EED\u671F runtimeAccessToken \u5931\u8D25: %s",n instanceof Error?n.message:String(n)),n instanceof Error&&n.stack&&s.warn("[AgentClient] \u4E3B\u52A8\u7EED\u671F\u5F02\u5E38\u5806\u6808:",n.stack)}},e),this.runtimeTokenRefreshTimer.unref?.(),s.info(`[AgentClient] runtime token \u4E3B\u52A8\u7EED\u671F\u5DF2\u542F\u52A8\uFF0C\u95F4\u9694 ${e}ms`)}stopRuntimeTokenRefresh(){this.runtimeTokenRefreshTimer&&(clearInterval(this.runtimeTokenRefreshTimer),this.runtimeTokenRefreshTimer=null)}reportMcpNodeLog(e){const t=this.wsClient;!this.agentId||!this.wsClient.isConnected()||typeof t.sendMcpNodeLog!="function"||t.sendMcpNodeLog(e)}getStatusReporter(){return this.statusReporter}}export{v as AgentClient,T as PollLongPollTimeoutError};
2
2
 
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it, beforeEach, afterEach, mock } from "node:test";
3
- import { AgentClient } from "./agent-client.js";
3
+ import { AgentClient, PollLongPollTimeoutError } from "./agent-client.js";
4
4
  import { setMcpNodeLogSink } from "./logger.js";
5
5
  // Helper function to create a complete ProfileInfo
6
6
  function createProfileInfo(overrides = {}) {
@@ -188,6 +188,63 @@ describe("AgentClient", () => {
188
188
  assert.equal(disconnectCalled, true);
189
189
  });
190
190
  });
191
+ describe("callAgentScopedTool", () => {
192
+ it("should always use the registered agent id over a caller-supplied id", async () => {
193
+ const client = new AgentClient();
194
+ Reflect.set(client, "agentId", "registered-agent");
195
+ let capturedArgs;
196
+ Reflect.set(client, "httpClient", {
197
+ callTool: async (_toolName, args) => {
198
+ capturedArgs = args;
199
+ return { success: true };
200
+ },
201
+ });
202
+ await client.callAgentScopedTool("meeting_speak", {
203
+ agentId: "spoofed-agent",
204
+ meetingId: "meeting-1",
205
+ content: "hello",
206
+ });
207
+ assert.equal(capturedArgs?.agentId, "registered-agent");
208
+ assert.equal(capturedArgs?.meetingId, "meeting-1");
209
+ assert.equal(capturedArgs?.content, "hello");
210
+ });
211
+ });
212
+ describe("hire identity injection", () => {
213
+ it("hireColleague should always use the registered agent id over a caller-supplied id", async () => {
214
+ const client = new AgentClient();
215
+ Reflect.set(client, "agentId", "registered-agent");
216
+ let capturedArgs;
217
+ Reflect.set(client, "httpClient", {
218
+ callTool: async (_toolName, args) => {
219
+ capturedArgs = args;
220
+ return { success: true };
221
+ },
222
+ });
223
+ await client.hireColleague({
224
+ agentId: "spoofed-agent",
225
+ workspacePath: "/tmp/ws",
226
+ });
227
+ assert.equal(capturedArgs?.agentId, "registered-agent");
228
+ assert.equal(capturedArgs?.workspacePath, "/tmp/ws");
229
+ });
230
+ it("hireColleaguesBatch should always use the registered agent id over a caller-supplied id", async () => {
231
+ const client = new AgentClient();
232
+ Reflect.set(client, "agentId", "registered-agent");
233
+ let capturedArgs;
234
+ Reflect.set(client, "httpClient", {
235
+ callTool: async (_toolName, args) => {
236
+ capturedArgs = args;
237
+ return { success: true };
238
+ },
239
+ });
240
+ await client.hireColleaguesBatch({
241
+ agentId: "spoofed-agent",
242
+ items: [{ workspacePath: "/tmp/ws" }],
243
+ });
244
+ assert.equal(capturedArgs?.agentId, "registered-agent");
245
+ assert.deepEqual(capturedArgs?.items, [{ workspacePath: "/tmp/ws" }]);
246
+ });
247
+ });
191
248
  describe("sendGroupMessage", () => {
192
249
  it("should send group message with correct parameters", async () => {
193
250
  const client = new AgentClient();
@@ -362,6 +419,127 @@ describe("AgentClient", () => {
362
419
  assert.deepEqual(result, { directMessages: [], groupMessages: [] });
363
420
  assert.deepEqual(calledTools, ["poll_message", "discover_group_rooms"]);
364
421
  });
422
+ it("should pass preemptiveJoinWaitMs and pollMessageTimeoutMs through to poll_message", async () => {
423
+ const client = new AgentClient();
424
+ Reflect.set(client, "agentId", "agent-1");
425
+ Reflect.set(client, "projectId", "demo");
426
+ const capturedCalls = [];
427
+ Reflect.set(client, "httpClient", {
428
+ callTool: async () => ({}),
429
+ callToolOnce: async (toolName, args, timeoutMs) => {
430
+ capturedCalls.push({ toolName, args, timeoutMs });
431
+ if (toolName === "poll_message") {
432
+ return { messages: [] };
433
+ }
434
+ if (toolName === "discover_group_rooms") {
435
+ return { groups: [] };
436
+ }
437
+ return {};
438
+ },
439
+ });
440
+ await client.fetchUnreadMessages({
441
+ includePipelineTasks: true,
442
+ preemptiveJoinWaitMs: 0,
443
+ pollMessageTimeoutMs: 65_000,
444
+ skipGroupMessages: true,
445
+ });
446
+ const pollCall = capturedCalls.find((call) => call.toolName === "poll_message");
447
+ assert.ok(pollCall, "poll_message 应被调用");
448
+ // 显式 0 必须下发(语义为"立即加入但不阻塞等待"):省略会让服务端回退默认阻塞预算,
449
+ // 后台周期性 hydrate 在服务端堆积长阻塞 Callable
450
+ assert.equal(pollCall.args.preemptiveJoinWaitMs, 0);
451
+ // 会议邀请强制 hydrate 的单请求超时需覆盖服务端等待预算,否则合法长请求被误判超时
452
+ assert.equal(pollCall.timeoutMs, 65_000);
453
+ // 会议快速 hydrate 跳过群聊发现,避免群阶段耗时拖慢抢占返回
454
+ assert.equal(capturedCalls.some((call) => call.toolName === "discover_group_rooms"), false);
455
+ });
456
+ it("should propagate client poll_message timeout as PollLongPollTimeoutError when requested", async () => {
457
+ const client = new AgentClient();
458
+ Reflect.set(client, "agentId", "agent-1");
459
+ Reflect.set(client, "projectId", "demo");
460
+ const mockCallPollTimeout = async (toolName) => {
461
+ if (toolName === "poll_message") {
462
+ throw new Error("MCP tool poll_message timed out after 65000ms");
463
+ }
464
+ if (toolName === "discover_group_rooms") {
465
+ return { groups: [] };
466
+ }
467
+ return {};
468
+ };
469
+ Reflect.set(client, "httpClient", {
470
+ callTool: mockCallPollTimeout,
471
+ callToolOnce: mockCallPollTimeout,
472
+ });
473
+ // 超时必须与"服务端快速返回空"区分开:超时说明服务端大概率仍在执行同一请求
474
+ // (Callable 占线至预算结束),立即重试只会在服务端叠加长阻塞线程
475
+ await assert.rejects(() => client.fetchUnreadMessages({
476
+ includePipelineTasks: true,
477
+ propagatePollTimeout: true,
478
+ }), (error) => {
479
+ assert.ok(error instanceof PollLongPollTimeoutError);
480
+ assert.match(error.message, /timed out after 65000ms/);
481
+ return true;
482
+ });
483
+ });
484
+ it("should propagate server poll_message 503 as PollLongPollTimeoutError when requested", async () => {
485
+ const client = new AgentClient();
486
+ Reflect.set(client, "agentId", "agent-1");
487
+ Reflect.set(client, "projectId", "demo");
488
+ const mockCallPoll503 = async (toolName) => {
489
+ if (toolName === "poll_message") {
490
+ throw new Error('MCP tool poll_message call failed: 503 Service Unavailable - {"error":"Async request timed out"}');
491
+ }
492
+ if (toolName === "discover_group_rooms") {
493
+ return { groups: [] };
494
+ }
495
+ return {};
496
+ };
497
+ Reflect.set(client, "httpClient", {
498
+ callTool: mockCallPoll503,
499
+ callToolOnce: mockCallPoll503,
500
+ });
501
+ await assert.rejects(() => client.fetchUnreadMessages({
502
+ includePipelineTasks: true,
503
+ propagatePollTimeout: true,
504
+ }), PollLongPollTimeoutError);
505
+ });
506
+ it("should keep fetched DMs when discover_group_rooms fails in group phase", async () => {
507
+ const client = new AgentClient();
508
+ Reflect.set(client, "agentId", "agent-1");
509
+ Reflect.set(client, "projectId", "demo");
510
+ const meetingNotice = {
511
+ msgId: 42,
512
+ channel: "meeting",
513
+ meetingId: "m-9",
514
+ senderId: "MEETING-SYSTEM",
515
+ senderName: "会议系统",
516
+ content: "系统已自动为你加入会议「评审会」。",
517
+ isSystem: true,
518
+ timestamp: new Date().toISOString(),
519
+ };
520
+ Reflect.set(client, "httpClient", {
521
+ callTool: async (toolName) => {
522
+ if (toolName === "discover_group_rooms") {
523
+ throw new Error("MCP tool discover_group_rooms call failed: 500 Internal Server Error");
524
+ }
525
+ return {};
526
+ },
527
+ callToolOnce: async (toolName) => {
528
+ if (toolName === "poll_message") {
529
+ return { messages: [meetingNotice] };
530
+ }
531
+ if (toolName === "discover_group_rooms") {
532
+ throw new Error("MCP tool discover_group_rooms call failed: 500 Internal Server Error");
533
+ }
534
+ return {};
535
+ },
536
+ });
537
+ // 群阶段失败必须降级为无群消息,绝不能抛出导致已取到的私信(含会议自动加入提示)被整体丢弃
538
+ const result = await client.fetchUnreadMessages({ includePipelineTasks: true });
539
+ assert.equal(result.directMessages.length, 1);
540
+ assert.deepEqual(result.directMessages[0], meetingNotice);
541
+ assert.deepEqual(result.groupMessages, []);
542
+ });
365
543
  it("should not recover non-poll_message 503 errors", async () => {
366
544
  const client = new AgentClient();
367
545
  Reflect.set(client, "agentId", "agent-1");
@@ -1,2 +1,2 @@
1
- import{logger as o}from"./logger.js";const T="ws://localhost:8080/ws/agent",u=3e3,_=9e3,A=200,C=3e4,m=1e3,R=-1,d=12*60*60*1e3,I=12e4,v=`http://127.0.0.1:${process.env.AWS_RUNTIME_BRIDGE_PORT||"18081"}`;function i(n){return n.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i,"$1")}function c(n){const t=n.toLowerCase();return t==="localhost"||t==="::1"||t==="[::1]"?"127.0.0.1":t}function h(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);return e.protocol==="ws:"?e.protocol="http:":e.protocol==="wss:"?e.protocol="https:":e.protocol=e.protocol.toLowerCase(),e.hostname=c(e.hostname),e.origin}catch{return r.replace(/\/+$/,"")}}function S(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);return e.protocol==="http:"?e.protocol="ws:":e.protocol==="https:"?e.protocol="wss:":e.protocol!=="ws:"&&e.protocol!=="wss:"&&(e.protocol="ws:"),e.hostname=c(e.hostname),e.pathname=e.pathname.includes("/ws/agent")?e.pathname:`${e.pathname.replace(/\/$/,"")}/ws/agent`,e.search="",e.hash="",e.toString()}catch{const e=h(r);return e?`${e}/ws/agent`.replace(/^http:/,"ws:").replace(/^https:/,"wss:"):void 0}}function E(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);return e.protocol==="ws:"?e.protocol="http:":e.protocol==="wss:"?e.protocol="https:":e.protocol!=="http:"&&e.protocol!=="https:"&&(e.protocol="http:"),e.hostname=c(e.hostname),e.pathname=e.pathname.includes("/mcp/call")?e.pathname:`${e.pathname.replace(/\/$/,"")}/mcp/call`,e.search="",e.hash="",e.toString()}catch{const e=h(r);return e?`${e}/mcp/call`:void 0}}class U{agentConfig=null;serverConfig=null;loadConfig(){const t=process.env.AWS_WORKSPACE_PATH||process.cwd();this.agentConfig={roleName:process.env.AWS_ROLE_NAME||"",agentId:process.env.AWS_AGENT_ID||void 0,projectId:process.env.AWS_PROJECT_NAME||"default",workspacePath:t,prompt:process.env.AWS_PROMPT||"",describe:""},o.info("[Config] MCP \u5BA2\u6237\u7AEF\u914D\u7F6E\u5DF2\u52A0\u8F7D"),o.info(`[Config] \u5DE5\u4F5C\u76EE\u5F55: ${this.agentConfig.workspacePath}`),o.info(`[Config] \u9879\u76EE\u540D: ${this.agentConfig.projectId}`),this.agentConfig.agentId&&o.info(`[Config] \u9884\u8BBE AgentID: ${this.agentConfig.agentId}`),this.serverConfig={serverUrl:process.env.AWS_SERVER_URL||T,heartbeatInterval:parseInt(process.env.AWS_HEARTBEAT_INTERVAL||String(u),10),heartbeatTimeout:parseInt(process.env.AWS_HEARTBEAT_TIMEOUT||String(_),10),reconnectInterval:A,reconnectMaxInterval:this.parsePositiveIntegerEnv("AWS_RECONNECT_MAX_INTERVAL",C),reconnectJitterMs:this.parsePositiveIntegerEnv("AWS_RECONNECT_JITTER_MS",m),maxReconnectAttempts:R,userId:process.env.AWS_USER_ID||void 0,runtimeBridgeBaseUrl:process.env.AWS_RUNTIME_BRIDGE_BASE_URL||v,mcpHttpUrl:process.env.AWS_MCP_HTTP_URL||void 0,runtimeTokenRefreshIntervalMs:this.parseRuntimeTokenRefreshInterval(),mcpHttpRequestTimeoutMs:this.parsePositiveIntegerEnv("AWS_MCP_HTTP_REQUEST_TIMEOUT_MS",I)},o.info(`[Config] \u670D\u52A1\u5668\u5730\u5740: ${this.serverConfig.serverUrl}`)}applyRuntimeLaunchBinding(t){if(!this.agentConfig||!this.serverConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");const r=String(t.agentId||"").trim();r&&(this.agentConfig.agentId=r);const e=String(t.workspacePath||"").trim();e&&(this.agentConfig.workspacePath=e);const s=String(t.runtimeAccessToken||"").trim();s&&(this.serverConfig.runtimeAccessToken=s);const a=String(t.runtimeAccessTokenExpiresAt||"").trim();a&&(this.serverConfig.runtimeAccessTokenExpiresAt=a);const p=String(t.userId||"").trim();p&&(this.serverConfig.userId=p);const l=S(t.serverUrl||t.schedulerBaseUrl);l&&(this.serverConfig.serverUrl=l);const f=E(t.mcpHttpUrl);if(f)this.serverConfig.mcpHttpUrl=f;else if(t.schedulerBaseUrl){const g=E(t.schedulerBaseUrl);g&&(this.serverConfig.mcpHttpUrl=g)}o.info(`[Config] \u5DF2\u5E94\u7528 runtime-bridge \u542F\u52A8\u7ED1\u5B9A: agentId=${this.agentConfig.agentId||"(\u7A7A)"}`)}parseRuntimeTokenRefreshInterval(){return this.parsePositiveIntegerEnv("AWS_RUNTIME_TOKEN_REFRESH_INTERVAL_MS",d)}parsePositiveIntegerEnv(t,r){const e=process.env[t];if(!e||!e.trim())return r;const s=Number.parseInt(e,10);return!Number.isFinite(s)||s<=0?(o.warn(`[Config] ${t} \u65E0\u6548\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C ${r}ms`),r):s}updateRuntimeAccessToken(t){if(!this.serverConfig)return!1;const r=t.trim();return!r||r===this.serverConfig.runtimeAccessToken?!1:(this.serverConfig.runtimeAccessToken=r,o.info("[Config] runtimeAccessToken \u5DF2\u66F4\u65B0\uFF08401 \u6062\u590D\uFF09"),!0)}getAgentConfig(){if(!this.agentConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");return this.agentConfig}getServerConfig(){if(!this.serverConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");return this.serverConfig}}const M=new U;export{U as ConfigManager,M as configManager};
1
+ import{logger as o}from"./logger.js";const u="ws://localhost:8080/ws/agent",C=3e3,A=9e3,m=200,R=3e4,S=1e3,d=-1,v=12*60*60*1e3,I=12e4,U=`http://127.0.0.1:${process.env.AWS_RUNTIME_BRIDGE_PORT||"18081"}`;function i(n){return n.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i,"$1")}function c(n){const t=n.toLowerCase();return t==="localhost"||t==="::1"||t==="[::1]"?"127.0.0.1":t}const a=process.env.AWS_SCHEDULER_BACKEND_PORT?Number(process.env.AWS_SCHEDULER_BACKEND_PORT):8080;function w(n){const t=n.hostname.toLowerCase();return(t==="localhost"||t==="127.0.0.1"||t==="[::1]")&&n.port!==""&&Number(n.port)!==a}function h(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);return e.protocol==="ws:"?e.protocol="http:":e.protocol==="wss:"?e.protocol="https:":e.protocol=e.protocol.toLowerCase(),e.hostname=c(e.hostname),e.origin}catch{return r.replace(/\/+$/,"")}}function N(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);return e.protocol==="http:"?e.protocol="ws:":e.protocol==="https:"?e.protocol="wss:":e.protocol!=="ws:"&&e.protocol!=="wss:"&&(e.protocol="ws:"),e.hostname=c(e.hostname),e.pathname=e.pathname.includes("/ws/agent")?e.pathname:`${e.pathname.replace(/\/$/,"")}/ws/agent`,e.search="",e.hash="",e.toString()}catch{const e=h(r);return e?`${e}/ws/agent`.replace(/^http:/,"ws:").replace(/^https:/,"wss:"):void 0}}function T(n){const t=String(n||"").trim();if(!t)return;const r=i(t);try{const e=new URL(r);if(e.protocol==="ws:"?e.protocol="http:":e.protocol==="wss:"?e.protocol="https:":e.protocol!=="http:"&&e.protocol!=="https:"&&(e.protocol="http:"),e.hostname=c(e.hostname),w(e)){const s=e.port;e.port=String(a),L(t,s,a)}return e.pathname=e.pathname.includes("/mcp/call")?e.pathname:`${e.pathname.replace(/\/$/,"")}/mcp/call`,e.search="",e.hash="",e.toString()}catch{const e=h(r);return e?`${e}/mcp/call`:void 0}}let _=!1;function L(n,t,r){_||(_=!0,o.warn("[Config] \u68C0\u6D4B\u5230\u56DE\u73AF\u5730\u5740\u975E\u540E\u7AEF\u7AEF\u53E3\uFF0CMCP HTTP \u8C03\u7528\u6539\u9053\u76F4\u8FDE\u540E\u7AEF: %s -> \u7AEF\u53E3 %s -> %d\uFF08\u5982\u540E\u7AEF\u5B9E\u9645\u4F7F\u7528\u5176\u5B83\u7AEF\u53E3\u8BF7\u8BBE\u7F6E AWS_SCHEDULER_BACKEND_PORT\uFF09",n,t,r))}class P{agentConfig=null;serverConfig=null;loadConfig(){const t=process.env.AWS_WORKSPACE_PATH||process.cwd();this.agentConfig={roleName:process.env.AWS_ROLE_NAME||"",agentId:process.env.AWS_AGENT_ID||void 0,projectId:process.env.AWS_PROJECT_NAME||"default",workspacePath:t,prompt:process.env.AWS_PROMPT||"",describe:""},o.info("[Config] MCP \u5BA2\u6237\u7AEF\u914D\u7F6E\u5DF2\u52A0\u8F7D"),o.info(`[Config] \u5DE5\u4F5C\u76EE\u5F55: ${this.agentConfig.workspacePath}`),o.info(`[Config] \u9879\u76EE\u540D: ${this.agentConfig.projectId}`),this.agentConfig.agentId&&o.info(`[Config] \u9884\u8BBE AgentID: ${this.agentConfig.agentId}`),this.serverConfig={serverUrl:process.env.AWS_SERVER_URL||u,heartbeatInterval:parseInt(process.env.AWS_HEARTBEAT_INTERVAL||String(C),10),heartbeatTimeout:parseInt(process.env.AWS_HEARTBEAT_TIMEOUT||String(A),10),reconnectInterval:m,reconnectMaxInterval:this.parsePositiveIntegerEnv("AWS_RECONNECT_MAX_INTERVAL",R),reconnectJitterMs:this.parsePositiveIntegerEnv("AWS_RECONNECT_JITTER_MS",S),maxReconnectAttempts:d,userId:process.env.AWS_USER_ID||void 0,runtimeBridgeBaseUrl:process.env.AWS_RUNTIME_BRIDGE_BASE_URL||U,mcpHttpUrl:process.env.AWS_MCP_HTTP_URL||void 0,runtimeTokenRefreshIntervalMs:this.parseRuntimeTokenRefreshInterval(),mcpHttpRequestTimeoutMs:this.parsePositiveIntegerEnv("AWS_MCP_HTTP_REQUEST_TIMEOUT_MS",I)},o.info(`[Config] \u670D\u52A1\u5668\u5730\u5740: ${this.serverConfig.serverUrl}`)}applyRuntimeLaunchBinding(t){if(!this.agentConfig||!this.serverConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");const r=String(t.agentId||"").trim();r&&(this.agentConfig.agentId=r);const e=String(t.workspacePath||"").trim();e&&(this.agentConfig.workspacePath=e);const s=String(t.runtimeAccessToken||"").trim();s&&(this.serverConfig.runtimeAccessToken=s);const p=String(t.runtimeAccessTokenExpiresAt||"").trim();p&&(this.serverConfig.runtimeAccessTokenExpiresAt=p);const f=String(t.userId||"").trim();f&&(this.serverConfig.userId=f);const l=N(t.serverUrl||t.schedulerBaseUrl);l&&(this.serverConfig.serverUrl=l);const E=T(t.mcpHttpUrl);if(E)this.serverConfig.mcpHttpUrl=E;else if(t.schedulerBaseUrl){const g=T(t.schedulerBaseUrl);g&&(this.serverConfig.mcpHttpUrl=g)}o.info(`[Config] \u5DF2\u5E94\u7528 runtime-bridge \u542F\u52A8\u7ED1\u5B9A: agentId=${this.agentConfig.agentId||"(\u7A7A)"}`)}parseRuntimeTokenRefreshInterval(){return this.parsePositiveIntegerEnv("AWS_RUNTIME_TOKEN_REFRESH_INTERVAL_MS",v)}parsePositiveIntegerEnv(t,r){const e=process.env[t];if(!e||!e.trim())return r;const s=Number.parseInt(e,10);return!Number.isFinite(s)||s<=0?(o.warn(`[Config] ${t} \u65E0\u6548\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C ${r}ms`),r):s}updateRuntimeAccessToken(t){if(!this.serverConfig)return!1;const r=t.trim();return!r||r===this.serverConfig.runtimeAccessToken?!1:(this.serverConfig.runtimeAccessToken=r,o.info("[Config] runtimeAccessToken \u5DF2\u66F4\u65B0\uFF08401 \u6062\u590D\uFF09"),!0)}getAgentConfig(){if(!this.agentConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");return this.agentConfig}getServerConfig(){if(!this.serverConfig)throw new Error("Configuration is not loaded. Call loadConfig() first.");return this.serverConfig}}const D=new P;export{P as ConfigManager,D as configManager};
2
2
 
@@ -16,6 +16,8 @@ export declare class HttpClient {
16
16
  fetchRuntimeFeatures(): Promise<Record<string, boolean>>;
17
17
 
18
18
  callToolOnce<T = unknown>(toolName: string, args: Record<string, unknown>, timeoutMs?: number): Promise<T>;
19
+ private readTextWithTimeout;
20
+ private parseJsonWithTimeout;
19
21
 
20
22
  private singleFetchWithTimeout;
21
23
 
@@ -1,2 +1,2 @@
1
- import{logger as a}from"./logger.js";const d=12e4,w=5e3,g=3e4,m=1e3;function h(c){const t=c.trim();return t?t.length<=m?t:`${t.slice(0,m)}...(truncated)`:""}function T(c){return c.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i,"$1")}function y(c){const t=c.toLowerCase();return t==="localhost"||t==="::1"||t==="[::1]"?"127.0.0.1":t}function p(c,t={}){const r=String(c||"").trim();if(!r)return;const n=T(r);try{const e=new URL(n);e.protocol==="ws:"?e.protocol="http:":e.protocol==="wss:"?e.protocol="https:":e.protocol=e.protocol.toLowerCase(),e.hostname=y(e.hostname);const s=t.fromAgentWebSocketUrl?e.pathname.replace(/\/ws\/agent\/?$/,""):e.pathname;return e.pathname=s.includes("/mcp/call")?s:`${s.replace(/\/$/,"")}/mcp/call`,e.search="",e.hash="",e.toString()}catch{const e=(t.fromAgentWebSocketUrl?n.replace(/\/ws\/agent\/?$/i,""):n).replace(/\/+$/,"");return e.endsWith("/mcp/call")?e:`${e}/mcp/call`}}const C=6e5;class k{serverConfig;mcpHttpUrl;tokenRefreshPromise=null;constructor(t){this.serverConfig=t,this.mcpHttpUrl=this.resolveMcpHttpUrl()}async callTool(t,r){await this.ensureRuntimeAccessToken();const n=Date.now()+this.resolveRetryMaxAccumulatedMs();let e=0;for(;;){e++;let s;try{s=await this.executeToolRequest(t,r)}catch(o){if(Date.now()<n&&this.isTransientError(o)){await this.delayForRetry(n,t,e);continue}throw a.warn("[HttpClient] MCP tool %s \u8BF7\u6C42\u4E0D\u53EF\u6062\u590D\u5F02\u5E38 (attempt=%s): %s",t,e,o instanceof Error?o.message:String(o)),o}if(s.status===401){if(a.warn(`[HttpClient] MCP Tool ${t} \u8FD4\u56DE 401\uFF0C\u5C1D\u8BD5\u5411 bridge \u5237\u65B0 token`),await this.tryRefreshToken(r)){let u;try{u=await this.executeToolRequest(t,r)}catch(l){if(Date.now()<n&&this.isTransientError(l)){await this.delayForRetry(n,t,e);continue}throw a.warn("[HttpClient] MCP tool %s 401\u91CD\u8BD5\u5F02\u5E38 (attempt=%s): %s",t,e,l instanceof Error?l.message:String(l)),l}if(!u.ok){if(Date.now()<n&&this.isRetryableHttpStatus(u.status)){await this.delayForRetry(n,t,e);continue}const l=typeof u.text=="function"?h(await u.text().catch(()=>"")):"";throw new Error(`MCP tool ${t} call failed: ${u.status} ${u.statusText}${l?` - ${l}`:""}`)}const f=await u.json();if(typeof f.error=="string"&&f.error.trim().length>0)throw new Error(`MCP tool ${t} failed: ${f.error}`);return f}a.warn("[HttpClient] token \u5237\u65B0\u5931\u8D25\u6216\u65E0\u65B0 token\uFF0C\u8FD4\u56DE\u539F\u59CB 401 \u9519\u8BEF")}if(!s.ok){if(Date.now()<n&&this.isRetryableHttpStatus(s.status)){await this.delayForRetry(n,t,e);continue}const o=typeof s.text=="function"?h(await s.text().catch(()=>"")):"";throw new Error(`MCP tool ${t} call failed: ${s.status} ${s.statusText}${o?` - ${o}`:""}`)}const i=await s.json();if(typeof i.error=="string"&&i.error.trim().length>0)throw new Error(`MCP tool ${t} failed: ${i.error}`);return i}}async fetchRuntimeFeatures(){await this.ensureRuntimeAccessToken();const t=this.mcpHttpUrl.replace(/\/mcp\/call(\/)?$/,"/mcp/features$1");let r;try{r=await fetch(t,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({})})}catch(e){throw a.warn("[HttpClient] \u67E5\u8BE2\u80FD\u529B\u5F00\u5173\u8BF7\u6C42\u5931\u8D25: %s",e instanceof Error?e.message:String(e)),new Error(`fetch runtime features failed: ${e instanceof Error?e.message:String(e)}`)}if(!r.ok)throw a.warn("[HttpClient] \u67E5\u8BE2\u80FD\u529B\u5F00\u5173\u5931\u8D25: status=%s, statusText=%s",r.status,r.statusText),new Error(`fetch runtime features failed: ${r.status} ${r.statusText}`);const n=await r.json();return!n||typeof n.features!="object"||n.features===null?{}:n.features}async callToolOnce(t,r,n=C){await this.ensureRuntimeAccessToken();const e=o=>fetch(this.mcpHttpUrl,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({tool:t,arguments:r}),signal:o});let s=await this.singleFetchWithTimeout(e,n,t);if(s.status===401&&(a.warn(`[HttpClient] MCP Tool ${t} \u8FD4\u56DE 401\uFF0C\u5C1D\u8BD5\u5411 bridge \u5237\u65B0 token`),await this.tryRefreshToken(r)&&(s=await this.singleFetchWithTimeout(e,n,t))),!s.ok){const o=typeof s.text=="function"?h(await s.text().catch(()=>"")):"";throw new Error(`MCP tool ${t} call failed: ${s.status} ${s.statusText}${o?` - ${o}`:""}`)}const i=await s.json();if(typeof i.error=="string"&&i.error.trim().length>0)throw new Error(`MCP tool ${t} failed: ${i.error}`);return i}async singleFetchWithTimeout(t,r,n){const e=new AbortController;let s;r>0&&(s=setTimeout(()=>e.abort(),r));try{return await t(e.signal)}catch(i){throw e.signal.aborted?new Error(`MCP tool ${n} timed out after ${r}ms`):i}finally{s!==void 0&&clearTimeout(s)}}async executeToolRequest(t,r){return await this.fetchWithTimeout(this.mcpHttpUrl,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({tool:t,arguments:r})},`MCP tool ${t}`)}async fetchWithTimeout(t,r,n){const e=this.resolveRequestTimeoutMs(),s=new AbortController,i=setTimeout(()=>s.abort(),e);try{return await fetch(t,{...r,signal:s.signal})}catch(o){throw s.signal.aborted?new Error(`${n} timed out after ${e}ms`):o}finally{clearTimeout(i)}}resolveRequestTimeoutMs(){const t=this.serverConfig.mcpHttpRequestTimeoutMs;return typeof t=="number"&&Number.isFinite(t)&&t>0?t:d}resolveRetryIntervalMs(){const t=this.serverConfig.mcpRetryIntervalMs;return typeof t=="number"&&Number.isFinite(t)&&t>=0?t:w}resolveRetryMaxAccumulatedMs(){const t=this.serverConfig.mcpRetryMaxAccumulatedMs;return typeof t=="number"&&Number.isFinite(t)&&t>=0?t:g}isTransientError(t){if(!(t instanceof Error))return!1;const r=t.message;return!!(/timed out after \d+ms/.test(r)||/fetch failed/i.test(r)||/network/i.test(r)||/ECONNREFUSED/i.test(r)||/ECONNRESET/i.test(r))}isRetryableHttpStatus(t){return t>=500||t===429}async delayForRetry(t,r,n){const e=t-Date.now(),s=Math.min(this.resolveRetryIntervalMs(),Math.max(0,e));a.warn(`[HttpClient] MCP tool ${r} \u8BF7\u6C42\u5931\u8D25\uFF0C${s}ms \u540E\u91CD\u8BD5 (\u7B2C${n}\u6B21)`),s>0&&await new Promise(i=>setTimeout(i,s))}buildRequestHeaders(){const t={"Content-Type":"application/json"};return this.serverConfig.runtimeAccessToken&&(t["X-Runtime-Token"]=this.serverConfig.runtimeAccessToken,t.Authorization=`Bearer ${this.serverConfig.runtimeAccessToken}`),t}async ensureRuntimeAccessToken(){if(!(this.serverConfig.runtimeAccessToken||!this.serverConfig.runtimeBridgeBaseUrl))throw new Error("runtimeAccessToken is required; MCP nodes must claim a session token from launch binding")}async tryRefreshToken(t){if(this.tokenRefreshPromise)return this.tokenRefreshPromise;this.tokenRefreshPromise=this.doRefreshToken(t);try{return await this.tokenRefreshPromise}finally{this.tokenRefreshPromise=null}}async doRefreshToken(t){const r=this.serverConfig.runtimeBridgeBaseUrl;if(!r)return a.warn("[HttpClient] \u65E0 runtimeBridgeBaseUrl\uFF0C\u65E0\u6CD5\u5237\u65B0 token"),!1;const n=String(t?.agentId||"").trim();if(!n)return a.warn("[HttpClient] \u65E0 agentId\uFF0C\u65E0\u6CD5\u5237\u65B0 token"),!1;try{const e=await fetch(`${r}/runtime/binding/refresh-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agentId:n,currentToken:this.serverConfig.runtimeAccessToken||""})});if(!e.ok)return a.warn(`[HttpClient] bridge refresh-token \u8FD4\u56DE ${e.status}\uFF0C\u65E0\u6CD5\u5237\u65B0`),!1;const i=(await e.json()).runtimeAccessToken;return i?i===this.serverConfig.runtimeAccessToken?!1:(this.serverConfig.runtimeAccessToken=i,a.info("[HttpClient] runtimeAccessToken \u5DF2\u901A\u8FC7 bridge refresh \u66F4\u65B0"),!0):(a.warn("[HttpClient] bridge \u672A\u8FD4\u56DE\u65B0 token"),!1)}catch(e){return a.warn("[HttpClient] \u5237\u65B0 token \u5931\u8D25: %s",e instanceof Error?e.message:String(e)),e instanceof Error&&e.stack&&a.warn("[HttpClient] \u5237\u65B0 token \u5F02\u5E38\u5806\u6808:",e.stack),!1}}async refreshRuntimeAccessToken(t){return this.tryRefreshToken({agentId:t})}resolveMcpHttpUrl(){const t=this.serverConfig.mcpHttpUrl;if(t&&t.trim().length>0){const n=p(t);if(n)return n}const r=p(this.serverConfig.serverUrl,{fromAgentWebSocketUrl:!0});return r||this.serverConfig.serverUrl}getMcpHttpUrl(){return this.mcpHttpUrl}}export{k as HttpClient};
1
+ import{logger as o}from"./logger.js";import{meetingDebugLog as h}from"./meeting-debug-log.js";const w=12e4,g=5e3,T=3e4,d=1e3;function p(l){const e=l.trim();return e?e.length<=d?e:`${e.slice(0,d)}...(truncated)`:""}function y(l){return l.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i,"$1")}function C(l){const e=l.toLowerCase();return e==="localhost"||e==="::1"||e==="[::1]"?"127.0.0.1":e}function m(l,e={}){const r=String(l||"").trim();if(!r)return;const s=y(r);try{const t=new URL(s);t.protocol==="ws:"?t.protocol="http:":t.protocol==="wss:"?t.protocol="https:":t.protocol=t.protocol.toLowerCase(),t.hostname=C(t.hostname);const n=e.fromAgentWebSocketUrl?t.pathname.replace(/\/ws\/agent\/?$/,""):t.pathname;return t.pathname=n.includes("/mcp/call")?n:`${n.replace(/\/$/,"")}/mcp/call`,t.search="",t.hash="",t.toString()}catch{const t=(e.fromAgentWebSocketUrl?s.replace(/\/ws\/agent\/?$/i,""):s).replace(/\/+$/,"");return t.endsWith("/mcp/call")?t:`${t}/mcp/call`}}const R=6e5;class E{serverConfig;mcpHttpUrl;tokenRefreshPromise=null;constructor(e){this.serverConfig=e,this.mcpHttpUrl=this.resolveMcpHttpUrl()}async callTool(e,r){await this.ensureRuntimeAccessToken();const s=Date.now()+this.resolveRetryMaxAccumulatedMs();let t=0;for(;;){t++;let n;try{n=await this.executeToolRequest(e,r)}catch(a){if(Date.now()<s&&this.isTransientError(a)){await this.delayForRetry(s,e,t);continue}throw o.warn("[HttpClient] MCP tool %s \u8BF7\u6C42\u4E0D\u53EF\u6062\u590D\u5F02\u5E38 (attempt=%s): %s",e,t,a instanceof Error?a.message:String(a)),a}if(n.status===401){if(o.warn(`[HttpClient] MCP Tool ${e} \u8FD4\u56DE 401\uFF0C\u5C1D\u8BD5\u5411 bridge \u5237\u65B0 token`),await this.tryRefreshToken(r)){let c;try{c=await this.executeToolRequest(e,r)}catch(f){if(Date.now()<s&&this.isTransientError(f)){await this.delayForRetry(s,e,t);continue}throw o.warn("[HttpClient] MCP tool %s 401\u91CD\u8BD5\u5F02\u5E38 (attempt=%s): %s",e,t,f instanceof Error?f.message:String(f)),f}if(!c.ok){if(Date.now()<s&&this.isRetryableHttpStatus(c.status)){await this.delayForRetry(s,e,t);continue}const f=typeof c.text=="function"?p(await c.text().catch(()=>"")):"";throw new Error(`MCP tool ${e} call failed: ${c.status} ${c.statusText}${f?` - ${f}`:""}`)}const u=await c.json();if(typeof u.error=="string"&&u.error.trim().length>0)throw new Error(`MCP tool ${e} failed: ${u.error}`);return u}o.warn("[HttpClient] token \u5237\u65B0\u5931\u8D25\u6216\u65E0\u65B0 token\uFF0C\u8FD4\u56DE\u539F\u59CB 401 \u9519\u8BEF")}if(!n.ok){if(Date.now()<s&&this.isRetryableHttpStatus(n.status)){await this.delayForRetry(s,e,t);continue}const a=typeof n.text=="function"?p(await n.text().catch(()=>"")):"";throw new Error(`MCP tool ${e} call failed: ${n.status} ${n.statusText}${a?` - ${a}`:""}`)}const i=await n.json();if(typeof i.error=="string"&&i.error.trim().length>0)throw new Error(`MCP tool ${e} failed: ${i.error}`);return i}}async fetchRuntimeFeatures(){await this.ensureRuntimeAccessToken();const e=this.mcpHttpUrl.replace(/\/mcp\/call(\/)?$/,"/mcp/features$1");let r;try{r=await fetch(e,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({})})}catch(t){throw o.warn("[HttpClient] \u67E5\u8BE2\u80FD\u529B\u5F00\u5173\u8BF7\u6C42\u5931\u8D25: %s",t instanceof Error?t.message:String(t)),new Error(`fetch runtime features failed: ${t instanceof Error?t.message:String(t)}`)}if(!r.ok)throw o.warn("[HttpClient] \u67E5\u8BE2\u80FD\u529B\u5F00\u5173\u5931\u8D25: status=%s, statusText=%s",r.status,r.statusText),new Error(`fetch runtime features failed: ${r.status} ${r.statusText}`);const s=await r.json();return!s||typeof s.features!="object"||s.features===null?{}:s.features}async callToolOnce(e,r,s=R){await this.ensureRuntimeAccessToken(),o.info("[HttpClient] callToolOnce \u5F00\u59CB: tool=%s, timeoutMs=%d, url=%s, agentId=%s",e,s,this.mcpHttpUrl,String(r?.agentId??"")),e==="poll_message"&&h("http.callToolOnce.start",`tool=${e} timeoutMs=${s} preemptiveJoinWaitMs=${String(r?.preemptiveJoinWaitMs??"-")}`);const t=c=>fetch(this.mcpHttpUrl,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({tool:e,arguments:r}),signal:c}),n=Date.now();let i=await this.singleFetchWithTimeout(t,s,e);if(o.info("[HttpClient] callToolOnce \u6536\u5230\u54CD\u5E94: tool=%s, status=%d, \u8017\u65F6=%dms",e,i.status,Date.now()-n),e==="poll_message"&&h("http.callToolOnce.response",`status=${i.status} elapsedMs=${Date.now()-n}`),i.status===401&&(o.warn(`[HttpClient] MCP Tool ${e} \u8FD4\u56DE 401\uFF0C\u5C1D\u8BD5\u5411 bridge \u5237\u65B0 token`),await this.tryRefreshToken(r)&&(i=await this.singleFetchWithTimeout(t,s,e),o.info("[HttpClient] callToolOnce token \u5237\u65B0\u540E\u91CD\u8BD5\u6536\u5230\u54CD\u5E94: tool=%s, status=%d, \u7D2F\u8BA1\u8017\u65F6=%dms",e,i.status,Date.now()-n))),!i.ok){const c=typeof i.text=="function"?p(await this.readTextWithTimeout(i,s,e).catch(()=>"")):"";throw o.info("[HttpClient] callToolOnce \u975E 2xx \u54CD\u5E94: tool=%s, status=%d, body=%s",e,i.status,c||"(\u7A7A)"),new Error(`MCP tool ${e} call failed: ${i.status} ${i.statusText}${c?` - ${c}`:""}`)}const a=await this.parseJsonWithTimeout(i,s,e);if(o.info("[HttpClient] callToolOnce \u5B8C\u6210: tool=%s, \u603B\u8017\u65F6=%dms, error=%s",e,Date.now()-n,typeof a.error=="string"?a.error:"(\u65E0)"),e==="poll_message"){const c=Array.isArray(a.messages)?a.messages:[];h("http.callToolOnce.parsed",`totalElapsedMs=${Date.now()-n} serverMessages=${c.length} `+JSON.stringify(c.map(u=>({msgId:u.msgId,channel:u.channel,sender:u.senderId}))))}if(typeof a.error=="string"&&a.error.trim().length>0)throw new Error(`MCP tool ${e} failed: ${a.error}`);return a}async readTextWithTimeout(e,r,s){if(r<=0)return await e.text();let t;try{return await Promise.race([e.text(),new Promise((n,i)=>{t=setTimeout(()=>i(new Error(`MCP tool ${s} timed out after ${r}ms`)),r)})])}finally{t!==void 0&&clearTimeout(t)}}async parseJsonWithTimeout(e,r,s){if(r<=0)return await e.json();let t;try{return await Promise.race([e.json(),new Promise((n,i)=>{t=setTimeout(()=>i(new Error(`MCP tool ${s} timed out after ${r}ms`)),r)})])}finally{t!==void 0&&clearTimeout(t)}}async singleFetchWithTimeout(e,r,s){const t=new AbortController;let n;r>0&&(n=setTimeout(()=>t.abort(),r));try{return await e(t.signal)}catch(i){throw t.signal.aborted?(s==="poll_message"&&h("http.callToolOnce.timeout",`timeoutMs=${r} \u670D\u52A1\u7AEF\u53EF\u80FD\u4ECD\u5728\u6267\u884C\u540C\u4E00\u8BF7\u6C42\uFF08Callable \u5360\u7EBF\u81F3\u9884\u7B97\u7ED3\u675F\uFF09`),new Error(`MCP tool ${s} timed out after ${r}ms`)):i}finally{n!==void 0&&clearTimeout(n)}}async executeToolRequest(e,r){return await this.fetchWithTimeout(this.mcpHttpUrl,{method:"POST",headers:this.buildRequestHeaders(),body:JSON.stringify({tool:e,arguments:r})},`MCP tool ${e}`)}async fetchWithTimeout(e,r,s){const t=this.resolveRequestTimeoutMs(),n=new AbortController,i=setTimeout(()=>n.abort(),t);try{return await fetch(e,{...r,signal:n.signal})}catch(a){throw n.signal.aborted?new Error(`${s} timed out after ${t}ms`):a}finally{clearTimeout(i)}}resolveRequestTimeoutMs(){const e=this.serverConfig.mcpHttpRequestTimeoutMs;return typeof e=="number"&&Number.isFinite(e)&&e>0?e:w}resolveRetryIntervalMs(){const e=this.serverConfig.mcpRetryIntervalMs;return typeof e=="number"&&Number.isFinite(e)&&e>=0?e:g}resolveRetryMaxAccumulatedMs(){const e=this.serverConfig.mcpRetryMaxAccumulatedMs;return typeof e=="number"&&Number.isFinite(e)&&e>=0?e:T}isTransientError(e){if(!(e instanceof Error))return!1;const r=e.message;return!!(/timed out after \d+ms/.test(r)||/fetch failed/i.test(r)||/network/i.test(r)||/ECONNREFUSED/i.test(r)||/ECONNRESET/i.test(r))}isRetryableHttpStatus(e){return e>=500||e===429}async delayForRetry(e,r,s){const t=e-Date.now(),n=Math.min(this.resolveRetryIntervalMs(),Math.max(0,t));o.warn(`[HttpClient] MCP tool ${r} \u8BF7\u6C42\u5931\u8D25\uFF0C${n}ms \u540E\u91CD\u8BD5 (\u7B2C${s}\u6B21)`),n>0&&await new Promise(i=>setTimeout(i,n))}buildRequestHeaders(){const e={"Content-Type":"application/json"};return this.serverConfig.runtimeAccessToken&&(e["X-Runtime-Token"]=this.serverConfig.runtimeAccessToken,e.Authorization=`Bearer ${this.serverConfig.runtimeAccessToken}`),e}async ensureRuntimeAccessToken(){if(this.serverConfig.runtimeAccessToken||!this.serverConfig.runtimeBridgeBaseUrl){o.info("[HttpClient] ensureRuntimeAccessToken: tokenLen=%d, bridgeBaseUrl=%s",(this.serverConfig.runtimeAccessToken||"").length,this.serverConfig.runtimeBridgeBaseUrl||"(\u7A7A)");return}throw new Error("runtimeAccessToken is required; MCP nodes must claim a session token from launch binding")}async tryRefreshToken(e){if(this.tokenRefreshPromise)return this.tokenRefreshPromise;this.tokenRefreshPromise=this.doRefreshToken(e);try{return await this.tokenRefreshPromise}finally{this.tokenRefreshPromise=null}}async doRefreshToken(e){const r=this.serverConfig.runtimeBridgeBaseUrl;if(!r)return o.warn("[HttpClient] \u65E0 runtimeBridgeBaseUrl\uFF0C\u65E0\u6CD5\u5237\u65B0 token"),!1;const s=String(e?.agentId||"").trim();if(!s)return o.warn("[HttpClient] \u65E0 agentId\uFF0C\u65E0\u6CD5\u5237\u65B0 token"),!1;try{const t=await fetch(`${r}/runtime/binding/refresh-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agentId:s,currentToken:this.serverConfig.runtimeAccessToken||""})});if(!t.ok)return o.warn(`[HttpClient] bridge refresh-token \u8FD4\u56DE ${t.status}\uFF0C\u65E0\u6CD5\u5237\u65B0`),!1;const i=(await t.json()).runtimeAccessToken;return i?i===this.serverConfig.runtimeAccessToken?!1:(this.serverConfig.runtimeAccessToken=i,o.info("[HttpClient] runtimeAccessToken \u5DF2\u901A\u8FC7 bridge refresh \u66F4\u65B0"),!0):(o.warn("[HttpClient] bridge \u672A\u8FD4\u56DE\u65B0 token"),!1)}catch(t){return o.warn("[HttpClient] \u5237\u65B0 token \u5931\u8D25: %s",t instanceof Error?t.message:String(t)),t instanceof Error&&t.stack&&o.warn("[HttpClient] \u5237\u65B0 token \u5F02\u5E38\u5806\u6808:",t.stack),!1}}async refreshRuntimeAccessToken(e){return this.tryRefreshToken({agentId:e})}resolveMcpHttpUrl(){const e=this.serverConfig.mcpHttpUrl;if(e&&e.trim().length>0){const s=m(e);if(s)return s}const r=m(this.serverConfig.serverUrl,{fromAgentWebSocketUrl:!0});return r||this.serverConfig.serverUrl}getMcpHttpUrl(){return this.mcpHttpUrl}}export{E as HttpClient};
2
2
 
@@ -6,7 +6,7 @@ import { afterEach, describe, it } from "node:test";
6
6
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
7
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
8
8
  import { McpServer } from "./mcp-server.js";
9
- import { MCP_TOOLS, LAUNCH_CONFIG_TOOL_NAMES } from "./mcp-tools.js";
9
+ import { MCP_TOOLS, LAUNCH_CONFIG_TOOL_NAMES, HIRE_TOOL_NAMES } from "./mcp-tools.js";
10
10
  import { MessageBuffer } from "./message-buffer.js";
11
11
  import { MemoryStore } from "./memory-store.js";
12
12
  const LAUNCH_ITEM_FEATURE_KEY = "agentSys.launchItem";
@@ -57,7 +57,11 @@ describe("launch_config_* 能力开关门控(aws-mcp 侧)", () => {
57
57
  for (const launchTool of LAUNCH_TOOL_NAMES) {
58
58
  assert.equal(names.includes(launchTool), false, `${launchTool} 不应暴露`);
59
59
  }
60
- assert.equal(names.length, MCP_TOOLS.length - LAUNCH_TOOL_NAMES.length);
60
+ // 能力开关 stub 返回空对象:启动项管理与招聘工具(默认关闭)均不暴露
61
+ for (const hireTool of HIRE_TOOL_NAMES) {
62
+ assert.equal(names.includes(hireTool), false, `${hireTool} 不应暴露`);
63
+ }
64
+ assert.equal(names.length, MCP_TOOLS.length - LAUNCH_TOOL_NAMES.length - HIRE_TOOL_NAMES.size);
61
65
  });
62
66
  it("开启「启动项管理」后暴露全部 9 个启动项工具", async () => {
63
67
  const { client, dir } = await buildServer(async () => ({
@@ -32,6 +32,10 @@ export declare class McpServer {
32
32
 
33
33
  private static readonly LAUNCH_TOOLS_ENV_KEY;
34
34
 
35
+ private static readonly HIRE_FEATURE_KEY;
36
+
37
+ private static readonly HIRE_TOOLS_ENV_KEY;
38
+
35
39
  private leaseRenewalTimer;
36
40
  private activeLeaseUris;
37
41
  private static readonly LEASE_DURATION_MINUTES;
@@ -39,6 +43,18 @@ export declare class McpServer {
39
43
 
40
44
  private profileSynced;
41
45
 
46
+ private urgentMeetingHydrateRequested;
47
+ private static readonly MEETING_HYDRATE_RETRY_COUNT;
48
+ private static readonly MEETING_HYDRATE_RETRY_DELAY_MS;
49
+
50
+ private static readonly MEETING_HYDRATE_TIMEOUT_REARM_DELAY_MS;
51
+
52
+ private meetingHydrateTimeoutRearmDelayMs;
53
+
54
+ private static readonly MEETING_HYDRATE_POLL_TIMEOUT_MS;
55
+
56
+ private static readonly MEETING_PREEMPTIVE_JOIN_WAIT_MS;
57
+
42
58
  private pendingPollMessage;
43
59
 
44
60
  private syncRoleMemoryStore;
@@ -68,6 +84,8 @@ export declare class McpServer {
68
84
 
69
85
  private isLaunchConfigToolsEnabled;
70
86
 
87
+ private isHireToolsEnabled;
88
+
71
89
  private fetchRuntimeFeaturesWithStatus;
72
90
  private static readonly MEMORY_TYPES;
73
91
  private static readonly MEMORY_TOOL_NAMES;
@@ -198,6 +216,9 @@ export declare class McpServer {
198
216
 
199
217
  private handleIncomingMessage;
200
218
 
219
+ private handleSystemNotify;
220
+ private parseNotificationPayload;
221
+
201
222
  private shouldBufferIncomingMessage;
202
223
 
203
224
  private isGroupMessage;