openclaw-openagent 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/src/messaging/executor.d.ts +2 -3
  2. package/dist/src/messaging/executor.js +4 -31
  3. package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
  4. package/dist/src/runtime/update-checker.d.ts +1 -1
  5. package/dist/src/runtime/update-checker.js +9 -46
  6. package/package.json +1 -3
  7. package/src/app/channel-tools.ts +0 -249
  8. package/src/app/discovery-tools.ts +0 -273
  9. package/src/app/download-file-tool.ts +0 -133
  10. package/src/app/hooks.ts +0 -144
  11. package/src/app/index.ts +0 -84
  12. package/src/app/messaging-tools.ts +0 -79
  13. package/src/app/ops-tools.ts +0 -155
  14. package/src/app/remote-agent-tool.ts +0 -762
  15. package/src/app/types.ts +0 -68
  16. package/src/app/upload-file-tool.ts +0 -411
  17. package/src/app/verbose-preflight.ts +0 -190
  18. package/src/auth/config.ts +0 -266
  19. package/src/auth/credential-manager.ts +0 -146
  20. package/src/auth/index.ts +0 -24
  21. package/src/auth/verify.ts +0 -99
  22. package/src/channel.ts +0 -566
  23. package/src/compat.ts +0 -94
  24. package/src/config/config-schema.ts +0 -45
  25. package/src/messaging/aggregator.ts +0 -120
  26. package/src/messaging/collector.ts +0 -89
  27. package/src/messaging/executor.ts +0 -72
  28. package/src/messaging/inbound.ts +0 -150
  29. package/src/messaging/index.ts +0 -11
  30. package/src/messaging/mention-protocol.ts +0 -94
  31. package/src/messaging/process-c2c-request.ts +0 -564
  32. package/src/messaging/process-message.ts +0 -373
  33. package/src/messaging/scheduler.ts +0 -55
  34. package/src/messaging/types.ts +0 -38
  35. package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
  36. package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
  37. package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
  38. package/src/plugin-ui/adapters/oc-unknown.js +0 -48
  39. package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
  40. package/src/plugin-ui/assets/bg.png +0 -0
  41. package/src/plugin-ui/assets/icon.png +0 -0
  42. package/src/plugin-ui/assets/magic.svg +0 -5
  43. package/src/plugin-ui/assets/openagent-override.js +0 -11263
  44. package/src/plugin-ui/build.cjs +0 -463
  45. package/src/plugin-ui/index.ts +0 -18
  46. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
  47. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
  48. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
  49. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
  50. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
  51. package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
  52. package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
  53. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
  54. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
  55. package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
  56. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
  57. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
  58. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
  59. package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
  60. package/src/plugin-ui/modules/loader/shared-state.js +0 -838
  61. package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
  62. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
  63. package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
  64. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
  65. package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
  66. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
  67. package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
  68. package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
  69. package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
  70. package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
  71. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
  72. package/src/plugin-ui/postinstall-deploy.cjs +0 -52
  73. package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
  74. package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
  75. package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
  76. package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
  77. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
  78. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
  79. package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
  80. package/src/proxy/auth-proxy.ts +0 -414
  81. package/src/runtime/account.ts +0 -549
  82. package/src/runtime/index.ts +0 -7
  83. package/src/runtime/plugin-runtime.ts +0 -94
  84. package/src/runtime/registry.ts +0 -71
  85. package/src/runtime/update-checker.ts +0 -286
  86. package/src/sdk/CLASS_MAP.md +0 -143
  87. package/src/sdk/index.d.ts +0 -126
  88. package/src/sdk/index.js +0 -23990
  89. package/src/sdk/modules/cloud-search-module.js +0 -1117
  90. package/src/sdk/modules/follow-module.js +0 -1069
  91. package/src/sdk/modules/group-module.js +0 -7397
  92. package/src/sdk/modules/relationship-module.js +0 -2269
  93. package/src/sdk/modules/signaling-module.js +0 -1468
  94. package/src/sdk/modules/tim-upload-plugin.js +0 -730
  95. package/src/sdk/node-env/http-request.js +0 -90
  96. package/src/sdk/node-env/index.js +0 -57
  97. package/src/sdk/node-env/storage.js +0 -114
  98. package/src/sdk/package.json +0 -10
  99. package/src/sdk/tsconfig.json +0 -16
  100. package/src/state/pending-invocation-store.ts +0 -43
  101. package/src/state/store.ts +0 -756
  102. package/src/tim/c2c.ts +0 -451
  103. package/src/tim/channels.ts +0 -364
  104. package/src/tim/client.ts +0 -330
  105. package/src/tim/index.ts +0 -18
  106. package/src/tim/messages.ts +0 -166
  107. package/src/tim/sdk-logger-init.ts +0 -50
  108. package/src/tools.ts +0 -10
  109. package/src/transport/factory.ts +0 -95
  110. package/src/transport/oasn/index.ts +0 -17
  111. package/src/transport/oasn/oasn-agent-card.ts +0 -111
  112. package/src/transport/oasn/oasn-discovery.ts +0 -108
  113. package/src/transport/oasn/oasn-files.ts +0 -210
  114. package/src/transport/oasn/oasn-http.ts +0 -529
  115. package/src/transport/oasn/oasn-invocation.ts +0 -604
  116. package/src/transport/oasn/oasn-normalize.ts +0 -133
  117. package/src/transport/oasn/oasn-register.ts +0 -33
  118. package/src/transport/oasn/oasn-transport.ts +0 -318
  119. package/src/transport/oasn/oasn-types.ts +0 -404
  120. package/src/transport/tim/index.ts +0 -8
  121. package/src/transport/tim/tim-transport.ts +0 -515
  122. package/src/transport/types.ts +0 -538
  123. package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
  124. package/src/types/openclaw.d.ts +0 -97
  125. package/src/types/tencentcloud-chat.d.ts +0 -15
  126. package/src/util/http.ts +0 -113
  127. package/src/util/logger.ts +0 -131
  128. package/src/util/url-resolver.ts +0 -77
@@ -1,762 +0,0 @@
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 { ArtifactRef, OasnTransport, TaskHandle, TaskRequest } from '../transport/types.js';
30
- import fs from 'node:fs';
31
- import nodePath from 'node:path';
32
- import { resolveOasnAccessUrl, resolveOasnWebuiUrl } from '../util/url-resolver.js';
33
-
34
- // ── Helpers ──
35
-
36
- function fail(error: string): ToolResult {
37
- return toolResult(JSON.stringify({ success: false, error }));
38
- }
39
-
40
- /**
41
- * Generate request UUID.
42
- * Aligned with OpenClaw subagent pattern (crypto.randomUUID).
43
- */
44
- function generateRequestId(): string {
45
- return crypto.randomUUID();
46
- }
47
-
48
- function isAbortSignalLike(value: unknown): value is AbortSignal {
49
- return Boolean(
50
- value &&
51
- typeof value === 'object' &&
52
- 'aborted' in value &&
53
- typeof (value as { addEventListener?: unknown }).addEventListener === 'function',
54
- );
55
- }
56
-
57
- function getObjectCallback(value: unknown, key: string): ToolUpdateCallback | undefined {
58
- if (!value || typeof value !== 'object') return undefined;
59
- const candidate = (value as Record<string, unknown>)[key];
60
- return typeof candidate === 'function' ? candidate as ToolUpdateCallback : undefined;
61
- }
62
-
63
- function getObjectSignal(value: unknown): AbortSignal | undefined {
64
- if (!value || typeof value !== 'object') return undefined;
65
- const candidate = (value as Record<string, unknown>).signal;
66
- return isAbortSignalLike(candidate) ? candidate : undefined;
67
- }
68
-
69
- function describeExecuteArg(value: unknown): string {
70
- if (typeof value === 'function') return 'function';
71
- if (isAbortSignalLike(value)) return 'AbortSignal';
72
- if (value === null) return 'null';
73
- if (typeof value !== 'object') return typeof value;
74
- const ctor = (value as { constructor?: { name?: string } }).constructor?.name;
75
- const keys = Object.keys(value as Record<string, unknown>).slice(0, 8).join(',');
76
- return `${ctor || 'object'}{${keys}}`;
77
- }
78
-
79
- function isTextArtifact(candidate: { displayName?: string; mimeType?: string }): boolean {
80
- const mime = (candidate.mimeType || '').toLowerCase();
81
- const name = (candidate.displayName || '').toLowerCase();
82
- return mime.startsWith('text/')
83
- || mime.includes('markdown')
84
- || mime.includes('json')
85
- || name.endsWith('.md')
86
- || name.endsWith('.txt')
87
- || name.endsWith('.json');
88
- }
89
-
90
- function artifactContentToText(content: ArrayBuffer): string {
91
- return Buffer.from(content).toString('utf8').trim();
92
- }
93
-
94
- const OPENAGENT_MEDIA_DIR = '/tmp/openclaw/openagent-media';
95
-
96
- const EXT_BY_MIME: Record<string, string> = {
97
- 'application/json': '.json',
98
- 'text/markdown': '.md',
99
- 'text/plain': '.txt',
100
- 'application/pdf': '.pdf',
101
- 'image/png': '.png',
102
- 'image/jpeg': '.jpg',
103
- 'image/gif': '.gif',
104
- 'image/webp': '.webp',
105
- 'application/zip': '.zip',
106
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
107
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
108
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
109
- };
110
-
111
- function sanitizeMediaFileName(name: string): string {
112
- const base = nodePath.basename(name || 'artifact');
113
- return base
114
- .replace(/[\0\r\n]/g, '')
115
- .replace(/[/:\\]/g, '_')
116
- .replace(/^\.+$/, 'artifact')
117
- .slice(0, 180) || 'artifact';
118
- }
119
-
120
- function inferExtension(buffer: Buffer, mimeType: string): string {
121
- const mime = mimeType.split(';')[0]?.trim().toLowerCase() || '';
122
- const byMime = EXT_BY_MIME[mime];
123
- if (byMime) return byMime;
124
-
125
- if (buffer.subarray(0, 2).toString('latin1') === 'PK') {
126
- const zipNames = buffer.toString('latin1');
127
- if (zipNames.includes('[Content_Types].xml') && zipNames.includes('ppt/')) return '.pptx';
128
- if (zipNames.includes('[Content_Types].xml') && zipNames.includes('word/')) return '.docx';
129
- if (zipNames.includes('[Content_Types].xml') && zipNames.includes('xl/')) return '.xlsx';
130
- return '.zip';
131
- }
132
-
133
- if (buffer.subarray(0, 4).toString('latin1') === '%PDF') return '.pdf';
134
- if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return '.png';
135
- if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) return '.jpg';
136
- return '.bin';
137
- }
138
-
139
- function buildOasnMediaFileName(artifact: ArtifactRef, buffer: Buffer, downloadedName: string, downloadedMime: string): string {
140
- const preferredName = downloadedName && downloadedName !== 'artifact'
141
- ? downloadedName
142
- : artifact.displayName && artifact.displayName !== 'artifact'
143
- ? artifact.displayName
144
- : artifact.id || 'artifact';
145
-
146
- let safeName = sanitizeMediaFileName(preferredName);
147
- if (!nodePath.extname(safeName)) {
148
- safeName += inferExtension(buffer, downloadedMime || artifact.mimeType);
149
- }
150
- return safeName;
151
- }
152
-
153
- async function cacheOasnArtifactsForMedia(
154
- oasnTransport: OasnTransport,
155
- artifacts: ArtifactRef[],
156
- ): Promise<string[]> {
157
- const localPaths: string[] = [];
158
-
159
- for (const artifact of artifacts) {
160
- const ref = artifact.contentUrl || artifact.id;
161
- if (!ref) continue;
162
-
163
- try {
164
- const downloaded = await oasnTransport.downloadArtifact(ref);
165
- const buffer = Buffer.from(downloaded.content);
166
- const fileName = buildOasnMediaFileName(
167
- artifact,
168
- buffer,
169
- downloaded.displayName,
170
- downloaded.mimeType,
171
- );
172
- const filePath = nodePath.join(OPENAGENT_MEDIA_DIR, fileName);
173
- fs.mkdirSync(nodePath.dirname(filePath), { recursive: true });
174
- fs.writeFileSync(filePath, buffer);
175
- localPaths.push(filePath);
176
- logger.info(
177
- `[remote-tool] Cached OASN artifact for media: id=${artifact.id} file=${filePath} size=${buffer.byteLength}`,
178
- );
179
- } catch (err) {
180
- logger.warn(
181
- `[remote-tool] Failed to cache OASN artifact ${artifact.id || ref}: ${(err as Error).message}`,
182
- );
183
- }
184
- }
185
-
186
- return localPaths;
187
- }
188
-
189
- function mediaDownloadLabel(url: string): string {
190
- let fileName = 'artifact';
191
- try {
192
- fileName = decodeURIComponent(new URL(url).pathname.split('/').pop() || fileName);
193
- } catch {
194
- fileName = decodeURIComponent(url.split('?')[0]?.split('/').pop() || fileName);
195
- }
196
- return sanitizeMediaFileName(fileName).replace(/[[\]\r\n]/g, ' ') || 'artifact';
197
- }
198
-
199
- function formatMediaDownloadLines(urls: string[]): string {
200
- const lines = urls
201
- .filter((url) => typeof url === 'string' && url.trim())
202
- .map((url) => `- [${mediaDownloadLabel(url)}](${url})`);
203
- return lines.length ? `下载链接:\n${lines.join('\n')}` : '';
204
- }
205
-
206
- function normalizeRemoteToolExecuteArgs(
207
- signalOrUpdate?: AbortSignal | ToolUpdateCallback,
208
- onUpdateOrContext?: ToolUpdateCallback | unknown,
209
- contextOrSignal?: unknown,
210
- ): { signal?: AbortSignal; onUpdate?: ToolUpdateCallback; context?: unknown; signature: string } {
211
- const contextCandidate = (value: unknown): unknown | undefined => {
212
- if (!value || typeof value !== 'object' || isAbortSignalLike(value)) return undefined;
213
- return value;
214
- };
215
-
216
- if (typeof signalOrUpdate === 'function') {
217
- return {
218
- onUpdate: signalOrUpdate,
219
- signal: isAbortSignalLike(contextOrSignal)
220
- ? contextOrSignal
221
- : getObjectSignal(onUpdateOrContext),
222
- context: contextCandidate(onUpdateOrContext),
223
- signature: 'legacy:update-third',
224
- };
225
- }
226
-
227
- if (typeof onUpdateOrContext === 'function') {
228
- return {
229
- signal: isAbortSignalLike(signalOrUpdate) ? signalOrUpdate : undefined,
230
- onUpdate: onUpdateOrContext as ToolUpdateCallback,
231
- context: contextCandidate(contextOrSignal),
232
- signature: contextCandidate(contextOrSignal) ? 'modern:signal-update-context' : 'modern:signal-update',
233
- };
234
- }
235
-
236
- const nestedUpdate =
237
- getObjectCallback(onUpdateOrContext, 'onUpdate') ||
238
- getObjectCallback(onUpdateOrContext, 'update') ||
239
- getObjectCallback(onUpdateOrContext, 'onToolUpdate');
240
- const nestedSignal =
241
- isAbortSignalLike(signalOrUpdate)
242
- ? signalOrUpdate
243
- : getObjectSignal(onUpdateOrContext) || (isAbortSignalLike(contextOrSignal) ? contextOrSignal : undefined);
244
-
245
- return {
246
- signal: nestedSignal,
247
- onUpdate: nestedUpdate,
248
- context: contextCandidate(contextOrSignal) || contextCandidate(onUpdateOrContext),
249
- signature: nestedUpdate ? 'context:update-callback' : 'unknown:no-update',
250
- };
251
- }
252
-
253
- function readNonEmptyString(value: unknown): string | undefined {
254
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
255
- }
256
-
257
- function readRecord(value: unknown): Record<string, unknown> | undefined {
258
- return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
259
- }
260
-
261
- function callStringGetter(target: unknown, methodName: string): string | undefined {
262
- const record = readRecord(target);
263
- const fn = record?.[methodName];
264
- if (typeof fn !== 'function') return undefined;
265
- try {
266
- return readNonEmptyString(fn.call(target));
267
- } catch {
268
- return undefined;
269
- }
270
- }
271
-
272
- function readNestedString(root: unknown, path: string[]): string | undefined {
273
- let cur: unknown = root;
274
- for (const key of path) {
275
- const record = readRecord(cur);
276
- if (!record) return undefined;
277
- cur = record[key];
278
- }
279
- return readNonEmptyString(cur);
280
- }
281
-
282
- function resolveOpenclawSessionKey(context: unknown): string | undefined {
283
- const ctx = readRecord(context);
284
- if (!ctx) return undefined;
285
-
286
- const sessionManager = ctx.sessionManager;
287
- const managerSessionId = callStringGetter(sessionManager, 'getSessionId');
288
- if (managerSessionId) return `session:${managerSessionId}`;
289
-
290
- const directSessionId =
291
- readNonEmptyString(ctx.sessionId) ||
292
- readNonEmptyString(ctx.openclawSessionId) ||
293
- readNonEmptyString(ctx.agentSessionId) ||
294
- readNestedString(ctx, ['session', 'sessionId']);
295
- if (directSessionId) return `session:${directSessionId}`;
296
-
297
- const sessionFile =
298
- callStringGetter(sessionManager, 'getSessionFile') ||
299
- readNestedString(ctx, ['session', 'sessionFile']);
300
- if (sessionFile) return `file:${sessionFile}`;
301
-
302
- return undefined;
303
- }
304
-
305
- function getRememberedRemoteSessionId(
306
- runtime: { store?: { getRemoteAgentSession?: (openclawSessionKey: string, targetAgentId: string) => string | null } | null },
307
- openclawSessionKey: string | undefined,
308
- agentId: string,
309
- ): string | undefined {
310
- if (!openclawSessionKey) return undefined;
311
- try {
312
- return runtime.store?.getRemoteAgentSession?.(openclawSessionKey, agentId) || undefined;
313
- } catch (err) {
314
- logger.warn(`[remote-tool] Failed to read remembered OASN session: ${(err as Error).message}`);
315
- return undefined;
316
- }
317
- }
318
-
319
- function rememberRemoteSessionId(
320
- runtime: {
321
- store?: {
322
- upsertRemoteAgentSession?: (record: {
323
- openclaw_session_key: string;
324
- target_agent_id: string;
325
- session_id: string;
326
- }) => void;
327
- } | null;
328
- },
329
- openclawSessionKey: string | undefined,
330
- agentId: string,
331
- sessionId: string | undefined,
332
- ): void {
333
- if (!openclawSessionKey || !sessionId) return;
334
- try {
335
- runtime.store?.upsertRemoteAgentSession?.({
336
- openclaw_session_key: openclawSessionKey,
337
- target_agent_id: agentId,
338
- session_id: sessionId,
339
- });
340
- } catch (err) {
341
- logger.warn(`[remote-tool] Failed to remember OASN session: ${(err as Error).message}`);
342
- }
343
- }
344
-
345
- // ── Tool Factory ──
346
-
347
- /**
348
- * Create the openagent_call_remote_agent tool.
349
- *
350
- * Tool description instructs LLM to:
351
- * - Detect {openagent:agent_id="...",agent_name="..."} markers in user messages
352
- * - Extract agent_id from the marker
353
- * - Pass user's natural language request as task parameter
354
- * - NOT display the marker to the user
355
- */
356
- export function createCallRemoteAgentTool(): ToolDescriptor {
357
- return {
358
- name: 'openagent_call_remote_agent',
359
- label: 'Call Remote Agent',
360
- description:
361
- 'Route a user request to a remote specialist Agent via the OpenAgent network. ' +
362
- 'TRIGGER: When the user message contains a {openagent_call_remote_agent:agent_id="...",agent_name="...",agent_bio="...",instruction="..."} marker, ' +
363
- 'call this tool to forward the request to the specified remote Agent. ' +
364
- 'Extract agent_id, agent_name, and optional agent_bio from the marker; use the remaining natural-language text as the task parameter. ' +
365
- 'Do not display the raw marker to the user. ' +
366
- 'mode "session" (default) preserves remote conversation history; mode "run" starts fresh — include full context in task.',
367
- parameters: {
368
- type: 'object',
369
- properties: {
370
- agent_id: {
371
- type: 'string',
372
- description: 'Remote Agent UUID. Extract from the {openagent_call_remote_agent:agent_id="..."} marker in the user message.',
373
- },
374
- agent_name: {
375
- type: 'string',
376
- description: 'Display name of the remote Agent from the marker. Use this in your reply to the user.',
377
- },
378
- agent_bio: {
379
- type: 'string',
380
- description: 'Short description of the remote Agent from the marker. Preserve it for UI display.',
381
- },
382
- task: {
383
- type: 'string',
384
- description: 'The user\'s natural-language request text, excluding the marker.',
385
- },
386
- mode: {
387
- type: 'string',
388
- enum: ['session', 'run'],
389
- description:
390
- '"session" (default): preserves remote conversation history. ' +
391
- '"run": stateless — task must contain full context.',
392
- default: 'session',
393
- },
394
- input_file_refs: {
395
- type: 'array',
396
- items: { type: 'string' },
397
- description:
398
- 'Optional OASN file_ref strings from openagent_upload_file. ' +
399
- 'The remote agent will receive these files as inputs.',
400
- },
401
- },
402
- required: ['agent_id', 'agent_name', 'task'],
403
- },
404
-
405
- async execute(
406
- _id: unknown,
407
- params: ToolParams,
408
- signalOrUpdate?: AbortSignal | ToolUpdateCallback,
409
- onUpdateOrContext?: ToolUpdateCallback | unknown,
410
- contextOrSignal?: unknown,
411
- ): Promise<ToolResult> {
412
- const { signal, onUpdate, context, signature } = normalizeRemoteToolExecuteArgs(
413
- signalOrUpdate,
414
- onUpdateOrContext,
415
- contextOrSignal,
416
- );
417
- logger.info(
418
- `[remote-tool] Execute args: toolCallId=${String(_id)} signature=${signature} ` +
419
- `hasSignal=${Boolean(signal)} hasOnUpdate=${Boolean(onUpdate)} ` +
420
- `third=${describeExecuteArg(signalOrUpdate)} fourth=${describeExecuteArg(onUpdateOrContext)} ` +
421
- `fifth=${describeExecuteArg(contextOrSignal)}`,
422
- );
423
-
424
- // ① Check runtime (same pattern as messaging-tools.ts)
425
- const rt = registry.getDefault();
426
- if (!rt || !rt.isRunning) {
427
- logger.warn('[remote-tool] OpenAgent runtime not running');
428
- return fail('OpenAgent not connected');
429
- }
430
-
431
- // ② Ensure TIM connected (supports lazy connect from half-alive mode)
432
- try {
433
- await rt.ensureConnected();
434
- } catch (connErr) {
435
- logger.error(`[remote-tool] ensureConnected failed: ${(connErr as Error).message}`);
436
- return fail(`OpenAgent connection failed: ${(connErr as Error).message}`);
437
- }
438
-
439
- const agentId = String(params.agent_id || '').trim();
440
- const task = String(params.task || '').trim();
441
- const mode = String(params.mode || 'session').trim() === 'run' ? 'run' : 'session';
442
-
443
- // ② Validate required params
444
- if (!agentId) {
445
- logger.warn('[remote-tool] Missing agent_id parameter');
446
- return fail('agent_id is required');
447
- }
448
- if (!task) {
449
- logger.warn('[remote-tool] Missing task parameter');
450
- return fail('task is required');
451
- }
452
-
453
- // ③ Generate requestId
454
- // 这里的 requestId 同时承担两个职责(v3.9+ 双轨语义):
455
- // - TIM 路径:作为 C2C 关联 id(sendC2CCustomMessage 的 request_id 字段)
456
- // - OASN 路径:作为 toolCallId 透传给 OasnInvocation —— §11.1 用它去 SQLite
457
- // 里查/写 idempotency_key + invocation_id,进程崩溃重启后可由
458
- // listUnsettledInvocations() 续轮询,避免重复扣费
459
- const requestId = generateRequestId();
460
- const TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟:远程 agent 可能跑长工具
461
- const OASN_INVOCATION_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 小时
462
-
463
- logger.info(
464
- `[remote-tool] Calling remote agent: agentId=${agentId} requestId=${requestId} ` +
465
- `mode=${mode} transport=${rt.transportType}`,
466
- );
467
-
468
- // 进度回调适配器:两条路径共用一套桥接逻辑,统一把字符串内容包装成 ToolResult
469
- const bridgeProgress = (content: string) => {
470
- if (onUpdate) {
471
- try {
472
- onUpdate(toolResult(content));
473
- } catch (err) {
474
- logger.warn(`[remote-tool] onUpdate callback failed: ${(err as Error).message}`);
475
- }
476
- }
477
- logger.debug(
478
- `[remote-tool] Progress: requestId=${requestId} hasOnUpdate=${Boolean(onUpdate)} ` +
479
- `content=${content.slice(0, 80)}`,
480
- );
481
- };
482
-
483
- // ④ 按 transport 分发 ——
484
- // - OASN:HTTP API 路径(sendTask + waitForTaskResult),无 C2C
485
- // - TIM :保留原 C2C 流程(sendC2CCustomMessage + waitForC2CReply)
486
- // 两条路径最终都返回相同形状的 reply,下方 ⑤ 统一处理 media + content。
487
- let reply: { status: 'complete' | 'error'; content: string; mediaUrls?: string[] };
488
-
489
- if (rt.transportType === 'oasn') {
490
- // ── OASN 路径 ──
491
- // 走 transport 抽象。注意 rt.transport 在 half-alive 时可能为 null,
492
- // 上面 ensureConnected() 已尽量保证 ready,但仍要兜底,避免空指针。
493
- const transport = rt.transport;
494
- if (!transport || transport.transportType !== 'oasn') {
495
- logger.warn('[remote-tool] OASN transport unavailable after ensureConnected');
496
- return fail('OASN transport not ready');
497
- }
498
- const oasnTransport = transport as OasnTransport;
499
- try {
500
- rt.assertOasnInvocationAllowed();
501
- } catch (err) {
502
- logger.warn(`[remote-tool] OASN invocation blocked: ${(err as Error).message}`);
503
- return fail((err as Error).message);
504
- }
505
-
506
- const inputFileRefs: string[] | undefined = Array.isArray(params.input_file_refs)
507
- ? (params.input_file_refs as string[]).filter((r: unknown) => typeof r === 'string' && r.trim())
508
- : undefined;
509
- const openclawSessionKey = resolveOpenclawSessionKey(context);
510
- const rememberedSessionId = mode === 'session'
511
- ? getRememberedRemoteSessionId(rt, openclawSessionKey, agentId)
512
- : undefined;
513
- if (mode === 'session' && !openclawSessionKey) {
514
- logger.warn('[remote-tool] OASN session mode requested but OpenClaw session scope is unavailable');
515
- }
516
-
517
- const taskReq: TaskRequest = {
518
- requestId,
519
- task,
520
- sessionId: rememberedSessionId,
521
- inputFileRefs: inputFileRefs?.length ? inputFileRefs : undefined,
522
- timeoutMs: OASN_INVOCATION_TIMEOUT_MS,
523
- };
524
-
525
- let handle: TaskHandle;
526
- try {
527
- handle = await oasnTransport.sendTask(agentId, taskReq);
528
- } catch (err) {
529
- logger.error(`[remote-tool] OASN sendTask failed: ${(err as Error).message}`);
530
- return fail(`Failed to reach remote agent: ${(err as Error).message}`);
531
- }
532
- logger.info(
533
- `[remote-tool] OASN invocation created: invocationId=${handle.invocationId} ` +
534
- `sessionId=${handle.sessionId ?? '-'} effectiveTimeoutMs=${handle.effectiveTimeoutMs ?? '-'}`,
535
- );
536
- if (mode === 'session') {
537
- rememberRemoteSessionId(rt, openclawSessionKey, agentId, handle.sessionId);
538
- }
539
-
540
- try {
541
- const result = await oasnTransport.waitForTaskResult(handle, {
542
- timeoutMs: TIMEOUT_MS,
543
- signal,
544
- onProgress: (p) => bridgeProgress(p.content),
545
- });
546
-
547
- const artifacts = result.artifacts ?? [];
548
- let content = result.content;
549
- let mediaUrls: string[] = [];
550
- if (mode === 'session') {
551
- rememberRemoteSessionId(rt, openclawSessionKey, agentId, result.webuiContinuation?.sessionId);
552
- }
553
-
554
- if (!content.trim()) {
555
- for (const textArtifact of artifacts) {
556
- if (!textArtifact.contentUrl && !textArtifact.contentAvailable) continue;
557
- try {
558
- const downloaded = await oasnTransport.downloadArtifact(textArtifact.contentUrl || textArtifact.id);
559
- if (isTextArtifact(downloaded)) {
560
- content = artifactContentToText(downloaded.content);
561
- break;
562
- }
563
- } catch (err) {
564
- logger.warn(
565
- `[remote-tool] Failed to hydrate OASN text artifact ${textArtifact.id}: ${(err as Error).message}`,
566
- );
567
- }
568
- }
569
- }
570
-
571
- // WebUI continuation:声明了 webui 能力的 ServiceAgent 在 succeeded 后会
572
- // 在 visible_result.webui_continuation 里返回可继续交互的入口。
573
- const accessBase = rt.config?.accessApiBase || rt.config?.apiBase;
574
- if (result.webuiContinuation?.launchUrl) {
575
- const rawLink = result.webuiContinuation.launchUrl;
576
- const claimUrl = rt.config?.claimUrl;
577
- const link = resolveOasnWebuiUrl(accessBase, claimUrl, rawLink);
578
- const label = result.webuiContinuation.displayName || 'Open WebUI';
579
- content = content.trim()
580
- ? `${content}\n\n${label}: ${link}`
581
- : `${label}: ${link}`;
582
- logger.info(
583
- `[remote-tool] OASN webui continuation: id=${result.webuiContinuation.id} ` +
584
- `state=${result.webuiContinuation.visibleState} link=${link}`,
585
- );
586
- }
587
-
588
- // Non-text artifacts: expose content_url so the LLM can pass them to
589
- // openagent_download_file to save files locally.
590
- const nonTextArtifacts = artifacts.filter((a) => {
591
- if (!a.contentUrl) return false;
592
- // already inlined as text → skip
593
- if (!content.includes(a.contentUrl)) return true;
594
- return false;
595
- });
596
- if (nonTextArtifacts.length > 0) {
597
- const artifactLines = nonTextArtifacts.map((a) =>
598
- `- ${a.displayName || 'file'}: ${a.contentUrl}`,
599
- );
600
- content = content.trim()
601
- ? `${content}\n\nFiles from remote agent (use openagent_download_file to save):\n${artifactLines.join('\n')}`
602
- : `Files from remote agent:\n${artifactLines.join('\n')}`;
603
- }
604
-
605
- if (artifacts.length > 0) {
606
- mediaUrls = await cacheOasnArtifactsForMedia(oasnTransport, artifacts);
607
- }
608
-
609
- // Resolve OASN-relative URLs in content (remote agent may have embedded
610
- // /webui/continuations/... or /api/artifacts/... in its output text).
611
- if (content && accessBase) {
612
- content = content.replace(
613
- /(?<![a-zA-Z0-9])(\/webui\/[^\s"')\]]+|\/api\/[^\s"')\]]+)/g,
614
- (match) => resolveOasnAccessUrl(accessBase, match),
615
- );
616
- }
617
-
618
- // OASN artifacts are cached locally before being handed to Gateway media rendering.
619
- reply = {
620
- status: 'complete',
621
- content,
622
- mediaUrls,
623
- };
624
- } catch (err) {
625
- const msg = (err as Error).message;
626
- logger.error(`[remote-tool] OASN waitForTaskResult failed: ${msg}`);
627
- if (msg.includes('timeout')) return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
628
- if (msg.includes('aborted') || msg.includes('abort')) return fail('Request was cancelled');
629
- return fail(`Remote agent communication error: ${msg}`);
630
- }
631
- } else {
632
- // ── TIM 路径(保留原逻辑) ──
633
- try {
634
- await sendC2CCustomMessage(agentId, {
635
- type: 'openagent_task_request',
636
- request_id: requestId,
637
- task,
638
- session_mode: mode,
639
- });
640
- } catch (err) {
641
- logger.error(`[remote-tool] Failed to send C2C request: ${(err as Error).message}`);
642
- return fail(`Failed to reach remote agent: ${(err as Error).message}`);
643
- }
644
-
645
- try {
646
- const c2cReply = await waitForC2CReply(requestId, {
647
- timeoutMs: TIMEOUT_MS,
648
- signal,
649
- onProgress: bridgeProgress,
650
- });
651
- logger.info(`[remote-tool] Reply received: requestId=${requestId} status=${c2cReply.status}`);
652
- reply = {
653
- status: c2cReply.status,
654
- content: c2cReply.content,
655
- mediaUrls: c2cReply.mediaUrls,
656
- };
657
- } catch (err) {
658
- const msg = (err as Error).message;
659
- logger.error(`[remote-tool] waitForC2CReply failed: ${msg}`);
660
- if (msg.includes('timeout')) return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
661
- if (msg.includes('aborted')) return fail('Request was cancelled');
662
- return fail(`Remote agent communication error: ${msg}`);
663
- }
664
- }
665
-
666
- // ⑤ 统一结果处理(两路径汇合) ──
667
- // 错误已在 ④ 内分支里各自捕获并 fail();此处只处理"成功 reply"的数据装配。
668
- if (reply.status === 'error') {
669
- return fail(`Remote agent error: ${reply.content}`);
670
- }
671
-
672
- // T4: Include media files from remote file transfer.
673
- // Dual-path delivery for cross-version compatibility:
674
- // 1. MEDIA: lines in text → parsed by Gateway on ALL versions (v2026.3.x+)
675
- // 2. details.media → native rendering on v2026.4.11+
676
- //
677
- // TIM 路径下 mediaUrls 是本地落盘绝对路径 → 走 plugin HTTP route 代理;
678
- // OASN 路径下 mediaUrls 是 ArtifactRef.contentUrl(远程 https),暂直接透出。
679
- // 后续若 OASN 引入本地缓存策略,可在 ④ 内提前 downloadArtifact 后再走代理路径。
680
- if (reply.mediaUrls && reply.mediaUrls.length > 0) {
681
- logger.info(`[remote-tool] Reply includes ${reply.mediaUrls.length} file(s)`);
682
-
683
- const isOasnRemoteUrl = reply.mediaUrls.every((u) => /^https?:\/\//i.test(u));
684
- let httpMediaUrls: string[];
685
- if (isOasnRemoteUrl) {
686
- // OASN 已是 https URL,直接透出,不走 plugin HTTP route
687
- httpMediaUrls = reply.mediaUrls;
688
- } else {
689
- // TIM 路径:本地路径 → 代理 URL,并尝试安装 trip-card-data.json
690
- installTripCardDataIfPresent(reply.mediaUrls);
691
- const baseUrl = getGatewayBaseUrl();
692
- httpMediaUrls = reply.mediaUrls.map((localPath: string) => {
693
- const fileName = localPath.split('/').pop() ?? 'file';
694
- return `${baseUrl}/plugins/openagent/media/${encodeURIComponent(fileName)}`;
695
- });
696
- }
697
-
698
- // Path 1: explicit markdown links + MEDIA: lines for Gateway parser.
699
- // The markdown block makes the link visible even when the host does not
700
- // render MEDIA: attachments in assistant/tool-result bubbles.
701
- const downloadLines = formatMediaDownloadLines(httpMediaUrls);
702
- const mediaLines = httpMediaUrls
703
- .map((url: string) => `MEDIA:${url}`)
704
- .join('\n');
705
- const textWithMedia = [reply.content.trim(), downloadLines, mediaLines]
706
- .filter(Boolean)
707
- .join('\n\n');
708
-
709
- return {
710
- content: [{ type: 'text' as const, text: textWithMedia }],
711
- // Path 2: details.media for v2026.4.11+ native attachment rendering
712
- details: {
713
- count: httpMediaUrls.length,
714
- media: { mediaUrls: httpMediaUrls },
715
- paths: httpMediaUrls,
716
- },
717
- };
718
- }
719
- return toolResult(reply.content);
720
- },
721
- };
722
- }
723
-
724
- function installTripCardDataIfPresent(mediaUrls: string[]): void {
725
- const tripDataPath = mediaUrls.find((localPath) => {
726
- const fileName = nodePath.basename(localPath).toLowerCase();
727
- return fileName.endsWith('trip-card-data.json') || fileName.endsWith('ctrip-card-data.json');
728
- });
729
- if (!tripDataPath) return;
730
-
731
- const controlUiDir = resolveControlUiDirForRuntime();
732
- if (!controlUiDir) {
733
- logger.warn(`[remote-tool] Trip card data received but control-ui directory was not found`);
734
- return;
735
- }
736
-
737
- try {
738
- const dest = nodePath.join(controlUiDir, 'trip-card-data.json');
739
- fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
740
- fs.copyFileSync(tripDataPath, dest);
741
- logger.info(`[remote-tool] Installed trip card data: ${tripDataPath} → ${dest}`);
742
- } catch (err) {
743
- logger.warn(`[remote-tool] Failed to install trip card data: ${(err as Error).message}`);
744
- }
745
- }
746
-
747
- function resolveControlUiDirForRuntime(): string | null {
748
- const candidates = new Set<string>();
749
- const argvEntry = process.argv[1] || '';
750
- if (argvEntry) {
751
- candidates.add(nodePath.join(nodePath.dirname(argvEntry), 'control-ui'));
752
- }
753
- candidates.add(nodePath.join(process.cwd(), 'dist', 'control-ui'));
754
- candidates.add('/app/dist/control-ui');
755
-
756
- for (const candidate of candidates) {
757
- try {
758
- if (fs.existsSync(nodePath.join(candidate, 'index.html'))) return candidate;
759
- } catch {}
760
- }
761
- return null;
762
- }