openclaw-openagent 1.0.0
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/index.ts +114 -0
- package/openclaw.plugin.json +159 -0
- package/package.json +79 -0
- package/skills/clawlink/SKILL.md +145 -0
- package/skills/clawlink/SKILL.md.bak +165 -0
- package/src/app/channel-tools.ts +249 -0
- package/src/app/discovery-tools.ts +273 -0
- package/src/app/hooks.ts +60 -0
- package/src/app/index.ts +78 -0
- package/src/app/messaging-tools.ts +79 -0
- package/src/app/ops-tools.ts +155 -0
- package/src/app/remote-agent-tool.ts +476 -0
- package/src/app/types.ts +67 -0
- package/src/app/verbose-preflight.ts +190 -0
- package/src/auth/config.ts +197 -0
- package/src/auth/credential-manager.ts +146 -0
- package/src/auth/index.ts +24 -0
- package/src/auth/verify.ts +99 -0
- package/src/channel.ts +565 -0
- package/src/compat.ts +82 -0
- package/src/config/config-schema.ts +39 -0
- package/src/messaging/aggregator.ts +120 -0
- package/src/messaging/collector.ts +89 -0
- package/src/messaging/executor.ts +72 -0
- package/src/messaging/inbound.ts +150 -0
- package/src/messaging/index.ts +11 -0
- package/src/messaging/mention-protocol.ts +94 -0
- package/src/messaging/process-c2c-request.ts +564 -0
- package/src/messaging/process-message.ts +373 -0
- package/src/messaging/scheduler.ts +55 -0
- package/src/messaging/types.ts +38 -0
- package/src/plugin-ui/assets/agentbook-icon.svg +5 -0
- package/src/plugin-ui/assets/magic.svg +5 -0
- package/src/plugin-ui/assets/openagent-override.js +9329 -0
- package/src/plugin-ui/build.cjs +175 -0
- package/src/plugin-ui/index.ts +18 -0
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +458 -0
- package/src/plugin-ui/modules/agent-book/panel/agent-card.js +154 -0
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +644 -0
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +456 -0
- package/src/plugin-ui/modules/agent-book/panel/mention-state.js +206 -0
- package/src/plugin-ui/modules/agent-book/panel/styles.js +670 -0
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +293 -0
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +208 -0
- package/src/plugin-ui/modules/agent-book/scanner.js +119 -0
- package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +500 -0
- package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +652 -0
- package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +251 -0
- package/src/plugin-ui/modules/loader/bootstrap.js +38 -0
- package/src/plugin-ui/modules/loader/shared-state.js +560 -0
- package/src/plugin-ui/modules/loader/ws-intercept.js +199 -0
- package/src/plugin-ui/modules/remote-agent/chunk-parser.js +161 -0
- package/src/plugin-ui/modules/remote-agent/execution-card.js +269 -0
- package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +256 -0
- package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +146 -0
- package/src/plugin-ui/modules/remote-agent/output-card.js +259 -0
- package/src/plugin-ui/modules/remote-agent/progress-store.js +363 -0
- package/src/plugin-ui/modules/remote-agent/render-hooks.js +1609 -0
- package/src/plugin-ui/modules/remote-agent/styles.js +668 -0
- package/src/plugin-ui/modules/remote-agent/tool-card-model.js +56 -0
- package/src/plugin-ui/ui-extension-loader/backup.ts +92 -0
- package/src/plugin-ui/ui-extension-loader/index.ts +276 -0
- package/src/plugin-ui/ui-extension-loader/locator.ts +152 -0
- package/src/plugin-ui/ui-extension-loader/manifest.ts +57 -0
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +729 -0
- package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +70 -0
- package/src/plugin-ui/ui-extension-loader/types.ts +68 -0
- package/src/proxy/auth-proxy.ts +356 -0
- package/src/runtime/account.ts +572 -0
- package/src/runtime/index.ts +7 -0
- package/src/runtime/plugin-runtime.ts +94 -0
- package/src/runtime/registry.ts +71 -0
- package/src/sdk/CLASS_MAP.md +143 -0
- package/src/sdk/index.d.ts +126 -0
- package/src/sdk/index.js +23990 -0
- package/src/sdk/modules/cloud-search-module.js +1117 -0
- package/src/sdk/modules/follow-module.js +1069 -0
- package/src/sdk/modules/group-module.js +7397 -0
- package/src/sdk/modules/relationship-module.js +2269 -0
- package/src/sdk/modules/signaling-module.js +1468 -0
- package/src/sdk/modules/tim-upload-plugin.js +730 -0
- package/src/sdk/node-env/http-request.js +90 -0
- package/src/sdk/node-env/index.js +57 -0
- package/src/sdk/node-env/storage.js +114 -0
- package/src/sdk/package.json +10 -0
- package/src/sdk/tsconfig.json +16 -0
- package/src/state/pending-invocation-store.ts +43 -0
- package/src/state/store.ts +676 -0
- package/src/tim/c2c.ts +451 -0
- package/src/tim/channels.ts +364 -0
- package/src/tim/client.ts +330 -0
- package/src/tim/index.ts +18 -0
- package/src/tim/messages.ts +166 -0
- package/src/tim/sdk-logger-init.ts +50 -0
- package/src/tools.ts +10 -0
- package/src/transport/factory.ts +95 -0
- package/src/transport/oasn/index.ts +17 -0
- package/src/transport/oasn/oasn-agent-card.ts +111 -0
- package/src/transport/oasn/oasn-discovery.ts +108 -0
- package/src/transport/oasn/oasn-files.ts +210 -0
- package/src/transport/oasn/oasn-http.ts +483 -0
- package/src/transport/oasn/oasn-invocation.ts +527 -0
- package/src/transport/oasn/oasn-normalize.ts +159 -0
- package/src/transport/oasn/oasn-register.ts +106 -0
- package/src/transport/oasn/oasn-transport.ts +341 -0
- package/src/transport/oasn/oasn-types.ts +353 -0
- package/src/transport/tim/index.ts +8 -0
- package/src/transport/tim/tim-transport.ts +515 -0
- package/src/transport/types.ts +541 -0
- package/src/types/openclaw.d.ts +97 -0
- package/src/types/tencentcloud-chat.d.ts +15 -0
- package/src/util/http.ts +113 -0
- package/src/util/logger.ts +131 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Agent Tool — openagent_call_remote_agent / oasn_call_agent
|
|
3
|
+
*
|
|
4
|
+
* Calls a remote Agent through the active transport and streams results back.
|
|
5
|
+
* v3.9+ 双轨抽象:
|
|
6
|
+
* - transport='tim':保留原有 C2C 流程(sendC2CCustomMessage + waitForC2CReply)
|
|
7
|
+
* - transport='oasn':走 OASN HTTP API(transport.sendTask + waitForTaskResult),
|
|
8
|
+
* toolCallId 透传给 OasnInvocation 触发 §11.1 idempotency_key 持久化复用
|
|
9
|
+
*
|
|
10
|
+
* Accepts both OpenClaw execute signatures seen in the wild:
|
|
11
|
+
* modern: tool.execute(toolCallId, params, signal, onUpdate)
|
|
12
|
+
* legacy: tool.execute(toolCallId, params, onUpdate, ctx, signal)
|
|
13
|
+
*
|
|
14
|
+
* @see docs/specs/2026-06-16-oasn-transport-abstraction-design.md §3.2 / §4.1
|
|
15
|
+
* @see docs/products/orchestrator/specs/T2-local-tool.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { registry } from '../runtime/registry.js';
|
|
19
|
+
import { logger } from '../util/logger.js';
|
|
20
|
+
import { getGatewayBaseUrl } from '../proxy/auth-proxy.js';
|
|
21
|
+
import {
|
|
22
|
+
toolResult,
|
|
23
|
+
type ToolDescriptor,
|
|
24
|
+
type ToolParams,
|
|
25
|
+
type ToolResult,
|
|
26
|
+
type ToolUpdateCallback,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
import { sendC2CCustomMessage, waitForC2CReply } from '../tim/c2c.js';
|
|
29
|
+
import type { OasnTransport, TaskHandle, TaskRequest } from '../transport/types.js';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import nodePath from 'node:path';
|
|
32
|
+
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
|
|
35
|
+
function fail(error: string): ToolResult {
|
|
36
|
+
return toolResult(JSON.stringify({ success: false, error }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate request UUID.
|
|
41
|
+
* Aligned with OpenClaw subagent pattern (crypto.randomUUID).
|
|
42
|
+
*/
|
|
43
|
+
function generateRequestId(): string {
|
|
44
|
+
return crypto.randomUUID();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isAbortSignalLike(value: unknown): value is AbortSignal {
|
|
48
|
+
return Boolean(
|
|
49
|
+
value &&
|
|
50
|
+
typeof value === 'object' &&
|
|
51
|
+
'aborted' in value &&
|
|
52
|
+
typeof (value as { addEventListener?: unknown }).addEventListener === 'function',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getObjectCallback(value: unknown, key: string): ToolUpdateCallback | undefined {
|
|
57
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
58
|
+
const candidate = (value as Record<string, unknown>)[key];
|
|
59
|
+
return typeof candidate === 'function' ? candidate as ToolUpdateCallback : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getObjectSignal(value: unknown): AbortSignal | undefined {
|
|
63
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
64
|
+
const candidate = (value as Record<string, unknown>).signal;
|
|
65
|
+
return isAbortSignalLike(candidate) ? candidate : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function describeExecuteArg(value: unknown): string {
|
|
69
|
+
if (typeof value === 'function') return 'function';
|
|
70
|
+
if (isAbortSignalLike(value)) return 'AbortSignal';
|
|
71
|
+
if (value === null) return 'null';
|
|
72
|
+
if (typeof value !== 'object') return typeof value;
|
|
73
|
+
const ctor = (value as { constructor?: { name?: string } }).constructor?.name;
|
|
74
|
+
const keys = Object.keys(value as Record<string, unknown>).slice(0, 8).join(',');
|
|
75
|
+
return `${ctor || 'object'}{${keys}}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isTextArtifact(candidate: { displayName?: string; mimeType?: string }): boolean {
|
|
79
|
+
const mime = (candidate.mimeType || '').toLowerCase();
|
|
80
|
+
const name = (candidate.displayName || '').toLowerCase();
|
|
81
|
+
return mime.startsWith('text/')
|
|
82
|
+
|| mime.includes('markdown')
|
|
83
|
+
|| mime.includes('json')
|
|
84
|
+
|| name.endsWith('.md')
|
|
85
|
+
|| name.endsWith('.txt')
|
|
86
|
+
|| name.endsWith('.json');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function artifactContentToText(content: ArrayBuffer): string {
|
|
90
|
+
return Buffer.from(content).toString('utf8').trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeRemoteToolExecuteArgs(
|
|
94
|
+
signalOrUpdate?: AbortSignal | ToolUpdateCallback,
|
|
95
|
+
onUpdateOrContext?: ToolUpdateCallback | unknown,
|
|
96
|
+
legacySignal?: AbortSignal,
|
|
97
|
+
): { signal?: AbortSignal; onUpdate?: ToolUpdateCallback; signature: string } {
|
|
98
|
+
if (typeof signalOrUpdate === 'function') {
|
|
99
|
+
return {
|
|
100
|
+
onUpdate: signalOrUpdate,
|
|
101
|
+
signal: isAbortSignalLike(legacySignal)
|
|
102
|
+
? legacySignal
|
|
103
|
+
: getObjectSignal(onUpdateOrContext),
|
|
104
|
+
signature: 'legacy:update-third',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof onUpdateOrContext === 'function') {
|
|
109
|
+
return {
|
|
110
|
+
signal: isAbortSignalLike(signalOrUpdate) ? signalOrUpdate : undefined,
|
|
111
|
+
onUpdate: onUpdateOrContext as ToolUpdateCallback,
|
|
112
|
+
signature: 'modern:signal-update',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const nestedUpdate =
|
|
117
|
+
getObjectCallback(onUpdateOrContext, 'onUpdate') ||
|
|
118
|
+
getObjectCallback(onUpdateOrContext, 'update') ||
|
|
119
|
+
getObjectCallback(onUpdateOrContext, 'onToolUpdate');
|
|
120
|
+
const nestedSignal =
|
|
121
|
+
isAbortSignalLike(signalOrUpdate)
|
|
122
|
+
? signalOrUpdate
|
|
123
|
+
: getObjectSignal(onUpdateOrContext) || (isAbortSignalLike(legacySignal) ? legacySignal : undefined);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
signal: nestedSignal,
|
|
127
|
+
onUpdate: nestedUpdate,
|
|
128
|
+
signature: nestedUpdate ? 'context:update-callback' : 'unknown:no-update',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Tool Factory ──
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create the openagent_call_remote_agent tool.
|
|
136
|
+
*
|
|
137
|
+
* Tool description instructs LLM to:
|
|
138
|
+
* - Detect {openagent:agent_id="...",agent_name="..."} markers in user messages
|
|
139
|
+
* - Extract agent_id from the marker
|
|
140
|
+
* - Pass user's natural language request as task parameter
|
|
141
|
+
* - NOT display the marker to the user
|
|
142
|
+
*/
|
|
143
|
+
export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
144
|
+
return {
|
|
145
|
+
name: 'openagent_call_remote_agent',
|
|
146
|
+
label: 'Call Remote Agent',
|
|
147
|
+
description:
|
|
148
|
+
'Route a user request to a remote specialist Agent via the OpenAgent network. ' +
|
|
149
|
+
'TRIGGER: When the user message contains a {openagent_call_remote_agent:agent_id="...",agent_name="...",instruction="..."} marker, ' +
|
|
150
|
+
'call this tool to forward the request to the specified remote Agent. ' +
|
|
151
|
+
'Extract agent_id and agent_name from the marker; use the remaining natural-language text as the task parameter. ' +
|
|
152
|
+
'Do not display the raw marker to the user. ' +
|
|
153
|
+
'mode "session" (default) preserves remote conversation history; mode "run" starts fresh — include full context in task.',
|
|
154
|
+
parameters: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
agent_id: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: 'Remote Agent UUID. Extract from the {openagent_call_remote_agent:agent_id="..."} marker in the user message.',
|
|
160
|
+
},
|
|
161
|
+
agent_name: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'Display name of the remote Agent from the marker. Use this in your reply to the user.',
|
|
164
|
+
},
|
|
165
|
+
task: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The user\'s natural-language request text, excluding the marker.',
|
|
168
|
+
},
|
|
169
|
+
mode: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
enum: ['session', 'run'],
|
|
172
|
+
description:
|
|
173
|
+
'"session" (default): preserves remote conversation history. ' +
|
|
174
|
+
'"run": stateless — task must contain full context.',
|
|
175
|
+
default: 'session',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['agent_id', 'agent_name', 'task'],
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async execute(
|
|
182
|
+
_id: unknown,
|
|
183
|
+
params: ToolParams,
|
|
184
|
+
signalOrUpdate?: AbortSignal | ToolUpdateCallback,
|
|
185
|
+
onUpdateOrContext?: ToolUpdateCallback | unknown,
|
|
186
|
+
legacySignal?: AbortSignal,
|
|
187
|
+
): Promise<ToolResult> {
|
|
188
|
+
const { signal, onUpdate, signature } = normalizeRemoteToolExecuteArgs(
|
|
189
|
+
signalOrUpdate,
|
|
190
|
+
onUpdateOrContext,
|
|
191
|
+
legacySignal,
|
|
192
|
+
);
|
|
193
|
+
logger.info(
|
|
194
|
+
`[remote-tool] Execute args: toolCallId=${String(_id)} signature=${signature} ` +
|
|
195
|
+
`hasSignal=${Boolean(signal)} hasOnUpdate=${Boolean(onUpdate)} ` +
|
|
196
|
+
`third=${describeExecuteArg(signalOrUpdate)} fourth=${describeExecuteArg(onUpdateOrContext)} ` +
|
|
197
|
+
`fifth=${describeExecuteArg(legacySignal)}`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ① Check runtime (same pattern as messaging-tools.ts)
|
|
201
|
+
const rt = registry.getDefault();
|
|
202
|
+
if (!rt || !rt.isRunning) {
|
|
203
|
+
logger.warn('[remote-tool] OpenAgent runtime not running');
|
|
204
|
+
return fail('OpenAgent not connected');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ② Ensure TIM connected (supports lazy connect from half-alive mode)
|
|
208
|
+
try {
|
|
209
|
+
await rt.ensureConnected();
|
|
210
|
+
} catch (connErr) {
|
|
211
|
+
logger.error(`[remote-tool] ensureConnected failed: ${(connErr as Error).message}`);
|
|
212
|
+
return fail(`OpenAgent connection failed: ${(connErr as Error).message}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const agentId = String(params.agent_id || '').trim();
|
|
216
|
+
const task = String(params.task || '').trim();
|
|
217
|
+
const mode = String(params.mode || 'session').trim();
|
|
218
|
+
|
|
219
|
+
// ② Validate required params
|
|
220
|
+
if (!agentId) {
|
|
221
|
+
logger.warn('[remote-tool] Missing agent_id parameter');
|
|
222
|
+
return fail('agent_id is required');
|
|
223
|
+
}
|
|
224
|
+
if (!task) {
|
|
225
|
+
logger.warn('[remote-tool] Missing task parameter');
|
|
226
|
+
return fail('task is required');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ③ Generate requestId
|
|
230
|
+
// 这里的 requestId 同时承担两个职责(v3.9+ 双轨语义):
|
|
231
|
+
// - TIM 路径:作为 C2C 关联 id(sendC2CCustomMessage 的 request_id 字段)
|
|
232
|
+
// - OASN 路径:作为 toolCallId 透传给 OasnInvocation —— §11.1 用它去 SQLite
|
|
233
|
+
// 里查/写 idempotency_key + invocation_id,进程崩溃重启后可由
|
|
234
|
+
// listUnsettledInvocations() 续轮询,避免重复扣费
|
|
235
|
+
const requestId = generateRequestId();
|
|
236
|
+
const TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟:远程 agent 可能跑长工具
|
|
237
|
+
const OASN_INVOCATION_TIMEOUT_MS = 60_000; // compute02 REL 当前允许的 ServiceAgent 执行预算
|
|
238
|
+
|
|
239
|
+
logger.info(
|
|
240
|
+
`[remote-tool] Calling remote agent: agentId=${agentId} requestId=${requestId} ` +
|
|
241
|
+
`mode=${mode} transport=${rt.transportType}`,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// 进度回调适配器:两条路径共用一套桥接逻辑,统一把字符串内容包装成 ToolResult
|
|
245
|
+
const bridgeProgress = (content: string) => {
|
|
246
|
+
if (onUpdate) {
|
|
247
|
+
try {
|
|
248
|
+
onUpdate(toolResult(content));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.warn(`[remote-tool] onUpdate callback failed: ${(err as Error).message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
logger.debug(
|
|
254
|
+
`[remote-tool] Progress: requestId=${requestId} hasOnUpdate=${Boolean(onUpdate)} ` +
|
|
255
|
+
`content=${content.slice(0, 80)}`,
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// ④ 按 transport 分发 ——
|
|
260
|
+
// - OASN:HTTP API 路径(sendTask + waitForTaskResult),无 C2C
|
|
261
|
+
// - TIM :保留原 C2C 流程(sendC2CCustomMessage + waitForC2CReply)
|
|
262
|
+
// 两条路径最终都返回相同形状的 reply,下方 ⑤ 统一处理 media + content。
|
|
263
|
+
let reply: { status: 'complete' | 'error'; content: string; mediaUrls?: string[] };
|
|
264
|
+
|
|
265
|
+
if (rt.transportType === 'oasn') {
|
|
266
|
+
// ── OASN 路径 ──
|
|
267
|
+
// 走 transport 抽象。注意 rt.transport 在 half-alive 时可能为 null,
|
|
268
|
+
// 上面 ensureConnected() 已尽量保证 ready,但仍要兜底,避免空指针。
|
|
269
|
+
const transport = rt.transport;
|
|
270
|
+
if (!transport || transport.transportType !== 'oasn') {
|
|
271
|
+
logger.warn('[remote-tool] OASN transport unavailable after ensureConnected');
|
|
272
|
+
return fail('OASN transport not ready');
|
|
273
|
+
}
|
|
274
|
+
const oasnTransport = transport as OasnTransport;
|
|
275
|
+
try {
|
|
276
|
+
rt.assertOasnInvocationAllowed();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger.warn(`[remote-tool] OASN invocation blocked: ${(err as Error).message}`);
|
|
279
|
+
return fail((err as Error).message);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const taskReq: TaskRequest = {
|
|
283
|
+
requestId,
|
|
284
|
+
task,
|
|
285
|
+
timeoutMs: OASN_INVOCATION_TIMEOUT_MS,
|
|
286
|
+
// mode='session' 时不在协议层 join 已有 session(OASN 默认每次起新 Invocation)。
|
|
287
|
+
// 上游若需要保留对话历史,请在 task 文本里显式带 session_id;
|
|
288
|
+
// 暂不在工具层做 session 续传,避免误用导致跨用户串号。
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let handle: TaskHandle;
|
|
292
|
+
try {
|
|
293
|
+
handle = await oasnTransport.sendTask(agentId, taskReq);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger.error(`[remote-tool] OASN sendTask failed: ${(err as Error).message}`);
|
|
296
|
+
return fail(`Failed to reach remote agent: ${(err as Error).message}`);
|
|
297
|
+
}
|
|
298
|
+
logger.info(
|
|
299
|
+
`[remote-tool] OASN invocation created: invocationId=${handle.invocationId} ` +
|
|
300
|
+
`sessionId=${handle.sessionId ?? '-'} effectiveTimeoutMs=${handle.effectiveTimeoutMs ?? '-'}`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await oasnTransport.waitForTaskResult(handle, {
|
|
305
|
+
timeoutMs: TIMEOUT_MS,
|
|
306
|
+
signal,
|
|
307
|
+
onProgress: (p) => bridgeProgress(p.content),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const artifacts = result.artifacts ?? [];
|
|
311
|
+
let content = result.content;
|
|
312
|
+
let mediaUrls = artifacts.map((a) => a.contentUrl).filter(Boolean);
|
|
313
|
+
|
|
314
|
+
if (!content.trim()) {
|
|
315
|
+
for (const textArtifact of artifacts) {
|
|
316
|
+
if (!textArtifact.contentUrl && !textArtifact.contentAvailable) continue;
|
|
317
|
+
try {
|
|
318
|
+
const downloaded = await oasnTransport.downloadArtifact(textArtifact.contentUrl || textArtifact.id);
|
|
319
|
+
if (isTextArtifact(downloaded)) {
|
|
320
|
+
content = artifactContentToText(downloaded.content);
|
|
321
|
+
mediaUrls = artifacts
|
|
322
|
+
.filter((a) => a.id !== textArtifact.id)
|
|
323
|
+
.map((a) => a.contentUrl)
|
|
324
|
+
.filter(Boolean);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
logger.warn(
|
|
329
|
+
`[remote-tool] Failed to hydrate OASN text artifact ${textArtifact.id}: ${(err as Error).message}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// OASN 的非文本 artifacts 暂继续复用 ArtifactRef.contentUrl 作为 mediaUrls。
|
|
336
|
+
reply = {
|
|
337
|
+
status: 'complete',
|
|
338
|
+
content,
|
|
339
|
+
mediaUrls,
|
|
340
|
+
};
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const msg = (err as Error).message;
|
|
343
|
+
logger.error(`[remote-tool] OASN waitForTaskResult failed: ${msg}`);
|
|
344
|
+
if (msg.includes('timeout')) return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
|
|
345
|
+
if (msg.includes('aborted') || msg.includes('abort')) return fail('Request was cancelled');
|
|
346
|
+
return fail(`Remote agent communication error: ${msg}`);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// ── TIM 路径(保留原逻辑) ──
|
|
350
|
+
try {
|
|
351
|
+
await sendC2CCustomMessage(agentId, {
|
|
352
|
+
type: 'openagent_task_request',
|
|
353
|
+
request_id: requestId,
|
|
354
|
+
task,
|
|
355
|
+
session_mode: mode,
|
|
356
|
+
});
|
|
357
|
+
} catch (err) {
|
|
358
|
+
logger.error(`[remote-tool] Failed to send C2C request: ${(err as Error).message}`);
|
|
359
|
+
return fail(`Failed to reach remote agent: ${(err as Error).message}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const c2cReply = await waitForC2CReply(requestId, {
|
|
364
|
+
timeoutMs: TIMEOUT_MS,
|
|
365
|
+
signal,
|
|
366
|
+
onProgress: bridgeProgress,
|
|
367
|
+
});
|
|
368
|
+
logger.info(`[remote-tool] Reply received: requestId=${requestId} status=${c2cReply.status}`);
|
|
369
|
+
reply = {
|
|
370
|
+
status: c2cReply.status,
|
|
371
|
+
content: c2cReply.content,
|
|
372
|
+
mediaUrls: c2cReply.mediaUrls,
|
|
373
|
+
};
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const msg = (err as Error).message;
|
|
376
|
+
logger.error(`[remote-tool] waitForC2CReply failed: ${msg}`);
|
|
377
|
+
if (msg.includes('timeout')) return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
|
|
378
|
+
if (msg.includes('aborted')) return fail('Request was cancelled');
|
|
379
|
+
return fail(`Remote agent communication error: ${msg}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ⑤ 统一结果处理(两路径汇合) ──
|
|
384
|
+
// 错误已在 ④ 内分支里各自捕获并 fail();此处只处理"成功 reply"的数据装配。
|
|
385
|
+
if (reply.status === 'error') {
|
|
386
|
+
return fail(`Remote agent error: ${reply.content}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// T4: Include media files from remote file transfer.
|
|
390
|
+
// Dual-path delivery for cross-version compatibility:
|
|
391
|
+
// 1. MEDIA: lines in text → parsed by Gateway on ALL versions (v2026.3.x+)
|
|
392
|
+
// 2. details.media → native rendering on v2026.4.11+
|
|
393
|
+
//
|
|
394
|
+
// TIM 路径下 mediaUrls 是本地落盘绝对路径 → 走 plugin HTTP route 代理;
|
|
395
|
+
// OASN 路径下 mediaUrls 是 ArtifactRef.contentUrl(远程 https),暂直接透出。
|
|
396
|
+
// 后续若 OASN 引入本地缓存策略,可在 ④ 内提前 downloadArtifact 后再走代理路径。
|
|
397
|
+
if (reply.mediaUrls && reply.mediaUrls.length > 0) {
|
|
398
|
+
logger.info(`[remote-tool] Reply includes ${reply.mediaUrls.length} file(s)`);
|
|
399
|
+
|
|
400
|
+
const isOasnRemoteUrl = reply.mediaUrls.every((u) => /^https?:\/\//i.test(u));
|
|
401
|
+
let httpMediaUrls: string[];
|
|
402
|
+
if (isOasnRemoteUrl) {
|
|
403
|
+
// OASN 已是 https URL,直接透出,不走 plugin HTTP route
|
|
404
|
+
httpMediaUrls = reply.mediaUrls;
|
|
405
|
+
} else {
|
|
406
|
+
// TIM 路径:本地路径 → 代理 URL,并尝试安装 trip-card-data.json
|
|
407
|
+
installTripCardDataIfPresent(reply.mediaUrls);
|
|
408
|
+
const baseUrl = getGatewayBaseUrl();
|
|
409
|
+
httpMediaUrls = reply.mediaUrls.map((localPath: string) => {
|
|
410
|
+
const fileName = localPath.split('/').pop() ?? 'file';
|
|
411
|
+
return `${baseUrl}/plugins/openagent/media/${encodeURIComponent(fileName)}`;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Path 1: MEDIA: lines in text for Gateway MEDIA: parser + LLM echo
|
|
416
|
+
const mediaLines = httpMediaUrls
|
|
417
|
+
.map((url: string) => `MEDIA:${url}`)
|
|
418
|
+
.join('\n');
|
|
419
|
+
const textWithMedia = reply.content
|
|
420
|
+
? `${reply.content}\n${mediaLines}`
|
|
421
|
+
: mediaLines;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: 'text' as const, text: textWithMedia }],
|
|
425
|
+
// Path 2: details.media for v2026.4.11+ native attachment rendering
|
|
426
|
+
details: {
|
|
427
|
+
count: httpMediaUrls.length,
|
|
428
|
+
media: { mediaUrls: httpMediaUrls },
|
|
429
|
+
paths: httpMediaUrls,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return toolResult(reply.content);
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function installTripCardDataIfPresent(mediaUrls: string[]): void {
|
|
439
|
+
const tripDataPath = mediaUrls.find((localPath) => {
|
|
440
|
+
const fileName = nodePath.basename(localPath).toLowerCase();
|
|
441
|
+
return fileName.endsWith('trip-card-data.json') || fileName.endsWith('ctrip-card-data.json');
|
|
442
|
+
});
|
|
443
|
+
if (!tripDataPath) return;
|
|
444
|
+
|
|
445
|
+
const controlUiDir = resolveControlUiDirForRuntime();
|
|
446
|
+
if (!controlUiDir) {
|
|
447
|
+
logger.warn(`[remote-tool] Trip card data received but control-ui directory was not found`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const dest = nodePath.join(controlUiDir, 'trip-card-data.json');
|
|
453
|
+
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
454
|
+
fs.copyFileSync(tripDataPath, dest);
|
|
455
|
+
logger.info(`[remote-tool] Installed trip card data: ${tripDataPath} → ${dest}`);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
logger.warn(`[remote-tool] Failed to install trip card data: ${(err as Error).message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function resolveControlUiDirForRuntime(): string | null {
|
|
462
|
+
const candidates = new Set<string>();
|
|
463
|
+
const argvEntry = process.argv[1] || '';
|
|
464
|
+
if (argvEntry) {
|
|
465
|
+
candidates.add(nodePath.join(nodePath.dirname(argvEntry), 'control-ui'));
|
|
466
|
+
}
|
|
467
|
+
candidates.add(nodePath.join(process.cwd(), 'dist', 'control-ui'));
|
|
468
|
+
candidates.add('/app/dist/control-ui');
|
|
469
|
+
|
|
470
|
+
for (const candidate of candidates) {
|
|
471
|
+
try {
|
|
472
|
+
if (fs.existsSync(nodePath.join(candidate, 'index.html'))) return candidate;
|
|
473
|
+
} catch {}
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
package/src/app/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool types
|
|
3
|
+
*
|
|
4
|
+
* Return format aligned with OpenClaw SDK's AgentToolResult:
|
|
5
|
+
* @see https://docs.openclaw.ai (Plugin SDK → Building Plugins)
|
|
6
|
+
* @see docs/reference/openclaw/v2026.3.23/dist/plugin-sdk/src/agents/tools/common.d.ts
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ToolParams {
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** SDK-compatible tool result (matches AgentToolResult structure) */
|
|
14
|
+
export interface ToolResult {
|
|
15
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
16
|
+
/** Optional details for rich media. Matches OpenClaw AgentToolResult<TDetails>. */
|
|
17
|
+
details?: {
|
|
18
|
+
media?: { mediaUrls: string[] };
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ToolUpdateCallback = (partialResult: ToolResult) => void;
|
|
24
|
+
|
|
25
|
+
export interface ToolDescriptor {
|
|
26
|
+
name: string;
|
|
27
|
+
label: string;
|
|
28
|
+
description: string;
|
|
29
|
+
parameters: {
|
|
30
|
+
type: 'object';
|
|
31
|
+
properties: Record<string, {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
enum?: string[];
|
|
35
|
+
default?: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
required: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Execute the tool.
|
|
41
|
+
*
|
|
42
|
+
* OpenClaw runtime usually calls:
|
|
43
|
+
* tool.execute(toolCallId, params, signal, onUpdate)
|
|
44
|
+
*
|
|
45
|
+
* Older adapters used:
|
|
46
|
+
* tool.execute(toolCallId, params, onUpdate, ctx, signal)
|
|
47
|
+
*
|
|
48
|
+
* The wider signature lets streaming tools normalize both shapes while
|
|
49
|
+
* existing 2-param tools continue to work unchanged.
|
|
50
|
+
*
|
|
51
|
+
* @see docs/reference/openclaw/v2026.3.23/dist/pi-embedded-CswW9luA.js L169823
|
|
52
|
+
*/
|
|
53
|
+
execute: (
|
|
54
|
+
_id: unknown,
|
|
55
|
+
params: ToolParams,
|
|
56
|
+
signalOrUpdate?: AbortSignal | ToolUpdateCallback,
|
|
57
|
+
onUpdateOrContext?: ToolUpdateCallback | unknown,
|
|
58
|
+
legacySignal?: AbortSignal,
|
|
59
|
+
) => Promise<ToolResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ToolFactory = () => ToolDescriptor;
|
|
63
|
+
|
|
64
|
+
/** Helper: create a successful text result in SDK format */
|
|
65
|
+
export function toolResult(text: string): ToolResult {
|
|
66
|
+
return { content: [{ type: 'text', text }] };
|
|
67
|
+
}
|