@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 +37 -0
- package/CHANGELOG.zh_CN.md +39 -0
- package/README.md +2 -4
- package/README.zh_CN.md +2 -4
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
- package/src/api/api.ts +14 -8
- package/src/auth/login-qr.ts +0 -3
- package/src/compat.ts +1 -3
- package/src/messaging/markdown-filter.ts +391 -0
- package/src/messaging/process-message.ts +8 -3
- package/src/messaging/send.ts +2 -23
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).
|
package/CHANGELOG.zh_CN.md
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
68
|
+
默认情况下,私聊可能共用同一会话桶。**多个微信号同时登录**时,建议按「账号 + 渠道 + 对端」隔离:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
openclaw config set
|
|
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 与后端网关通信。二次开发者若需对接自有后端,需实现以下接口。
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
140
|
-
const
|
|
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
|
}
|
package/src/auth/login-qr.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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:
|
|
422
|
+
replyOptions: { ...replyOptions, disableBlockStreaming: true },
|
|
418
423
|
}),
|
|
419
424
|
});
|
|
420
425
|
logger.debug(`dispatchReplyFromConfig: done agentId=${route.agentId ?? "(none)"}`);
|
package/src/messaging/send.ts
CHANGED
|
@@ -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;
|