@workclaw/openclaw-workclaw 1.0.17 → 1.0.18

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 (51) hide show
  1. package/README.md +21 -1
  2. package/index.ts +210 -210
  3. package/openclaw.plugin.json +1 -0
  4. package/package.json +11 -4
  5. package/setup-entry.ts +6 -0
  6. package/skills/openclaw-workclaw-cron/SKILL.md +45 -28
  7. package/src/accounts.ts +62 -37
  8. package/src/api/accounts-api.ts +88 -89
  9. package/src/api/prompts-api.ts +70 -77
  10. package/src/api/session-api.ts +99 -108
  11. package/src/api/skills-api.ts +35 -37
  12. package/src/api/workspace.ts +27 -29
  13. package/src/channel.ts +200 -202
  14. package/src/config-schema.ts +9 -9
  15. package/src/connection/workclaw-client.ts +554 -567
  16. package/src/gateway/agent-handlers.ts +392 -426
  17. package/src/gateway/config-writer.ts +228 -243
  18. package/src/gateway/message-context.ts +534 -362
  19. package/src/gateway/message-dispatcher.ts +529 -489
  20. package/src/gateway/reconnect.ts +217 -113
  21. package/src/gateway/skills-handler.ts +408 -472
  22. package/src/gateway/skills-list-handler.ts +9 -9
  23. package/src/gateway/tools-list-handler.ts +70 -72
  24. package/src/gateway/workclaw-gateway.ts +328 -486
  25. package/src/media/upload.ts +83 -94
  26. package/src/outbound/index.ts +57 -55
  27. package/src/outbound/workclaw-sender.ts +134 -133
  28. package/src/runtime.ts +291 -194
  29. package/src/send.ts +1 -1
  30. package/src/tools/openclaw-workclaw-cron/api/index.ts +6 -6
  31. package/src/tools/openclaw-workclaw-cron/src/add/params.ts +20 -19
  32. package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +2 -2
  33. package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +1 -1
  34. package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +3 -3
  35. package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +1 -1
  36. package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +3 -3
  37. package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +2 -2
  38. package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +1 -1
  39. package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +3 -3
  40. package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -197
  41. package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +4 -4
  42. package/src/tools/openclaw-workclaw-system/src/get/index.ts +2 -2
  43. package/src/tools/openclaw-workclaw-system/src/token/index.ts +4 -4
  44. package/src/types.ts +38 -40
  45. package/src/utils/content.ts +16 -21
  46. package/tests/accounts.test.ts +285 -0
  47. package/tests/message-context.test.ts +313 -0
  48. package/tests/reconnect.test.ts +257 -0
  49. package/tests/workclaw-client.test.ts +112 -0
  50. package/tsconfig.json +8 -5
  51. package/vitest.config.ts +8 -0
@@ -2,38 +2,42 @@
2
2
  * Message Dispatcher - handles all WorkClaw WebSocket message types
3
3
  */
4
4
 
5
- import type { ResolvedOpenclawWorkclawAccount, WorkClawAccountConfig, WorkClawBaseConfig } from '../types.js'
6
- import type { InboundMessage, ParseWorkClawResult } from './message-context.js'
7
-
8
- import { resolveAccountByUserIdAndAgentId, resolveOpenclawWorkclawAccount } from '../accounts.js'
9
- import { getOpenclawWorkclawAccessToken } from '../connection/workclaw-client.js'
10
-
11
- import { sendMessageOpenclawWorkclaw } from '../outbound/index.js'
12
- import { getOpenclawWorkclawRuntime, getOpenclawWorkclawWsConnection, setToolContext } from '../runtime.js'
13
- import { handleAgentCreated, handleAgentDeleted, handleAgentUpdated } from './agent-handlers.js'
14
- import { saveOpenConversationId, saveWorkClawAgentId, saveWorkClawApiKey, saveWorkClawUserId } from './config-writer.js'
15
- import { buildInboundContext, parseWorkClawMessage } from './message-context.js'
5
+ import type { ResolvedWorkclawAccount, WorkClawBaseConfig, WorkClawAccountConfig } from "../types.js";
6
+ import { parseWorkClawMessage, type ParseWorkClawResult } from "./message-context.js";
7
+ import { buildInboundContext, type InboundMessage } from "./message-context.js";
8
+ import { handleAgentCreated, handleAgentUpdated, handleAgentDeleted } from "./agent-handlers.js";
9
+ import { saveOpenConversationId, saveWorkClawAgentId, saveWorkClawApiKey, saveWorkClawUserId } from "./config-writer.js";
10
+ import { getWorkclawWsConnection, setLastInboundAt } from "../runtime.js";
11
+ import { getWorkclawRuntime } from "../runtime.js";
12
+ import { sendMessageWorkclaw } from "../outbound/index.js";
13
+ import { resolveAccountByUserIdAndAgentId, resolveWorkclawAccountWithCache } from "../accounts.js";
14
+ import { getWorkclawAccessToken, getWorkclawModelConfigByAppKey } from "../connection/workclaw-client.js";
15
+ import { setToolContext } from "../runtime.js";
16
+ import { handleSkillsEvent } from "./skills-handler.js";
17
+ import { handleToolsListEvent as doHandleToolsListEvent } from "./tools-list-handler.js";
18
+ import { handleSkillsListEvent as doHandleSkillsListEvent } from "./skills-list-handler.js";
19
+ import { request } from "undici";
16
20
 
17
21
  export interface MessageDispatcherContext {
18
- accountId: string
19
- account: ResolvedOpenclawWorkclawAccount
20
- cfg: any
21
- baseConfig: WorkClawBaseConfig
22
- accountConfig: WorkClawAccountConfig
23
- log?: {
24
- info?: (msg: string) => void
25
- warn?: (msg: string) => void
26
- error?: (msg: string) => void
27
- debug?: (msg: string) => void
28
- }
29
- scheduleReconnect: () => void
22
+ accountId: string;
23
+ account: ResolvedWorkclawAccount;
24
+ cfg: any;
25
+ baseConfig: WorkClawBaseConfig;
26
+ accountConfig: WorkClawAccountConfig;
27
+ log?: {
28
+ info?: (msg: string) => void;
29
+ warn?: (msg: string) => void;
30
+ error?: (msg: string) => void;
31
+ debug?: (msg: string) => void;
32
+ };
33
+ scheduleReconnect: () => void;
30
34
  }
31
35
 
32
36
  /** Log an error safely */
33
- function logError(log: MessageDispatcherContext['log'], context: string, err: unknown): void {
34
- const errAny = err as { stack?: string }
35
- const stack = typeof errAny?.stack === 'string' ? `\nStack: ${errAny.stack}` : ''
36
- log?.error?.(`Dispatcher ${context}: ${String(err)}${stack}`)
37
+ function logError(log: MessageDispatcherContext["log"], context: string, err: unknown): void {
38
+ const errAny = err as { stack?: string };
39
+ const stack = typeof errAny?.stack === "string" ? `\nStack: ${errAny.stack}` : "";
40
+ log?.error?.(`Dispatcher ${context}: ${String(err)}${stack}`);
37
41
  }
38
42
 
39
43
  // ---------------------------------------------------------------------------
@@ -41,377 +45,380 @@ function logError(log: MessageDispatcherContext['log'], context: string, err: un
41
45
  // ---------------------------------------------------------------------------
42
46
 
43
47
  async function handlePing(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
44
- const { accountId, log } = ctx
45
- const ws = getOpenclawWorkclawWsConnection(accountId)
46
- if (!ws)
47
- return
48
-
49
- try {
50
- const pongResponse = {
51
- code: 200,
52
- message: 'pong',
53
- metadata: { contentType: 'application/json' },
54
- data: JSON.stringify(result.pongData),
48
+ const { accountId, log } = ctx;
49
+ const ws = getWorkclawWsConnection(accountId);
50
+ if (!ws) return;
51
+
52
+ try {
53
+ const pongResponse = {
54
+ code: 200,
55
+ message: "pong",
56
+ metadata: { contentType: "application/json" },
57
+ data: JSON.stringify(result.pongData),
58
+ };
59
+ ws.send(JSON.stringify(pongResponse));
60
+ log?.debug?.(`pong`);
61
+ } catch (err) {
62
+ logError(log, "Failed to send pong", err);
55
63
  }
56
- ws.send(JSON.stringify(pongResponse))
57
- log?.debug?.(`pong`)
58
- }
59
- catch (err) {
60
- logError(log, 'Failed to send pong', err)
61
- }
62
64
  }
63
65
 
64
66
  async function handleDisconnect(ctx: MessageDispatcherContext): Promise<void> {
65
- const { accountId, log, scheduleReconnect } = ctx
66
- const ws = getOpenclawWorkclawWsConnection(accountId)
67
- if (ws) {
68
- try {
69
- ws.close()
70
- log?.info?.(`Closed WebSocket due to disconnect command`)
71
- }
72
- catch (err) {
73
- logError(log, 'Failed to close WebSocket', err)
67
+ const { accountId, log, scheduleReconnect } = ctx;
68
+ const ws = getWorkclawWsConnection(accountId);
69
+ if (ws) {
70
+ try {
71
+ ws.close();
72
+ log?.info?.(`Closed WebSocket due to disconnect command`);
73
+ } catch (err) {
74
+ logError(log, "Failed to close WebSocket", err);
75
+ }
76
+ } else {
77
+ log?.info?.(`WebSocket already cleared, triggering reconnect...`);
78
+ scheduleReconnect();
74
79
  }
75
- }
76
- else {
77
- log?.info?.(`WebSocket already cleared, triggering reconnect...`)
78
- scheduleReconnect()
79
- }
80
80
  }
81
81
 
82
82
  async function handleAgentEvent(
83
- ctx: MessageDispatcherContext,
84
- result: ParseWorkClawResult,
83
+ ctx: MessageDispatcherContext,
84
+ result: ParseWorkClawResult,
85
85
  ): Promise<void> {
86
- const { cfg, log, baseConfig, accountId } = ctx
87
- const eventData = result.eventData
88
- if (!eventData) {
89
- log?.warn?.(`Received ${result.type} event but no data`)
90
- return
91
- }
92
-
93
- const agentId = String(eventData.id || '')
94
- const userId = String(eventData.futureId || '')
95
-
96
- if (!agentId) {
97
- log?.warn?.(`Received ${result.type} event but missing agent id`)
98
- return
99
- }
100
-
101
- log?.info?.(`Processing ${result.type} event for agent ${agentId}, userId: ${userId}`)
102
-
103
- const accountData = {
104
- agentId,
105
- userId,
106
- nickName: eventData.nickName,
107
- phone: eventData.phone,
108
- status: eventData.status,
109
- introduction: eventData.introduction,
110
- name: eventData.name,
111
- tip: eventData.tip,
112
- characterSettings: eventData.characterSettings,
113
- personFeatures: eventData.personFeatures,
114
- workFeatures: eventData.workFeatures,
115
- learningFeatures: eventData.learningFeatures,
116
- socializeFeatures: eventData.socializeFeatures,
117
- job: eventData.job,
118
- city: eventData.city,
119
- }
120
-
121
- switch (result.type) {
122
- case 'agent_created':
123
- await handleAgentCreated(accountData, cfg, log)
124
- handleAgentCreatedMessage(baseConfig, agentId, accountId, log)
125
- break
126
- case 'agent_updated':
127
- await handleAgentUpdated(accountData, cfg, log)
128
- break
129
- case 'agent_deleted':
130
- await handleAgentDeleted(accountData, cfg, log)
131
- break
132
- }
86
+ const { cfg, log, baseConfig, accountId } = ctx;
87
+ const eventData = result.eventData;
88
+ if (!eventData) {
89
+ log?.warn?.(`Received ${result.type} event but no data`);
90
+ return;
91
+ }
92
+
93
+ const agentId = String(eventData.id || "");
94
+ const userId = String(eventData.futureId || "");
95
+
96
+ if (!agentId) {
97
+ log?.warn?.(`Received ${result.type} event but missing agent id`);
98
+ return;
99
+ }
100
+
101
+ log?.info?.(`Processing ${result.type} event for agent ${agentId}, userId: ${userId}`);
102
+
103
+ const accountData = {
104
+ agentId,
105
+ userId,
106
+ nickName: eventData.nickName,
107
+ phone: eventData.phone,
108
+ status: eventData.status,
109
+ introduction: eventData.introduction,
110
+ name: eventData.name,
111
+ tip: eventData.tip,
112
+ characterSettings: eventData.characterSettings,
113
+ personFeatures: eventData.personFeatures,
114
+ workFeatures: eventData.workFeatures,
115
+ learningFeatures: eventData.learningFeatures,
116
+ socializeFeatures: eventData.socializeFeatures,
117
+ job: eventData.job,
118
+ city: eventData.city,
119
+ };
120
+
121
+ switch (result.type) {
122
+ case "agent_created":
123
+ await handleAgentCreated(accountData, cfg, log);
124
+ handleAgentCreatedMessage(baseConfig, agentId, accountId, log)
125
+ break;
126
+ case "agent_updated":
127
+ await handleAgentUpdated(accountData, cfg, log);
128
+ break;
129
+ case "agent_deleted":
130
+ await handleAgentDeleted(accountData, cfg, log);
131
+ break;
132
+ }
133
133
  }
134
134
 
135
135
  async function handleSkillsProcessingEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
136
- const { accountId, baseConfig, log } = ctx
137
- if (!result.eventData)
138
- return
139
- try {
140
- const { handleSkillsEvent } = await import('./skills-handler.js')
141
- const connConfig = {
142
- baseUrl: baseConfig.baseUrl || '',
143
- websocketUrl: baseConfig.websocketUrl,
144
- appKey: baseConfig.appKey,
145
- appSecret: baseConfig.appSecret,
146
- localIp: baseConfig.localIp,
147
- allowInsecureTls: baseConfig.allowInsecureTls,
148
- requestTimeout: baseConfig.requestTimeout,
136
+ const { accountId, baseConfig, log } = ctx;
137
+ if (!result.eventData) return;
138
+ try {
139
+ const connConfig = {
140
+ baseUrl: baseConfig.baseUrl || "",
141
+ websocketUrl: baseConfig.websocketUrl,
142
+ appKey: baseConfig.appKey,
143
+ appSecret: baseConfig.appSecret,
144
+ localIp: baseConfig.localIp,
145
+ allowInsecureTls: baseConfig.allowInsecureTls,
146
+ requestTimeout: baseConfig.requestTimeout,
147
+ };
148
+ const tokenCacheKey = baseConfig.appKey || accountId;
149
+ const token = await getWorkclawAccessToken(tokenCacheKey, connConfig);
150
+ await handleSkillsEvent(result.eventData, token, connConfig.baseUrl, connConfig.appKey || accountId, log);
151
+ } catch (err) {
152
+ logError(log, "handleSkillsEvent failed", err);
149
153
  }
150
- const tokenCacheKey = baseConfig.appKey || accountId
151
- const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
152
- await handleSkillsEvent(result.eventData, token, connConfig.baseUrl, connConfig.appKey || accountId, log)
153
- }
154
- catch (err) {
155
- logError(log, 'handleSkillsEvent failed', err)
156
- }
157
154
  }
158
155
 
159
156
  async function handleToolsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
160
- const { accountId, baseConfig, log } = ctx
161
- if (!result.eventData)
162
- return
163
- try {
164
- const { handleToolsListEvent } = await import('./tools-list-handler.js')
165
- const connConfig = {
166
- baseUrl: baseConfig.baseUrl || '',
167
- websocketUrl: baseConfig.websocketUrl,
168
- appKey: baseConfig.appKey,
169
- appSecret: baseConfig.appSecret,
170
- localIp: baseConfig.localIp,
171
- allowInsecureTls: baseConfig.allowInsecureTls,
172
- requestTimeout: baseConfig.requestTimeout,
157
+ const { accountId, baseConfig, log } = ctx;
158
+ if (!result.eventData) return;
159
+ try {
160
+ const connConfig = {
161
+ baseUrl: baseConfig.baseUrl || "",
162
+ websocketUrl: baseConfig.websocketUrl,
163
+ appKey: baseConfig.appKey,
164
+ appSecret: baseConfig.appSecret,
165
+ localIp: baseConfig.localIp,
166
+ allowInsecureTls: baseConfig.allowInsecureTls,
167
+ requestTimeout: baseConfig.requestTimeout,
168
+ };
169
+ const tokenCacheKey = baseConfig.appKey || accountId;
170
+ const token = await getWorkclawAccessToken(tokenCacheKey, connConfig);
171
+ await doHandleToolsListEvent(result.eventData, connConfig.baseUrl, token, log);
172
+ } catch (err) {
173
+ logError(log, "handleToolsListEvent failed", err);
173
174
  }
174
- const tokenCacheKey = baseConfig.appKey || accountId
175
- const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
176
- await handleToolsListEvent(result.eventData, connConfig.baseUrl, token, log)
177
- }
178
- catch (err) {
179
- logError(log, 'handleToolsListEvent failed', err)
180
- }
181
175
  }
182
176
 
183
177
  async function handleSkillsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
184
- const { accountId, baseConfig, log } = ctx
185
- if (!result.eventData)
186
- return
187
- try {
188
- const { handleSkillsListEvent } = await import('./skills-list-handler.js')
189
- const connConfig = {
190
- baseUrl: baseConfig.baseUrl || '',
191
- websocketUrl: baseConfig.websocketUrl,
192
- appKey: baseConfig.appKey,
193
- appSecret: baseConfig.appSecret,
194
- localIp: baseConfig.localIp,
195
- allowInsecureTls: baseConfig.allowInsecureTls,
196
- requestTimeout: baseConfig.requestTimeout,
178
+ const { accountId, baseConfig, log } = ctx;
179
+ if (!result.eventData) return;
180
+ try {
181
+ const connConfig = {
182
+ baseUrl: baseConfig.baseUrl || "",
183
+ websocketUrl: baseConfig.websocketUrl,
184
+ appKey: baseConfig.appKey,
185
+ appSecret: baseConfig.appSecret,
186
+ localIp: baseConfig.localIp,
187
+ allowInsecureTls: baseConfig.allowInsecureTls,
188
+ requestTimeout: baseConfig.requestTimeout,
189
+ };
190
+ const tokenCacheKey = baseConfig.appKey || accountId;
191
+ const token = await getWorkclawAccessToken(tokenCacheKey, connConfig);
192
+ await doHandleSkillsListEvent(result.eventData, connConfig.baseUrl, token, log);
193
+ } catch (err) {
194
+ logError(log, "handleSkillsListEvent failed", err);
197
195
  }
198
- const tokenCacheKey = baseConfig.appKey || accountId
199
- const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
200
- await handleSkillsListEvent(result.eventData, connConfig.baseUrl, token, log)
201
- }
202
- catch (err) {
203
- logError(log, 'handleSkillsListEvent failed', err)
204
- }
205
196
  }
206
197
 
207
198
  async function handleInitAgentEvent(
208
- ctx: MessageDispatcherContext,
209
- result: ParseWorkClawResult,
199
+ ctx: MessageDispatcherContext,
200
+ result: ParseWorkClawResult,
210
201
  ): Promise<void> {
211
- const { accountId, cfg, baseConfig, log } = ctx
212
- const initStartedAt = Date.now()
213
- const eventData = result.eventData
214
-
215
- if (!eventData) {
216
- log?.warn?.(`init_agent received but no data`)
217
- return
218
- }
219
-
220
- log?.info?.(
221
- `init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`,
222
- )
223
-
224
- const connConfig = {
225
- baseUrl: baseConfig.baseUrl || '',
226
- websocketUrl: baseConfig.websocketUrl,
227
- appKey: baseConfig.appKey,
228
- appSecret: baseConfig.appSecret,
229
- localIp: baseConfig.localIp,
230
- allowInsecureTls: baseConfig.allowInsecureTls,
231
- requestTimeout: baseConfig.requestTimeout,
232
- }
233
-
234
- // Fetch and save apiKey
235
- try {
236
- const { getOpenclawWorkclawModelConfigByAppKey } = await import('../connection/workclaw-client.js')
237
- const tokenCacheKey = baseConfig.appKey || accountId
238
- const { baseUrl: modelBaseUrl, apiKey } = await getOpenclawWorkclawModelConfigByAppKey(tokenCacheKey, connConfig)
239
- const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : 'missing'
240
-
241
- log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`)
242
-
243
- await saveWorkClawApiKey(apiKey, cfg, log)
244
- log?.info?.(`init_agent model config saved elapsedMs=${Date.now() - initStartedAt}`)
245
- }
246
- catch (err) {
247
- logError(log, 'init_agent model fetch/save failed', err)
248
- }
249
-
250
- // Save default agentId
251
- const agentId = String(eventData?.id || eventData?.agentId || '')
252
- if (agentId) {
253
- try {
254
- await saveWorkClawAgentId('default', agentId, cfg, log)
255
- log?.info?.(`init_agent saved default agentId=${agentId}`)
256
- }
257
- catch (err) {
258
- logError(log, 'init_agent agentId save failed', err)
202
+ const { accountId, cfg, baseConfig, log } = ctx;
203
+ const initStartedAt = Date.now();
204
+ const eventData = result.eventData;
205
+
206
+ if (!eventData) {
207
+ log?.warn?.(`init_agent received but no data`);
208
+ return;
259
209
  }
260
- }
261
210
 
262
- // Save default userId
263
- const userId = String(eventData?.futureId || eventData?.userId || '')
264
- if (userId) {
211
+ log?.info?.(
212
+ `init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`,
213
+ );
214
+
215
+ const connConfig = {
216
+ baseUrl: baseConfig.baseUrl || "",
217
+ websocketUrl: baseConfig.websocketUrl,
218
+ appKey: baseConfig.appKey,
219
+ appSecret: baseConfig.appSecret,
220
+ localIp: baseConfig.localIp,
221
+ allowInsecureTls: baseConfig.allowInsecureTls,
222
+ requestTimeout: baseConfig.requestTimeout,
223
+ };
224
+
225
+ // Fetch and save apiKey
265
226
  try {
266
- await saveWorkClawUserId('default', userId, cfg, log)
267
- log?.info?.(`init_agent saved default userId=${userId}`)
227
+ const tokenCacheKey = baseConfig.appKey || accountId;
228
+ const { baseUrl: modelBaseUrl, apiKey } = await getWorkclawModelConfigByAppKey(tokenCacheKey, connConfig);
229
+ const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : "missing";
230
+
231
+ log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`);
232
+
233
+
234
+ await saveWorkClawApiKey(apiKey, cfg, log);
235
+ log?.info?.(`init_agent model config saved elapsedMs=${Date.now() - initStartedAt}`);
236
+ } catch (err) {
237
+ logError(log, "init_agent model fetch/save failed", err);
268
238
  }
269
- catch (err) {
270
- logError(log, 'init_agent userId save failed', err)
239
+
240
+ // Save default agentId
241
+ const agentId = String(eventData?.id || eventData?.agentId || "");
242
+ if (agentId) {
243
+ try {
244
+ await saveWorkClawAgentId("default", agentId, cfg, log);
245
+ log?.info?.(`init_agent saved default agentId=${agentId}`);
246
+ } catch (err) {
247
+ logError(log, "init_agent agentId save failed", err);
248
+ }
271
249
  }
272
- }
273
250
 
274
- log?.info?.(`init_agent handled elapsedMs=${Date.now() - initStartedAt}`)
251
+ // Save default userId
252
+ const userId = String(eventData?.futureId || eventData?.userId || "");
253
+ if (userId) {
254
+ try {
255
+ await saveWorkClawUserId("default", userId, cfg, log);
256
+ log?.info?.(`init_agent saved default userId=${userId}`);
257
+ } catch (err) {
258
+ logError(log, "init_agent userId save failed", err);
259
+ }
260
+ }
261
+
262
+ log?.info?.(`init_agent handled elapsedMs=${Date.now() - initStartedAt}`);
275
263
  }
276
264
 
277
265
  async function handleAgentMessage(
278
- ctx: MessageDispatcherContext,
279
- data: string,
280
- result: ParseWorkClawResult,
266
+ ctx: MessageDispatcherContext,
267
+ data: string,
268
+ result: ParseWorkClawResult,
281
269
  ): Promise<void> {
282
- const { accountId, account, cfg, accountConfig, log } = ctx
283
- const { message } = result
284
- if (!message)
285
- return
286
-
287
- // 这里的msgAgentId是云平台的agentId
288
- const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message
289
-
290
- // Resolve account from message
291
- let messageAccountId = accountId
292
- let messageAccount = account
293
-
294
- if (userId && msgAgentId) {
295
- const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId))
296
- if (resolvedAccountId) {
297
- const resolved = resolveOpenclawWorkclawAccount({ cfg, accountId: resolvedAccountId })
298
- messageAccountId = resolvedAccountId
299
- messageAccount = resolved
300
- log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`)
301
- }
302
- else if (String(msgAgentId) !== String(accountConfig.agentId)) {
303
- log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`)
304
- return
270
+ const { accountId, account, cfg, accountConfig, log } = ctx;
271
+ const { message } = result;
272
+ if (!message) return;
273
+
274
+ // 这里的msgAgentId是云平台的agentId
275
+ const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message;
276
+
277
+ // Resolve account from message
278
+ let messageAccountId = accountId;
279
+ let messageAccount = account;
280
+
281
+ if (userId && msgAgentId) {
282
+ const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId));
283
+ if (resolvedAccountId) {
284
+ const resolved = resolveWorkclawAccountWithCache({ cfg, accountId: resolvedAccountId });
285
+ messageAccountId = resolvedAccountId;
286
+ messageAccount = resolved;
287
+ log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`);
288
+ } else if (String(msgAgentId) !== String(accountConfig.agentId)) {
289
+ log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`);
290
+ return;
291
+ }
305
292
  }
306
- }
307
293
 
308
- const resolvedAccountConfig = messageAccount.config as unknown as WorkClawAccountConfig
309
- const msgAgentIdResolved = msgAgentId ?? accountConfig.agentId
294
+ const resolvedAccountConfig = messageAccount.config as unknown as WorkClawAccountConfig;
295
+ const msgAgentIdResolved = msgAgentId ?? accountConfig.agentId;
310
296
 
311
- // Save agentId if not already set
312
- if (msgAgentId && !resolvedAccountConfig.agentId) {
313
- try {
314
- await saveWorkClawAgentId(messageAccountId, msgAgentId, cfg, log)
315
- log?.info?.(`Saved agentId ${msgAgentId} to account ${messageAccountId}`)
297
+ // Save agentId if not already set
298
+ if (msgAgentId && !resolvedAccountConfig.agentId) {
299
+ try {
300
+ await saveWorkClawAgentId(messageAccountId, msgAgentId, cfg, log);
301
+ log?.info?.(`Saved agentId ${msgAgentId} to account ${messageAccountId}`);
302
+ } catch (err) {
303
+ logError(log, "saveWorkClawAgentId failed", err);
304
+ }
316
305
  }
317
- catch (err) {
318
- logError(log, 'saveWorkClawAgentId failed', err)
306
+
307
+ // Save openConversationId if new
308
+ if (openConversationId && openConversationId !== resolvedAccountConfig.openConversationId) {
309
+ try {
310
+ await saveOpenConversationId(messageAccountId, openConversationId, cfg, log);
311
+ log?.info?.(`Saved openConversationId ${openConversationId} to account ${messageAccountId}`);
312
+ } catch (err) {
313
+ logError(log, "saveOpenConversationId failed", err);
314
+ }
319
315
  }
320
- }
321
316
 
322
- // Save openConversationId if new
323
- if (openConversationId && openConversationId !== resolvedAccountConfig.openConversationId) {
324
- try {
325
- await saveOpenConversationId(messageAccountId, openConversationId, cfg, log)
326
- log?.info?.(`Saved openConversationId ${openConversationId} to account ${messageAccountId}`)
317
+ // Save userId if not already set (directly to openclawWorkclaw level)
318
+ const workclawConfig = cfg?.channels?.['openclaw-workclaw'] || {};
319
+ if (userId && !workclawConfig.userId) {
320
+ try {
321
+ await saveWorkClawUserId(messageAccountId, userId, cfg, log);
322
+ log?.info?.(`Saved userId ${userId} to workclaw config`);
323
+ } catch (err) {
324
+ logError(log, "saveWorkClawUserId failed", err);
325
+ }
327
326
  }
328
- catch (err) {
329
- logError(log, 'saveOpenConversationId failed', err)
327
+
328
+ const resolvedOpenConversationId = openConversationId || resolvedAccountConfig.openConversationId || "";
329
+ const to = userId;
330
+ const bodyText = text || data;
331
+
332
+ if (!bodyText) {
333
+ log?.warn?.(`Empty body, ignoring`);
334
+ return;
330
335
  }
331
- }
332
336
 
333
- // Save userId if not already set (directly to openclawWorkclaw level)
334
- const openclawWorkclawConfig = cfg?.channels?.['openclaw-workclaw'] || {}
335
- if (userId && !openclawWorkclawConfig.userId) {
336
- try {
337
- await saveWorkClawUserId(messageAccountId, userId, cfg, log)
338
- log?.info?.(`Saved userId ${userId} to openclawWorkclaw config`)
337
+ // Route via bindings
338
+ const bindings = cfg?.bindings ?? [];
339
+ let targetAgentId = "default";
340
+ const matchedBinding = bindings.find((binding: any) => {
341
+ const match = binding?.match;
342
+ if (!match || match.channel !== "openclaw-workclaw") return false;
343
+ return match.accountId === messageAccountId || match.accountId === "*";
344
+ });
345
+ if (matchedBinding) {
346
+ targetAgentId = matchedBinding.agentId;
347
+ log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`);
339
348
  }
340
- catch (err) {
341
- logError(log, 'saveWorkClawUserId failed', err)
349
+
350
+ if (!targetAgentId) {
351
+ log?.warn?.(`Missing target, ignoring`);
352
+ return;
342
353
  }
343
- }
344
-
345
- const resolvedOpenConversationId = openConversationId || resolvedAccountConfig.openConversationId || ''
346
- const to = userId
347
- const bodyText = text || data
348
-
349
- if (!bodyText) {
350
- log?.warn?.(`Empty body, ignoring`)
351
- return
352
- }
353
-
354
- // Route via bindings
355
- const bindings = cfg?.bindings ?? []
356
- let targetAgentId = 'default'
357
- const matchedBinding = bindings.find((binding: any) => {
358
- const match = binding?.match
359
- if (!match || match.channel !== 'openclaw-workclaw')
360
- return false
361
- return match.accountId === messageAccountId || match.accountId === '*'
362
- })
363
- if (matchedBinding) {
364
- targetAgentId = matchedBinding.agentId
365
- log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`)
366
- }
367
-
368
- if (!targetAgentId) {
369
- log?.warn?.(`Missing target, ignoring`)
370
- return
371
- }
372
-
373
- // 构建用于解析 sessionKey 的消息对象
374
- // to = agentId (智能体/机器人), from = userId (用户)
375
- const inboundMessage: InboundMessage = {
376
- text: bodyText,
377
- to,
378
- from: userId,
379
- chatType: 'dm',
380
- messageId: parsedMessageId,
381
- rawPayload: data,
382
- timestamp: Date.now(),
383
- }
384
-
385
- const pluginRuntime = getOpenclawWorkclawRuntime()
386
-
387
- // 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
388
- const finalized = await buildInboundContext(
389
- inboundMessage,
390
- messageAccountId,
391
- messageAccount.config,
392
- targetAgentId,
393
- pluginRuntime,
394
- true,
395
- log,
396
- )
397
-
398
- await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
399
- ctx: finalized,
400
- cfg,
401
- replyOptions: {
402
- onAgentRunStart(runId: string) {
403
- // 保存 runId 和 ctx 的关联,供 hook 使用
404
- setToolContext(runId, {
405
- target: targetAgentId,
406
- replyToMessageId: parsedMessageId,
407
- openConversationId: resolvedOpenConversationId,
408
- accountId: messageAccountId,
409
- agentId: String(msgAgentIdResolved),
410
- sessionKey: (finalized.SessionKey as string) || '',
411
- })
412
- log?.info?.(`[AGENT] onAgentRunStart: runId=${runId} target=${targetAgentId}`)
413
- },
414
- /* onToolStart(payload) {
354
+
355
+ // 构建用于解析 sessionKey 的消息对象
356
+ // to = agentId (智能体/机器人), from = userId (用户)
357
+ const attachments = message.attachments
358
+ const inboundMessage: InboundMessage = {
359
+ text: bodyText,
360
+ to: to,
361
+ from: userId,
362
+ chatType: "dm",
363
+ messageId: parsedMessageId,
364
+ rawPayload: data,
365
+ timestamp: Date.now(),
366
+ attachments: attachments,
367
+ };
368
+ log?.info?.(`handleAgentMessage: text=${bodyText?.substring(0, 50)}, attachments=${attachments?.length || 0}`)
369
+ const pluginRuntime = getWorkclawRuntime();
370
+
371
+ // 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
372
+ const finalized = await buildInboundContext(
373
+ inboundMessage,
374
+ messageAccountId,
375
+ messageAccount.config,
376
+ targetAgentId,
377
+ pluginRuntime,
378
+ true,
379
+ log,
380
+ );
381
+
382
+ // 维护当前工具名称状态
383
+ let currentToolName: string | null = null;
384
+ // 追踪 final 是否已投递(框架在流式输出时会 drop final,需要手动补发 last: true)
385
+ let finalDelivered = false;
386
+
387
+ // 发送工具消息的辅助函数
388
+ const sendToolMessage = async (text: string, isStart: boolean = false) => {
389
+ try {
390
+ await sendMessageWorkclaw({
391
+ cfg,
392
+ to: to,
393
+ text,
394
+ accountId: messageAccountId,
395
+ openConversationId: resolvedOpenConversationId,
396
+ agentId: msgAgentIdResolved,
397
+ replyToMessageId: parsedMessageId,
398
+ last: false,
399
+ });
400
+ } catch (err) {
401
+ logError(log, isStart ? "Tool start message send failed" : "Tool result message send failed", err);
402
+ }
403
+ };
404
+
405
+ await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
406
+ ctx: finalized,
407
+ cfg,
408
+ replyOptions: {
409
+ onAgentRunStart(runId: string) {
410
+ // 保存 runId 和 ctx 的关联,供 hook 使用
411
+ setToolContext(runId, {
412
+ target: targetAgentId,
413
+ replyToMessageId: parsedMessageId,
414
+ openConversationId: resolvedOpenConversationId,
415
+ accountId: messageAccountId,
416
+ agentId: String(msgAgentIdResolved),
417
+ sessionKey: (finalized.SessionKey as string) || "",
418
+ });
419
+ log?.info?.(`[AGENT] onAgentRunStart: runId=${runId} target=${targetAgentId}`);
420
+ },
421
+ /*onToolStart(payload) {
415
422
  log?.info?.(`[AGENT] onToolStart: ${JSON.stringify(payload)}`);
416
423
  if (payload.phase === "start") {
417
424
  currentToolName = payload.name ?? null;
@@ -421,37 +428,36 @@ async function handleAgentMessage(
421
428
  logError(log, "Tool start hint send failed", err);
422
429
  });
423
430
  }
424
- } */
425
- },
426
- dispatcherOptions: {
427
- deliver: async (payload: any, info?: any) => {
428
- // 记录每次 deliver 调用,帮助调试 info.kind 问题
429
- log?.info?.(`[DELIVER] deliver called: info.kind=${info?.kind}, payload.text=${String(payload?.text ?? '').substring(0, 100)}`)
430
- const resolvedReplyToId = payload?.replyToId || parsedMessageId
431
- const textOut = String(payload.text ?? payload.body ?? '')
432
- const target = to || String(payload.to ?? '')
433
-
434
- if (!target) {
435
- log?.warn?.(`Skipping reply: no target`)
436
- return
437
- }
431
+ }*/
432
+ },
433
+ dispatcherOptions: {
434
+ deliver: async (payload: any, info?: any) => {
435
+ // 记录每次 deliver 调用,帮助调试 info.kind 问题
436
+ log?.info?.(`[DELIVER] deliver called: info.kind=${info?.kind}, payload.text=${String(payload?.text ?? "").substring(0, 100)}`);
437
+ const resolvedReplyToId = payload?.replyToId || parsedMessageId;
438
+ let textOut = String(payload.text ?? payload.body ?? "");
439
+ const target = to || String(payload.to ?? "");
440
+
441
+ if (!target) {
442
+ log?.warn?.(`Skipping reply: no target`);
443
+ return;
444
+ }
438
445
 
439
- if (!textOut.trim()) {
440
- log?.warn?.(`Empty reply text, skipping`)
441
- return
442
- }
446
+ if (!textOut.trim()) {
447
+ log?.warn?.(`Empty reply text, skipping`);
448
+ return;
449
+ }
443
450
 
444
- if (info?.kind === 'tool')
445
- return
451
+ if (info?.kind === "tool") return;
446
452
 
447
- // 处理工具类型的 deliver
448
- /* if (info?.kind === "tool") {
453
+ // 处理工具类型的 deliver
454
+ /*if (info?.kind === "tool") {
449
455
  // log?.info?.(`[DELIVER] tool reply: ${JSON.stringify(payload)}`);
450
456
  const toolText = payload.text ?? "";
451
457
  if (toolText.trim()) {
452
458
  // 使用 formattedToolOutput 格式发送
453
459
  try {
454
- await sendMessageOpenclawWorkclaw({
460
+ await sendMessageWorkclaw({
455
461
  cfg,
456
462
  to: targetAgentId,
457
463
  text: JSON.stringify({
@@ -470,132 +476,166 @@ async function handleAgentMessage(
470
476
  }
471
477
  }
472
478
  return;
473
- } */
479
+ }*/
480
+
481
+ // final 时清空工具状态
482
+ if (info?.kind === "final") {
483
+ currentToolName = null;
484
+ finalDelivered = true;
485
+ log?.info?.(`[DELIVER] final received, cleared currentToolName`);
486
+ }
474
487
 
475
- log?.info?.(`[DELIVER] is last: ${info?.kind === 'final'}`)
488
+ // 明确设置 last:final 时为 true,否则为 false(显式转换,避免 undefined)
489
+ const isFinal = info?.kind === "final";
490
+ const isLast = isFinal ? true : false;
491
+ log?.info?.(`[DELIVER] is last: ${isLast} (info.kind=${info?.kind})`);
492
+
493
+ try {
494
+ const result = await sendMessageWorkclaw({
495
+ cfg, to: target, text: textOut,
496
+ accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
497
+ agentId: msgAgentIdResolved, replyToMessageId: resolvedReplyToId,
498
+ last: isLast,
499
+ });
500
+ log?.info?.(`Reply sent messageId=${result.messageId}`);
501
+ } catch (err) {
502
+ logError(log, "Reply send failed", err);
503
+ throw err;
504
+ }
505
+ },
506
+ },
507
+ });
476
508
 
509
+ // 框架在流式输出时会 drop final payload,检测到时应手动补发 last: true
510
+ if (!finalDelivered) {
511
+ log?.warn?.(`[DELIVER] final was dropped by framework, sending last: true`);
477
512
  try {
478
- const result = await sendMessageOpenclawWorkclaw({
479
- cfg,
480
- to: target,
481
- text: textOut,
482
- accountId: messageAccountId,
483
- openConversationId: resolvedOpenConversationId,
484
- agentId: msgAgentIdResolved,
485
- replyToMessageId: resolvedReplyToId,
486
- last: info?.kind === 'final',
487
- })
488
- log?.info?.(`Reply sent messageId=${result.messageId}`)
513
+ // 发送一个空格作为占位内容,确保 last: true 能被服务端接受
514
+ await sendMessageWorkclaw({
515
+ cfg, to: to || String(userId || ""),
516
+ text: "",
517
+ accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
518
+ agentId: msgAgentIdResolved, replyToMessageId: parsedMessageId,
519
+ last: true,
520
+ });
521
+ } catch (err) {
522
+ // 服务端可能拒绝空内容的 last 信号,忽略即可
523
+ const errStr = String(err);
524
+ if (errStr.includes("content can't be empty")) {
525
+ log?.info?.(`[DELIVER] last signal skipped (empty content not supported)`);
526
+ } else {
527
+ logError(log, "Final/last signal send failed", err);
528
+ }
489
529
  }
490
- catch (err) {
491
- logError(log, 'Reply send failed', err)
492
- throw err
493
- }
494
- },
495
- },
496
- })
530
+ }
497
531
  }
498
532
 
499
533
  // ---------------------------------------------------------------------------
500
534
  // Main dispatcher
501
535
  // ---------------------------------------------------------------------------
502
536
 
503
- export async function dispatchOpenclawWorkclawMessage(
504
- data: string,
505
- ctx: MessageDispatcherContext,
537
+ export async function dispatchWorkclawMessage(
538
+ data: string,
539
+ ctx: MessageDispatcherContext,
506
540
  ): Promise<void> {
507
- const { log } = ctx
508
-
509
- const rawHasAuthToken
510
- = data.includes('authToken')
511
- || data.toLowerCase().includes('authorization')
512
- || data.toLowerCase().includes('bearer ')
513
- const rawPreview = data.length > 300 ? `${data.slice(0, 300)}...` : data
514
- log?.info?.(
515
- `Raw message bytes=${data.length} hasAuthToken=${rawHasAuthToken} preview=${rawPreview}`,
516
- )
517
-
518
- const result = parseWorkClawMessage(data, log)
519
- if (!result) {
520
- log?.warn?.(`Failed to parse message`)
521
- return
522
- }
523
-
524
- log?.info?.(`Message type=${result.type}`)
525
-
526
- switch (result.type) {
527
- case 'ping':
528
- await handlePing(ctx, result)
529
- return
530
-
531
- case 'disconnect':
532
- await handleDisconnect(ctx)
533
- return
534
-
535
- case 'agent_created':
536
- case 'agent_updated':
537
- case 'agent_deleted':
538
- await handleAgentEvent(ctx, result)
539
- return
540
-
541
- case 'skills_event':
542
- await handleSkillsProcessingEvent(ctx, result)
543
- return
544
-
545
- case 'tools_list':
546
- await handleToolsListEvent(ctx, result)
547
- return
548
-
549
- case 'skills_list':
550
- await handleSkillsListEvent(ctx, result)
551
- return
552
-
553
- case 'init_agent':
554
- await handleInitAgentEvent(ctx, result)
555
- return
556
-
557
- case 'ignored':
558
- log?.info?.(`Ignoring message type=${result.type}`)
559
- return
560
-
561
- case 'agent_message':
562
- await handleAgentMessage(ctx, data, result)
563
- return
564
-
565
- default:
566
- log?.info?.(`Unknown message type: ${result.type}, ignoring`)
567
- }
568
- }
541
+ const { log } = ctx;
542
+
543
+ const rawHasAuthToken =
544
+ data.includes("authToken") ||
545
+ data.toLowerCase().includes("authorization") ||
546
+ data.toLowerCase().includes("bearer ");
547
+ const rawPreview = data.length > 300 ? `${data.slice(0, 300)}...` : data;
548
+ /*log?.info?.(
549
+ `Raw message bytes=${data.length} hasAuthToken=${rawHasAuthToken} preview=${rawPreview}`,
550
+ );*/
551
+
552
+ const result = parseWorkClawMessage(data, log);
553
+ if (!result) {
554
+ log?.warn?.(`Failed to parse message`);
555
+ return;
556
+ }
569
557
 
570
- // 创建成功回调
571
- async function handleAgentCreatedMessage(baseConfig: WorkClawBaseConfig, agentId: string, accountId: string, log?: any): Promise<void> {
572
- // 通知
573
- try {
574
- const callbackUrl = `${baseConfig.baseUrl}/open-apis/instance/agent-created/status`
575
- const tokenCacheKey = baseConfig.appKey || accountId
576
- const callbackData = {
577
- agentId,
578
- success: true,
558
+ log?.info?.(`Message type=${result.type}`);
559
+
560
+ // 更新上次消息时间(始终用 accountId,因为每个账号的入站时间需要独立追踪)
561
+ const { accountId } = ctx;
562
+ if (accountId) {
563
+ setLastInboundAt(accountId, Date.now());
579
564
  }
580
565
 
581
- log?.info?.(`[AgentCreated] Calling callback API: ${callbackUrl}`)
582
- const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, baseConfig)
583
-
584
- const { request } = await import('undici')
585
- const response = await request(callbackUrl, {
586
- method: 'POST',
587
- headers: {
588
- 'Content-Type': 'application/json',
589
- 'Authorization': `Bearer ${token}`,
590
- },
591
- body: JSON.stringify(callbackData),
592
- })
593
-
594
- const responseBody = await response.body.text()
595
- log?.info?.(`[AgentCreated] Callback API response: status=${response.statusCode}, body=${responseBody}`)
596
- }
597
- catch (callbackErr) {
598
- // 接口调用失败不影响主流程,只记录错误
599
- log?.error?.(`[AgentCreated] Callback API failed: ${String(callbackErr)}`)
600
- }
566
+ switch (result.type) {
567
+ case "ping":
568
+ await handlePing(ctx, result);
569
+ return;
570
+
571
+ case "disconnect":
572
+ await handleDisconnect(ctx);
573
+ return;
574
+
575
+ case "agent_created":
576
+ case "agent_updated":
577
+ case "agent_deleted":
578
+ await handleAgentEvent(ctx, result);
579
+ return;
580
+
581
+ case "skills_event":
582
+ await handleSkillsProcessingEvent(ctx, result);
583
+ return;
584
+
585
+ case "tools_list":
586
+ await handleToolsListEvent(ctx, result);
587
+ return;
588
+
589
+ case "skills_list":
590
+ await handleSkillsListEvent(ctx, result);
591
+ return;
592
+
593
+ case "init_agent":
594
+ await handleInitAgentEvent(ctx, result);
595
+ return;
596
+
597
+ case "ignored":
598
+ log?.info?.(`Ignoring message type=${result.type}`);
599
+ return;
600
+
601
+ case "agent_message":
602
+ await handleAgentMessage(ctx, data, result);
603
+ return;
604
+
605
+ default:
606
+ log?.info?.(`Unknown message type: ${result.type}, ignoring`);
607
+ }
601
608
  }
609
+
610
+
611
+ // 创建成功回调
612
+ async function handleAgentCreatedMessage(baseConfig: WorkClawBaseConfig, agentId: string, accountId: string, log?: any) {
613
+ // 通知
614
+ try {
615
+ const callbackUrl = `${baseConfig.baseUrl}/open-apis/instance/agent-created/status`;
616
+ const tokenCacheKey = baseConfig.appKey || accountId;
617
+ const callbackData = {
618
+ "agentId": agentId,
619
+ "success": true
620
+ };
621
+
622
+ log?.info?.(`[AgentCreated] Calling callback API: ${callbackUrl}`);
623
+ const token = await getWorkclawAccessToken(tokenCacheKey, baseConfig);
624
+
625
+ const response = await request(callbackUrl, {
626
+ method: "POST",
627
+ headers: {
628
+ "Content-Type": "application/json",
629
+ "Authorization": `Bearer ${token}`,
630
+ },
631
+ body: JSON.stringify(callbackData),
632
+ });
633
+
634
+ const responseBody = await response.body.text();
635
+ log?.info?.(`[AgentCreated] Callback API response: status=${response.statusCode}, body=${responseBody}`);
636
+
637
+ } catch (callbackErr) {
638
+ // 接口调用失败不影响主流程,只记录错误
639
+ log?.error?.(`[AgentCreated] Callback API failed: ${String(callbackErr)}`);
640
+ }
641
+ }