@tencent-weixin/openclaw-weixin 2.1.2 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -3,3 +3,40 @@
3
3
  [简体中文](CHANGELOG.zh_CN.md)
4
4
 
5
5
  This project follows the [Keep a Changelog](https://keepachangelog.com/) format.
6
+
7
+ ## [2.1.4] - 2026-04-03
8
+
9
+ ### Changed
10
+
11
+ - **QR login:** Remove client-side timeout for `get_bot_qrcode`; the request is no longer aborted on a fixed deadline (server / stack limits still apply).
12
+
13
+ ## [2.1.3] - 2026-04-02
14
+
15
+ ### Added
16
+
17
+ - **`StreamingMarkdownFilter`** (`src/messaging/markdown-filter.ts`): outbound text no longer runs through whole-string `markdownToPlainText` stripping; a streaming character filter replaces it, so Markdown goes from **effectively unsupported** to **partially supported**.
18
+
19
+ ### Changed
20
+
21
+ - **Outbound text path:** `process-message` uses `StreamingMarkdownFilter` (`feed` / `flush`) per deliver chunk instead of `markdownToPlainText`.
22
+
23
+ ### Removed
24
+
25
+ - **`markdownToPlainText`** from `src/messaging/send.ts` (and its tests from `send.test.ts`); coverage moves to `markdown-filter.test.ts`.
26
+
27
+ ## [2.1.2] - 2026-04-02
28
+
29
+ ### Changed
30
+
31
+ - **Config reload after login:** On each successful Weixin login, bump `channels.openclaw-weixin.channelConfigUpdatedAt` (ISO 8601) in `openclaw.json` so the gateway reloads config from disk, instead of writing an empty `accounts: {}` placeholder.
32
+ - **QR login:** Increase client timeout for `get_bot_qrcode` from 5s to 10s.
33
+ - **Docs:** Uninstall instructions now use `openclaw plugins uninstall @tencent-weixin/openclaw-weixin` (aligned with the plugins CLI).
34
+ - **Logging:** `debug-check` log line no longer includes `stateDir` / `OPENCLAW_STATE_DIR`.
35
+
36
+ ### Removed
37
+
38
+ - **`openclaw-weixin` CLI subcommands** (`src/weixin-cli.ts` and registration in `index.ts`). Use the host `openclaw plugins uninstall …` flow instead.
39
+
40
+ ### Fixed
41
+
42
+ - Resolves the **dangerous code pattern** warning when installing the plugin on **OpenClaw 2026.3.31+** (host plugin install / static checks).
@@ -1,3 +1,42 @@
1
1
  # 变更日志
2
2
 
3
+ [English](CHANGELOG.md)
4
+
3
5
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/) 格式。
6
+
7
+ ## [2.1.4] - 2026-04-03
8
+
9
+ ### 变更
10
+
11
+ - **扫码登录:** 移除 `get_bot_qrcode` 的客户端超时,请求不再因固定时限被 abort(仍受服务端与网络栈限制)。
12
+
13
+ ## [2.1.3] - 2026-04-02
14
+
15
+ ### 新增
16
+
17
+ - **`StreamingMarkdownFilter`**(`src/messaging/markdown-filter.ts`):外发文本由原先 `markdownToPlainText` 整段剥离 Markdown,改为流式逐字符过滤;**对 Markdown 从完全不支持变为部分支持**。
18
+
19
+ ### 变更
20
+
21
+ - **外发文本:** `process-message` 在每次 `deliver` 时用 `StreamingMarkdownFilter`(`feed` / `flush`)处理回复,替代 `markdownToPlainText`。
22
+
23
+ ### 移除
24
+
25
+ - 从 `src/messaging/send.ts` 删除 **`markdownToPlainText`**(相关用例从 `send.test.ts` 迁至 `markdown-filter.test.ts`)。
26
+
27
+ ## [2.1.2] - 2026-04-02
28
+
29
+ ### 变更
30
+
31
+ - **登录后配置刷新:** 每次微信登录成功后,在 `openclaw.json` 中更新 `channels.openclaw-weixin.channelConfigUpdatedAt`(ISO 8601),让网关从磁盘重新加载配置;不再写入空的 `accounts: {}` 占位。
32
+ - **扫码登录:** `get_bot_qrcode` 客户端超时由 5s 调整为 10s。
33
+ - **文档:** 卸载说明改为使用 `openclaw plugins uninstall @tencent-weixin/openclaw-weixin`,与插件 CLI 一致。
34
+ - **日志:** `debug-check` 日志不再输出 `stateDir` / `OPENCLAW_STATE_DIR`。
35
+
36
+ ### 移除
37
+
38
+ - **`openclaw-weixin` 子命令**(删除 `src/weixin-cli.ts` 及 `index.ts` 中的注册)。请使用宿主自带的 `openclaw plugins uninstall …` 卸载流程。
39
+
40
+ ### 修复
41
+
42
+ - 解决在 **OpenClaw 2026.3.31 及更新版本**上安装插件时出现的 **dangerous code pattern** 提示(宿主插件安装 / 静态检查)。
package/README.md CHANGED
@@ -66,14 +66,12 @@ Each QR code login creates a new account entry, supporting multiple WeChat accou
66
66
 
67
67
  ## Multi-Account Context Isolation
68
68
 
69
- By default, all channels share the same AI conversation context. To isolate conversation context for each WeChat account:
69
+ By default, DMs can share one session bucket. For **multiple logged-in WeChat accounts**, isolate by account + channel + sender:
70
70
 
71
71
  ```bash
72
- openclaw config set agents.mode per-channel-per-peer
72
+ openclaw config set session.dmScope per-account-channel-peer
73
73
  ```
74
74
 
75
- This gives each "WeChat account + message sender" combination its own independent AI memory, preventing context cross-talk between accounts.
76
-
77
75
  ## Backend API Protocol
78
76
 
79
77
  This plugin communicates with the backend gateway via HTTP JSON API. Developers integrating with their own backend need to implement the following interfaces.
package/README.zh_CN.md CHANGED
@@ -65,14 +65,12 @@ openclaw channels login --channel openclaw-weixin
65
65
 
66
66
  ## 多账号上下文隔离
67
67
 
68
- 默认情况下,所有渠道的 AI 会话共享同一个上下文。如果希望每个微信账号的对话上下文相互隔离:
68
+ 默认情况下,私聊可能共用同一会话桶。**多个微信号同时登录**时,建议按「账号 + 渠道 + 对端」隔离:
69
69
 
70
70
  ```bash
71
- openclaw config set agents.mode per-channel-per-peer
71
+ openclaw config set session.dmScope per-account-channel-peer
72
72
  ```
73
73
 
74
- 这样每个「微信账号 + 发消息用户」组合都会拥有独立的 AI 记忆,账号之间不会串台。
75
-
76
74
  ## 后端 API 协议
77
75
 
78
76
  本插件通过 HTTP JSON API 与后端网关通信。二次开发者若需对接自有后端,需实现以下接口。
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "id": "openclaw-weixin",
3
- "version": "2.1.1",
4
- "channels": ["openclaw-weixin"],
3
+ "version": "2.1.4",
4
+ "channels": [
5
+ "openclaw-weixin"
6
+ ],
5
7
  "configSchema": {
6
8
  "type": "object",
7
9
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-weixin/openclaw-weixin",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "OpenClaw Weixin channel",
5
5
  "license": "MIT",
6
6
  "author": "Tencent",
package/src/api/api.ts CHANGED
@@ -121,14 +121,15 @@ function buildHeaders(opts: { token?: string; body: string }): Record<string, st
121
121
  }
122
122
 
123
123
  /**
124
- * GET fetch wrapper: send a GET request to a Weixin API endpoint with timeout + abort.
124
+ * GET fetch wrapper: send a GET request to a Weixin API endpoint.
125
+ * When `timeoutMs` is set, the request is aborted after that many milliseconds.
125
126
  * Query parameters should already be encoded in `endpoint`.
126
- * Returns the raw response text on success; throws on HTTP error or timeout.
127
+ * Returns the raw response text on success; throws on HTTP error or (if used) timeout abort.
127
128
  */
128
129
  export async function apiGetFetch(params: {
129
130
  baseUrl: string;
130
131
  endpoint: string;
131
- timeoutMs: number;
132
+ timeoutMs?: number;
132
133
  label: string;
133
134
  }): Promise<string> {
134
135
  const base = ensureTrailingSlash(params.baseUrl);
@@ -136,15 +137,20 @@ export async function apiGetFetch(params: {
136
137
  const hdrs = buildCommonHeaders();
137
138
  logger.debug(`GET ${redactUrl(url.toString())}`);
138
139
 
139
- const controller = new AbortController();
140
- const t = setTimeout(() => controller.abort(), params.timeoutMs);
140
+ const timeoutMs = params.timeoutMs;
141
+ const controller =
142
+ timeoutMs != null && timeoutMs > 0 ? new AbortController() : undefined;
143
+ const t =
144
+ controller != null && timeoutMs != null
145
+ ? setTimeout(() => controller.abort(), timeoutMs)
146
+ : undefined;
141
147
  try {
142
148
  const res = await fetch(url.toString(), {
143
149
  method: "GET",
144
150
  headers: hdrs,
145
- signal: controller.signal,
151
+ ...(controller ? { signal: controller.signal } : {}),
146
152
  });
147
- clearTimeout(t);
153
+ if (t !== undefined) clearTimeout(t);
148
154
  const rawText = await res.text();
149
155
  logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
150
156
  if (!res.ok) {
@@ -152,7 +158,7 @@ export async function apiGetFetch(params: {
152
158
  }
153
159
  return rawText;
154
160
  } catch (err) {
155
- clearTimeout(t);
161
+ if (t !== undefined) clearTimeout(t);
156
162
  throw err;
157
163
  }
158
164
  }
@@ -18,8 +18,6 @@ type ActiveLogin = {
18
18
  };
19
19
 
20
20
  const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
21
- /** Client-side timeout for the get_bot_qrcode request. */
22
- const GET_QRCODE_TIMEOUT_MS = 10_000;
23
21
  /** Client-side timeout for the long-poll get_qrcode_status request. */
24
22
  const QR_LONG_POLL_TIMEOUT_MS = 35_000;
25
23
 
@@ -65,7 +63,6 @@ async function fetchQRCode(apiBaseUrl: string, botType: string): Promise<QRCodeR
65
63
  const rawText = await apiGetFetch({
66
64
  baseUrl: apiBaseUrl,
67
65
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
68
- timeoutMs: GET_QRCODE_TIMEOUT_MS,
69
66
  label: "fetchQRCode",
70
67
  });
71
68
  return JSON.parse(rawText) as QRCodeResponse;
package/src/compat.ts CHANGED
@@ -8,8 +8,6 @@
8
8
 
9
9
  import { logger } from "./util/logger.js";
10
10
 
11
- export const PLUGIN_VERSION = "2.0.0";
12
-
13
11
  export const SUPPORTED_HOST_MIN = "2026.3.22";
14
12
 
15
13
  export interface OpenClawVersion {
@@ -71,7 +69,7 @@ export function assertHostCompatibility(hostVersion: string | undefined): void {
71
69
  return;
72
70
  }
73
71
  throw new Error(
74
- `openclaw-weixin@${PLUGIN_VERSION} requires OpenClaw >=${SUPPORTED_HOST_MIN}, ` +
72
+ `This version of openclaw-weixin requires OpenClaw >=${SUPPORTED_HOST_MIN}, ` +
75
73
  `but found ${hostVersion}. ` +
76
74
  `Please upgrade OpenClaw, or install the compatible track for older hosts:\n` +
77
75
  ` npx @tencent-weixin/openclaw-weixin-cli install`,
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Streaming markdown filter — character-level state machine that strips
3
+ * unsupported markdown syntax on-the-fly.
4
+ *
5
+ * Outputs as much filtered text as possible on each `feed()` call, only
6
+ * holding back the minimum characters needed for pattern disambiguation
7
+ * (e.g. a trailing `*` that might become `***`).
8
+ *
9
+ * States:
10
+ * - **sol** (start-of-line): checks for line-start patterns (```, >, #####, indent)
11
+ * - **body**: scans for inline patterns (`, ![, ~~, ***) and outputs safe chars
12
+ * - **fence**: inside a fenced code block, passes through until closing ```
13
+ * - **inline**: accumulating content inside an inline marker pair
14
+ */
15
+ export class StreamingMarkdownFilter {
16
+ private buf = "";
17
+ private fence = false;
18
+ private sol = true;
19
+ private inl: { type: "code" | "image" | "strike" | "bold3" | "italic" | "ubold3" | "uitalic" | "table"; acc: string } | null = null;
20
+
21
+ feed(delta: string): string {
22
+ this.buf += delta;
23
+ return this.pump(false);
24
+ }
25
+
26
+ flush(): string {
27
+ return this.pump(true);
28
+ }
29
+
30
+ private pump(eof: boolean): string {
31
+ let out = "";
32
+ while (this.buf) {
33
+ const sLen = this.buf.length;
34
+ const sSol = this.sol;
35
+ const sFence = this.fence;
36
+ const sInl = this.inl;
37
+
38
+ if (this.fence) out += this.pumpFence(eof);
39
+ else if (this.inl) out += this.pumpInline(eof);
40
+ else if (this.sol) out += this.pumpSOL(eof);
41
+ else out += this.pumpBody(eof);
42
+
43
+ if (this.buf.length === sLen && this.sol === sSol &&
44
+ this.fence === sFence && this.inl === sInl) break;
45
+ }
46
+
47
+ if (eof && this.inl) {
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
+ }
54
+ this.inl = null;
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /** Inside a code fence: pass content through, watch for closing ``` at SOL. */
60
+ private pumpFence(eof: boolean): string {
61
+ if (this.sol) {
62
+ if (this.buf.length < 3 && !eof) return "";
63
+ if (this.buf.startsWith("```")) {
64
+ this.fence = false;
65
+ const nl = this.buf.indexOf("\n", 3);
66
+ this.buf = nl !== -1 ? this.buf.slice(nl + 1) : "";
67
+ this.sol = true;
68
+ return "";
69
+ }
70
+ this.sol = false;
71
+ }
72
+ const nl = this.buf.indexOf("\n");
73
+ if (nl !== -1) {
74
+ const chunk = this.buf.slice(0, nl + 1);
75
+ this.buf = this.buf.slice(nl + 1);
76
+ this.sol = true;
77
+ return chunk;
78
+ }
79
+ const chunk = this.buf;
80
+ this.buf = "";
81
+ return chunk;
82
+ }
83
+
84
+ /** At start of line: detect and consume line-start patterns, then transition to body. */
85
+ private pumpSOL(eof: boolean): string {
86
+ const b = this.buf;
87
+
88
+ if (b[0] === "\n") {
89
+ this.buf = b.slice(1);
90
+ return "\n";
91
+ }
92
+
93
+ if (b[0] === "`") {
94
+ if (b.length < 3 && !eof) return "";
95
+ if (b.startsWith("```")) {
96
+ this.fence = true;
97
+ const nl = b.indexOf("\n", 3);
98
+ this.buf = nl !== -1 ? b.slice(nl + 1) : "";
99
+ this.sol = true;
100
+ return "";
101
+ }
102
+ this.sol = false;
103
+ return "";
104
+ }
105
+
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);
109
+ this.sol = false;
110
+ return "";
111
+ }
112
+
113
+ if (b[0] === "#") {
114
+ let n = 0;
115
+ while (n < b.length && b[n] === "#") n++;
116
+ if (n === b.length && !eof) return "";
117
+ if (n >= 5 && n <= 6 && n < b.length && b[n] === " ") {
118
+ this.buf = b.slice(n + 1);
119
+ this.sol = false;
120
+ return "";
121
+ }
122
+ this.sol = false;
123
+ return "";
124
+ }
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
+
133
+ if (b[0] === " " || b[0] === "\t") {
134
+ if (b.search(/[^ \t]/) === -1 && !eof) return "";
135
+ this.sol = false;
136
+ return "";
137
+ }
138
+
139
+ if (b[0] === "-" || b[0] === "*" || b[0] === "_") {
140
+ const ch = b[0];
141
+ let j = 0;
142
+ while (j < b.length && (b[j] === ch || b[j] === " ")) j++;
143
+ if (j === b.length && !eof) return "";
144
+ if (j === b.length || b[j] === "\n") {
145
+ let count = 0;
146
+ for (let k = 0; k < j; k++) if (b[k] === ch) count++;
147
+ if (count >= 3) {
148
+ this.buf = j < b.length ? b.slice(j + 1) : "";
149
+ this.sol = true;
150
+ return "";
151
+ }
152
+ }
153
+ this.sol = false;
154
+ return "";
155
+ }
156
+
157
+ this.sol = false;
158
+ return "";
159
+ }
160
+
161
+ /** Scan line body for inline pattern triggers; output safe chars eagerly. */
162
+ private pumpBody(eof: boolean): string {
163
+ let out = "";
164
+ let i = 0;
165
+ while (i < this.buf.length) {
166
+ const c = this.buf[i];
167
+ if (c === "\n") {
168
+ out += this.buf.slice(0, i + 1);
169
+ this.buf = this.buf.slice(i + 1);
170
+ this.sol = true;
171
+ return out;
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
+ }
179
+ if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
180
+ out += this.buf.slice(0, i);
181
+ this.buf = this.buf.slice(i + 2);
182
+ this.inl = { type: "image", acc: "" };
183
+ return out;
184
+ }
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;
190
+ }
191
+ if (c === "*") {
192
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
193
+ out += this.buf.slice(0, i);
194
+ this.buf = this.buf.slice(i + 3);
195
+ this.inl = { type: "bold3", acc: "" };
196
+ return out;
197
+ }
198
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "*") {
199
+ i += 2;
200
+ continue;
201
+ }
202
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== "\n") {
203
+ out += this.buf.slice(0, i);
204
+ this.buf = this.buf.slice(i + 1);
205
+ this.inl = { type: "italic", acc: "" };
206
+ return out;
207
+ }
208
+ i++;
209
+ continue;
210
+ }
211
+ if (c === "_") {
212
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "_" && this.buf[i + 2] === "_") {
213
+ out += this.buf.slice(0, i);
214
+ this.buf = this.buf.slice(i + 3);
215
+ this.inl = { type: "ubold3", acc: "" };
216
+ return out;
217
+ }
218
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "_") {
219
+ i += 2;
220
+ continue;
221
+ }
222
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== "\n") {
223
+ out += this.buf.slice(0, i);
224
+ this.buf = this.buf.slice(i + 1);
225
+ this.inl = { type: "uitalic", acc: "" };
226
+ return out;
227
+ }
228
+ i++;
229
+ continue;
230
+ }
231
+ i++;
232
+ }
233
+
234
+ let hold = 0;
235
+ if (!eof) {
236
+ if (this.buf.endsWith("**")) hold = 2;
237
+ else if (this.buf.endsWith("__")) hold = 2;
238
+ else if (this.buf.endsWith("*")) hold = 1;
239
+ else if (this.buf.endsWith("_")) hold = 1;
240
+ else if (this.buf.endsWith("~")) hold = 1;
241
+ else if (this.buf.endsWith("!")) hold = 1;
242
+ }
243
+ out += this.buf.slice(0, this.buf.length - hold);
244
+ this.buf = hold > 0 ? this.buf.slice(-hold) : "";
245
+ return out;
246
+ }
247
+
248
+ /** Accumulate inline content until closing marker is found. */
249
+ private pumpInline(_eof: boolean): string {
250
+ if (!this.inl) return "";
251
+ this.inl.acc += this.buf;
252
+ this.buf = "";
253
+
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
+ }
283
+ case "bold3": {
284
+ const idx = this.inl.acc.indexOf("***");
285
+ if (idx !== -1) {
286
+ const content = this.inl.acc.slice(0, idx);
287
+ this.buf = this.inl.acc.slice(idx + 3);
288
+ this.inl = null;
289
+ return content;
290
+ }
291
+ return "";
292
+ }
293
+ case "ubold3": {
294
+ const idx = this.inl.acc.indexOf("___");
295
+ if (idx !== -1) {
296
+ const content = this.inl.acc.slice(0, idx);
297
+ this.buf = this.inl.acc.slice(idx + 3);
298
+ this.inl = null;
299
+ return content;
300
+ }
301
+ return "";
302
+ }
303
+ case "italic": {
304
+ for (let j = 0; j < this.inl.acc.length; j++) {
305
+ if (this.inl.acc[j] === "\n") {
306
+ const r = "*" + this.inl.acc.slice(0, j + 1);
307
+ this.buf = this.inl.acc.slice(j + 1);
308
+ this.inl = null;
309
+ this.sol = true;
310
+ return r;
311
+ }
312
+ if (this.inl.acc[j] === "*") {
313
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "*") {
314
+ j++;
315
+ continue;
316
+ }
317
+ const content = this.inl.acc.slice(0, j);
318
+ this.buf = this.inl.acc.slice(j + 1);
319
+ this.inl = null;
320
+ return content;
321
+ }
322
+ }
323
+ return "";
324
+ }
325
+ case "uitalic": {
326
+ for (let j = 0; j < this.inl.acc.length; j++) {
327
+ if (this.inl.acc[j] === "\n") {
328
+ const r = "_" + this.inl.acc.slice(0, j + 1);
329
+ this.buf = this.inl.acc.slice(j + 1);
330
+ this.inl = null;
331
+ this.sol = true;
332
+ return r;
333
+ }
334
+ if (this.inl.acc[j] === "_") {
335
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "_") {
336
+ j++;
337
+ continue;
338
+ }
339
+ const content = this.inl.acc.slice(0, j);
340
+ this.buf = this.inl.acc.slice(j + 1);
341
+ this.inl = null;
342
+ return content;
343
+ }
344
+ }
345
+ return "";
346
+ }
347
+ case "image": {
348
+ const cb = this.inl.acc.indexOf("]");
349
+ if (cb === -1) return "";
350
+ if (cb + 1 >= this.inl.acc.length) return "";
351
+ if (this.inl.acc[cb + 1] !== "(") {
352
+ const r = "![" + this.inl.acc.slice(0, cb + 1);
353
+ this.buf = this.inl.acc.slice(cb + 1);
354
+ this.inl = null;
355
+ return r;
356
+ }
357
+ const cp = this.inl.acc.indexOf(")", cb + 2);
358
+ if (cp !== -1) {
359
+ this.buf = this.inl.acc.slice(cp + 1);
360
+ this.inl = null;
361
+ return "";
362
+ }
363
+ return "";
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
+ }
377
+ }
378
+ return "";
379
+ }
380
+
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");
390
+ }
391
+ }
@@ -28,7 +28,8 @@ import {
28
28
  } from "./inbound.js";
29
29
  import type { WeixinInboundMediaOpts } from "./inbound.js";
30
30
  import { sendWeixinMediaFile } from "./send-media.js";
31
- import { markdownToPlainText, sendMessageWeixin } from "./send.js";
31
+ import { StreamingMarkdownFilter } from "./markdown-filter.js";
32
+ import { sendMessageWeixin } from "./send.js";
32
33
  import { handleSlashCommand } from "./slash-commands.js";
33
34
 
34
35
  const MEDIA_OUTBOUND_TEMP_DIR = path.join(resolvePreferredOpenClawTmpDir(), "weixin/media/outbound-temp");
@@ -309,7 +310,11 @@ export async function processOneMessage(
309
310
  humanDelay,
310
311
  typingCallbacks,
311
312
  deliver: async (payload) => {
312
- const text = markdownToPlainText(payload.text ?? "");
313
+ const rawText = payload.text ?? "";
314
+ const text = (() => {
315
+ const f = new StreamingMarkdownFilter();
316
+ return f.feed(rawText) + f.flush();
317
+ })();
313
318
  const mediaUrl = payload.mediaUrl ?? payload.mediaUrls?.[0];
314
319
  logger.debug(`outbound payload: ${redactBody(JSON.stringify(payload))}`);
315
320
  logger.info(
@@ -414,7 +419,7 @@ export async function processOneMessage(
414
419
  ctx: finalized,
415
420
  cfg: deps.config,
416
421
  dispatcher,
417
- replyOptions: { ...replyOptions, disableBlockStreaming: false },
422
+ replyOptions: { ...replyOptions, disableBlockStreaming: true },
418
423
  }),
419
424
  });
420
425
  logger.debug(`dispatchReplyFromConfig: done agentId=${route.agentId ?? "(none)"}`);
@@ -1,5 +1,4 @@
1
1
  import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
2
- import { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
3
2
 
4
3
  import { sendMessage as sendMessageApi } from "../api/api.js";
5
4
  import type { WeixinApiOptions } from "../api/api.js";
@@ -9,32 +8,12 @@ import type { MessageItem, SendMessageReq } from "../api/types.js";
9
8
  import { MessageItemType, MessageState, MessageType } from "../api/types.js";
10
9
  import type { UploadedFileInfo } from "../cdn/upload.js";
11
10
 
11
+ export { StreamingMarkdownFilter } from "./markdown-filter.js";
12
+
12
13
  function generateClientId(): string {
13
14
  return generateId("openclaw-weixin");
14
15
  }
15
16
 
16
- /**
17
- * Convert markdown-formatted model reply to plain text for Weixin delivery.
18
- * Preserves newlines; strips markdown syntax.
19
- */
20
- export function markdownToPlainText(text: string): string {
21
- let result = text;
22
- // Code blocks: strip fences, keep code content
23
- result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code: string) => code.trim());
24
- // Images: remove entirely
25
- result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
26
- // Links: keep display text only
27
- result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
28
- // Tables: remove separator rows, then strip leading/trailing pipes and convert inner pipes to spaces
29
- result = result.replace(/^\|[\s:|-]+\|$/gm, "");
30
- result = result.replace(/^\|(.+)\|$/gm, (_, inner: string) =>
31
- inner.split("|").map((cell) => cell.trim()).join(" "),
32
- );
33
- result = stripMarkdown(result);
34
- return result;
35
- }
36
-
37
-
38
17
  /** Build a SendMessageReq containing a single text message. */
39
18
  function buildTextMessageReq(params: {
40
19
  to: string;