@tencent-weixin/openclaw-weixin 2.4.2 → 2.4.4

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 (53) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CHANGELOG.zh_CN.md +45 -0
  3. package/dist/src/api/api.js +84 -8
  4. package/dist/src/api/api.js.map +1 -1
  5. package/dist/src/api/types.js +2 -0
  6. package/dist/src/api/types.js.map +1 -1
  7. package/dist/src/auth/login-qr.js +1 -0
  8. package/dist/src/auth/login-qr.js.map +1 -1
  9. package/dist/src/channel.js +15 -1
  10. package/dist/src/channel.js.map +1 -1
  11. package/dist/src/config/config-schema.js +1 -0
  12. package/dist/src/config/config-schema.js.map +1 -1
  13. package/dist/src/config/reply-progress.js +5 -0
  14. package/dist/src/config/reply-progress.js.map +1 -0
  15. package/dist/src/media/voice-outbound.js +98 -1
  16. package/dist/src/media/voice-outbound.js.map +1 -1
  17. package/dist/src/messaging/batch-session.js +271 -0
  18. package/dist/src/messaging/batch-session.js.map +1 -0
  19. package/dist/src/messaging/error-notice.js +1 -0
  20. package/dist/src/messaging/error-notice.js.map +1 -1
  21. package/dist/src/messaging/outbound-hooks.js +2 -0
  22. package/dist/src/messaging/outbound-hooks.js.map +1 -1
  23. package/dist/src/messaging/process-message.js +32 -7
  24. package/dist/src/messaging/process-message.js.map +1 -1
  25. package/dist/src/messaging/reply-progress-sender.js +93 -0
  26. package/dist/src/messaging/reply-progress-sender.js.map +1 -0
  27. package/dist/src/messaging/run-context.js +9 -0
  28. package/dist/src/messaging/run-context.js.map +1 -0
  29. package/dist/src/messaging/run-report-session.js +123 -0
  30. package/dist/src/messaging/run-report-session.js.map +1 -0
  31. package/dist/src/messaging/send.js +40 -2
  32. package/dist/src/messaging/send.js.map +1 -1
  33. package/dist/src/monitor/monitor.js +3 -0
  34. package/dist/src/monitor/monitor.js.map +1 -1
  35. package/dist/src/streaming/batch-session.js +271 -0
  36. package/dist/src/streaming/batch-session.js.map +1 -0
  37. package/dist/src/streaming/stream-piece.js +54 -0
  38. package/dist/src/streaming/stream-piece.js.map +1 -0
  39. package/openclaw.plugin.json +1 -1
  40. package/package.json +1 -1
  41. package/src/api/api.ts +92 -8
  42. package/src/api/types.ts +16 -0
  43. package/src/auth/login-qr.ts +8 -0
  44. package/src/channel.ts +16 -1
  45. package/src/config/config-schema.ts +1 -0
  46. package/src/config/reply-progress.ts +10 -0
  47. package/src/messaging/error-notice.ts +2 -0
  48. package/src/messaging/outbound-hooks.ts +4 -0
  49. package/src/messaging/process-message.ts +32 -7
  50. package/src/messaging/reply-progress-sender.ts +122 -0
  51. package/src/messaging/send-media.ts +1 -1
  52. package/src/messaging/send.ts +60 -7
  53. package/src/monitor/monitor.ts +3 -0
@@ -20,6 +20,7 @@ export async function applyWeixinMessageSendingHook(params: {
20
20
  text: string;
21
21
  accountId?: string;
22
22
  mediaUrl?: string;
23
+ runId?: string;
23
24
  }): Promise<{ cancelled: boolean; text: string }> {
24
25
  const hookRunner = getGlobalHookRunner();
25
26
  if (!hookRunner?.hasHooks("message_sending")) {
@@ -33,6 +34,7 @@ export async function applyWeixinMessageSendingHook(params: {
33
34
  metadata: {
34
35
  channel: CHANNEL_ID,
35
36
  accountId: params.accountId,
37
+ runId: params.runId,
36
38
  ...(params.mediaUrl ? { mediaUrls: [params.mediaUrl] } : {}),
37
39
  },
38
40
  },
@@ -60,6 +62,7 @@ export function emitWeixinMessageSent(params: {
60
62
  success: boolean;
61
63
  error?: string;
62
64
  accountId?: string;
65
+ runId?: string;
63
66
  }): void {
64
67
  const hookRunner = getGlobalHookRunner();
65
68
  if (!hookRunner?.hasHooks("message_sent")) return;
@@ -71,6 +74,7 @@ export function emitWeixinMessageSent(params: {
71
74
  channelId: CHANNEL_ID,
72
75
  accountId: params.accountId,
73
76
  conversationId: params.to,
77
+ runId: params.runId,
74
78
  });
75
79
  fireAndForgetHook(
76
80
  Promise.resolve(
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
2
3
 
3
4
  import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime";
4
5
  import {
@@ -14,6 +15,7 @@ import { MessageItemType, TypingStatus } from "../api/types.js";
14
15
  import { loadWeixinAccount } from "../auth/accounts.js";
15
16
  import { readFrameworkAllowFromList } from "../auth/pairing.js";
16
17
  import { downloadRemoteImageToTemp } from "../cdn/upload.js";
18
+ import { resolveReplyProgressMessagesEnabled } from "../config/reply-progress.js";
17
19
  import { downloadMediaFromItem } from "../media/media-download.js";
18
20
  import { logger } from "../util/logger.js";
19
21
  import { redactBody, redactToken } from "../util/redact.js";
@@ -31,6 +33,7 @@ import type { WeixinInboundMediaOpts } from "./inbound.js";
31
33
  import { sendWeixinMediaFile } from "./send-media.js";
32
34
  import { StreamingMarkdownFilter } from "./markdown-filter.js";
33
35
  import { sendMessageWeixin } from "./send.js";
36
+ import { WeixinReplyProgressSender } from "./reply-progress-sender.js";
34
37
  import { handleSlashCommand } from "./slash-commands.js";
35
38
 
36
39
  const MEDIA_OUTBOUND_TEMP_DIR = path.join(resolvePreferredOpenClawTmpDir(), "weixin/media/outbound-temp");
@@ -270,6 +273,19 @@ export async function processOneMessage(
270
273
  if (contextToken) {
271
274
  setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
272
275
  }
276
+ const runId = randomUUID();
277
+ const replyProgressSender = resolveReplyProgressMessagesEnabled(deps.config)
278
+ ? new WeixinReplyProgressSender({
279
+ runId,
280
+ to: ctx.To,
281
+ accountId: deps.accountId,
282
+ opts: {
283
+ baseUrl: deps.baseUrl,
284
+ token: deps.token,
285
+ contextToken,
286
+ },
287
+ })
288
+ : undefined;
273
289
  const humanDelay = deps.channelRuntime.reply.resolveHumanDelayConfig(deps.config, route.agentId);
274
290
 
275
291
  const hasTypingTicket = Boolean(deps.typingTicket);
@@ -336,6 +352,7 @@ export async function processOneMessage(
336
352
  text,
337
353
  accountId: deps.accountId,
338
354
  mediaUrl,
355
+ runId,
339
356
  });
340
357
  if (sendingResult.cancelled) {
341
358
  logger.info(`outbound: cancelled by message_sending hook to=${ctx.To}`);
@@ -368,8 +385,9 @@ export async function processOneMessage(
368
385
  baseUrl: deps.baseUrl,
369
386
  token: deps.token,
370
387
  contextToken,
388
+ runId,
371
389
  }});
372
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
390
+ emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
373
391
  logger.info(`outbound: text sent to=${ctx.To}`);
374
392
  return;
375
393
  }
@@ -377,10 +395,10 @@ export async function processOneMessage(
377
395
  filePath,
378
396
  to: ctx.To,
379
397
  text,
380
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
398
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
381
399
  cdnBaseUrl: deps.cdnBaseUrl,
382
400
  });
383
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
401
+ emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
384
402
  logger.info(`outbound: media sent OK to=${ctx.To}`);
385
403
  } else {
386
404
  logger.debug(`outbound: sending text message to=${ctx.To}`);
@@ -388,12 +406,13 @@ export async function processOneMessage(
388
406
  baseUrl: deps.baseUrl,
389
407
  token: deps.token,
390
408
  contextToken,
409
+ runId,
391
410
  }});
392
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
411
+ emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
393
412
  logger.info(`outbound: text sent OK to=${ctx.To}`);
394
413
  }
395
414
  } catch (err) {
396
- emitWeixinMessageSent({ to: ctx.To, content: text, success: false, error: String(err), accountId: deps.accountId });
415
+ emitWeixinMessageSent({ to: ctx.To, content: text, success: false, error: String(err), accountId: deps.accountId, runId });
397
416
  logger.error(
398
417
  `outbound: FAILED to=${ctx.To} mediaUrl=${mediaUrl ?? "none"} err=${String(err)} stack=${(err as Error).stack ?? ""}`,
399
418
  );
@@ -421,6 +440,7 @@ export async function processOneMessage(
421
440
  message: notice,
422
441
  baseUrl: deps.baseUrl,
423
442
  token: deps.token,
443
+ runId,
424
444
  errLog: deps.errLog,
425
445
  });
426
446
  },
@@ -435,7 +455,11 @@ export async function processOneMessage(
435
455
  ctx: finalized,
436
456
  cfg: deps.config,
437
457
  dispatcher,
438
- replyOptions: { ...replyOptions, disableBlockStreaming: true },
458
+ replyOptions: {
459
+ ...replyOptions,
460
+ ...(replyProgressSender?.replyOptions ?? {}),
461
+ disableBlockStreaming: true,
462
+ },
439
463
  }),
440
464
  });
441
465
  logger.debug(`dispatchReplyFromConfig: done agentId=${route.agentId ?? "(none)"}`);
@@ -446,6 +470,7 @@ export async function processOneMessage(
446
470
  throw err;
447
471
  } finally {
448
472
  markDispatchIdle();
473
+ await replyProgressSender?.finalize();
449
474
 
450
475
  logger.info(
451
476
  `debug-check: accountId=${deps.accountId} debug=${String(debug)} hasContextToken=${Boolean(contextToken)}`,
@@ -489,7 +514,7 @@ export async function processOneMessage(
489
514
  await sendMessageWeixin({
490
515
  to: ctx.To,
491
516
  text: timingText,
492
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
517
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
493
518
  });
494
519
  logger.info(`debug-timing: sent OK`);
495
520
  } catch (debugErr) {
@@ -0,0 +1,122 @@
1
+ import type { WeixinApiOptions } from "../api/api.js";
2
+ import type { MessageItem } from "../api/types.js";
3
+ import { MessageItemType } from "../api/types.js";
4
+ import { logger } from "../util/logger.js";
5
+
6
+ import { sendMessageItemWeixin } from "./send.js";
7
+
8
+ export type WeixinReplyProgressSenderDeps = {
9
+ runId: string;
10
+ to: string;
11
+ accountId: string;
12
+ opts: WeixinApiOptions & {
13
+ contextToken?: string;
14
+ runId?: string;
15
+ };
16
+ };
17
+
18
+ type ToolItemEventPayload = {
19
+ itemId?: string;
20
+ kind?: string;
21
+ title?: string;
22
+ name?: string;
23
+ phase?: string;
24
+ status?: string;
25
+ };
26
+
27
+ function normalizeToolStatus(status?: string): string {
28
+ if (status === "completed") return "completed";
29
+ if (status === "failed") return "failed";
30
+ if (status === "blocked") return "blocked";
31
+ return "unknown";
32
+ }
33
+
34
+ export class WeixinReplyProgressSender {
35
+ readonly runId: string;
36
+
37
+ private readonly to: string;
38
+ private readonly accountId: string;
39
+ private readonly opts: WeixinReplyProgressSenderDeps["opts"];
40
+ private finalized = false;
41
+ private sendChain: Promise<void> = Promise.resolve();
42
+
43
+ constructor(deps: WeixinReplyProgressSenderDeps) {
44
+ this.runId = deps.runId;
45
+ this.to = deps.to;
46
+ this.accountId = deps.accountId;
47
+ this.opts = { ...deps.opts, runId: deps.runId };
48
+ }
49
+
50
+ get replyOptions() {
51
+ return {
52
+ runId: this.runId,
53
+ onItemEvent: (payload: ToolItemEventPayload) => this.handleToolItemEvent(payload),
54
+ };
55
+ }
56
+
57
+ private enqueueMessage(item: MessageItem, label: string): void {
58
+ if (this.finalized) return;
59
+ this.sendChain = this.sendChain
60
+ .then(async () => {
61
+ await sendMessageItemWeixin({
62
+ to: this.to,
63
+ item,
64
+ opts: this.opts,
65
+ label,
66
+ });
67
+ })
68
+ .catch((err) => {
69
+ logger.warn(`${label}: failed to=${this.to} accountId=${this.accountId} runId=${this.runId} err=${String(err)}`);
70
+ });
71
+ }
72
+
73
+ private handleToolItemEvent(payload: ToolItemEventPayload): void {
74
+ if (this.finalized) return;
75
+ if (payload.kind !== "tool") return;
76
+ if (payload.phase !== "start" && payload.phase !== "end") return;
77
+
78
+ const now = Date.now();
79
+ const toolName = payload.name?.trim() || payload.title?.trim() || "tool";
80
+ const toolCallId = payload.itemId?.trim() || undefined;
81
+
82
+ if (payload.phase === "start") {
83
+ this.enqueueMessage(
84
+ {
85
+ type: MessageItemType.TOOL_CALL_START,
86
+ create_time_ms: now,
87
+ is_completed: false,
88
+ tool_call_start_item: {
89
+ tool_name: toolName,
90
+ tool_call_id: toolCallId,
91
+ },
92
+ },
93
+ "sendToolCallStartMessage",
94
+ );
95
+ return;
96
+ }
97
+
98
+ this.enqueueMessage(
99
+ {
100
+ type: MessageItemType.TOOL_CALL_RESULT,
101
+ create_time_ms: now,
102
+ is_completed: true,
103
+ tool_call_result_item: {
104
+ tool_name: toolName,
105
+ tool_call_id: toolCallId,
106
+ status: normalizeToolStatus(payload.status),
107
+ },
108
+ },
109
+ "sendToolCallResultMessage",
110
+ );
111
+ }
112
+
113
+ async finalize(): Promise<void> {
114
+ if (this.finalized) return;
115
+ this.finalized = true;
116
+ try {
117
+ await this.sendChain;
118
+ } catch (err) {
119
+ logger.warn(`WeixinReplyProgressSender.finalize: send drain failed runId=${this.runId} err=${String(err)}`);
120
+ }
121
+ }
122
+ }
@@ -18,7 +18,7 @@ export async function sendWeixinMediaFile(params: {
18
18
  filePath: string;
19
19
  to: string;
20
20
  text: string;
21
- opts: WeixinApiOptions & { contextToken?: string };
21
+ opts: WeixinApiOptions & { contextToken?: string; runId?: string };
22
22
  cdnBaseUrl: string;
23
23
  }): Promise<{ messageId: string }> {
24
24
  const { filePath, to, text, opts, cdnBaseUrl } = params;
@@ -10,6 +10,11 @@ import type { UploadedFileInfo } from "../cdn/upload.js";
10
10
 
11
11
  export { StreamingMarkdownFilter } from "./markdown-filter.js";
12
12
 
13
+ type WeixinMessageSendOptions = WeixinApiOptions & {
14
+ contextToken?: string;
15
+ runId?: string;
16
+ };
17
+
13
18
  function generateClientId(): string {
14
19
  return generateId("openclaw-weixin");
15
20
  }
@@ -19,9 +24,10 @@ function buildTextMessageReq(params: {
19
24
  to: string;
20
25
  text: string;
21
26
  contextToken?: string;
27
+ runId?: string;
22
28
  clientId: string;
23
29
  }): SendMessageReq {
24
- const { to, text, contextToken, clientId } = params;
30
+ const { to, text, contextToken, runId, clientId } = params;
25
31
  const item_list: MessageItem[] = text
26
32
  ? [{ type: MessageItemType.TEXT, text_item: { text } }]
27
33
  : [];
@@ -34,6 +40,7 @@ function buildTextMessageReq(params: {
34
40
  message_state: MessageState.FINISH,
35
41
  item_list: item_list.length ? item_list : undefined,
36
42
  context_token: contextToken ?? undefined,
43
+ run_id: runId ?? undefined,
37
44
  },
38
45
  };
39
46
  }
@@ -42,14 +49,16 @@ function buildTextMessageReq(params: {
42
49
  function buildSendMessageReq(params: {
43
50
  to: string;
44
51
  contextToken?: string;
52
+ runId?: string;
45
53
  payload: ReplyPayload;
46
54
  clientId: string;
47
55
  }): SendMessageReq {
48
- const { to, contextToken, payload, clientId } = params;
56
+ const { to, contextToken, runId, payload, clientId } = params;
49
57
  return buildTextMessageReq({
50
58
  to,
51
59
  text: payload.text ?? "",
52
60
  contextToken,
61
+ runId,
53
62
  clientId,
54
63
  });
55
64
  }
@@ -60,7 +69,7 @@ function buildSendMessageReq(params: {
60
69
  export async function sendMessageWeixin(params: {
61
70
  to: string;
62
71
  text: string;
63
- opts: WeixinApiOptions & { contextToken?: string };
72
+ opts: WeixinMessageSendOptions;
64
73
  }): Promise<{ messageId: string }> {
65
74
  const { to, text, opts } = params;
66
75
  if (!opts.contextToken) {
@@ -70,6 +79,7 @@ export async function sendMessageWeixin(params: {
70
79
  const req = buildSendMessageReq({
71
80
  to,
72
81
  contextToken: opts.contextToken,
82
+ runId: opts.runId,
73
83
  payload: { text },
74
84
  clientId,
75
85
  });
@@ -87,6 +97,47 @@ export async function sendMessageWeixin(params: {
87
97
  return { messageId: clientId };
88
98
  }
89
99
 
100
+ /** Send a single structured MessageItem downstream. */
101
+ export async function sendMessageItemWeixin(params: {
102
+ to: string;
103
+ item: MessageItem;
104
+ opts: WeixinMessageSendOptions;
105
+ clientId?: string;
106
+ label?: string;
107
+ }): Promise<{ messageId: string }> {
108
+ const { to, item, opts } = params;
109
+ if (!opts.contextToken) {
110
+ logger.warn(`sendMessageItemWeixin: contextToken missing for to=${to}, sending without context`);
111
+ }
112
+ const clientId = params.clientId ?? generateClientId();
113
+ const req: SendMessageReq = {
114
+ msg: {
115
+ from_user_id: "",
116
+ to_user_id: to,
117
+ client_id: clientId,
118
+ message_type: MessageType.BOT,
119
+ message_state: MessageState.FINISH,
120
+ item_list: [item],
121
+ context_token: opts.contextToken ?? undefined,
122
+ run_id: opts.runId,
123
+ },
124
+ };
125
+ try {
126
+ await sendMessageApi({
127
+ baseUrl: opts.baseUrl,
128
+ token: opts.token,
129
+ timeoutMs: opts.timeoutMs,
130
+ body: req,
131
+ });
132
+ } catch (err) {
133
+ logger.error(
134
+ `${params.label ?? "sendMessageItemWeixin"}: failed to=${to} clientId=${clientId} err=${String(err)}`,
135
+ );
136
+ throw err;
137
+ }
138
+ return { messageId: clientId };
139
+ }
140
+
90
141
  /**
91
142
  * Send one or more MessageItems (optionally preceded by a text caption) downstream.
92
143
  * Each item is sent as its own request so that item_list always has exactly one entry.
@@ -95,10 +146,11 @@ async function sendMediaItems(params: {
95
146
  to: string;
96
147
  text: string;
97
148
  mediaItem: MessageItem;
98
- opts: WeixinApiOptions & { contextToken?: string };
149
+ opts: WeixinMessageSendOptions;
99
150
  label: string;
100
151
  }): Promise<{ messageId: string }> {
101
152
  const { to, text, mediaItem, opts, label } = params;
153
+ const runId = opts.runId;
102
154
 
103
155
  const items: MessageItem[] = [];
104
156
  if (text) {
@@ -118,6 +170,7 @@ async function sendMediaItems(params: {
118
170
  message_state: MessageState.FINISH,
119
171
  item_list: [item],
120
172
  context_token: opts.contextToken ?? undefined,
173
+ run_id: runId,
121
174
  },
122
175
  };
123
176
  try {
@@ -152,7 +205,7 @@ export async function sendImageMessageWeixin(params: {
152
205
  to: string;
153
206
  text: string;
154
207
  uploaded: UploadedFileInfo;
155
- opts: WeixinApiOptions & { contextToken?: string };
208
+ opts: WeixinMessageSendOptions;
156
209
  }): Promise<{ messageId: string }> {
157
210
  const { to, text, uploaded, opts } = params;
158
211
  if (!opts.contextToken) {
@@ -186,7 +239,7 @@ export async function sendVideoMessageWeixin(params: {
186
239
  to: string;
187
240
  text: string;
188
241
  uploaded: UploadedFileInfo;
189
- opts: WeixinApiOptions & { contextToken?: string };
242
+ opts: WeixinMessageSendOptions;
190
243
  }): Promise<{ messageId: string }> {
191
244
  const { to, text, uploaded, opts } = params;
192
245
  if (!opts.contextToken) {
@@ -218,7 +271,7 @@ export async function sendFileMessageWeixin(params: {
218
271
  text: string;
219
272
  fileName: string;
220
273
  uploaded: UploadedFileInfo;
221
- opts: WeixinApiOptions & { contextToken?: string };
274
+ opts: WeixinMessageSendOptions;
222
275
  }): Promise<{ messageId: string }> {
223
276
  const { to, text, fileName, uploaded, opts } = params;
224
277
  if (!opts.contextToken) {
@@ -96,6 +96,9 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
96
96
  token,
97
97
  get_updates_buf: getUpdatesBuf,
98
98
  timeoutMs: nextTimeoutMs,
99
+ // Stop/hot-reload should cancel the in-flight long-poll immediately
100
+ // instead of waiting for the server-side long-poll timeout.
101
+ abortSignal,
99
102
  });
100
103
  aLog.debug(
101
104
  `getUpdates response: ret=${resp.ret}, msgs=${resp.msgs?.length ?? 0}, get_updates_buf_length=${resp.get_updates_buf?.length ?? 0}`,