openclaw-openagent 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +25 -0
- package/dist/index.js +105 -0
- package/dist/src/app/channel-tools.d.ts +28 -0
- package/dist/src/app/channel-tools.js +251 -0
- package/dist/src/app/discovery-tools.d.ts +35 -0
- package/dist/src/app/discovery-tools.js +243 -0
- package/dist/src/app/download-file-tool.d.ts +10 -0
- package/dist/src/app/download-file-tool.js +104 -0
- package/dist/src/app/hooks.d.ts +14 -0
- package/dist/src/app/hooks.js +118 -0
- package/dist/src/app/index.d.ts +13 -0
- package/dist/src/app/index.js +44 -0
- package/dist/src/app/messaging-tools.d.ts +11 -0
- package/dist/src/app/messaging-tools.js +79 -0
- package/dist/src/app/ops-tools.d.ts +21 -0
- package/dist/src/app/ops-tools.js +158 -0
- package/dist/src/app/remote-agent-tool.d.ts +27 -0
- package/dist/src/app/remote-agent-tool.js +461 -0
- package/dist/src/app/types.d.ts +61 -0
- package/dist/src/app/types.js +11 -0
- package/dist/src/app/upload-file-tool.d.ts +16 -0
- package/dist/src/app/upload-file-tool.js +353 -0
- package/dist/src/app/verbose-preflight.d.ts +2 -0
- package/dist/src/app/verbose-preflight.js +145 -0
- package/dist/src/auth/config.d.ts +79 -0
- package/dist/src/auth/config.js +133 -0
- package/dist/src/auth/credential-manager.d.ts +65 -0
- package/dist/src/auth/credential-manager.js +122 -0
- package/dist/src/auth/index.d.ts +6 -0
- package/dist/src/auth/index.js +6 -0
- package/dist/src/auth/verify.d.ts +42 -0
- package/dist/src/auth/verify.js +60 -0
- package/dist/src/channel.d.ts +269 -0
- package/dist/src/channel.js +488 -0
- package/dist/src/compat.d.ts +37 -0
- package/dist/src/compat.js +70 -0
- package/dist/src/config/config-schema.d.ts +56 -0
- package/dist/src/config/config-schema.js +34 -0
- package/dist/src/messaging/aggregator.d.ts +25 -0
- package/dist/src/messaging/aggregator.js +90 -0
- package/dist/src/messaging/collector.d.ts +27 -0
- package/dist/src/messaging/collector.js +76 -0
- package/dist/src/messaging/executor.d.ts +14 -0
- package/dist/src/messaging/executor.js +59 -0
- package/dist/src/messaging/inbound.d.ts +97 -0
- package/dist/src/messaging/inbound.js +63 -0
- package/dist/src/messaging/index.d.ts +10 -0
- package/dist/src/messaging/index.js +9 -0
- package/dist/src/messaging/mention-protocol.d.ts +42 -0
- package/dist/src/messaging/mention-protocol.js +74 -0
- package/dist/src/messaging/process-c2c-request.d.ts +55 -0
- package/dist/src/messaging/process-c2c-request.js +445 -0
- package/dist/src/messaging/process-message.d.ts +62 -0
- package/dist/src/messaging/process-message.js +282 -0
- package/dist/src/messaging/scheduler.d.ts +17 -0
- package/dist/src/messaging/scheduler.js +47 -0
- package/dist/src/messaging/types.d.ts +34 -0
- package/dist/src/messaging/types.js +4 -0
- package/dist/src/plugin-ui/assets/openagent-override.js +9278 -0
- package/dist/src/plugin-ui/index.d.ts +13 -0
- package/dist/src/plugin-ui/index.js +16 -0
- package/dist/src/plugin-ui/ui-extension-loader/backup.d.ts +23 -0
- package/dist/src/plugin-ui/ui-extension-loader/backup.js +82 -0
- package/dist/src/plugin-ui/ui-extension-loader/index.d.ts +28 -0
- package/dist/src/plugin-ui/ui-extension-loader/index.js +240 -0
- package/dist/src/plugin-ui/ui-extension-loader/locator.d.ts +35 -0
- package/dist/src/plugin-ui/ui-extension-loader/locator.js +129 -0
- package/dist/src/plugin-ui/ui-extension-loader/manifest.d.ts +26 -0
- package/dist/src/plugin-ui/ui-extension-loader/manifest.js +45 -0
- package/dist/src/plugin-ui/ui-extension-loader/registry-regex.d.ts +9 -0
- package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +724 -0
- package/dist/src/plugin-ui/ui-extension-loader/removed-extensions.d.ts +8 -0
- package/dist/src/plugin-ui/ui-extension-loader/removed-extensions.js +58 -0
- package/dist/src/plugin-ui/ui-extension-loader/types.d.ts +55 -0
- package/dist/src/plugin-ui/ui-extension-loader/types.js +23 -0
- package/dist/src/proxy/auth-proxy.d.ts +20 -0
- package/dist/src/proxy/auth-proxy.js +337 -0
- package/dist/src/runtime/account.d.ts +156 -0
- package/dist/src/runtime/account.js +491 -0
- package/dist/src/runtime/index.d.ts +5 -0
- package/dist/src/runtime/index.js +6 -0
- package/dist/src/runtime/plugin-runtime.d.ts +40 -0
- package/dist/src/runtime/plugin-runtime.js +72 -0
- package/dist/src/runtime/registry.d.ts +41 -0
- package/dist/src/runtime/registry.js +60 -0
- package/dist/src/sdk/CLASS_MAP.md +143 -0
- package/dist/src/sdk/index.d.ts +126 -0
- package/dist/src/sdk/index.js +23990 -0
- package/dist/src/sdk/modules/cloud-search-module.js +1117 -0
- package/dist/src/sdk/modules/follow-module.js +1069 -0
- package/dist/src/sdk/modules/group-module.js +7397 -0
- package/dist/src/sdk/modules/relationship-module.js +2269 -0
- package/dist/src/sdk/modules/signaling-module.js +1468 -0
- package/dist/src/sdk/modules/tim-upload-plugin.js +730 -0
- package/dist/src/sdk/node-env/http-request.js +90 -0
- package/dist/src/sdk/node-env/index.js +57 -0
- package/dist/src/sdk/node-env/storage.js +114 -0
- package/dist/src/sdk/package.json +10 -0
- package/dist/src/sdk/sdk/CLASS_MAP.md +143 -0
- package/dist/src/sdk/sdk/index.d.ts +126 -0
- package/dist/src/sdk/sdk/index.js +23990 -0
- package/dist/src/sdk/sdk/modules/cloud-search-module.js +1117 -0
- package/dist/src/sdk/sdk/modules/follow-module.js +1069 -0
- package/dist/src/sdk/sdk/modules/group-module.js +7397 -0
- package/dist/src/sdk/sdk/modules/relationship-module.js +2269 -0
- package/dist/src/sdk/sdk/modules/signaling-module.js +1468 -0
- package/dist/src/sdk/sdk/modules/tim-upload-plugin.js +730 -0
- package/dist/src/sdk/sdk/node-env/http-request.js +90 -0
- package/dist/src/sdk/sdk/node-env/index.js +57 -0
- package/dist/src/sdk/sdk/node-env/storage.js +114 -0
- package/dist/src/sdk/sdk/package.json +10 -0
- package/dist/src/sdk/sdk/tsconfig.json +16 -0
- package/dist/src/sdk/tsconfig.json +16 -0
- package/dist/src/state/pending-invocation-store.d.ts +26 -0
- package/dist/src/state/pending-invocation-store.js +39 -0
- package/dist/src/state/store.d.ts +165 -0
- package/dist/src/state/store.js +535 -0
- package/dist/src/tim/c2c.d.ts +83 -0
- package/dist/src/tim/c2c.js +318 -0
- package/dist/src/tim/channels.d.ts +95 -0
- package/dist/src/tim/channels.js +279 -0
- package/dist/src/tim/client.d.ts +54 -0
- package/dist/src/tim/client.js +268 -0
- package/dist/src/tim/index.d.ts +6 -0
- package/dist/src/tim/index.js +6 -0
- package/dist/src/tim/messages.d.ts +50 -0
- package/dist/src/tim/messages.js +104 -0
- package/dist/src/tim/sdk-logger-init.d.ts +13 -0
- package/dist/src/tim/sdk-logger-init.js +46 -0
- package/dist/src/tools.d.ts +9 -0
- package/dist/src/tools.js +8 -0
- package/dist/src/transport/factory.d.ts +63 -0
- package/dist/src/transport/factory.js +54 -0
- package/dist/src/transport/oasn/index.d.ts +12 -0
- package/dist/src/transport/oasn/index.js +9 -0
- package/dist/src/transport/oasn/oasn-agent-card.d.ts +38 -0
- package/dist/src/transport/oasn/oasn-agent-card.js +102 -0
- package/dist/src/transport/oasn/oasn-discovery.d.ts +33 -0
- package/dist/src/transport/oasn/oasn-discovery.js +97 -0
- package/dist/src/transport/oasn/oasn-files.d.ts +64 -0
- package/dist/src/transport/oasn/oasn-files.js +174 -0
- package/dist/src/transport/oasn/oasn-http.d.ts +98 -0
- package/dist/src/transport/oasn/oasn-http.js +362 -0
- package/dist/src/transport/oasn/oasn-invocation.d.ts +154 -0
- package/dist/src/transport/oasn/oasn-invocation.js +432 -0
- package/dist/src/transport/oasn/oasn-normalize.d.ts +6 -0
- package/dist/src/transport/oasn/oasn-normalize.js +112 -0
- package/dist/src/transport/oasn/oasn-register.d.ts +19 -0
- package/dist/src/transport/oasn/oasn-register.js +24 -0
- package/dist/src/transport/oasn/oasn-transport.d.ts +114 -0
- package/dist/src/transport/oasn/oasn-transport.js +230 -0
- package/dist/src/transport/oasn/oasn-types.d.ts +331 -0
- package/dist/src/transport/oasn/oasn-types.js +44 -0
- package/dist/src/transport/tim/index.d.ts +7 -0
- package/dist/src/transport/tim/index.js +6 -0
- package/dist/src/transport/tim/tim-transport.d.ts +122 -0
- package/dist/src/transport/tim/tim-transport.js +402 -0
- package/dist/src/transport/types.d.ts +450 -0
- package/dist/src/transport/types.js +38 -0
- package/dist/src/util/http.d.ts +21 -0
- package/dist/src/util/http.js +93 -0
- package/dist/src/util/logger.d.ts +20 -0
- package/dist/src/util/logger.js +100 -0
- package/dist/src/util/url-resolver.d.ts +7 -0
- package/dist/src/util/url-resolver.js +20 -0
- package/index.ts +11 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +7 -4
- package/src/app/download-file-tool.ts +133 -0
- package/src/app/hooks.ts +89 -5
- package/src/app/index.ts +6 -0
- package/src/app/remote-agent-tool.ts +46 -0
- package/src/app/types.ts +1 -0
- package/src/app/upload-file-tool.ts +411 -0
- package/src/plugin-ui/assets/openagent-override.js +15 -66
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -61
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -1
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +14 -3
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +102 -63
- package/src/runtime/account.ts +0 -23
- package/src/transport/oasn/oasn-files.ts +5 -5
- package/src/transport/oasn/oasn-invocation.ts +44 -2
- package/src/transport/oasn/oasn-normalize.ts +0 -26
- package/src/transport/oasn/oasn-register.ts +8 -81
- package/src/transport/oasn/oasn-transport.ts +9 -32
- package/src/transport/oasn/oasn-types.ts +53 -6
- package/src/transport/types.ts +33 -36
- package/src/util/url-resolver.ts +17 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ops Tools — 4 operational tools
|
|
3
|
+
*
|
|
4
|
+
* set_strategy, get_status, set_purpose, get_purpose
|
|
5
|
+
*
|
|
6
|
+
* Contract ref: §0.2 refactoring-plan.md
|
|
7
|
+
*/
|
|
8
|
+
import { registry } from '../runtime/registry.js';
|
|
9
|
+
import { toolResult } from './types.js';
|
|
10
|
+
function fail(error) {
|
|
11
|
+
return toolResult(JSON.stringify({ success: false, error }));
|
|
12
|
+
}
|
|
13
|
+
function getRuntime() {
|
|
14
|
+
const rt = registry.getDefault();
|
|
15
|
+
if (!rt || !rt.isRunning)
|
|
16
|
+
return null;
|
|
17
|
+
return rt;
|
|
18
|
+
}
|
|
19
|
+
export function createSetStrategyTool() {
|
|
20
|
+
return {
|
|
21
|
+
name: 'openagent_set_strategy',
|
|
22
|
+
label: 'Set Message Strategy',
|
|
23
|
+
description: 'Configure how you receive and respond to channel messages. Changes take effect immediately.',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
batchWindowMs: { type: 'integer', description: 'How long to collect messages before processing (ms, default 20000)' },
|
|
28
|
+
maxBatchSize: { type: 'integer', description: 'Max messages to buffer before forced flush (default 20)' },
|
|
29
|
+
},
|
|
30
|
+
required: [],
|
|
31
|
+
},
|
|
32
|
+
async execute(_id, params) {
|
|
33
|
+
const rt = getRuntime();
|
|
34
|
+
if (!rt)
|
|
35
|
+
return fail('Not connected');
|
|
36
|
+
try {
|
|
37
|
+
const newConfig = {};
|
|
38
|
+
if (params?.batchWindowMs != null)
|
|
39
|
+
newConfig.batchWindowMs = Number(params.batchWindowMs);
|
|
40
|
+
if (params?.maxBatchSize != null)
|
|
41
|
+
newConfig.maxBatchSize = Number(params.maxBatchSize);
|
|
42
|
+
const current = rt.updateStrategy(newConfig);
|
|
43
|
+
return toolResult(JSON.stringify({
|
|
44
|
+
success: true,
|
|
45
|
+
batchWindowMs: current.batchWindowMs,
|
|
46
|
+
maxBatchSize: current.maxBatchSize,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return fail(err.message);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* get_status — frozen return shape per §0.2:
|
|
57
|
+
*
|
|
58
|
+
* {
|
|
59
|
+
* success, connected, agentId,
|
|
60
|
+
* schedulerQueueSize, joinedChannels,
|
|
61
|
+
* channels: [{ id, name, lastVisit, lastReply, lastMsgSeq }]
|
|
62
|
+
* }
|
|
63
|
+
*/
|
|
64
|
+
export function createGetStatusTool() {
|
|
65
|
+
return {
|
|
66
|
+
name: 'openagent_get_status',
|
|
67
|
+
label: 'Get OpenAgent Status',
|
|
68
|
+
description: 'Get current status of the OpenAgent connection and channel state',
|
|
69
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
70
|
+
async execute() {
|
|
71
|
+
const rt = getRuntime();
|
|
72
|
+
if (!rt) {
|
|
73
|
+
return fail('OpenAgent is not running');
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const joined = rt.store?.getJoinedChannels() || [];
|
|
77
|
+
return toolResult(JSON.stringify({
|
|
78
|
+
success: true,
|
|
79
|
+
connected: rt.client?.isReady ?? false,
|
|
80
|
+
agentId: rt.client?.agentId ?? '',
|
|
81
|
+
schedulerQueueSize: rt.schedulerQueueSize,
|
|
82
|
+
joinedChannels: joined.length,
|
|
83
|
+
channels: joined.map((c) => ({
|
|
84
|
+
id: c.channel_id,
|
|
85
|
+
name: c.channel_name,
|
|
86
|
+
lastVisit: c.last_visit ? new Date(c.last_visit).toISOString() : 'never',
|
|
87
|
+
lastReply: c.last_reply ? new Date(c.last_reply).toISOString() : 'never',
|
|
88
|
+
lastMsgSeq: c.last_msg_seq,
|
|
89
|
+
})),
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return fail(err.message);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function createSetPurposeTool() {
|
|
99
|
+
return {
|
|
100
|
+
name: 'openagent_set_purpose',
|
|
101
|
+
label: 'Set Channel Purpose',
|
|
102
|
+
description: 'Set or update your purpose/goal for a channel',
|
|
103
|
+
parameters: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
channelId: { type: 'string', description: 'Channel ID' },
|
|
107
|
+
purpose: { type: 'string', description: 'Your purpose/goal for this channel' },
|
|
108
|
+
},
|
|
109
|
+
required: ['channelId', 'purpose'],
|
|
110
|
+
},
|
|
111
|
+
async execute(_id, params) {
|
|
112
|
+
const rt = getRuntime();
|
|
113
|
+
if (!rt?.store)
|
|
114
|
+
return fail('Not connected');
|
|
115
|
+
try {
|
|
116
|
+
rt.store.setPurpose(String(params.channelId), String(params.purpose));
|
|
117
|
+
return toolResult(JSON.stringify({
|
|
118
|
+
success: true,
|
|
119
|
+
channelId: params.channelId,
|
|
120
|
+
purpose: params.purpose,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return fail(err.message);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function createGetPurposeTool() {
|
|
130
|
+
return {
|
|
131
|
+
name: 'openagent_get_purpose',
|
|
132
|
+
label: 'Get Channel Purpose',
|
|
133
|
+
description: 'Get your current purpose/goal for a channel',
|
|
134
|
+
parameters: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
channelId: { type: 'string', description: 'Channel ID' },
|
|
138
|
+
},
|
|
139
|
+
required: ['channelId'],
|
|
140
|
+
},
|
|
141
|
+
async execute(_id, params) {
|
|
142
|
+
const rt = getRuntime();
|
|
143
|
+
if (!rt?.store)
|
|
144
|
+
return fail('Not connected');
|
|
145
|
+
try {
|
|
146
|
+
const purpose = rt.store.getPurpose(String(params.channelId));
|
|
147
|
+
return toolResult(JSON.stringify({
|
|
148
|
+
success: true,
|
|
149
|
+
channelId: params.channelId,
|
|
150
|
+
purpose: purpose || '(no purpose set)',
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return fail(err.message);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
import { type ToolDescriptor } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Create the openagent_call_remote_agent tool.
|
|
20
|
+
*
|
|
21
|
+
* Tool description instructs LLM to:
|
|
22
|
+
* - Detect {openagent:agent_id="...",agent_name="..."} markers in user messages
|
|
23
|
+
* - Extract agent_id from the marker
|
|
24
|
+
* - Pass user's natural language request as task parameter
|
|
25
|
+
* - NOT display the marker to the user
|
|
26
|
+
*/
|
|
27
|
+
export declare function createCallRemoteAgentTool(): ToolDescriptor;
|
|
@@ -0,0 +1,461 @@
|
|
|
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
|
+
import { registry } from '../runtime/registry.js';
|
|
18
|
+
import { logger } from '../util/logger.js';
|
|
19
|
+
import { getGatewayBaseUrl } from '../proxy/auth-proxy.js';
|
|
20
|
+
import { toolResult, } from './types.js';
|
|
21
|
+
import { sendC2CCustomMessage, waitForC2CReply } from '../tim/c2c.js';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import nodePath from 'node:path';
|
|
24
|
+
import { resolveOasnAccessUrl } from '../util/url-resolver.js';
|
|
25
|
+
// ── Helpers ──
|
|
26
|
+
function fail(error) {
|
|
27
|
+
return toolResult(JSON.stringify({ success: false, error }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate request UUID.
|
|
31
|
+
* Aligned with OpenClaw subagent pattern (crypto.randomUUID).
|
|
32
|
+
*/
|
|
33
|
+
function generateRequestId() {
|
|
34
|
+
return crypto.randomUUID();
|
|
35
|
+
}
|
|
36
|
+
function isAbortSignalLike(value) {
|
|
37
|
+
return Boolean(value &&
|
|
38
|
+
typeof value === 'object' &&
|
|
39
|
+
'aborted' in value &&
|
|
40
|
+
typeof value.addEventListener === 'function');
|
|
41
|
+
}
|
|
42
|
+
function getObjectCallback(value, key) {
|
|
43
|
+
if (!value || typeof value !== 'object')
|
|
44
|
+
return undefined;
|
|
45
|
+
const candidate = value[key];
|
|
46
|
+
return typeof candidate === 'function' ? candidate : undefined;
|
|
47
|
+
}
|
|
48
|
+
function getObjectSignal(value) {
|
|
49
|
+
if (!value || typeof value !== 'object')
|
|
50
|
+
return undefined;
|
|
51
|
+
const candidate = value.signal;
|
|
52
|
+
return isAbortSignalLike(candidate) ? candidate : undefined;
|
|
53
|
+
}
|
|
54
|
+
function describeExecuteArg(value) {
|
|
55
|
+
if (typeof value === 'function')
|
|
56
|
+
return 'function';
|
|
57
|
+
if (isAbortSignalLike(value))
|
|
58
|
+
return 'AbortSignal';
|
|
59
|
+
if (value === null)
|
|
60
|
+
return 'null';
|
|
61
|
+
if (typeof value !== 'object')
|
|
62
|
+
return typeof value;
|
|
63
|
+
const ctor = value.constructor?.name;
|
|
64
|
+
const keys = Object.keys(value).slice(0, 8).join(',');
|
|
65
|
+
return `${ctor || 'object'}{${keys}}`;
|
|
66
|
+
}
|
|
67
|
+
function isTextArtifact(candidate) {
|
|
68
|
+
const mime = (candidate.mimeType || '').toLowerCase();
|
|
69
|
+
const name = (candidate.displayName || '').toLowerCase();
|
|
70
|
+
return mime.startsWith('text/')
|
|
71
|
+
|| mime.includes('markdown')
|
|
72
|
+
|| mime.includes('json')
|
|
73
|
+
|| name.endsWith('.md')
|
|
74
|
+
|| name.endsWith('.txt')
|
|
75
|
+
|| name.endsWith('.json');
|
|
76
|
+
}
|
|
77
|
+
function artifactContentToText(content) {
|
|
78
|
+
return Buffer.from(content).toString('utf8').trim();
|
|
79
|
+
}
|
|
80
|
+
function normalizeRemoteToolExecuteArgs(signalOrUpdate, onUpdateOrContext, legacySignal) {
|
|
81
|
+
if (typeof signalOrUpdate === 'function') {
|
|
82
|
+
return {
|
|
83
|
+
onUpdate: signalOrUpdate,
|
|
84
|
+
signal: isAbortSignalLike(legacySignal)
|
|
85
|
+
? legacySignal
|
|
86
|
+
: getObjectSignal(onUpdateOrContext),
|
|
87
|
+
signature: 'legacy:update-third',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (typeof onUpdateOrContext === 'function') {
|
|
91
|
+
return {
|
|
92
|
+
signal: isAbortSignalLike(signalOrUpdate) ? signalOrUpdate : undefined,
|
|
93
|
+
onUpdate: onUpdateOrContext,
|
|
94
|
+
signature: 'modern:signal-update',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const nestedUpdate = getObjectCallback(onUpdateOrContext, 'onUpdate') ||
|
|
98
|
+
getObjectCallback(onUpdateOrContext, 'update') ||
|
|
99
|
+
getObjectCallback(onUpdateOrContext, 'onToolUpdate');
|
|
100
|
+
const nestedSignal = isAbortSignalLike(signalOrUpdate)
|
|
101
|
+
? signalOrUpdate
|
|
102
|
+
: getObjectSignal(onUpdateOrContext) || (isAbortSignalLike(legacySignal) ? legacySignal : undefined);
|
|
103
|
+
return {
|
|
104
|
+
signal: nestedSignal,
|
|
105
|
+
onUpdate: nestedUpdate,
|
|
106
|
+
signature: nestedUpdate ? 'context:update-callback' : 'unknown:no-update',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ── Tool Factory ──
|
|
110
|
+
/**
|
|
111
|
+
* Create the openagent_call_remote_agent tool.
|
|
112
|
+
*
|
|
113
|
+
* Tool description instructs LLM to:
|
|
114
|
+
* - Detect {openagent:agent_id="...",agent_name="..."} markers in user messages
|
|
115
|
+
* - Extract agent_id from the marker
|
|
116
|
+
* - Pass user's natural language request as task parameter
|
|
117
|
+
* - NOT display the marker to the user
|
|
118
|
+
*/
|
|
119
|
+
export function createCallRemoteAgentTool() {
|
|
120
|
+
return {
|
|
121
|
+
name: 'openagent_call_remote_agent',
|
|
122
|
+
label: 'Call Remote Agent',
|
|
123
|
+
description: 'Route a user request to a remote specialist Agent via the OpenAgent network. ' +
|
|
124
|
+
'TRIGGER: When the user message contains a {openagent_call_remote_agent:agent_id="...",agent_name="...",instruction="..."} marker, ' +
|
|
125
|
+
'call this tool to forward the request to the specified remote Agent. ' +
|
|
126
|
+
'Extract agent_id and agent_name from the marker; use the remaining natural-language text as the task parameter. ' +
|
|
127
|
+
'Do not display the raw marker to the user. ' +
|
|
128
|
+
'mode "session" (default) preserves remote conversation history; mode "run" starts fresh — include full context in task.',
|
|
129
|
+
parameters: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
agent_id: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'Remote Agent UUID. Extract from the {openagent_call_remote_agent:agent_id="..."} marker in the user message.',
|
|
135
|
+
},
|
|
136
|
+
agent_name: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Display name of the remote Agent from the marker. Use this in your reply to the user.',
|
|
139
|
+
},
|
|
140
|
+
task: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'The user\'s natural-language request text, excluding the marker.',
|
|
143
|
+
},
|
|
144
|
+
mode: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
enum: ['session', 'run'],
|
|
147
|
+
description: '"session" (default): preserves remote conversation history. ' +
|
|
148
|
+
'"run": stateless — task must contain full context.',
|
|
149
|
+
default: 'session',
|
|
150
|
+
},
|
|
151
|
+
input_file_refs: {
|
|
152
|
+
type: 'array',
|
|
153
|
+
items: { type: 'string' },
|
|
154
|
+
description: 'Optional OASN file_ref strings from openagent_upload_file. ' +
|
|
155
|
+
'The remote agent will receive these files as inputs.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: ['agent_id', 'agent_name', 'task'],
|
|
159
|
+
},
|
|
160
|
+
async execute(_id, params, signalOrUpdate, onUpdateOrContext, legacySignal) {
|
|
161
|
+
const { signal, onUpdate, signature } = normalizeRemoteToolExecuteArgs(signalOrUpdate, onUpdateOrContext, legacySignal);
|
|
162
|
+
logger.info(`[remote-tool] Execute args: toolCallId=${String(_id)} signature=${signature} ` +
|
|
163
|
+
`hasSignal=${Boolean(signal)} hasOnUpdate=${Boolean(onUpdate)} ` +
|
|
164
|
+
`third=${describeExecuteArg(signalOrUpdate)} fourth=${describeExecuteArg(onUpdateOrContext)} ` +
|
|
165
|
+
`fifth=${describeExecuteArg(legacySignal)}`);
|
|
166
|
+
// ① Check runtime (same pattern as messaging-tools.ts)
|
|
167
|
+
const rt = registry.getDefault();
|
|
168
|
+
if (!rt || !rt.isRunning) {
|
|
169
|
+
logger.warn('[remote-tool] OpenAgent runtime not running');
|
|
170
|
+
return fail('OpenAgent not connected');
|
|
171
|
+
}
|
|
172
|
+
// ② Ensure TIM connected (supports lazy connect from half-alive mode)
|
|
173
|
+
try {
|
|
174
|
+
await rt.ensureConnected();
|
|
175
|
+
}
|
|
176
|
+
catch (connErr) {
|
|
177
|
+
logger.error(`[remote-tool] ensureConnected failed: ${connErr.message}`);
|
|
178
|
+
return fail(`OpenAgent connection failed: ${connErr.message}`);
|
|
179
|
+
}
|
|
180
|
+
const agentId = String(params.agent_id || '').trim();
|
|
181
|
+
const task = String(params.task || '').trim();
|
|
182
|
+
const mode = String(params.mode || 'session').trim();
|
|
183
|
+
// ② Validate required params
|
|
184
|
+
if (!agentId) {
|
|
185
|
+
logger.warn('[remote-tool] Missing agent_id parameter');
|
|
186
|
+
return fail('agent_id is required');
|
|
187
|
+
}
|
|
188
|
+
if (!task) {
|
|
189
|
+
logger.warn('[remote-tool] Missing task parameter');
|
|
190
|
+
return fail('task is required');
|
|
191
|
+
}
|
|
192
|
+
// ③ Generate requestId
|
|
193
|
+
// 这里的 requestId 同时承担两个职责(v3.9+ 双轨语义):
|
|
194
|
+
// - TIM 路径:作为 C2C 关联 id(sendC2CCustomMessage 的 request_id 字段)
|
|
195
|
+
// - OASN 路径:作为 toolCallId 透传给 OasnInvocation —— §11.1 用它去 SQLite
|
|
196
|
+
// 里查/写 idempotency_key + invocation_id,进程崩溃重启后可由
|
|
197
|
+
// listUnsettledInvocations() 续轮询,避免重复扣费
|
|
198
|
+
const requestId = generateRequestId();
|
|
199
|
+
const TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟:远程 agent 可能跑长工具
|
|
200
|
+
const OASN_INVOCATION_TIMEOUT_MS = 60_000; // compute02 REL 当前允许的 ServiceAgent 执行预算
|
|
201
|
+
logger.info(`[remote-tool] Calling remote agent: agentId=${agentId} requestId=${requestId} ` +
|
|
202
|
+
`mode=${mode} transport=${rt.transportType}`);
|
|
203
|
+
// 进度回调适配器:两条路径共用一套桥接逻辑,统一把字符串内容包装成 ToolResult
|
|
204
|
+
const bridgeProgress = (content) => {
|
|
205
|
+
if (onUpdate) {
|
|
206
|
+
try {
|
|
207
|
+
onUpdate(toolResult(content));
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
logger.warn(`[remote-tool] onUpdate callback failed: ${err.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
logger.debug(`[remote-tool] Progress: requestId=${requestId} hasOnUpdate=${Boolean(onUpdate)} ` +
|
|
214
|
+
`content=${content.slice(0, 80)}`);
|
|
215
|
+
};
|
|
216
|
+
// ④ 按 transport 分发 ——
|
|
217
|
+
// - OASN:HTTP API 路径(sendTask + waitForTaskResult),无 C2C
|
|
218
|
+
// - TIM :保留原 C2C 流程(sendC2CCustomMessage + waitForC2CReply)
|
|
219
|
+
// 两条路径最终都返回相同形状的 reply,下方 ⑤ 统一处理 media + content。
|
|
220
|
+
let reply;
|
|
221
|
+
if (rt.transportType === 'oasn') {
|
|
222
|
+
// ── OASN 路径 ──
|
|
223
|
+
// 走 transport 抽象。注意 rt.transport 在 half-alive 时可能为 null,
|
|
224
|
+
// 上面 ensureConnected() 已尽量保证 ready,但仍要兜底,避免空指针。
|
|
225
|
+
const transport = rt.transport;
|
|
226
|
+
if (!transport || transport.transportType !== 'oasn') {
|
|
227
|
+
logger.warn('[remote-tool] OASN transport unavailable after ensureConnected');
|
|
228
|
+
return fail('OASN transport not ready');
|
|
229
|
+
}
|
|
230
|
+
const oasnTransport = transport;
|
|
231
|
+
try {
|
|
232
|
+
rt.assertOasnInvocationAllowed();
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
logger.warn(`[remote-tool] OASN invocation blocked: ${err.message}`);
|
|
236
|
+
return fail(err.message);
|
|
237
|
+
}
|
|
238
|
+
const inputFileRefs = Array.isArray(params.input_file_refs)
|
|
239
|
+
? params.input_file_refs.filter((r) => typeof r === 'string' && r.trim())
|
|
240
|
+
: undefined;
|
|
241
|
+
const taskReq = {
|
|
242
|
+
requestId,
|
|
243
|
+
task,
|
|
244
|
+
inputFileRefs: inputFileRefs?.length ? inputFileRefs : undefined,
|
|
245
|
+
timeoutMs: OASN_INVOCATION_TIMEOUT_MS,
|
|
246
|
+
// mode='session' 时不在协议层 join 已有 session(OASN 默认每次起新 Invocation)。
|
|
247
|
+
// 上游若需要保留对话历史,请在 task 文本里显式带 session_id;
|
|
248
|
+
// 暂不在工具层做 session 续传,避免误用导致跨用户串号。
|
|
249
|
+
};
|
|
250
|
+
let handle;
|
|
251
|
+
try {
|
|
252
|
+
handle = await oasnTransport.sendTask(agentId, taskReq);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
logger.error(`[remote-tool] OASN sendTask failed: ${err.message}`);
|
|
256
|
+
return fail(`Failed to reach remote agent: ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
logger.info(`[remote-tool] OASN invocation created: invocationId=${handle.invocationId} ` +
|
|
259
|
+
`sessionId=${handle.sessionId ?? '-'} effectiveTimeoutMs=${handle.effectiveTimeoutMs ?? '-'}`);
|
|
260
|
+
try {
|
|
261
|
+
const result = await oasnTransport.waitForTaskResult(handle, {
|
|
262
|
+
timeoutMs: TIMEOUT_MS,
|
|
263
|
+
signal,
|
|
264
|
+
onProgress: (p) => bridgeProgress(p.content),
|
|
265
|
+
});
|
|
266
|
+
const artifacts = result.artifacts ?? [];
|
|
267
|
+
let content = result.content;
|
|
268
|
+
let mediaUrls = artifacts.map((a) => a.contentUrl).filter(Boolean);
|
|
269
|
+
if (!content.trim()) {
|
|
270
|
+
for (const textArtifact of artifacts) {
|
|
271
|
+
if (!textArtifact.contentUrl && !textArtifact.contentAvailable)
|
|
272
|
+
continue;
|
|
273
|
+
try {
|
|
274
|
+
const downloaded = await oasnTransport.downloadArtifact(textArtifact.contentUrl || textArtifact.id);
|
|
275
|
+
if (isTextArtifact(downloaded)) {
|
|
276
|
+
content = artifactContentToText(downloaded.content);
|
|
277
|
+
mediaUrls = artifacts
|
|
278
|
+
.filter((a) => a.id !== textArtifact.id)
|
|
279
|
+
.map((a) => a.contentUrl)
|
|
280
|
+
.filter(Boolean);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
logger.warn(`[remote-tool] Failed to hydrate OASN text artifact ${textArtifact.id}: ${err.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// WebUI continuation:声明了 webui 能力的 ServiceAgent 在 succeeded 后会
|
|
290
|
+
// 在 visible_result.webui_continuation 里返回可继续交互的入口。
|
|
291
|
+
if (result.webuiContinuation?.launchUrl) {
|
|
292
|
+
const rawLink = result.webuiContinuation.launchUrl;
|
|
293
|
+
const accessBase = rt.config?.accessApiBase;
|
|
294
|
+
const link = resolveOasnAccessUrl(accessBase, rawLink);
|
|
295
|
+
const label = result.webuiContinuation.displayName || 'Open WebUI';
|
|
296
|
+
content = content.trim()
|
|
297
|
+
? `${content}\n\n${label}: ${link}`
|
|
298
|
+
: `${label}: ${link}`;
|
|
299
|
+
logger.info(`[remote-tool] OASN webui continuation: id=${result.webuiContinuation.id} ` +
|
|
300
|
+
`state=${result.webuiContinuation.visibleState} link=${link}`);
|
|
301
|
+
}
|
|
302
|
+
// Non-text artifacts: expose content_url so the LLM can pass them to
|
|
303
|
+
// openagent_download_file to save files locally.
|
|
304
|
+
const nonTextArtifacts = artifacts.filter((a) => {
|
|
305
|
+
if (!a.contentUrl)
|
|
306
|
+
return false;
|
|
307
|
+
// already inlined as text → skip
|
|
308
|
+
if (!content.includes(a.contentUrl))
|
|
309
|
+
return true;
|
|
310
|
+
return false;
|
|
311
|
+
});
|
|
312
|
+
if (nonTextArtifacts.length > 0) {
|
|
313
|
+
const artifactLines = nonTextArtifacts.map((a) => `- ${a.displayName || 'file'}: ${a.contentUrl}`);
|
|
314
|
+
content = content.trim()
|
|
315
|
+
? `${content}\n\nFiles from remote agent (use openagent_download_file to save):\n${artifactLines.join('\n')}`
|
|
316
|
+
: `Files from remote agent:\n${artifactLines.join('\n')}`;
|
|
317
|
+
}
|
|
318
|
+
// OASN 的非文本 artifacts 暂继续复用 ArtifactRef.contentUrl 作为 mediaUrls。
|
|
319
|
+
reply = {
|
|
320
|
+
status: 'complete',
|
|
321
|
+
content,
|
|
322
|
+
mediaUrls,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
const msg = err.message;
|
|
327
|
+
logger.error(`[remote-tool] OASN waitForTaskResult failed: ${msg}`);
|
|
328
|
+
if (msg.includes('timeout'))
|
|
329
|
+
return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
|
|
330
|
+
if (msg.includes('aborted') || msg.includes('abort'))
|
|
331
|
+
return fail('Request was cancelled');
|
|
332
|
+
return fail(`Remote agent communication error: ${msg}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// ── TIM 路径(保留原逻辑) ──
|
|
337
|
+
try {
|
|
338
|
+
await sendC2CCustomMessage(agentId, {
|
|
339
|
+
type: 'openagent_task_request',
|
|
340
|
+
request_id: requestId,
|
|
341
|
+
task,
|
|
342
|
+
session_mode: mode,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
logger.error(`[remote-tool] Failed to send C2C request: ${err.message}`);
|
|
347
|
+
return fail(`Failed to reach remote agent: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const c2cReply = await waitForC2CReply(requestId, {
|
|
351
|
+
timeoutMs: TIMEOUT_MS,
|
|
352
|
+
signal,
|
|
353
|
+
onProgress: bridgeProgress,
|
|
354
|
+
});
|
|
355
|
+
logger.info(`[remote-tool] Reply received: requestId=${requestId} status=${c2cReply.status}`);
|
|
356
|
+
reply = {
|
|
357
|
+
status: c2cReply.status,
|
|
358
|
+
content: c2cReply.content,
|
|
359
|
+
mediaUrls: c2cReply.mediaUrls,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
const msg = err.message;
|
|
364
|
+
logger.error(`[remote-tool] waitForC2CReply failed: ${msg}`);
|
|
365
|
+
if (msg.includes('timeout'))
|
|
366
|
+
return fail(`Remote agent did not respond within ${TIMEOUT_MS / 1000}s`);
|
|
367
|
+
if (msg.includes('aborted'))
|
|
368
|
+
return fail('Request was cancelled');
|
|
369
|
+
return fail(`Remote agent communication error: ${msg}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ⑤ 统一结果处理(两路径汇合) ──
|
|
373
|
+
// 错误已在 ④ 内分支里各自捕获并 fail();此处只处理"成功 reply"的数据装配。
|
|
374
|
+
if (reply.status === 'error') {
|
|
375
|
+
return fail(`Remote agent error: ${reply.content}`);
|
|
376
|
+
}
|
|
377
|
+
// T4: Include media files from remote file transfer.
|
|
378
|
+
// Dual-path delivery for cross-version compatibility:
|
|
379
|
+
// 1. MEDIA: lines in text → parsed by Gateway on ALL versions (v2026.3.x+)
|
|
380
|
+
// 2. details.media → native rendering on v2026.4.11+
|
|
381
|
+
//
|
|
382
|
+
// TIM 路径下 mediaUrls 是本地落盘绝对路径 → 走 plugin HTTP route 代理;
|
|
383
|
+
// OASN 路径下 mediaUrls 是 ArtifactRef.contentUrl(远程 https),暂直接透出。
|
|
384
|
+
// 后续若 OASN 引入本地缓存策略,可在 ④ 内提前 downloadArtifact 后再走代理路径。
|
|
385
|
+
if (reply.mediaUrls && reply.mediaUrls.length > 0) {
|
|
386
|
+
logger.info(`[remote-tool] Reply includes ${reply.mediaUrls.length} file(s)`);
|
|
387
|
+
const isOasnRemoteUrl = reply.mediaUrls.every((u) => /^https?:\/\//i.test(u));
|
|
388
|
+
let httpMediaUrls;
|
|
389
|
+
if (isOasnRemoteUrl) {
|
|
390
|
+
// OASN 已是 https URL,直接透出,不走 plugin HTTP route
|
|
391
|
+
httpMediaUrls = reply.mediaUrls;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// TIM 路径:本地路径 → 代理 URL,并尝试安装 trip-card-data.json
|
|
395
|
+
installTripCardDataIfPresent(reply.mediaUrls);
|
|
396
|
+
const baseUrl = getGatewayBaseUrl();
|
|
397
|
+
httpMediaUrls = reply.mediaUrls.map((localPath) => {
|
|
398
|
+
const fileName = localPath.split('/').pop() ?? 'file';
|
|
399
|
+
return `${baseUrl}/plugins/openagent/media/${encodeURIComponent(fileName)}`;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
// Path 1: MEDIA: lines in text for Gateway MEDIA: parser + LLM echo
|
|
403
|
+
const mediaLines = httpMediaUrls
|
|
404
|
+
.map((url) => `MEDIA:${url}`)
|
|
405
|
+
.join('\n');
|
|
406
|
+
const textWithMedia = reply.content
|
|
407
|
+
? `${reply.content}\n${mediaLines}`
|
|
408
|
+
: mediaLines;
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: 'text', text: textWithMedia }],
|
|
411
|
+
// Path 2: details.media for v2026.4.11+ native attachment rendering
|
|
412
|
+
details: {
|
|
413
|
+
count: httpMediaUrls.length,
|
|
414
|
+
media: { mediaUrls: httpMediaUrls },
|
|
415
|
+
paths: httpMediaUrls,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return toolResult(reply.content);
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function installTripCardDataIfPresent(mediaUrls) {
|
|
424
|
+
const tripDataPath = mediaUrls.find((localPath) => {
|
|
425
|
+
const fileName = nodePath.basename(localPath).toLowerCase();
|
|
426
|
+
return fileName.endsWith('trip-card-data.json') || fileName.endsWith('ctrip-card-data.json');
|
|
427
|
+
});
|
|
428
|
+
if (!tripDataPath)
|
|
429
|
+
return;
|
|
430
|
+
const controlUiDir = resolveControlUiDirForRuntime();
|
|
431
|
+
if (!controlUiDir) {
|
|
432
|
+
logger.warn(`[remote-tool] Trip card data received but control-ui directory was not found`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const dest = nodePath.join(controlUiDir, 'trip-card-data.json');
|
|
437
|
+
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
438
|
+
fs.copyFileSync(tripDataPath, dest);
|
|
439
|
+
logger.info(`[remote-tool] Installed trip card data: ${tripDataPath} → ${dest}`);
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
logger.warn(`[remote-tool] Failed to install trip card data: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function resolveControlUiDirForRuntime() {
|
|
446
|
+
const candidates = new Set();
|
|
447
|
+
const argvEntry = process.argv[1] || '';
|
|
448
|
+
if (argvEntry) {
|
|
449
|
+
candidates.add(nodePath.join(nodePath.dirname(argvEntry), 'control-ui'));
|
|
450
|
+
}
|
|
451
|
+
candidates.add(nodePath.join(process.cwd(), 'dist', 'control-ui'));
|
|
452
|
+
candidates.add('/app/dist/control-ui');
|
|
453
|
+
for (const candidate of candidates) {
|
|
454
|
+
try {
|
|
455
|
+
if (fs.existsSync(nodePath.join(candidate, 'index.html')))
|
|
456
|
+
return candidate;
|
|
457
|
+
}
|
|
458
|
+
catch { }
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|