@tencent-weixin/openclaw-weixin 2.1.5 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  This project follows the [Keep a Changelog](https://keepachangelog.com/) format.
6
6
 
7
+ ## [2.1.7] - 2026-04-07
8
+
9
+ ### Fixed
10
+
11
+ - **Plugin registration re-entrance:** Lazy-import `monitorWeixinProvider` inside `startAccount` in `channel.ts` to avoid pulling in the monitor → process-message → command-auth chain at plugin registration time, which could re-enter the plugin/provider registry before the account starts.
12
+ - **Initialization side effect:** Lazy-import `resolveSenderCommandAuthorizationWithRuntime` / `resolveDirectDmAuthorizationOutcome` in `process-message.ts` to prevent `ensureContextWindowCacheLoaded` from being triggered during module initialization, which caused `loadOpenClawPlugins` re-entrance.
13
+
14
+ ### Changed
15
+
16
+ - **Tool-call outbound path:** `sendWeixinOutbound` now applies `StreamingMarkdownFilter` to the outbound text, consistent with the model-output path in `process-message`.
17
+
7
18
  ## [2.1.4] - 2026-04-03
8
19
 
9
20
  ### Changed
@@ -4,6 +4,17 @@
4
4
 
5
5
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/) 格式。
6
6
 
7
+ ## [2.1.7] - 2026-04-07
8
+
9
+ ### 修复
10
+
11
+ - **插件注册重入:** `channel.ts` 中将 `monitorWeixinProvider` 改为在 `startAccount` 内部懒加载(`await import(...)`),避免插件注册阶段提前拉取 monitor → process-message → command-auth 依赖链,导致 plugin/provider registry 重入。
12
+ - **初始化副作用:** `process-message.ts` 中将 `resolveSenderCommandAuthorizationWithRuntime` / `resolveDirectDmAuthorizationOutcome` 改为懒加载,避免模块初始化时触发宿主的 `ensureContextWindowCacheLoaded` 副作用,进而导致 `loadOpenClawPlugins` 重入。
13
+
14
+ ### 变更
15
+
16
+ - **tool-call 外发路径:** `sendWeixinOutbound` 现在对发送文本应用 `StreamingMarkdownFilter`,与 `process-message` 中的 model-output 路径保持一致。
17
+
7
18
  ## [2.1.4] - 2026-04-03
8
19
 
9
20
  ### 变更
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-weixin",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "channels": [
5
5
  "openclaw-weixin"
6
6
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-weixin/openclaw-weixin",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "description": "OpenClaw Weixin channel",
5
5
  "license": "MIT",
6
6
  "author": "Tencent",
package/src/channel.ts CHANGED
@@ -24,9 +24,11 @@ import {
24
24
  waitForWeixinLogin,
25
25
  } from "./auth/login-qr.js";
26
26
  import type { WeixinQrStartResult, WeixinQrWaitResult } from "./auth/login-qr.js";
27
- import { monitorWeixinProvider } from "./monitor/monitor.js";
27
+ // Lazy-imported inside startAccount to avoid pulling in the monitor -> process-message ->
28
+ // command-auth chain during plugin registration, which can re-enter plugin/provider registry
29
+ // resolution before the account actually starts.
28
30
  import { sendWeixinMediaFile } from "./messaging/send-media.js";
29
- import { sendMessageWeixin } from "./messaging/send.js";
31
+ import { sendMessageWeixin, StreamingMarkdownFilter } from "./messaging/send.js";
30
32
  import { downloadRemoteImageToTemp } from "./cdn/upload.js";
31
33
 
32
34
  /** Returns true when mediaUrl refers to a local filesystem path (absolute or relative). */
@@ -119,7 +121,10 @@ async function sendWeixinOutbound(params: {
119
121
  if (!params.contextToken) {
120
122
  aLog.warn(`sendWeixinOutbound: contextToken missing for to=${params.to}, sending without context`);
121
123
  }
122
- const result = await sendMessageWeixin({ to: params.to, text: params.text, opts: {
124
+ const f = new StreamingMarkdownFilter();
125
+ const rawText = params.text ?? "";
126
+ const filteredText = f.feed(rawText) + f.flush();
127
+ const result = await sendMessageWeixin({ to: params.to, text: filteredText, opts: {
123
128
  baseUrl: account.baseUrl,
124
129
  token: account.token,
125
130
  contextToken: params.contextToken,
@@ -383,6 +388,7 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
383
388
  const logPath = aLog.getLogFilePath();
384
389
  ctx.log?.info?.(`[${account.accountId}] weixin logs: ${logPath}`);
385
390
 
391
+ const { monitorWeixinProvider } = await import("./monitor/monitor.js");
386
392
  return monitorWeixinProvider({
387
393
  baseUrl: account.baseUrl,
388
394
  cdnBaseUrl: account.cdnBaseUrl,
@@ -6,22 +6,9 @@
6
6
  * holding back the minimum characters needed for pattern disambiguation
7
7
  * (e.g. a trailing `*` that might become `***`).
8
8
  *
9
- * Constructs passed through (not filtered):
10
- * - Code fences (```)
11
- * - Inline code (`)
12
- * - Tables (|...|)
13
- * - Horizontal rules (---, ***, ___)
14
- * - Bold (**)
15
- * - Italic/bold-italic wrapping non-CJK content
16
- *
17
- * Constructs filtered (markers stripped, content kept):
18
- * - Italic/bold-italic wrapping CJK content
19
- * - Headings H5/H6 (#####, ######)
20
- * - Images (![alt](url)) — removed entirely
21
- *
22
9
  * States:
23
10
  * - **sol** (start-of-line): checks for line-start patterns (```, >, #####, indent)
24
- * - **body**: scans for inline patterns (![, ~~, ***) and outputs safe chars
11
+ * - **body**: scans for inline patterns (`, ![, ~~, ***) and outputs safe chars
25
12
  * - **fence**: inside a fenced code block, passes through until closing ```
26
13
  * - **inline**: accumulating content inside an inline marker pair
27
14
  */
@@ -29,7 +16,7 @@ export class StreamingMarkdownFilter {
29
16
  private buf = "";
30
17
  private fence = false;
31
18
  private sol = true;
32
- private inl: { type: "image" | "bold3" | "italic" | "ubold3" | "uitalic"; acc: string } | null = null;
19
+ private inl: { type: "code" | "image" | "strike" | "bold3" | "italic" | "ubold3" | "uitalic" | "table"; acc: string } | null = null;
33
20
 
34
21
  feed(delta: string): string {
35
22
  this.buf += delta;
@@ -58,32 +45,26 @@ export class StreamingMarkdownFilter {
58
45
  }
59
46
 
60
47
  if (eof && this.inl) {
61
- const markers: Record<string, string> = { image: "![", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
62
- out += (markers[this.inl.type] ?? "") + this.inl.acc;
48
+ if (this.inl.type === "table") {
49
+ out += StreamingMarkdownFilter.extractTableRow(this.inl.acc);
50
+ } else {
51
+ const markers: Record<string, string> = { code: "`", image: "![", strike: "~~", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
52
+ out += (markers[this.inl.type] ?? "") + this.inl.acc;
53
+ }
63
54
  this.inl = null;
64
55
  }
65
56
  return out;
66
57
  }
67
58
 
68
- /** Inside a code fence: pass content and markers through verbatim. */
59
+ /** Inside a code fence: pass content through, watch for closing ``` at SOL. */
69
60
  private pumpFence(eof: boolean): string {
70
61
  if (this.sol) {
71
62
  if (this.buf.length < 3 && !eof) return "";
72
63
  if (this.buf.startsWith("```")) {
64
+ this.fence = false;
73
65
  const nl = this.buf.indexOf("\n", 3);
74
- if (nl !== -1) {
75
- this.fence = false;
76
- const line = this.buf.slice(0, nl + 1);
77
- this.buf = this.buf.slice(nl + 1);
78
- this.sol = true;
79
- return line;
80
- }
81
- if (eof) {
82
- this.fence = false;
83
- const line = this.buf;
84
- this.buf = "";
85
- return line;
86
- }
66
+ this.buf = nl !== -1 ? this.buf.slice(nl + 1) : "";
67
+ this.sol = true;
87
68
  return "";
88
69
  }
89
70
  this.sol = false;
@@ -112,18 +93,10 @@ export class StreamingMarkdownFilter {
112
93
  if (b[0] === "`") {
113
94
  if (b.length < 3 && !eof) return "";
114
95
  if (b.startsWith("```")) {
96
+ this.fence = true;
115
97
  const nl = b.indexOf("\n", 3);
116
- if (nl !== -1) {
117
- this.fence = true;
118
- const line = b.slice(0, nl + 1);
119
- this.buf = b.slice(nl + 1);
120
- this.sol = true;
121
- return line;
122
- }
123
- if (eof) {
124
- this.buf = "";
125
- return b;
126
- }
98
+ this.buf = nl !== -1 ? b.slice(nl + 1) : "";
99
+ this.sol = true;
127
100
  return "";
128
101
  }
129
102
  this.sol = false;
@@ -131,6 +104,8 @@ export class StreamingMarkdownFilter {
131
104
  }
132
105
 
133
106
  if (b[0] === ">") {
107
+ if (b.length < 2 && !eof) return "";
108
+ this.buf = b.length >= 2 && b[1] === " " ? b.slice(2) : b.slice(1);
134
109
  this.sol = false;
135
110
  return "";
136
111
  }
@@ -148,6 +123,13 @@ export class StreamingMarkdownFilter {
148
123
  return "";
149
124
  }
150
125
 
126
+ if (b[0] === "|") {
127
+ this.buf = b.slice(1);
128
+ this.inl = { type: "table", acc: "" };
129
+ this.sol = false;
130
+ return "";
131
+ }
132
+
151
133
  if (b[0] === " " || b[0] === "\t") {
152
134
  if (b.search(/[^ \t]/) === -1 && !eof) return "";
153
135
  this.sol = false;
@@ -163,13 +145,9 @@ export class StreamingMarkdownFilter {
163
145
  let count = 0;
164
146
  for (let k = 0; k < j; k++) if (b[k] === ch) count++;
165
147
  if (count >= 3) {
166
- if (j < b.length) {
167
- this.buf = b.slice(j + 1);
168
- this.sol = true;
169
- return b.slice(0, j + 1);
170
- }
171
- this.buf = "";
172
- return b;
148
+ this.buf = j < b.length ? b.slice(j + 1) : "";
149
+ this.sol = true;
150
+ return "";
173
151
  }
174
152
  }
175
153
  this.sol = false;
@@ -192,15 +170,23 @@ export class StreamingMarkdownFilter {
192
170
  this.sol = true;
193
171
  return out;
194
172
  }
173
+ if (c === "`") {
174
+ out += this.buf.slice(0, i);
175
+ this.buf = this.buf.slice(i + 1);
176
+ this.inl = { type: "code", acc: "" };
177
+ return out;
178
+ }
195
179
  if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
196
180
  out += this.buf.slice(0, i);
197
181
  this.buf = this.buf.slice(i + 2);
198
182
  this.inl = { type: "image", acc: "" };
199
183
  return out;
200
184
  }
201
- if (c === "~") {
202
- i++;
203
- continue;
185
+ if (c === "~" && i + 1 < this.buf.length && this.buf[i + 1] === "~") {
186
+ out += this.buf.slice(0, i);
187
+ this.buf = this.buf.slice(i + 2);
188
+ this.inl = { type: "strike", acc: "" };
189
+ return out;
204
190
  }
205
191
  if (c === "*") {
206
192
  if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
@@ -251,6 +237,7 @@ export class StreamingMarkdownFilter {
251
237
  else if (this.buf.endsWith("__")) hold = 2;
252
238
  else if (this.buf.endsWith("*")) hold = 1;
253
239
  else if (this.buf.endsWith("_")) hold = 1;
240
+ else if (this.buf.endsWith("~")) hold = 1;
254
241
  else if (this.buf.endsWith("!")) hold = 1;
255
242
  }
256
243
  out += this.buf.slice(0, this.buf.length - hold);
@@ -265,14 +252,41 @@ export class StreamingMarkdownFilter {
265
252
  this.buf = "";
266
253
 
267
254
  switch (this.inl.type) {
255
+ case "code": {
256
+ const idx = this.inl.acc.indexOf("`");
257
+ if (idx !== -1) {
258
+ const content = this.inl.acc.slice(0, idx);
259
+ this.buf = this.inl.acc.slice(idx + 1);
260
+ this.inl = null;
261
+ return content;
262
+ }
263
+ const nl = this.inl.acc.indexOf("\n");
264
+ if (nl !== -1) {
265
+ const r = "`" + this.inl.acc.slice(0, nl + 1);
266
+ this.buf = this.inl.acc.slice(nl + 1);
267
+ this.inl = null;
268
+ this.sol = true;
269
+ return r;
270
+ }
271
+ return "";
272
+ }
273
+ case "strike": {
274
+ const idx = this.inl.acc.indexOf("~~");
275
+ if (idx !== -1) {
276
+ const content = this.inl.acc.slice(0, idx);
277
+ this.buf = this.inl.acc.slice(idx + 2);
278
+ this.inl = null;
279
+ return content;
280
+ }
281
+ return "";
282
+ }
268
283
  case "bold3": {
269
284
  const idx = this.inl.acc.indexOf("***");
270
285
  if (idx !== -1) {
271
286
  const content = this.inl.acc.slice(0, idx);
272
287
  this.buf = this.inl.acc.slice(idx + 3);
273
288
  this.inl = null;
274
- if (StreamingMarkdownFilter.containsCJK(content)) return content;
275
- return `***${content}***`;
289
+ return content;
276
290
  }
277
291
  return "";
278
292
  }
@@ -282,8 +296,7 @@ export class StreamingMarkdownFilter {
282
296
  const content = this.inl.acc.slice(0, idx);
283
297
  this.buf = this.inl.acc.slice(idx + 3);
284
298
  this.inl = null;
285
- if (StreamingMarkdownFilter.containsCJK(content)) return content;
286
- return `___${content}___`;
299
+ return content;
287
300
  }
288
301
  return "";
289
302
  }
@@ -304,8 +317,7 @@ export class StreamingMarkdownFilter {
304
317
  const content = this.inl.acc.slice(0, j);
305
318
  this.buf = this.inl.acc.slice(j + 1);
306
319
  this.inl = null;
307
- if (StreamingMarkdownFilter.containsCJK(content)) return content;
308
- return `*${content}*`;
320
+ return content;
309
321
  }
310
322
  }
311
323
  return "";
@@ -327,8 +339,7 @@ export class StreamingMarkdownFilter {
327
339
  const content = this.inl.acc.slice(0, j);
328
340
  this.buf = this.inl.acc.slice(j + 1);
329
341
  this.inl = null;
330
- if (StreamingMarkdownFilter.containsCJK(content)) return content;
331
- return `_${content}_`;
342
+ return content;
332
343
  }
333
344
  }
334
345
  return "";
@@ -351,11 +362,30 @@ export class StreamingMarkdownFilter {
351
362
  }
352
363
  return "";
353
364
  }
365
+ case "table": {
366
+ const nl = this.inl.acc.indexOf("\n");
367
+ if (nl !== -1) {
368
+ const line = this.inl.acc.slice(0, nl);
369
+ this.buf = this.inl.acc.slice(nl + 1);
370
+ this.inl = null;
371
+ this.sol = true;
372
+ const row = StreamingMarkdownFilter.extractTableRow(line);
373
+ return row ? row + "\n" : "";
374
+ }
375
+ return "";
376
+ }
354
377
  }
355
378
  return "";
356
379
  }
357
380
 
358
- private static containsCJK(text: string): boolean {
359
- return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
381
+ /** Extract cell contents from a table row, or return "" for separator rows. */
382
+ private static extractTableRow(line: string): string {
383
+ if (/^[\s|:\-]+$/.test(line) && line.includes("-")) return "";
384
+ const parts = line.split("|").map(c => c.trim());
385
+ const cells = parts.slice(
386
+ parts[0] === "" ? 1 : 0,
387
+ parts[parts.length - 1] === "" ? parts.length - 1 : parts.length,
388
+ );
389
+ return cells.join("\t");
360
390
  }
361
391
  }