beeos-claw 0.5.30 → 0.5.32
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.
- package/dist/acp-gateway/gateway-events-file-artifact.test.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/missing-artifacts-reconciler.js +1 -1
- package/dist/acp-gateway/missing-artifacts-reconciler.test.js +1 -1
- package/dist/acp-gateway/tool-result-payload-strategies.js +1 -1
- package/dist/acp-gateway/tool-result-payload-strategies.test.js +1 -1
- package/dist/index.js +1 -1
- package/dist/media/arouter-config.js +1 -0
- package/dist/media/image-provider.js +1 -0
- package/dist/media/image-provider.test.js +1 -0
- package/dist/media/index.js +1 -0
- package/dist/media/types.js +1 -0
- package/dist/media/video-provider.js +1 -0
- package/dist/media/video-provider.test.js +1 -0
- package/openclaw.plugin.json +6 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeHarness(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},
|
|
1
|
+
import{describe,expect,it,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeHarness(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},o=new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"},canvasEnabled:!0},logger:e,writeObs:vi.fn(),completePrompt:vi.fn(),failPrompt:vi.fn(),schedulePromptTimeout:vi.fn(),maybeCompletePromptFromChatState:vi.fn()}),a={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:"run-1",pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return t.sessions.set(a.sessionId,a),t.promptsByRunId.set("run-1",a),{events:o,sendSessionUpdate:s}}function sentUpdates(e){return e.mock.calls.map(e=>e[1])}async function flushAsyncToolResult(){await Promise.resolve(),await new Promise(e=>setImmediate(e)),await Promise.resolve()}function screenshotPayload(){return{status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]}}function expectToolUpdateResourceLink(e){expect(e).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",content:expect.arrayContaining([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}))}function expectPresenterResourceLink(e){expect(e).toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}))}describe("gateway-events file artifact presentation",()=>{it("presents structured files from chat.toolResult content wrappers as chat resource links",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify(screenshotPayload())}}]}};e.events.handleChatEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("presents structured files from agent assistant toolResult blocks",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,stream:"assistant",ts:Date.now(),data:{content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify(screenshotPayload())}}]}};e.events.handleAgentEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("presents structured files from live tool result events",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,stream:"tool",ts:Date.now(),data:{phase:"result",toolCall:{id:"tc-1",name:"beeos_get_task"},result:{content:JSON.stringify(screenshotPayload())}}};e.events.handleAgentEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("does not present text-only beeos-file URI tool output as a resource link",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",text:"beeos-file://file-1"}]}};e.events.handleChatEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expect(s).not.toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:expect.objectContaining({type:"resource_link"})})),expect(s).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",content:[{type:"content",content:{type:"text",text:"beeos-file://file-1"}}]}))})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if("string"==typeof t){const e=parseJsonObject(t);return e?extractResourceLinksFromRawResult(e):void 0}if(!isRecord(t))return;if(!Array.isArray(t.files)){for(const e of["content","output","result","details"]){const s=t[e];if(s===t)continue;const n=extractResourceLinksFromRawResult(s);if(n?.length)return n}return}const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}function parseJsonObject(t){const e=t.trim();if(e.startsWith("{"))try{const t=JSON.parse(e);return isRecord(t)?t:void 0}catch{return}}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"timeout"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=extractResourceLinksFromRawResult(n),c=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:l,timestamp:r});this.sendTurnUpdate(e,c)&&e.assistantMq?.push(c),this.presentToolResultFiles(e,t,l).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,c),e.assistantMq?.push(c)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);if(i){const t=extractResourceLinksFromRawResult(r);n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:t,timestamp:s})),this.presentToolResultFiles(e,o,t).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)})}continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===n||"cancel"===n)this.completePrompt(e,"cancelled");else if("error"===n){const n=s?.error,o=n&&"object"==typeof n?n:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?n:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,c=a??r,d=n?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:c,toolName:i,rawText:l,rawResult:d})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const h=[],g=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?g.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&h.push(t.content.text));const p=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:d,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,p)&&t.assistantMq?.push(p),await this.presentToolResultFiles(t,r,g)}async presentToolResultFiles(t,e,s){s?.length&&await presentFileArtifacts(t,e,s,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e={...t},s="string"==typeof t.sessionKey?t.sessionKey:void 0;s&&null==e.sessionKey&&(e.sessionKey=s);const n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(e.sessionId=n),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)if(isRecord(s)&&s.id===t.jobId&&"string"==typeof s.name&&s.name){e.name=s.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:e})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const c=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:c}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageClient?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const n="string"==typeof t.jobId?t.jobId:void 0;if(!n)return;const o=t.summary;let a=this.cronChannelCache?.get(n);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===n){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:o,cron_job_id:n,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${n} err=${e}`),void this.cronChannelCache?.delete(n);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const o=roleFromSessionUpdate(e);o&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[o]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"timeout"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const n=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const o of s.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,o)&&e.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",t,JSON.stringify(o));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:o,timestamp:r});this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=n.result,a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:o,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(n.text);if(!s)continue;const o=classifyHeartbeatFilter(s);if(o.filtered){this.logHeartbeatSkip({source:"chat-text",reason:o.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const d={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,d),e.assistantMq?.push(d)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),o=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(n);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:n,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const o=normalizeText(t.thinking)??normalizeText(t.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);o(buildToolCallPayload({toolCallId:n,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const n=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.emitNormalizedToolResult(e,{toolCallId:n,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:o}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const o=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const n=t.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",r=resolveToolArgs(n);o(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=n?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(n?.name)??asString(n?.toolName)??"tool",r={...n,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);o(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,n=o&&"object"==typeof o?o:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const o=e.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,d=a??r,c=o?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:d,toolName:i,rawText:l,rawResult:c,timestamp:s})}async emitNormalizedToolResult(t,e){const s=await normalizeToolResultForAcpUpdate({toolCallId:e.normalizeToolCallId??e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult});if(!s.outputText&&!s.resourceLinks?.length)return;const o=buildToolCallUpdatePayload({toolCallId:e.toolCallId,toolName:e.toolName,outputText:s.outputText,rawOutput:e.rawResult,resourceLinks:s.resourceLinks,timestamp:e.timestamp});(e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}))(o),await this.presentToolResultFiles(t,e.toolCallId,s.resourceLinks)}async presentToolResultFiles(t,e,s){s?.length&&await presentFileArtifacts(t,e,s,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e={...t},s="string"==typeof t.sessionKey?t.sessionKey:void 0;s&&null==e.sessionKey&&(e.sessionKey=s);const o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(e.sessionId=o),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)if(isRecord(s)&&s.id===t.jobId&&"string"==typeof s.name&&s.name){e.name=s.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:e})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),o=t?.jobs;if(Array.isArray(o))for(const t of o){if(!t||"object"!=typeof t)continue;const o=t;if(o.id===e){s=o;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const t=decodeBeeosChannelId(o);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const d=encodeBeeosChannelTag(o,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:d}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageClient?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const o="string"==typeof t.jobId?t.jobId:void 0;if(!o)return;const n=t.summary;let a=this.cronChannelCache?.get(o);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===o){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:n,cron_job_id:o,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${o} err=${e}`),void this.cronChannelCache?.delete(o);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{stripTransportMetadata}from"../message-filter.js";export function reconcileMissingArtifacts(t,e){const{session:s,turnId:
|
|
1
|
+
import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";import{stripTransportMetadata}from"../message-filter.js";export function reconcileMissingArtifacts(t,e){const{session:s,turnId:o,messageId:n,runId:i}=e;s.agentId&&(s.hasAgentAssistantStream&&s.hasAgentThinkingStream&&s.hasAgentToolStartStream&&s.hasAgentToolResultStream||setImmediate(()=>{(async()=>{if(t.isSessionAlive(s.sessionId))try{const e=findEntryByMessageId(t.localHistory.readEntries({agentId:s.agentId,sessionKey:s.gatewaySessionKey,sessionId:s.sessionId}),n);if(!e)return;const r=Array.isArray(e.content)?e.content:[];if(0===r.length)return;const a="number"==typeof e.timestamp?new Date(e.timestamp).toISOString():"string"==typeof e.timestamp?e.timestamp:void 0;let l=0;const c=e=>{void 0===e._turnId&&(e._turnId=o),t.transport.sendSessionUpdate(s.sessionId,e,{turnId:o})&&(l+=1)};for(const t of r){if(!isRecord(t))continue;const e=asString(t.type);if(e){if("text"===e&&!s.hasAgentAssistantStream){const e=asString(t.text);if(!e)continue;const s=stripTransportMetadata(e);if(!s)continue;c({sessionUpdate:"agent_message_chunk",content:{type:"text",text:s},...a?{timestamp:a}:{}});continue}if("thinking"===e&&!s.hasAgentThinkingStream){const e=asString(t.thinking)??asString(t.text);if(!e)continue;c({sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...a?{timestamp:a}:{}});continue}if("toolCall"===e&&!s.hasAgentToolStartStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",o=resolveToolArgs(t);c(buildToolCallPayload({toolCallId:e,toolName:s,args:o,timestamp:a}));continue}if("toolResult"===e&&!s.hasAgentToolResultStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",o=t.result,n=asString(t.text)??asString(t.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,null,2):void 0);if(!n)continue;const i=await normalizeToolResultForAcpUpdate({toolCallId:e,toolName:s,rawText:n,rawResult:o});if(!i.outputText&&!i.resourceLinks?.length)continue;c(buildToolCallUpdatePayload({toolCallId:e,toolName:s,outputText:i.outputText,rawOutput:o,resourceLinks:i.resourceLinks,timestamp:a}));continue}}}l>0&&(t.logger.info?.(`[missing-artifacts] reconciled ${l} block(s) sessionId=${s.sessionId} runId=${i??"n/a"} messageId=${n}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.emitted",severity:"info",payload:{sessionId:s.sessionId,runId:i,messageId:n,emitted:l}}))}catch(e){t.logger.warn?.(`[missing-artifacts] reconcile failed sessionId=${s.sessionId} messageId=${n}: ${e instanceof Error?e.message:String(e)}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.failed",severity:"warn",payload:{sessionId:s.sessionId,messageId:n,error:e instanceof Error?e.message:String(e)}})}else t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.skipped_stale",severity:"debug",payload:{sessionId:s.sessionId,messageId:n,runId:i}})})()}))}function findEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const o=t[s];if(!o)continue;const n=o.__openclaw;if(isRecord(n)&&n.id===e)return o;if(o.id===e)return o}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function asString(t){if("string"==typeof t&&t.length>0)return t}function resolveToolCallId(t){const e=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.id);return e||`reconcile_${hashStable(JSON.stringify(t))}`}function resolveToolArgs(t){const e=[t.arguments,t.args,t.input];for(const t of e){if(isRecord(t))return t;if("string"==typeof t)try{const e=JSON.parse(t);if(isRecord(e))return e}catch{}}return{}}function hashStable(t){let e=5381;for(let s=0;s<t.length;s++)e=(e<<5)+e+t.charCodeAt(s)|0;return(e>>>0).toString(16)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,vi}from"vitest";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}describe("reconcileMissingArtifacts — stale guard",()=>{it("skips dispatch when isSessionAlive returns false",async()=>{const e=vi.fn(),
|
|
1
|
+
import{describe,it,expect,vi}from"vitest";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}describe("reconcileMissingArtifacts — stale guard",()=>{it("skips dispatch when isSessionAlive returns false",async()=>{const e=vi.fn(),t=vi.fn(),s=vi.fn(),n=makeLogger(),i={sessionId:"s-stale",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:e},transport:{sendSessionUpdate:t},logger:n,writeObs:s,isSessionAlive:()=>!1},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(t).not.toHaveBeenCalled(),expect(e).not.toHaveBeenCalled(),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"reconcile.missing_artifacts.skipped_stale",payload:expect.objectContaining({sessionId:"s-stale",messageId:"msg-1",runId:"run-1"})}))}),it("fast-paths (no setImmediate) when every stream flag is set",async()=>{const e=vi.fn(()=>!0),t=vi.fn(),s=vi.fn(),n=vi.fn(),i={sessionId:"s-fast",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,hasAgentAssistantStream:!0,hasAgentThinkingStream:!0,hasAgentToolStartStream:!0,hasAgentToolResultStream:!0};reconcileMissingArtifacts({localHistory:{readEntries:t},transport:{sendSessionUpdate:s},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),expect(n).not.toHaveBeenCalled()}),it("bails out synchronously when agentId is missing",async()=>{const e=vi.fn(()=>!0),t=vi.fn(),s=vi.fn(),n=vi.fn(),i={sessionId:"s-no-agent",gatewaySessionKey:"agent:main:s1",agentId:"",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:t},transport:{sendSessionUpdate:s},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled()}),it("reconciles persisted tool result resource links through the shared normalizer",async()=>{const e=vi.fn().mockReturnValue([{__openclaw:{id:"msg-1"},timestamp:"2026-07-02T14:15:22.658Z",content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",text:JSON.stringify({content:JSON.stringify({status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})})}]}]),t=vi.fn().mockReturnValue(!0),s=vi.fn(),n={sessionId:"s-artifact",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:e},transport:{sendSessionUpdate:t},logger:makeLogger(),writeObs:s,isSessionAlive:()=>!0},{session:n,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),await Promise.resolve();const i=t.mock.calls.map(e=>e[1]);expect(i).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",toolCallId:"tc-1",name:"beeos_get_task",content:expect.arrayContaining([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"reconcile.missing_artifacts.emitted",payload:expect.objectContaining({emitted:1})}))})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=asStr(e.content);if(r){const e=buildBlocksFromJsonText(t,r);if(e.length>0)return appendFileBlocks(e,n)}const l=e.content??e.result;if(Array.isArray(l)){for(const t of l){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const s=e.result;if(s&&"object"==typeof s&&!Array.isArray(s)){const t=s.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const i=asStr(e.output)??asStr(e.text)??asStr(e.result)??r??asStr(e.message)??asStr(e.error);return i&&o.push({type:"content",content:{type:"text",text:i}}),appendFileBlocks(o,n)}function buildBlocksFromJsonText(t,e){const o=e.trim();if(!o||!o.startsWith("{")&&!o.startsWith("["))return[];try{return buildToolResultSessionContentBlocks(t,JSON.parse(o))}catch{return[]}}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
|
|
1
|
+
import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}export async function normalizeToolResultForAcpUpdate(t){let e;try{e=await resolveToolResultBlocks(t)}catch{e=t.rawText?[{type:"content",content:{type:"text",text:t.rawText}}]:[]}0===e.length&&t.rawText&&(e=[{type:"content",content:{type:"text",text:t.rawText}}]);const o=[],n=[];for(const t of e){const{content:e}=t;"resource_link"===e.type&&e.uri?n.push({uri:e.uri,name:e.name??"file",...e.mimeType?{mimeType:e.mimeType}:{}}):"text"===e.type&&e.text&&o.push(e.text)}return{blocks:e,outputText:o.join("\n"),rawOutput:t.rawResult,...n.length>0?{resourceLinks:n}:{}}}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=asStr(e.content);if(r){const e=buildBlocksFromJsonText(t,r);if(e.length>0)return appendFileBlocks(e,n)}const l=e.content??e.result;if(Array.isArray(l)){for(const t of l){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const s=e.result;if(s&&"object"==typeof s&&!Array.isArray(s)){const t=s.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const i=asStr(e.output)??asStr(e.text)??asStr(e.result)??r??asStr(e.message)??asStr(e.error);return i&&o.push({type:"content",content:{type:"text",text:i}}),appendFileBlocks(o,n)}function buildBlocksFromJsonText(t,e){const o=e.trim();if(!o||!o.startsWith("{")&&!o.startsWith("["))return[];try{return buildToolResultSessionContentBlocks(t,JSON.parse(o))}catch{return[]}}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("extracts structured files from a JSON string content wrapper",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})});
|
|
1
|
+
import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks,normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("extracts structured files from a JSON string content wrapper",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})}),describe("normalizeToolResultForAcpUpdate",()=>{it("normalizes beeos_get_task wrapped content JSON files into resource links",async()=>{const e=await normalizeToolResultForAcpUpdate({toolCallId:"tc-1",toolName:"beeos_get_task",rawText:JSON.stringify({content:JSON.stringify({status:"completed",result:"beeos-file://abc",files:[{uri:"beeos-file://abc",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})})});expect(e.outputText).toBe(""),expect(e.resourceLinks).toEqual([{uri:"beeos-file://abc",name:"screenshot.jpeg",mimeType:"image/jpeg"}])}),it("does not turn text-only beeos-file URI into a resource link",async()=>{const e=await normalizeToolResultForAcpUpdate({toolCallId:"tc-1",toolName:"beeos_get_task",rawText:"beeos-file://abc"});expect(e.outputText).toBe("beeos-file://abc"),expect(e.resourceLinks).toBeUndefined()})});
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{BEEOS_FILE_RESOURCE_ROUTING_RULE}from"./files/beeos-file-uri.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const n=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};n({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),n({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)n({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:n,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,t,agentKeys,n);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),a({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeListFiles(o,t,agentKeys,n);return a({component:"file-list-tool",domain:"tool",name:s.ok?"list_files.success":"list_files.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeFetchFile(o,t,agentKeys,{signal:n,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:s.ok?"fetch_file.success":"fetch_file.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,path:s.path,size:s.size,error:s.error}}),s}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:n}=r,s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,s,i,n));const l=sharedAcpServer;o.current=l;const c=new PromptRunStore;e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&s){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,s);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:g,url:d,principalId:p,a2aEnabled:u}=t.messageBus;let f;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);f=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const m=g&&(f||t.messageBus.serviceUrl),y=m?l.getMessageClient():null;if(m&&y){y.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),y.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});const r=p||process.env.BEEOS_PRINCIPAL_ID||"",o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:d,tokenProvider:f,logger:e.logger},n=new Map;let s,i=!1;const g=()=>{if(r)return[{localAgentId:l.getLocalAgentId(),principalId:r}];const e=l.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=l.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:l.getLocalAgentId(),principalId:o}]:[]},u=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{l.ensureImInboundSession(e,r,o)}}),m=async(r,s)=>{if(n.has(r.principalId))return;const i=s?y:new MessageClient(o);s&&l.setMessageClient(o,r.principalId);const g=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:u(r),promptRunStore:c,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),d=new MessageServiceRecoveryScanner({client:i,imInbound:g,logger:e.logger});i.on("connect",()=>{d.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,f?"gateway":"direct")}),i.on("message",r=>{(async()=>{"chat_message"===r.type?await g.onChatMessage(r):"chat_cancel"===r.type&&g.onChatCancel(r)})().catch(r=>{e.logger.warn?.(`[beeos-claw] message handler failed: ${r instanceof Error?r.message:String(r)}`)})}),n.set(r.principalId,{client:i,scanner:d,primary:s});try{await i.connect({identityId:r.principalId}),d.start(r.principalId)}catch(e){throw n.delete(r.principalId),d.stop(),s||await i.disconnect().catch(()=>{}),e}},_=async r=>{if(i)if(s)await s;else{s=(async()=>{const o=g();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,s]of n)t.has(o)||(s.primary&&(a=!0),s.scanner.stop(),await s.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&n.size>0)for(const[o,t]of n)t.scanner.stop(),await t.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let s=[...n.values()].some(e=>e.primary);for(const e of o){const r=!s;await m(e,r),r&&(s=!0)}})();try{await s}finally{s=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,l.onAgentMappingUpdated(()=>{_("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),!r)try{await l.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await _("startup")},async stop(){i=!1;const r=[...n.values()];n.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else g?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",g);if(!1!==u&&t.agentGatewayUrl){const r=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),a=(e,r)=>{l.broadcastNotification(e,r)},n=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},n,r,o,a,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const n=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});n.connect(),r.setRelayClient(n),r.setAgentId(a.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const r=l.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const r=l.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!!e&&l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},s=new FormSchemaRegistry,i=buildCronSchemas(n,{capMs:t.cronMaxDurationCapMs});s.register(i.create),s.register(i.update);const c=new ResourceDescriptorRegistry;c.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(l.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:c});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:s,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(c,e,async(e,r)=>l.gatewayRequest(e,r),p,e.logger)}const u=`\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\n- ${BEEOS_FILE_RESOURCE_ROUTING_RULE}\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.`,f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&l.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?l.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const _=new TerminalManager,b=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:_,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){b.stop(),_.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
|
1
|
+
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{BEEOS_FILE_RESOURCE_ROUTING_RULE}from"./files/beeos-file-uri.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";import{registerArouterMediaProviders}from"./media/index.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const n=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};n({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),n({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)n({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:n,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,t,agentKeys,n);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),a({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeListFiles(o,t,agentKeys,n);return a({component:"file-list-tool",domain:"tool",name:s.ok?"list_files.success":"list_files.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeFetchFile(o,t,agentKeys,{signal:n,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:s.ok?"fetch_file.success":"fetch_file.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,path:s.path,size:s.size,error:s.error}}),s}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:n}=r,s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,s,i,n));const l=sharedAcpServer;o.current=l;const c=new PromptRunStore;e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&s){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,s);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:g,url:d,principalId:p,a2aEnabled:u}=t.messageBus;let f;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);f=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const m=g&&(f||t.messageBus.serviceUrl),y=m?l.getMessageClient():null;if(m&&y){y.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),y.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});const r=p||process.env.BEEOS_PRINCIPAL_ID||"",o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:d,tokenProvider:f,logger:e.logger},n=new Map;let s,i=!1;const g=()=>{if(r)return[{localAgentId:l.getLocalAgentId(),principalId:r}];const e=l.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=l.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:l.getLocalAgentId(),principalId:o}]:[]},u=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{l.ensureImInboundSession(e,r,o)}}),m=async(r,s)=>{if(n.has(r.principalId))return;const i=s?y:new MessageClient(o);s&&l.setMessageClient(o,r.principalId);const g=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:u(r),promptRunStore:c,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),d=new MessageServiceRecoveryScanner({client:i,imInbound:g,logger:e.logger});i.on("connect",()=>{d.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,f?"gateway":"direct")}),i.on("message",r=>{(async()=>{"chat_message"===r.type?await g.onChatMessage(r):"chat_cancel"===r.type&&g.onChatCancel(r)})().catch(r=>{e.logger.warn?.(`[beeos-claw] message handler failed: ${r instanceof Error?r.message:String(r)}`)})}),n.set(r.principalId,{client:i,scanner:d,primary:s});try{await i.connect({identityId:r.principalId}),d.start(r.principalId)}catch(e){throw n.delete(r.principalId),d.stop(),s||await i.disconnect().catch(()=>{}),e}},_=async r=>{if(i)if(s)await s;else{s=(async()=>{const o=g();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,s]of n)t.has(o)||(s.primary&&(a=!0),s.scanner.stop(),await s.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&n.size>0)for(const[o,t]of n)t.scanner.stop(),await t.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let s=[...n.values()].some(e=>e.primary);for(const e of o){const r=!s;await m(e,r),r&&(s=!0)}})();try{await s}finally{s=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,l.onAgentMappingUpdated(()=>{_("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),!r)try{await l.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await _("startup")},async stop(){i=!1;const r=[...n.values()];n.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else g?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",g);if(!1!==u&&t.agentGatewayUrl){const r=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),a=(e,r)=>{l.broadcastNotification(e,r)},n=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},n,r,o,a,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const n=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});n.connect(),r.setRelayClient(n),r.setAgentId(a.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const r=l.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const r=l.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!!e&&l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},s=new FormSchemaRegistry,i=buildCronSchemas(n,{capMs:t.cronMaxDurationCapMs});s.register(i.create),s.register(i.update);const c=new ResourceDescriptorRegistry;c.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(l.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:c});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:s,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(c,e,async(e,r)=>l.gatewayRequest(e,r),p,e.logger)}const u=`\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\n- ${BEEOS_FILE_RESOURCE_ROUTING_RULE}\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.`,f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&l.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?l.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const _=new TerminalManager,b=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:_,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){b.stop(),_.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerArouterMediaProviders(e),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AROUTER_PROVIDER_ID="arouter";export const DEFAULT_AROUTER_IMAGE_MODEL="gemini-2.5-flash-image";export const DEFAULT_AROUTER_VIDEO_MODEL="seedance-2.0-fast";const FALLBACK_AROUTER_BASE_URL="https://api.arouter.ai/v1";function trimTrailingSlash(t){return t.replace(/\/+$/u,"")}export function resolveArouterApiKey(){const t=process.env.AROUTER_API_KEY?.trim();return t||void 0}export function resolveArouterBaseUrl(t){const e=t?.models?.providers?.arouter?.baseUrl?.trim();if(e)return trimTrailingSlash(e);const r=process.env.AROUTER_BASE_URL?.trim();return r?`${trimTrailingSlash(r)}/v1`:FALLBACK_AROUTER_BASE_URL}export function arouterAuthHeaders(t){return{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}}const DEFAULT_REQUEST_TIMEOUT_MS=6e4;export async function arouterFetch(t,e={}){const r=e.timeoutMs??6e4,o=void 0!==e.deadlineAt?Math.max(0,e.deadlineAt-Date.now()):r,n=Math.max(1,Math.min(r,o)),a={method:e.method??"GET",signal:AbortSignal.timeout(n)};return e.headers&&(a.headers=e.headers),void 0!==e.body&&(a.body=e.body),fetch(t,a)}export async function readErrorBody(t){try{return(await t.text()).slice(0,500)}catch{return""}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AROUTER_PROVIDER_ID,DEFAULT_AROUTER_IMAGE_MODEL,arouterAuthHeaders,arouterFetch,readErrorBody,resolveArouterApiKey,resolveArouterBaseUrl}from"./arouter-config.js";const IMAGE_MODELS=[DEFAULT_AROUTER_IMAGE_MODEL],DEFAULT_IMAGE_TIMEOUT_MS=12e4;function inferImageMimeType(e){return e.length>=8&&137===e[0]&&80===e[1]?"image/png":e.length>=3&&255===e[0]&&216===e[1]&&255===e[2]?"image/jpeg":e.length>=12&&"RIFF"===e.toString("ascii",0,4)&&"WEBP"===e.toString("ascii",8,12)?"image/webp":"image/png"}async function assetFromEntry(e,r,t){let o=null;if(e.b64_json)o=Buffer.from(e.b64_json,"base64");else if(e.url){const r=await arouterFetch(e.url,{deadlineAt:t});if(!r.ok)throw new Error(`ARouter image download failed (${r.status}): ${await readErrorBody(r)}`);o=Buffer.from(await r.arrayBuffer())}if(!o||0===o.length)return null;const a=inferImageMimeType(o);return{buffer:o,mimeType:a,fileName:`image-${r+1}.${"image/jpeg"===a?"jpg":"image/webp"===a?"webp":"png"}`,...e.revised_prompt?{revisedPrompt:e.revised_prompt}:{}}}export function createArouterImageProvider(){return{id:AROUTER_PROVIDER_ID,label:"ARouter Image",defaultModel:DEFAULT_AROUTER_IMAGE_MODEL,defaultTimeoutMs:12e4,models:IMAGE_MODELS,capabilities:{generate:{maxCount:1,supportsSize:!0},edit:{enabled:!1}},isConfigured:()=>Boolean(resolveArouterApiKey()),async generateImage(e){const r=resolveArouterApiKey();if(!r)throw new Error("ARouter image generation: AROUTER_API_KEY is not configured.");const t=resolveArouterBaseUrl(e.cfg),o=e.model?.trim()||DEFAULT_AROUTER_IMAGE_MODEL,a=Date.now()+(e.timeoutMs??12e4),i={model:o,prompt:e.prompt};e.size?.trim()&&(i.size=e.size.trim());const n=e.count&&e.count>1?Math.floor(e.count):1;n>1&&(i.n=n);const s=await arouterFetch(`${t}/images/generations`,{method:"POST",headers:arouterAuthHeaders(r),body:JSON.stringify(i),deadlineAt:a});if(!s.ok)throw new Error(`ARouter image generation failed (${s.status}): ${await readErrorBody(s)}`);const u=await s.json(),g=u.data??[],m=[];for(let e=0;e<g.length;e+=1){const r=await assetFromEntry(g[e],e,a);r&&m.push(r)}if(0===m.length)throw new Error("ARouter image generation response missing image data.");return{images:m,model:o,...u.usage?{metadata:{usage:u.usage}}:{}}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{afterEach,beforeEach,describe,expect,it,vi}from"vitest";import{createArouterImageProvider}from"./image-provider.js";const PNG_BYTES=Buffer.from([137,80,78,71,13,10,26,10,0]);function jsonResponse(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"content-type":"application/json"}})}describe("createArouterImageProvider",()=>{const e=process.env.AROUTER_API_KEY;beforeEach(()=>{process.env.AROUTER_API_KEY="test-key"}),afterEach(()=>{void 0===e?delete process.env.AROUTER_API_KEY:process.env.AROUTER_API_KEY=e,vi.restoreAllMocks()}),it("isConfigured reflects AROUTER_API_KEY presence",()=>{const e=createArouterImageProvider();expect(e.isConfigured?.({})).toBe(!0),delete process.env.AROUTER_API_KEY,expect(e.isConfigured?.({})).toBe(!1)}),it("posts to /images/generations and decodes data[0].b64_json",async()=>{const e=vi.fn(async(e,t)=>{expect(e).toBe("https://api.arouter.ai/v1/images/generations"),expect(t?.method).toBe("POST");const r=new Headers(t?.headers);expect(r.get("authorization")).toBe("Bearer test-key");const o=JSON.parse(String(t?.body));return expect(o).toMatchObject({model:"gemini-2.5-flash-image",prompt:"a cat"}),jsonResponse({created:1,data:[{b64_json:PNG_BYTES.toString("base64")}],usage:{cost:.01}})});vi.stubGlobal("fetch",e);const t=createArouterImageProvider(),r=await t.generateImage({provider:"arouter",model:"gemini-2.5-flash-image",prompt:"a cat",cfg:{models:{providers:{arouter:{baseUrl:"https://api.arouter.ai/v1"}}}}});expect(r.images).toHaveLength(1),expect(r.images[0].mimeType).toBe("image/png"),expect(r.images[0].buffer.equals(PNG_BYTES)).toBe(!0),expect(r.model).toBe("gemini-2.5-flash-image"),expect(e).toHaveBeenCalledTimes(1)}),it("throws a descriptive error on non-2xx",async()=>{vi.stubGlobal("fetch",vi.fn(async()=>new Response("bad model",{status:400})));const e=createArouterImageProvider();await expect(e.generateImage({provider:"arouter",model:"x",prompt:"p"})).rejects.toThrow(/ARouter image generation failed \(400\)/)}),it("throws when no image data is returned",async()=>{vi.stubGlobal("fetch",vi.fn(async()=>jsonResponse({data:[]})));const e=createArouterImageProvider();await expect(e.generateImage({provider:"arouter",model:"x",prompt:"p"})).rejects.toThrow(/missing image data/)})});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createArouterImageProvider}from"./image-provider.js";import{createArouterVideoProvider}from"./video-provider.js";export{createArouterImageProvider}from"./image-provider.js";export{createArouterVideoProvider}from"./video-provider.js";export{AROUTER_PROVIDER_ID,DEFAULT_AROUTER_IMAGE_MODEL,DEFAULT_AROUTER_VIDEO_MODEL}from"./arouter-config.js";export function registerArouterMediaProviders(e){"function"==typeof e.registerImageGenerationProvider?(e.registerImageGenerationProvider(createArouterImageProvider()),e.logger?.info?.("[beeos-claw] Registered arouter image-generation provider")):e.logger?.warn?.("[beeos-claw] registerImageGenerationProvider unavailable; skipping arouter image provider"),"function"==typeof e.registerVideoGenerationProvider?(e.registerVideoGenerationProvider(createArouterVideoProvider()),e.logger?.info?.("[beeos-claw] Registered arouter video-generation provider")):e.logger?.warn?.("[beeos-claw] registerVideoGenerationProvider unavailable; skipping arouter video provider")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AROUTER_PROVIDER_ID,DEFAULT_AROUTER_VIDEO_MODEL,arouterAuthHeaders,arouterFetch,readErrorBody,resolveArouterApiKey,resolveArouterBaseUrl}from"./arouter-config.js";const VIDEO_MODELS=[DEFAULT_AROUTER_VIDEO_MODEL],DEFAULT_VIDEO_TIMEOUT_MS=3e5;export const AROUTER_VIDEO_POLL_INTERVAL_MS=2500;function sleep(e){return new Promise(r=>setTimeout(r,e))}function errorMessage(e){return"string"==typeof e.error?e.error:e.error&&"object"==typeof e.error&&e.error.message?e.error.message:`status=${e.status??"unknown"}`}function buildSubmitBody(e,r){const o={model:r,prompt:e.prompt,generate_audio:e.audio??!1};"number"==typeof e.durationSeconds&&Number.isFinite(e.durationSeconds)&&(o.duration=Math.max(1,Math.round(e.durationSeconds))),e.resolution?.trim()&&(o.resolution=e.resolution.trim()),e.aspectRatio?.trim()&&(o.aspect_ratio=e.aspectRatio.trim()),e.size?.trim()&&(o.size=e.size.trim());const t=(e.inputImages??[]).map(e=>e.url?.trim()).filter(e=>Boolean(e));if(t.length>0&&(o.frame_images=t),e.providerOptions)for(const[r,t]of Object.entries(e.providerOptions))void 0===t||r in o||(o[r]=t);return o}export async function pollArouterVideoTask(e){const r=`${e.baseUrl}/videos/${encodeURIComponent(e.taskId)}`,o=e.pollIntervalMs??2500;for(;;){if(Date.now()>=e.deadlineAt)throw new Error(`ARouter video task ${e.taskId} did not finish before timeout.`);const t=await arouterFetch(r,{headers:e.headers,deadlineAt:e.deadlineAt});if(!t.ok)throw new Error(`ARouter video poll failed (${t.status}): ${await readErrorBody(t)}`);const i=await t.json(),a=i.status?.toString().toLowerCase();if("completed"===a)return i;if("failed"===a||"cancelled"===a||"expired"===a)throw new Error(`ARouter video task ${e.taskId} ${a}: ${errorMessage(i)}`);await sleep(o)}}export function createArouterVideoProvider(){return{id:AROUTER_PROVIDER_ID,label:"ARouter Video",defaultModel:DEFAULT_AROUTER_VIDEO_MODEL,defaultTimeoutMs:3e5,models:VIDEO_MODELS,capabilities:{generate:{maxVideos:1,maxDurationSeconds:12,supportsResolution:!0,supportsAspectRatio:!0,supportsAudio:!0},imageToVideo:{enabled:!0,maxVideos:1,maxInputImages:1,maxDurationSeconds:12}},isConfigured:()=>Boolean(resolveArouterApiKey()),async generateVideo(e){const r=resolveArouterApiKey();if(!r)throw new Error("ARouter video generation: AROUTER_API_KEY is not configured.");const o=resolveArouterBaseUrl(e.cfg),t=e.model?.trim()||DEFAULT_AROUTER_VIDEO_MODEL,i=arouterAuthHeaders(r),a=Date.now()+(e.timeoutMs??3e5),s=await arouterFetch(`${o}/videos`,{method:"POST",headers:i,body:JSON.stringify(buildSubmitBody(e,t)),deadlineAt:a});if(!s.ok)throw new Error(`ARouter video submit failed (${s.status}): ${await readErrorBody(s)}`);const n=await s.json(),d=n.id?.trim();if(!d)throw new Error("ARouter video submit response missing task id.");const u=await pollArouterVideoTask({baseUrl:o,taskId:d,headers:i,deadlineAt:a}),l=`${o}/videos/${encodeURIComponent(d)}/content?index=0`,m=await arouterFetch(l,{headers:i,deadlineAt:a});if(!m.ok)throw new Error(`ARouter video download failed (${m.status}): ${await readErrorBody(m)}`);return{videos:[{buffer:Buffer.from(await m.arrayBuffer()),mimeType:m.headers.get("content-type")?.trim()||"video/mp4",fileName:"video-1.mp4"}],model:t,metadata:{taskId:d,status:u.status,...u.generation_id?{generationId:u.generation_id}:{},...u.usage?{usage:u.usage}:{}}}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{afterEach,beforeEach,describe,expect,it,vi}from"vitest";import{createArouterVideoProvider,pollArouterVideoTask}from"./video-provider.js";const MP4_BYTES=Buffer.from([0,0,0,24,102,116,121,112]);function jsonResponse(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"content-type":"application/json"}})}function mp4Response(){return new Response(MP4_BYTES,{status:200,headers:{"content-type":"video/mp4"}})}describe("createArouterVideoProvider",()=>{const e=process.env.AROUTER_API_KEY;beforeEach(()=>{process.env.AROUTER_API_KEY="test-key"}),afterEach(()=>{void 0===e?delete process.env.AROUTER_API_KEY:process.env.AROUTER_API_KEY=e,vi.restoreAllMocks()}),it("runs submit -> poll(completed) -> content and returns an mp4 buffer",async()=>{const e=[],t=vi.fn(async(t,o)=>{if(e.push(`${o?.method??"GET"} ${t}`),"https://api.arouter.ai/v1/videos"===t){const e=JSON.parse(String(o?.body));return expect(e).toMatchObject({model:"seedance-2.0-fast",prompt:"a dog running",generate_audio:!1}),jsonResponse({id:"vid_123",status:"pending"},202)}if("https://api.arouter.ai/v1/videos/vid_123"===t)return jsonResponse({id:"vid_123",status:"completed",usage:{cost:.42}});if("https://api.arouter.ai/v1/videos/vid_123/content?index=0"===t)return mp4Response();throw new Error(`unexpected url ${t}`)});vi.stubGlobal("fetch",t);const o=createArouterVideoProvider(),s=await o.generateVideo({provider:"arouter",model:"seedance-2.0-fast",prompt:"a dog running",audio:!1,cfg:{models:{providers:{arouter:{baseUrl:"https://api.arouter.ai/v1"}}}}});expect(s.videos).toHaveLength(1),expect(s.videos[0].mimeType).toBe("video/mp4"),expect(s.videos[0].buffer?.equals(MP4_BYTES)).toBe(!0),expect(s.metadata).toMatchObject({taskId:"vid_123",status:"completed"}),expect(e).toEqual(["POST https://api.arouter.ai/v1/videos","GET https://api.arouter.ai/v1/videos/vid_123","GET https://api.arouter.ai/v1/videos/vid_123/content?index=0"])}),it("throws on failed task status",async()=>{vi.stubGlobal("fetch",vi.fn(async()=>jsonResponse({id:"vid_err",status:"failed",error:"boom"}))),await expect(pollArouterVideoTask({baseUrl:"https://api.arouter.ai/v1",taskId:"vid_err",headers:{},deadlineAt:Date.now()+1e4,pollIntervalMs:1})).rejects.toThrow(/vid_err failed: boom/)}),it("polls through in_progress before completing",async()=>{let e=0;vi.stubGlobal("fetch",vi.fn(async t=>(expect(t).toBe("https://api.arouter.ai/v1/videos/vid_9"),e+=1,jsonResponse({id:"vid_9",status:e>=2?"completed":"in_progress"}))));const t=await pollArouterVideoTask({baseUrl:"https://api.arouter.ai/v1",taskId:"vid_9",headers:{},deadlineAt:Date.now()+1e4,pollIntervalMs:1});expect(t.status).toBe("completed"),expect(e).toBe(2)})});
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|