openclaw-openagent 1.0.12 → 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 (132) hide show
  1. package/dist/index.js +2 -0
  2. package/dist/src/config/config-schema.d.ts +9 -0
  3. package/dist/src/config/config-schema.js +6 -0
  4. package/dist/src/messaging/executor.d.ts +2 -3
  5. package/dist/src/messaging/executor.js +4 -31
  6. package/dist/src/plugin-ui/assets/openagent-override.js +124 -18
  7. package/dist/src/proxy/auth-proxy.js +5 -0
  8. package/dist/src/runtime/update-checker.d.ts +18 -0
  9. package/dist/src/runtime/update-checker.js +216 -0
  10. package/index.ts +3 -0
  11. package/package.json +1 -3
  12. package/src/app/channel-tools.ts +0 -249
  13. package/src/app/discovery-tools.ts +0 -273
  14. package/src/app/download-file-tool.ts +0 -133
  15. package/src/app/hooks.ts +0 -144
  16. package/src/app/index.ts +0 -84
  17. package/src/app/messaging-tools.ts +0 -79
  18. package/src/app/ops-tools.ts +0 -155
  19. package/src/app/remote-agent-tool.ts +0 -762
  20. package/src/app/types.ts +0 -68
  21. package/src/app/upload-file-tool.ts +0 -411
  22. package/src/app/verbose-preflight.ts +0 -190
  23. package/src/auth/config.ts +0 -266
  24. package/src/auth/credential-manager.ts +0 -146
  25. package/src/auth/index.ts +0 -24
  26. package/src/auth/verify.ts +0 -99
  27. package/src/channel.ts +0 -566
  28. package/src/compat.ts +0 -94
  29. package/src/config/config-schema.ts +0 -39
  30. package/src/messaging/aggregator.ts +0 -120
  31. package/src/messaging/collector.ts +0 -89
  32. package/src/messaging/executor.ts +0 -72
  33. package/src/messaging/inbound.ts +0 -150
  34. package/src/messaging/index.ts +0 -11
  35. package/src/messaging/mention-protocol.ts +0 -94
  36. package/src/messaging/process-c2c-request.ts +0 -564
  37. package/src/messaging/process-message.ts +0 -373
  38. package/src/messaging/scheduler.ts +0 -55
  39. package/src/messaging/types.ts +0 -38
  40. package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
  41. package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
  42. package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
  43. package/src/plugin-ui/adapters/oc-unknown.js +0 -48
  44. package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
  45. package/src/plugin-ui/assets/bg.png +0 -0
  46. package/src/plugin-ui/assets/icon.png +0 -0
  47. package/src/plugin-ui/assets/magic.svg +0 -5
  48. package/src/plugin-ui/assets/openagent-override.js +0 -11156
  49. package/src/plugin-ui/build.cjs +0 -463
  50. package/src/plugin-ui/index.ts +0 -18
  51. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
  52. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
  53. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
  54. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
  55. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
  56. package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -833
  57. package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
  58. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
  59. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
  60. package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
  61. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
  62. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
  63. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -256
  64. package/src/plugin-ui/modules/loader/bootstrap.js +0 -38
  65. package/src/plugin-ui/modules/loader/shared-state.js +0 -838
  66. package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
  67. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
  68. package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
  69. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
  70. package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
  71. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
  72. package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
  73. package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
  74. package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
  75. package/src/plugin-ui/modules/remote-agent/styles.js +0 -896
  76. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
  77. package/src/plugin-ui/postinstall-deploy.cjs +0 -52
  78. package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
  79. package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
  80. package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
  81. package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
  82. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
  83. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
  84. package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
  85. package/src/proxy/auth-proxy.ts +0 -409
  86. package/src/runtime/account.ts +0 -549
  87. package/src/runtime/index.ts +0 -7
  88. package/src/runtime/plugin-runtime.ts +0 -94
  89. package/src/runtime/registry.ts +0 -71
  90. package/src/sdk/CLASS_MAP.md +0 -143
  91. package/src/sdk/index.d.ts +0 -126
  92. package/src/sdk/index.js +0 -23990
  93. package/src/sdk/modules/cloud-search-module.js +0 -1117
  94. package/src/sdk/modules/follow-module.js +0 -1069
  95. package/src/sdk/modules/group-module.js +0 -7397
  96. package/src/sdk/modules/relationship-module.js +0 -2269
  97. package/src/sdk/modules/signaling-module.js +0 -1468
  98. package/src/sdk/modules/tim-upload-plugin.js +0 -730
  99. package/src/sdk/node-env/http-request.js +0 -90
  100. package/src/sdk/node-env/index.js +0 -57
  101. package/src/sdk/node-env/storage.js +0 -114
  102. package/src/sdk/package.json +0 -10
  103. package/src/sdk/tsconfig.json +0 -16
  104. package/src/state/pending-invocation-store.ts +0 -43
  105. package/src/state/store.ts +0 -756
  106. package/src/tim/c2c.ts +0 -451
  107. package/src/tim/channels.ts +0 -364
  108. package/src/tim/client.ts +0 -330
  109. package/src/tim/index.ts +0 -18
  110. package/src/tim/messages.ts +0 -166
  111. package/src/tim/sdk-logger-init.ts +0 -50
  112. package/src/tools.ts +0 -10
  113. package/src/transport/factory.ts +0 -95
  114. package/src/transport/oasn/index.ts +0 -17
  115. package/src/transport/oasn/oasn-agent-card.ts +0 -111
  116. package/src/transport/oasn/oasn-discovery.ts +0 -108
  117. package/src/transport/oasn/oasn-files.ts +0 -210
  118. package/src/transport/oasn/oasn-http.ts +0 -529
  119. package/src/transport/oasn/oasn-invocation.ts +0 -604
  120. package/src/transport/oasn/oasn-normalize.ts +0 -133
  121. package/src/transport/oasn/oasn-register.ts +0 -33
  122. package/src/transport/oasn/oasn-transport.ts +0 -318
  123. package/src/transport/oasn/oasn-types.ts +0 -404
  124. package/src/transport/tim/index.ts +0 -8
  125. package/src/transport/tim/tim-transport.ts +0 -515
  126. package/src/transport/types.ts +0 -538
  127. package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
  128. package/src/types/openclaw.d.ts +0 -97
  129. package/src/types/tencentcloud-chat.d.ts +0 -15
  130. package/src/util/http.ts +0 -113
  131. package/src/util/logger.ts +0 -131
  132. 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
- }