@workclaw/openclaw-workclaw 1.0.18 → 1.0.19

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