dlinkclaw-openclaw 1.0.0

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 (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. package/package.json +95 -0
@@ -0,0 +1,1211 @@
1
+ import { MEDIA_DOCUMENT_PLACEHOLDER, MEDIA_IMAGE_PLACEHOLDER, MEDIA_VIDEO_PLACEHOLDER, MEDIA_VOICE_PLACEHOLDER } from "./const.js";
2
+ import { md5 } from "./crypto.js";
3
+ import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
4
+ import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
5
+ import { uuidStr } from "../utils.js";
6
+ //入站队列最大数
7
+ const MESSAGE_QUEUE_SIZE = 2_000;
8
+ // Message accumulator: stores the accumulated payload and original message fields by streamId
9
+ const messageAccumulators = new Map();
10
+ //依不同的中继及通信方式获取最大等待时间(被动回复时间,如超过该时间则改用主动回复)
11
+ const getExpireTime = (botType, websocket) => {
12
+ switch (botType) {
13
+ case "ewx":
14
+ //企业微信Websocket被动回复最长24小时,Webhook 1小时
15
+ return websocket ? (24 * 60 * 60 * 1000) : (60 * 60 * 1000);
16
+ case "qq":
17
+ case "dd":
18
+ case "feishu":
19
+ case "dLink":
20
+ return 24 * 60 * 60 * 1000;
21
+ }
22
+ };
23
+ /**
24
+ * mqtt接收到消息后预处理
25
+ * @param msg
26
+ * @param logger
27
+ */
28
+ export const messagePreviewHandler = (msg, accountId, logger) => {
29
+ logger.info(`account[${accountId}],[Message.ts(messagePreviewHandler)] message body:${msg.payload}`);
30
+ const now = Date.now();
31
+ //队列最多存放1000条消息,超过会移除最早一条
32
+ if (messageAccumulators.size >= MESSAGE_QUEUE_SIZE) {
33
+ const first = messageAccumulators.values().next().value;
34
+ messageAccumulators.delete(first.key);
35
+ logger.warn(`[messagePreviewHandler] account[${accountId}] The message [${first.key}] has been cleaned. `);
36
+ }
37
+ //清理超时队列
38
+ const expireList = Array.from(messageAccumulators.values()).filter(item => now >= item.expireTime);
39
+ if (expireList.length > 0) {
40
+ expireList.forEach(item => {
41
+ messageAccumulators.delete(item.key);
42
+ logger.warn(`[messagePreviewHandler] account[${accountId}] The message [${item.key}] has been cleaned. `);
43
+ });
44
+ }
45
+ //过滤重复的请求
46
+ if (messageAccumulators.has(msg.streamId)) {
47
+ logger.warn(`[messagePreviewHandler] account[${accountId}] The message has Multiple, Ignored`);
48
+ return {
49
+ key: msg.streamId,
50
+ accumulatedPayload: ''
51
+ };
52
+ }
53
+ //队列有效时间,超过该时间会自动被清理
54
+ const time = now + getExpireTime(msg.botType, msg.websocket);
55
+ //接收到的消息放入队列
56
+ messageAccumulators.set(msg.streamId, {
57
+ key: msg.streamId,
58
+ expireTime: time,
59
+ accumulatedPayload: '',
60
+ originalFields: {
61
+ toAgent: msg.toAgent,
62
+ fromPerson: msg.fromPerson,
63
+ msgId: msg.msgId,
64
+ botType: msg.botType,
65
+ contactType: msg.contactType,
66
+ contactId: msg.contactId,
67
+ notify: msg.notify,
68
+ notifyUrl: msg.notifyUrl,
69
+ chatId: msg.chatId,
70
+ group: msg.group,
71
+ groupIntroduction: msg.groupIntroduction,
72
+ reqId: msg.reqId,
73
+ websocket: msg.websocket,
74
+ askQuestion: msg.payload,
75
+ msgType: msg.msgType,
76
+ relayTimeRhreshold: time,
77
+ fromUsers: msg.fromUsers,
78
+ relayType: 'openClaw',
79
+ relayId: msg.relayId
80
+ }
81
+ });
82
+ msg.relayTimeRhreshold = time;
83
+ return {
84
+ key: msg.streamId,
85
+ accumulatedPayload: ''
86
+ };
87
+ };
88
+ /**
89
+ * 消息出站时构建回复消息
90
+ * @param param0
91
+ * @returns
92
+ */
93
+ export const buildDLReplyMessage = ({ streamId, content, finish, kind, runId }) => {
94
+ const accumulator = messageAccumulators.get(streamId);
95
+ if (!accumulator) {
96
+ //已超过队例时间,已被清除了
97
+ return undefined;
98
+ }
99
+ if (runId && runId.length > 0) {
100
+ //在agent开发执行任务时首先会输出该runId
101
+ accumulator.originalFields.runId = runId; //更新runId, 需要回传到服务端
102
+ }
103
+ accumulator.accumulatedPayload = content;
104
+ const resultMessage = {
105
+ payload: accumulator.accumulatedPayload,
106
+ finish: finish,
107
+ streamId: streamId,
108
+ msgId: accumulator.originalFields.msgId,
109
+ fromPerson: accumulator.originalFields.fromPerson,
110
+ toAgent: accumulator.originalFields.toAgent,
111
+ botType: accumulator.originalFields.botType,
112
+ contactType: accumulator.originalFields.contactType,
113
+ contactId: accumulator.originalFields.contactId,
114
+ notify: accumulator.originalFields.notify,
115
+ notifyUrl: accumulator.originalFields.notifyUrl,
116
+ chatId: accumulator.originalFields.chatId,
117
+ group: accumulator.originalFields.group,
118
+ reqId: accumulator.originalFields.reqId,
119
+ websocket: accumulator.originalFields.websocket,
120
+ askQuestion: accumulator.originalFields.askQuestion,
121
+ msgType: accumulator.originalFields.msgType,
122
+ timestamp: Date.now(),
123
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
124
+ relayTimeRhreshold: accumulator.originalFields.relayTimeRhreshold,
125
+ fromUsers: accumulator.originalFields.fromUsers,
126
+ kind,
127
+ runId: accumulator.originalFields.runId,
128
+ relayType: 'openClaw',
129
+ relayId: accumulator.originalFields.relayId
130
+ };
131
+ if (finish) {
132
+ messageAccumulators.delete(streamId);
133
+ }
134
+ return resultMessage;
135
+ };
136
+ /**
137
+ * 解析消息内容(支持单条消息、图文混排和引用消息)
138
+ * @returns 提取的文本数组、图片URL数组和引用消息内容
139
+ */
140
+ export function parseMessageContent(payload) {
141
+ const textParts = [];
142
+ const imageUrls = [];
143
+ const imageAesKeys = new Map();
144
+ const voiceUrls = [];
145
+ const voiceAesKeys = new Map();
146
+ const videoUrls = [];
147
+ const videoAesKeys = new Map();
148
+ const fileUrls = [];
149
+ const fileAesKeys = new Map();
150
+ let quoteContent;
151
+ let msgBody;
152
+ try {
153
+ msgBody = JSON.parse(payload);
154
+ }
155
+ catch (e) {
156
+ return undefined;
157
+ }
158
+ const msgtype = msgBody.msgType;
159
+ // 处理图文混排消息
160
+ switch (msgtype) {
161
+ case "text":
162
+ const textBody = msgBody;
163
+ if (textBody.text && textBody.text.length > 0) {
164
+ textParts.push(textBody.text);
165
+ }
166
+ const textQuote = textBody.quote;
167
+ if (textQuote) {
168
+ quoteContent = resolveQuote({
169
+ quote: textQuote,
170
+ quoteContent,
171
+ imageUrls,
172
+ imageAesKeys,
173
+ voiceUrls,
174
+ voiceAesKeys,
175
+ videoUrls,
176
+ videoAesKeys,
177
+ fileUrls,
178
+ fileAesKeys
179
+ });
180
+ }
181
+ break;
182
+ case "image":
183
+ const imageBody = msgBody;
184
+ if (imageBody.imageUrl && imageBody.imageUrl.length > 0) {
185
+ imageUrls.push(imageBody.imageUrl);
186
+ if (imageBody.aesKey && imageBody.aesKey.length > 0) {
187
+ imageAesKeys.set(md5(imageBody.imageUrl), imageBody.aesKey);
188
+ }
189
+ }
190
+ break;
191
+ case "voice":
192
+ const voiceBody = msgBody;
193
+ if (voiceBody.voiceUrl && voiceBody.voiceUrl.length > 0) {
194
+ voiceUrls.push(voiceBody.voiceUrl);
195
+ if (voiceBody.aesKey && voiceBody.aesKey.length > 0) {
196
+ voiceAesKeys.set(md5(voiceBody.voiceUrl), voiceBody.aesKey);
197
+ }
198
+ }
199
+ break;
200
+ case "video":
201
+ const videoBody = msgBody;
202
+ if (videoBody.videoUrl && videoBody.videoUrl.length > 0) {
203
+ videoUrls.push(videoBody.videoUrl);
204
+ if (videoBody.aesKey && videoBody.aesKey.length > 0) {
205
+ videoAesKeys.set(md5(videoBody.videoUrl), videoBody.aesKey);
206
+ }
207
+ }
208
+ break;
209
+ case "file":
210
+ const fileBody = msgBody;
211
+ if (fileBody.fileUrl && fileBody.fileUrl.length > 0) {
212
+ fileUrls.push(fileBody.fileUrl);
213
+ if (fileBody.aesKey && fileBody.aesKey.length > 0) {
214
+ fileAesKeys.set(md5(fileBody.fileUrl), fileBody.aesKey);
215
+ }
216
+ }
217
+ break;
218
+ case "mixed":
219
+ const mixedBody = msgBody;
220
+ for (const item of mixedBody.msgList) {
221
+ switch (item.msgType) {
222
+ case "text":
223
+ const text = item.text;
224
+ if (text && text.length > 0) {
225
+ textParts.push(text);
226
+ }
227
+ break;
228
+ case "image":
229
+ const image = item;
230
+ if (image.imageUrl && image.imageUrl.length > 0) {
231
+ imageUrls.push(image.imageUrl);
232
+ if (image.aesKey && image.aesKey.length > 0) {
233
+ imageAesKeys.set(md5(image.imageUrl), image.aesKey);
234
+ }
235
+ }
236
+ break;
237
+ case "voice":
238
+ const voice = item;
239
+ if (voice.voiceUrl && voice.voiceUrl.length > 0) {
240
+ voiceUrls.push(voice.voiceUrl);
241
+ if (voice.aesKey && voice.aesKey.length > 0) {
242
+ voiceAesKeys.set(md5(voice.voiceUrl), voice.aesKey);
243
+ }
244
+ }
245
+ break;
246
+ case "video":
247
+ const video = item;
248
+ if (video.videoUrl && video.videoUrl.length > 0) {
249
+ videoUrls.push(video.videoUrl);
250
+ if (video.aesKey && video.aesKey.length > 0) {
251
+ videoAesKeys.set(md5(video.videoUrl), video.aesKey);
252
+ }
253
+ }
254
+ break;
255
+ case "file":
256
+ const file = item;
257
+ if (file.fileUrl && file.fileUrl.length > 0) {
258
+ fileUrls.push(file.fileUrl);
259
+ if (file.aesKey && file.aesKey.length > 0) {
260
+ fileAesKeys.set(md5(file.fileUrl), file.aesKey);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ const quote = mixedBody.quote;
266
+ if (quote) {
267
+ quoteContent = resolveQuote({
268
+ quote,
269
+ quoteContent,
270
+ imageUrls,
271
+ imageAesKeys,
272
+ voiceUrls,
273
+ voiceAesKeys,
274
+ videoUrls,
275
+ videoAesKeys,
276
+ fileUrls,
277
+ fileAesKeys
278
+ });
279
+ }
280
+ break;
281
+ }
282
+ return {
283
+ textParts,
284
+ imageUrls,
285
+ imageAesKeys,
286
+ voiceUrls,
287
+ voiceAesKeys,
288
+ videoUrls,
289
+ videoAesKeys,
290
+ fileUrls,
291
+ fileAesKeys,
292
+ quoteContent
293
+ };
294
+ }
295
+ function resolveQuote(params) {
296
+ const { quote, quoteContent, imageUrls, imageAesKeys, voiceUrls, voiceAesKeys, videoUrls, videoAesKeys, fileUrls, fileAesKeys } = params;
297
+ let content = quoteContent ?? '';
298
+ if (quote.type === 'text') {
299
+ content = quote.text;
300
+ }
301
+ else if (quote.type === 'image') {
302
+ const body = quote;
303
+ if (body.imageUrl && body.imageUrl.length > 0) {
304
+ imageUrls.push(body.imageUrl);
305
+ if (body.aesKey && body.aesKey.length > 0) {
306
+ imageAesKeys.set(body.imageUrl, body.aesKey);
307
+ }
308
+ }
309
+ }
310
+ else if (quote.type === 'voice') {
311
+ const body = quote;
312
+ if (body.voiceUrl && body.voiceUrl.length > 0) {
313
+ voiceUrls.push(body.voiceUrl);
314
+ if (body.aesKey && body.aesKey.length > 0) {
315
+ voiceAesKeys.set(body.voiceUrl, body.aesKey);
316
+ }
317
+ }
318
+ }
319
+ else if (quote.type === 'video') {
320
+ const body = quote;
321
+ if (body.videoUrl && body.videoUrl.length > 0) {
322
+ videoUrls.push(body.videoUrl);
323
+ if (body.aesKey && body.aesKey.length > 0) {
324
+ videoAesKeys.set(body.videoUrl, body.aesKey);
325
+ }
326
+ }
327
+ }
328
+ else if (quote.type === 'file') {
329
+ const body = quote;
330
+ if (body.fileUrl && body.fileUrl.length > 0) {
331
+ fileUrls.push(body.fileUrl);
332
+ if (body.aesKey && body.aesKey.length > 0) {
333
+ fileAesKeys.set(body.fileUrl, body.aesKey);
334
+ }
335
+ }
336
+ }
337
+ else if (quote.type === 'mixed') {
338
+ const items = quote.msgList ?? [];
339
+ for (const item of items) {
340
+ if (item.msgType === 'text') {
341
+ const body = item;
342
+ if (body.text && body.text.length > 0) {
343
+ content += body.text;
344
+ }
345
+ }
346
+ else if (item.msgType === 'image') {
347
+ const body = item;
348
+ if (body.imageUrl && body.imageUrl.length > 0) {
349
+ imageUrls.push(body.imageUrl);
350
+ if (body.aesKey && body.aesKey.length > 0) {
351
+ imageAesKeys.set(body.imageUrl, body.aesKey);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ return content;
358
+ }
359
+ async function resetSession({ client, sessionKey, streamId, agentId }) {
360
+ //拦截/new,/reset 指令
361
+ //获取历史会话记录
362
+ const histories = await client.exeCmd({
363
+ cmd: 'chat.history',
364
+ requestId: uuidStr(36),
365
+ timestamp: Date.now(),
366
+ params: {
367
+ sessionKey,
368
+ limit: 100
369
+ }
370
+ });
371
+ if (!histories?.success || !histories.data) {
372
+ client.publishToServer({
373
+ streamId,
374
+ content: `[tag_error_session_history_fail]${histories?.error ?? ''}`,
375
+ finish: true,
376
+ kind: 'block'
377
+ });
378
+ return true;
379
+ }
380
+ //const sid: string = histories.data.sessionId;
381
+ const historyRecords = histories.data.messages ?? [];
382
+ const msgList = historyRecords.map((item) => {
383
+ const content = item.content;
384
+ if (typeof content === 'string') {
385
+ return `[${item.role}] ${item.content}`;
386
+ }
387
+ const items = content.map((msg) => {
388
+ try {
389
+ if (msg.type === 'text') {
390
+ return msg.text;
391
+ }
392
+ }
393
+ catch (e) {
394
+ }
395
+ return '';
396
+ });
397
+ const newItems = items.filter(rec => rec.length > 0);
398
+ return `[${item.role}] ${newItems.join(';')}`;
399
+ });
400
+ if (msgList.length === 0) {
401
+ client.publishToServer({
402
+ streamId,
403
+ content: '[tag_error_session_record_empty]',
404
+ finish: true,
405
+ kind: 'block'
406
+ });
407
+ return true;
408
+ }
409
+ const resetResult = await client.exeCmd({
410
+ cmd: 'sessions.reset',
411
+ requestId: uuidStr(36),
412
+ timestamp: Date.now(),
413
+ params: {
414
+ key: sessionKey,
415
+ reason: "new"
416
+ }
417
+ });
418
+ if (!resetResult?.success || !resetResult.data) {
419
+ client.publishToServer({
420
+ streamId,
421
+ content: '[tag_error_reset_session_fail]' + (resetResult?.error || ''),
422
+ kind: 'block',
423
+ finish: true,
424
+ });
425
+ return true;
426
+ }
427
+ const prompt = `请根据以下聊天记录,生成一个不超过128字的标题,概括这次对话的主题:
428
+ ${msgList.join('\n')}
429
+
430
+ 要求:
431
+ - 标题长度 ≤ 128 字
432
+ - 简洁准确地概括对话核心内容
433
+ - 直接输出标题,并在标题末尾符加上标记"[tag_reset]",不要多余文字`;
434
+ const askAgent = await client.exeCmd({
435
+ cmd: 'agent.ask',
436
+ requestId: uuidStr(36),
437
+ timestamp: Date.now(),
438
+ params: {
439
+ agentId,
440
+ message: prompt,
441
+ sessionKey,
442
+ deliver: false, //表示出站时不走deliver
443
+ idempotencyKey: uuidStr(36)
444
+ }
445
+ });
446
+ let askAgentError = '';
447
+ if (!askAgent?.success || !askAgent.data) {
448
+ askAgentError = `[tag_error_summarize_session_history_fail]\n ${askAgent?.error}`;
449
+ }
450
+ const status = askAgent?.data.status;
451
+ if (status !== 'accepted') {
452
+ askAgentError = `${agentId} [tag_error_reject_session_history]`;
453
+ }
454
+ if (askAgentError && askAgentError.length > 0) {
455
+ client.publishToServer({
456
+ streamId,
457
+ content: askAgentError,
458
+ kind: 'block',
459
+ finish: true,
460
+ });
461
+ return true;
462
+ }
463
+ const runId = askAgent?.data.runId;
464
+ const new_sessionId = resetResult.data.entry.sessionId;
465
+ if (new_sessionId) {
466
+ client.publishToServer({
467
+ streamId,
468
+ content: '[tag_new_session_success]',
469
+ finish: true,
470
+ kind: 'block',
471
+ session: {
472
+ sessionKey,
473
+ runId,
474
+ //sessionId: new_sessionId,
475
+ archived: false
476
+ }
477
+ });
478
+ return true;
479
+ }
480
+ return false;
481
+ }
482
+ async function archiveSession({ client, sessionKey, streamId, agentId, chatId, chatType, channelId }) {
483
+ //获取历史会话记录
484
+ const histories = await client.exeCmd({
485
+ cmd: 'chat.history',
486
+ requestId: uuidStr(36),
487
+ timestamp: Date.now(),
488
+ params: {
489
+ sessionKey,
490
+ limit: 100
491
+ }
492
+ });
493
+ if (!histories?.success || !histories.data) {
494
+ client.publishToServer({
495
+ streamId,
496
+ content: `[tag_error_session_history_fail]${histories?.error ?? ''}`,
497
+ finish: true,
498
+ kind: 'block'
499
+ });
500
+ return;
501
+ }
502
+ const historyRecords = histories.data.messages ?? [];
503
+ const msgList = historyRecords.map((item) => {
504
+ const content = item.content;
505
+ if (typeof content === 'string') {
506
+ return `[${item.role}] ${item.content}`;
507
+ }
508
+ const items = content.map((msg) => {
509
+ try {
510
+ if (msg.type === 'text') {
511
+ return msg.text;
512
+ }
513
+ }
514
+ catch (e) {
515
+ }
516
+ return '';
517
+ });
518
+ const newItems = items.filter(rec => rec.length > 0);
519
+ return `[${item.role}] ${newItems.join(';')}`;
520
+ });
521
+ if (msgList.length === 0) {
522
+ client.publishToServer({
523
+ streamId,
524
+ content: '[tag_error_session_record_empty]',
525
+ finish: true,
526
+ kind: 'block'
527
+ });
528
+ return;
529
+ }
530
+ //调用 sessions.archive 真正归档(不 reset,保留完整对话)
531
+ const archiveResult = await client.exeCmd({
532
+ cmd: 'sessions.archive',
533
+ requestId: uuidStr(36),
534
+ timestamp: Date.now(),
535
+ params: { sessionKey }
536
+ });
537
+ if (!archiveResult?.success || !archiveResult.data) {
538
+ client.publishToServer({
539
+ streamId,
540
+ content: '[tag_error_archive_session_fail]' + (archiveResult?.error || ''),
541
+ kind: 'block',
542
+ finish: true,
543
+ });
544
+ return;
545
+ }
546
+ //自动创建新会话(带后缀),后续消息走新会话
547
+ const newSuffix = Date.now().toString(36);
548
+ const createResult = await client.exeCmd({
549
+ cmd: 'sessions.create',
550
+ requestId: uuidStr(36),
551
+ timestamp: Date.now(),
552
+ params: {
553
+ agentId,
554
+ userId: chatId,
555
+ chatType,
556
+ suffix: newSuffix
557
+ }
558
+ });
559
+ if (!createResult?.success || !createResult.data) {
560
+ client.publishToServer({
561
+ streamId,
562
+ content: '[tag_error_create_session_fail]' + (createResult?.error || ''),
563
+ kind: 'block',
564
+ finish: true,
565
+ });
566
+ return;
567
+ }
568
+ const newSessionKey = `agent:${agentId}:${channelId}-${newSuffix}:${chatType}:${chatId.toLowerCase()}`;
569
+ //生成标题(在新的 agent 会话中执行)
570
+ const prompt = `请根据以下聊天记录,生成一个不超过128字的标题,概括这次对话的主题:
571
+ ${msgList.join('\n')}
572
+
573
+ 要求:
574
+ - 标题长度 ≤ 128 字
575
+ - 简洁准确地概括对话核心内容
576
+ - 直接输出标题,并在标题末尾符加上标记"[tag_archive]",不要多余文字`;
577
+ const askAgent = await client.exeCmd({
578
+ cmd: 'agent.ask',
579
+ requestId: uuidStr(36),
580
+ timestamp: Date.now(),
581
+ params: {
582
+ agentId,
583
+ message: prompt,
584
+ sessionKey,
585
+ deliver: false,
586
+ idempotencyKey: uuidStr(36)
587
+ }
588
+ });
589
+ let askAgentError = '';
590
+ if (!askAgent?.success || !askAgent.data) {
591
+ askAgentError = `[tag_error_summarize_session_history_fail]\n ${askAgent?.error}`;
592
+ }
593
+ const status = askAgent?.data.status;
594
+ if (status !== 'accepted') {
595
+ askAgentError = `${agentId} [tag_error_reject_session_history]`;
596
+ }
597
+ if (askAgentError && askAgentError.length > 0) {
598
+ client.publishToServer({
599
+ streamId,
600
+ content: askAgentError,
601
+ kind: 'block',
602
+ finish: true,
603
+ });
604
+ return;
605
+ }
606
+ const runId = askAgent?.data.runId;
607
+ // 返回归档成功 + 新会话 key
608
+ client.publishToServer({
609
+ streamId,
610
+ content: '[tag_archive_success]',
611
+ finish: true,
612
+ kind: 'block',
613
+ session: {
614
+ sessionKey,
615
+ runId,
616
+ newSessionKey,
617
+ archived: true
618
+ }
619
+ });
620
+ }
621
+ export async function buildMessageContext(params) {
622
+ const { core, channelId, account, clawConfig, msg, imageList, voiceList, videoList, fileList, textBody, quoteContent, commandAuthorized, chatHistories, historyLimit, logger, client, streamId, relayId } = params;
623
+ const toAgent = msg.toAgent;
624
+ const fromUser = msg.fromPerson;
625
+ const isGroup = msg.group;
626
+ const groupIntroduction = msg.groupIntroduction;
627
+ const chatType = isGroup ? "group" : "direct";
628
+ // 构建会话标签
629
+ let chatId, fromLabel, from, to;
630
+ if (isGroup) {
631
+ chatId = msg.chatId;
632
+ fromLabel = `group:${chatId}-${toAgent.id}-${msg.botType}-${relayId}`;
633
+ }
634
+ else {
635
+ chatId = fromUser.empId; //CorpEmployee.id
636
+ fromLabel = `user:${chatId}-${toAgent.id}-${msg.botType}-${relayId}`;
637
+ }
638
+ // 当客户端指定了 sessionKey 时,将其编码到 from/to 中(# 分隔),
639
+ // 便于 autoReply 主动出站时解析当前会话
640
+ const sessionLabel = (toAgent.sessionKey && toAgent.sessionKey.length > 0) ? `#${toAgent.sessionKey}` : '';
641
+ from = `${channelId}:${fromLabel}${sessionLabel}`;
642
+ to = `${channelId}:${fromLabel}${sessionLabel}`;
643
+ let messageBody = (textBody && textBody.length > 0) ? textBody : "";
644
+ const media_placeholder = [];
645
+ if (imageList.length > 0) {
646
+ media_placeholder.push(MEDIA_IMAGE_PLACEHOLDER);
647
+ }
648
+ if (voiceList.length > 0) {
649
+ media_placeholder.push(MEDIA_VOICE_PLACEHOLDER);
650
+ }
651
+ if (videoList.length > 0) {
652
+ media_placeholder.push(MEDIA_VIDEO_PLACEHOLDER);
653
+ }
654
+ if (fileList.length > 0) {
655
+ media_placeholder.push(MEDIA_DOCUMENT_PLACEHOLDER);
656
+ }
657
+ messageBody = media_placeholder.join('') + messageBody;
658
+ const mediaList = [...imageList, ...voiceList, ...videoList, ...fileList];
659
+ //会话路由
660
+ // let route = core.channel.routing.resolveAgentRoute({
661
+ // cfg: clawConfig,
662
+ // channel: channelId,
663
+ // accountId: account.accountId!,
664
+ // peer: {
665
+ // kind: chatType,
666
+ // id: chatId,
667
+ // },
668
+ // });
669
+ const agentId = toAgent.agentId;
670
+ // 客户端传了 sessionKey 则直接使用,否则自动构建默认会话
671
+ const sessionKey = toAgent.sessionKey || core.channel.routing.buildAgentSessionKey({
672
+ agentId: agentId,
673
+ channel: channelId,
674
+ accountId: account.accountId,
675
+ peer: {
676
+ kind: chatType,
677
+ id: chatId
678
+ },
679
+ dmScope: 'per-channel-peer'
680
+ });
681
+ //拦截 /archive 指令:真正归档当前会话 + 自动新建
682
+ if ("/archive" === messageBody.toLowerCase()) {
683
+ await archiveSession({
684
+ client,
685
+ sessionKey,
686
+ streamId,
687
+ agentId,
688
+ chatId,
689
+ chatType,
690
+ channelId
691
+ });
692
+ return undefined;
693
+ }
694
+ //拦截/new,/reset 指令
695
+ if (commandAuthorized && "/new" === messageBody.toLowerCase() || "/reset" === messageBody.toLowerCase()) {
696
+ const res = await resetSession({
697
+ client,
698
+ sessionKey,
699
+ streamId,
700
+ agentId
701
+ });
702
+ if (res) {
703
+ return undefined;
704
+ }
705
+ }
706
+ //重启openclaw gateway restart
707
+ if (commandAuthorized && "/restart" === messageBody.toLowerCase()) {
708
+ await client.publishToServer({
709
+ streamId,
710
+ content: '[tag_gateway_restart]',
711
+ finish: true,
712
+ kind: 'block'
713
+ });
714
+ }
715
+ // 强制停止当前 Agent run(/stop 指令,比 /queue interrupt 更快)
716
+ // 新增 cmd 通道 /stop 指令
717
+ if (commandAuthorized && "/stop" === messageBody.toLowerCase()) {
718
+ await client.exeCmd({
719
+ cmd: 'agent.stop',
720
+ requestId: uuidStr(36),
721
+ timestamp: Date.now(),
722
+ params: {
723
+ agentId,
724
+ userId: chatId,
725
+ chatType
726
+ }
727
+ });
728
+ await client.publishToServer({
729
+ streamId,
730
+ content: '[tag_agent_stop]',
731
+ finish: true,
732
+ kind: 'block'
733
+ });
734
+ return undefined;
735
+ }
736
+ // Dynamic agent creation for DM users
737
+ // When enabled, creates a unique agent instance with its own workspace for each DM user.
738
+ // const config = account.config;
739
+ let effectiveCfg = clawConfig;
740
+ // if (!isGroup && route.matchedBy === "default") {
741
+ // const dynamicCfg = config?.dynamicAgentConfig;
742
+ // //是否为当前chatId创建动态agent
743
+ // if (dynamicCfg?.enabled) {
744
+ // const result = await maybeCreateDynamicAgent({
745
+ // cfg: clawConfig,
746
+ // runtime: core,
747
+ // senderId: chatId,
748
+ // channelId: channelId,
749
+ // dynamicCfg,
750
+ // log: (msg) => logger.info(msg),
751
+ // });
752
+ // if (result.created) {
753
+ // //如果创建成功,则当前来自chatId的请求使用该路由
754
+ // effectiveCfg = result.updatedCfg;
755
+ // // Re-resolve route with updated config
756
+ // route = core.channel.routing.resolveAgentRoute({
757
+ // cfg: result.updatedCfg,
758
+ // channel: channelId,
759
+ // accountId: account.accountId,
760
+ // peer: { kind: "direct", id: chatId },
761
+ // });
762
+ // logger.info(
763
+ // `${channelId} account[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`
764
+ // );
765
+ // }
766
+ // }
767
+ // }
768
+ const preview = messageBody.replace(/\s+/g, " ").slice(0, 160);
769
+ const inboundLabel = `${channelId}:${account.accountId}:${chatId}`;
770
+ const isEnqueueEvent = core.system.enqueueSystemEvent(`${inboundLabel}:${preview}`, {
771
+ sessionKey: sessionKey, //route.sessionKey,
772
+ contextKey: `${channelId}:message:${msg.streamId}`,
773
+ });
774
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(effectiveCfg);
775
+ // Include a readable speaker label so the model can attribute instructions.
776
+ // (DMs already have per-sender sessions, but the prefix is still useful for clarity.)
777
+ if (!messageBody.startsWith('/')) {
778
+ const speaker = `${chatId}`;
779
+ messageBody = `${speaker}:${messageBody}`;
780
+ await client.publishProcessingState({
781
+ streamId,
782
+ content: '[tag_start_ctx]',
783
+ status: 'read',
784
+ kind: 'block',
785
+ });
786
+ }
787
+ // If there are mention targets, inform the agent that replies will auto-mention them
788
+ if (isGroup) {
789
+ // Build a lookup-friendly member map
790
+ const groupMembers = msg.groupMembers ?? [];
791
+ if (groupMembers.length > 0) {
792
+ const memberMapStr = groupMembers
793
+ .map((t) => `${t.name} -> ${t.id} -> AgentId:${t.agentId ?? 'n/a'} -> Resp.:${t.responsibilities ?? 'unknown'} -> UserType:${t.type}`)
794
+ .join("\n");
795
+ messageBody += `\n\nSystem: This is a group chat. Group members (Nickname -> ID -> AgentId -> Responsibilities -> UserType):\n${memberMapStr}\n\n`;
796
+ messageBody += `When mentioning someone, use: @[Nickname](ID:UserType)\n`;
797
+ messageBody += `Example: @[John](12345:agent) Hello, please check this\n\n`;
798
+ messageBody += `Rules:\n`;
799
+ messageBody += `- If the user's request relates to a specific responsibility, prioritize mentioning the member(s) whose responsibility matches.`;
800
+ messageBody += `- ID must come from the member list above, do not fabricate\n`;
801
+ messageBody += `- If the member isn't in the list or ID is uncertain, do not use the @ format\n`;
802
+ messageBody += `- Always include the full @[Nickname](ID:UserType) format`;
803
+ // 2026-08-13 群组 Agent 协作:当群内有其他 Agent 成员(type=agent 且 agentId 非空)时,
804
+ // 告知当前 Agent 可以用 sessions_send 工具以自身身份向对方 Agent 发协作消息(isUser=false),
805
+ // 消息不会伪装成人类,对方 Agent 会把它当作 Agent 间协作消息处理。
806
+ // 目标 sessionKey 固定为群会话:agent:<对方agentId>:<channelId>:group:<chatId>
807
+ const agentMembers = groupMembers.filter((m) => m.type === 'agent' && m.agentId && m.agentId.length > 0);
808
+ if (agentMembers.length > 0) {
809
+ const agentRoutes = agentMembers
810
+ .map((m) => `- ${m.name} (agentId=${m.agentId}): sessions_send -> agent:${m.agentId}:${channelId}:group:${chatId.toLowerCase()}`)
811
+ .join("\n");
812
+ messageBody += `\n\nSystem: Group agent-to-agent collaboration:\n`;
813
+ messageBody += `You may collaborate with other Agent members in this group using the sessions_send tool. `;
814
+ messageBody += `When you need another Agent's input, call sessions_send with its group session key below; `;
815
+ messageBody += `the message arrives as an inter-agent message (isUser=false), NOT as a human instruction. `;
816
+ messageBody += `Your final reply is still broadcast to the whole group as usual. Routes:\n${agentRoutes}\n`;
817
+ }
818
+ if (groupIntroduction && groupIntroduction.length > 0) {
819
+ messageBody += `\n\nSystem: Below is an introduction to this group:\n`;
820
+ messageBody += groupIntroduction;
821
+ }
822
+ }
823
+ }
824
+ // logger.info(
825
+ // `${channelId} account[${account.accountId}]: message body: ${messageBody}`
826
+ // )
827
+ const body = core.channel.reply.formatAgentEnvelope({
828
+ channel: channelId,
829
+ from: chatId,
830
+ timestamp: Date.now(),
831
+ envelope: envelopeOptions,
832
+ body: messageBody,
833
+ });
834
+ let combinedBody = body;
835
+ const historyKey = isGroup ? chatId : undefined;
836
+ if (isGroup && historyKey && chatHistories) {
837
+ combinedBody = createChannelHistoryWindow({
838
+ historyMap: chatHistories
839
+ }).buildPendingContext({
840
+ historyKey,
841
+ limit: historyLimit,
842
+ currentMessage: combinedBody,
843
+ formatEntry(entry) {
844
+ return core.channel.reply.formatAgentEnvelope({
845
+ channel: channelId,
846
+ // Preserve speaker identity in group history as well.
847
+ from: `${chatId}:${entry.sender}`,
848
+ timestamp: entry.timestamp,
849
+ body: entry.body,
850
+ envelope: envelopeOptions,
851
+ });
852
+ },
853
+ });
854
+ }
855
+ const inboundHistory = isGroup && historyKey && historyLimit > 0 && chatHistories
856
+ ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
857
+ sender: entry.sender,
858
+ body: entry.body,
859
+ timestamp: entry.timestamp,
860
+ }))
861
+ : undefined;
862
+ logger.info(`${channelId} account[${account.accountId}], agentId=${agentId},sessionKey=${sessionKey}, isEnqueueEvent=${isEnqueueEvent}`);
863
+ logger.info(`combined body:${combinedBody}\n`);
864
+ logger.info(`inbound History:${JSON.stringify(inboundHistory)}\n`);
865
+ // 构建标准消息上下文
866
+ return buildChannelInboundEventContext({
867
+ channel: channelId,
868
+ provider: channelId,
869
+ surface: channelId,
870
+ timestamp: Date.now(),
871
+ accountId: account.accountId,
872
+ messageId: msg.streamId,
873
+ from: from,
874
+ sender: {
875
+ id: fromUser.empId
876
+ },
877
+ conversation: {
878
+ kind: chatType,
879
+ id: chatId,
880
+ label: fromLabel + '_' + streamId //加上流ID要在session.message件事中获取
881
+ },
882
+ route: {
883
+ agentId: agentId, //route.agentId,
884
+ routeSessionKey: sessionKey //route.sessionKey
885
+ },
886
+ reply: {
887
+ to: to,
888
+ originatingTo: to,
889
+ replyToId: to
890
+ },
891
+ message: {
892
+ rawBody: messageBody,
893
+ body: combinedBody,
894
+ commandBody: combinedBody,
895
+ bodyForAgent: combinedBody,
896
+ inboundHistory: inboundHistory,
897
+ },
898
+ messageIdFull: msg.msgId,
899
+ access: {
900
+ commands: {
901
+ // 2026-09-06 适配 openclaw 2026.9.1:access.commands.authorized 由可选改为必填 boolean
902
+ authorized: commandAuthorized ?? false
903
+ }
904
+ },
905
+ command: {
906
+ kind: 'text-slash',
907
+ body: messageBody,
908
+ authorized: commandAuthorized
909
+ },
910
+ commandTurn: {
911
+ kind: "native",
912
+ source: "native",
913
+ authorized: commandAuthorized || false
914
+ },
915
+ media: mediaList,
916
+ supplemental: {
917
+ quote: {
918
+ body: quoteContent
919
+ }
920
+ },
921
+ contextVisibility: 'all',
922
+ //finalize: FinalizeInboundContextFn,
923
+ finalizeOptions: {
924
+ forceBodyForAgent: true,
925
+ forceChatType: true
926
+ // 2026-09-06 适配 openclaw 2026.9.1:forceConversationLabel 选项已移除,conversation.label 直接生效
927
+ }
928
+ //extra?: Record<string, unknown>;
929
+ });
930
+ }
931
+ /**
932
+ * 路由消息到核心处理流程并处理回复
933
+ */
934
+ export async function routeAndDispatchMessage(ctx) {
935
+ const { ctxPayload, core, cfg, client, accountId, streamId, accStatus, commandAuthorized, chatOpts, logger } = ctx;
936
+ if (!commandAuthorized) {
937
+ await client.publishProcessingState({
938
+ streamId,
939
+ content: '[tag_status_running]',
940
+ status: 'read',
941
+ kind: 'in_process'
942
+ });
943
+ }
944
+ try {
945
+ const ctx = await ctxPayload;
946
+ const mediaFiles = [];
947
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
948
+ ctx,
949
+ cfg,
950
+ dispatcherOptions: {
951
+ onReplyStart: async () => {
952
+ await client.publishProcessingState({
953
+ streamId,
954
+ content: '[tag_agent_doing]',
955
+ status: 'read',
956
+ kind: 'block'
957
+ });
958
+ },
959
+ deliver: async (payload, info) => {
960
+ if (payload.isError) {
961
+ logger.error(`account[${accountId}], payload error!`);
962
+ }
963
+ //info.kind='block'
964
+ const contents = payload.text ? [payload.text] : [];
965
+ if (payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
966
+ //处理媒体文件
967
+ // 2026-09-01 修复:mediaUrl 与 mediaUrls 可能同时携带同一文件(语音场景),去重避免重复发送导致空语音
968
+ let files = [...new Set([...(payload.mediaUrl ? [payload.mediaUrl] : []), ...(payload.mediaUrls ?? [])])];
969
+ files.forEach(item => mediaFiles.push(item));
970
+ logger.info(`account[${accountId}], reply media file >>> ${files.join(',')}`);
971
+ }
972
+ if (contents.length === 0) {
973
+ return;
974
+ }
975
+ const content = contents.join(',');
976
+ accStatus.accumulatedPayload += content;
977
+ if (info.kind === 'tool') {
978
+ logger.info(`account[${accountId}] 正在调用工具.🔧${content}`);
979
+ await client.publishProcessingState({
980
+ streamId,
981
+ content: `[tag_status_call_tool]`,
982
+ status: 'read',
983
+ kind: 'block',
984
+ });
985
+ }
986
+ },
987
+ onSettled: async () => {
988
+ if ((accStatus.accumulatedPayload && accStatus.accumulatedPayload.length > 0) || mediaFiles.length > 0) {
989
+ //发送结束状态
990
+ await client.publishProcessingState({
991
+ streamId,
992
+ content: '[tag_status_completed]',
993
+ status: 'read',
994
+ kind: 'in_process'
995
+ });
996
+ //处理完成整体回复
997
+ await client.publishToServer({
998
+ streamId,
999
+ content: accStatus.accumulatedPayload,
1000
+ finish: true,
1001
+ kind: 'block',
1002
+ mediaFiles
1003
+ }, chatOpts);
1004
+ }
1005
+ },
1006
+ onError: async (err, info) => {
1007
+ const errorStr = String(err);
1008
+ await client.publishProcessingState({
1009
+ streamId,
1010
+ content: `[tag_status_error] ${errorStr}`,
1011
+ status: 'read',
1012
+ kind: 'in_process'
1013
+ });
1014
+ client.publishToServer({
1015
+ streamId,
1016
+ content: `[tag_error_pross]\n${errorStr}`,
1017
+ finish: info.kind === 'final',
1018
+ kind: 'block',
1019
+ mediaFiles
1020
+ }, chatOpts);
1021
+ },
1022
+ },
1023
+ replyOptions: {
1024
+ suppressDefaultToolProgressMessages: true,
1025
+ allowProgressCallbacksWhenSourceDeliverySuppressed: true,
1026
+ enableHeartbeatTool: true,
1027
+ onAgentRunStart: (runId) => {
1028
+ logger.info(`Agent run start ... runId = ${runId}`);
1029
+ client.publishProcessingState({
1030
+ streamId,
1031
+ content: '[tag_agent_doing]',
1032
+ status: 'read',
1033
+ kind: 'in_process',
1034
+ runId
1035
+ });
1036
+ },
1037
+ async onReplyStart() {
1038
+ await client.publishProcessingState({
1039
+ streamId,
1040
+ content: '[tag_agent_to_reply]',
1041
+ status: 'read',
1042
+ kind: 'in_process'
1043
+ });
1044
+ },
1045
+ onReasoningStream: async (payload) => {
1046
+ await client.publishProcessingState({
1047
+ streamId,
1048
+ content: `[tag_status_start_reasoning]${payload.text ?? ''}`,
1049
+ status: 'read',
1050
+ kind: 'reasoning'
1051
+ });
1052
+ },
1053
+ onReasoningEnd: async () => {
1054
+ await client.publishProcessingState({
1055
+ streamId,
1056
+ content: '[tag_status_end_reasoning]',
1057
+ status: 'read',
1058
+ kind: 'end_reasoning'
1059
+ });
1060
+ },
1061
+ onToolStart: async (payload) => {
1062
+ if (!payload.name) {
1063
+ return;
1064
+ }
1065
+ const messages = [
1066
+ `[tag_status_call_tool]`,
1067
+ `[tag_status_phase_${payload.phase}]`,
1068
+ payload.name ?? payload.toolCallId
1069
+ ];
1070
+ await client.publishProcessingState({
1071
+ streamId,
1072
+ content: messages.join('\n'),
1073
+ status: 'read',
1074
+ kind: 'tool'
1075
+ });
1076
+ },
1077
+ onToolResult: async (payload) => {
1078
+ if (payload.isError) {
1079
+ await client.publishProcessingState({
1080
+ streamId,
1081
+ content: `[tag_status_error_tool]`,
1082
+ status: 'read',
1083
+ kind: 'tool',
1084
+ });
1085
+ return;
1086
+ }
1087
+ const content = payload.text || payload.spokenText || payload.btw?.question || '';
1088
+ if (content.length > 0) {
1089
+ await client.publishProcessingState({
1090
+ streamId,
1091
+ content,
1092
+ status: 'read',
1093
+ kind: 'plan',
1094
+ });
1095
+ }
1096
+ },
1097
+ async onItemEvent(payload) {
1098
+ const kind = payload.kind;
1099
+ if (!kind) {
1100
+ return;
1101
+ }
1102
+ const content = [];
1103
+ if (payload.name) {
1104
+ content.push(payload.name);
1105
+ }
1106
+ if (payload.title) {
1107
+ content.push(payload.title);
1108
+ }
1109
+ if (payload.progressText) {
1110
+ content.push(payload.progressText);
1111
+ }
1112
+ if (payload.summary) {
1113
+ content.push(payload.summary);
1114
+ }
1115
+ const messages = [
1116
+ `[tag_status_call_${kind}]`,
1117
+ `[tag_status_phase_${payload.phase}]`,
1118
+ `[tag_status_${payload.status}]`,
1119
+ ...content
1120
+ ];
1121
+ await client.publishProcessingState({
1122
+ streamId,
1123
+ content: messages.join('>'),
1124
+ status: 'read',
1125
+ kind: kind
1126
+ });
1127
+ logger.info(`onItemEvent >>> kind=${payload.kind}, phase=${payload.phase}, status =${payload.status}, content=${content.join('>')}`);
1128
+ },
1129
+ onCommandOutput: async (payload) => {
1130
+ if (!payload.phase) {
1131
+ return;
1132
+ }
1133
+ const messages = [
1134
+ `[tag_status_call_command]`,
1135
+ `[tag_status_phase_${payload.phase}]`,
1136
+ `[tag_status_${payload.status}]`,
1137
+ payload.name ?? '',
1138
+ payload.output ?? ''
1139
+ ];
1140
+ await client.publishProcessingState({
1141
+ streamId,
1142
+ content: messages.join('>'),
1143
+ status: 'read',
1144
+ kind: 'command'
1145
+ });
1146
+ },
1147
+ onPatchSummary: async (payload) => {
1148
+ const strs = [];
1149
+ if (payload.title) {
1150
+ strs.push(payload.title);
1151
+ }
1152
+ if (payload.summary) {
1153
+ strs.push(payload.summary);
1154
+ }
1155
+ if (strs.length > 0)
1156
+ logger.info(`onPatchSummary 输出 >>> ${strs.join('')}`);
1157
+ await client.publishProcessingState({
1158
+ streamId,
1159
+ content: strs.join(''),
1160
+ status: 'read',
1161
+ kind: 'patch'
1162
+ });
1163
+ },
1164
+ async onPlanUpdate(payload) {
1165
+ const strs = [];
1166
+ if (payload.explanation) {
1167
+ strs.push(payload.explanation);
1168
+ }
1169
+ if (payload.steps) {
1170
+ strs.push('\n\n');
1171
+ payload.steps.forEach(str => strs.push(`- ${str}`));
1172
+ }
1173
+ if (strs.length > 0) {
1174
+ logger.info(`onPlanUpdate 输出 >>> ${strs.join('')}`);
1175
+ await client.publishProcessingState({
1176
+ streamId,
1177
+ content: strs.join(''),
1178
+ status: 'read',
1179
+ kind: 'plan'
1180
+ });
1181
+ }
1182
+ },
1183
+ onCompactionStart: async () => {
1184
+ await client.publishProcessingState({
1185
+ streamId,
1186
+ content: '[tag_status_compaction_start]',
1187
+ status: 'read',
1188
+ kind: 'block'
1189
+ });
1190
+ },
1191
+ onCompactionEnd: async () => {
1192
+ await client.publishProcessingState({
1193
+ streamId,
1194
+ content: '[tag_status_compaction_end]',
1195
+ status: 'read',
1196
+ kind: 'block'
1197
+ });
1198
+ },
1199
+ }
1200
+ });
1201
+ }
1202
+ catch (err) {
1203
+ logger.error(`account[${accountId}] Failed to process message: ${String(err)}`);
1204
+ client.publishToServer({
1205
+ streamId,
1206
+ content: `[tag_error_reply_fail]:${String(err)}`,
1207
+ finish: true,
1208
+ kind: 'block'
1209
+ }, chatOpts);
1210
+ }
1211
+ }