dsh-plugin-weixin 0.0.0-stage → 0.1.1
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/LICENSE +29 -0
- package/README.md +283 -2
- package/THIRD-PARTY-NOTICES.md +111 -0
- package/bin/install.mjs +392 -0
- package/bin/login.js +100 -0
- package/cordis.patch.yml +30 -0
- package/package.json +87 -4
- package/src/agent-bridge.js +417 -0
- package/src/asr-remote.js +687 -0
- package/src/client.js +1304 -0
- package/src/index.js +714 -0
- package/src/login-remote.js +337 -0
- package/src/media/asr.js +373 -0
- package/src/media/inbound.js +557 -0
- package/src/media/outbound.js +778 -0
- package/src/message/chunker.js +40 -0
- package/src/message/dedupe.js +39 -0
- package/src/message/markdown.js +78 -0
- package/src/qr-png.js +110 -0
- package/src/security/allowlist.js +40 -0
- package/src/session-reader.js +121 -0
- package/src/state.js +206 -0
- package/src/transport/ilink.js +504 -0
- package/src/typert-js.js +19 -0
- package/src/types.js +118 -0
- package/vendor/openclaw-weixin/LICENSE +27 -0
- package/vendor/openclaw-weixin/api/api.js +564 -0
- package/vendor/openclaw-weixin/api/types.js +36 -0
- package/vendor/openclaw-weixin/auth/accounts.js +325 -0
- package/vendor/openclaw-weixin/auth/login-qr.js +336 -0
- package/vendor/openclaw-weixin/auth/pairing.js +55 -0
- package/vendor/openclaw-weixin/cdn/aes-ecb.js +18 -0
- package/vendor/openclaw-weixin/cdn/cdn-upload.js +73 -0
- package/vendor/openclaw-weixin/cdn/cdn-url.js +13 -0
- package/vendor/openclaw-weixin/cdn/pic-decrypt.js +88 -0
- package/vendor/openclaw-weixin/cdn/upload.js +114 -0
- package/vendor/openclaw-weixin/media/media-download.js +97 -0
- package/vendor/openclaw-weixin/media/mime.js +72 -0
- package/vendor/openclaw-weixin/media/silk-transcode.js +63 -0
- package/vendor/openclaw-weixin/messaging/markdown-filter.js +375 -0
- package/vendor/openclaw-weixin/messaging/partial-quote.js +50 -0
- package/vendor/openclaw-weixin/messaging/quote-store.js +521 -0
- package/vendor/openclaw-weixin/messaging/send-media.js +53 -0
- package/vendor/openclaw-weixin/messaging/send.js +304 -0
- package/vendor/openclaw-weixin/openclaw-shim.js +76 -0
- package/vendor/openclaw-weixin/package.json +9 -0
- package/vendor/openclaw-weixin/storage/state-dir.js +8 -0
- package/vendor/openclaw-weixin/storage/sync-buf.js +63 -0
- package/vendor/openclaw-weixin/util/logger.js +119 -0
- package/vendor/openclaw-weixin/util/random.js +15 -0
- package/vendor/openclaw-weixin/util/redact.js +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-weixin contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This project vendors a copy of the WeChat iLink transport from
|
|
26
|
+
`@tencent-weixin/openclaw-weixin` (Copyright (C) 2026 Tencent), also under the
|
|
27
|
+
MIT License. Its license text is retained verbatim at
|
|
28
|
+
`vendor/openclaw-weixin/LICENSE`. See `THIRD-PARTY-NOTICES.md` for the exact
|
|
29
|
+
scope of what was vendored and what was changed.
|
package/README.md
CHANGED
|
@@ -1,3 +1,284 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-weixin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把**个人微信**变成 DeepSeek Harness(DSH)的远程控制面:在微信里发一句话,DSH Agent 在本地开一个会话执行,把结果回给你。
|
|
4
|
+
|
|
5
|
+
底层用的是微信 **iLink Bot API**(`ilinkai.weixin.qq.com`),传输层来自腾讯官方开源插件
|
|
6
|
+
[`@tencent-weixin/openclaw-weixin`](https://github.com/Tencent/openclaw-weixin)(MIT,2.4.10),
|
|
7
|
+
本项目把它 vendor 进 `vendor/openclaw-weixin/`,剥掉 OpenClaw 的耦合后直接绑定到 DSH 的 `agents` 服务。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## 当前状态
|
|
14
|
+
|
|
15
|
+
**已真机验证**:插件加载、扫码登录、长轮询收消息、白名单、去重、开 DSH 会话、读回复、分段回发、
|
|
16
|
+
“正在输入”、入站图片与文件、入站语音**识别成文字**(SILK → WAV → ASR,热调用约 300ms)。
|
|
17
|
+
|
|
18
|
+
**尚未验证**:长时间运行、多账号、群聊;**发送**语音/视频(需要 TTS/视频编码,当前只支持发送
|
|
19
|
+
图片与文件);原生图像理解(入站图片以本地路径交给 Agent,由 Agent 用 `read_image` 打开——
|
|
20
|
+
这依赖当前模型与工具链,不是插件自身保证)。
|
|
21
|
+
|
|
22
|
+
## 目录结构
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/index.js 插件入口:name / inject / Config / apply
|
|
26
|
+
src/agent-bridge.js 微信会话 ↔ DSH agent 会话(create/resume + followup)
|
|
27
|
+
src/session-reader.js 从 session log 读回 assistant 回复与在跑的工具
|
|
28
|
+
src/transport/ilink.js iLink 长轮询 + send + sendMedia + typing
|
|
29
|
+
src/security/allowlist.js 发信人/会话白名单(默认拒绝所有人)
|
|
30
|
+
src/message/{chunker,dedupe,markdown}.js 分段、有界去重、Markdown→纯文本
|
|
31
|
+
src/media/inbound.js 入站媒体:下载、解密、落盘、语音转码
|
|
32
|
+
src/media/outbound.js 出站媒体:路径白名单、附件引用识别、上传
|
|
33
|
+
src/media/asr.js 语音识别:常驻 ASR 桥客户端(降级而非抛错)
|
|
34
|
+
src/state.js 把凭证目录从 ~/.openclaw 重定向到 ~/.dsh/weixin
|
|
35
|
+
bin/login.js 扫码登录 CLI
|
|
36
|
+
bin/install.mjs 安装到指定 DSH profile
|
|
37
|
+
vendor/openclaw-weixin/ 腾讯官方 MIT 传输层(已剥离 OpenClaw 依赖)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 安装
|
|
41
|
+
|
|
42
|
+
### 方式一:从 npm / git 安装(推荐给普通用户)
|
|
43
|
+
|
|
44
|
+
DSH 的插件页(侧边栏 **Plugins**)或 `plugin_manager` 工具可以直接从 registry 安装:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
dsh-plugin-weixin
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
也可以填 git 地址或绝对路径。安装后 DSH 会自动把 bundle 挂上,重启生效。
|
|
51
|
+
|
|
52
|
+
当前已发布版本:[`dsh-plugin-weixin@0.1.0`](https://www.npmjs.com/package/dsh-plugin-weixin)(2026-10-15 首发,含扫码轮询、登录后自动起循环、allowlist 合并、归档会话自动恢复等全部修复)。
|
|
53
|
+
|
|
54
|
+
### 方式二:从源码安装
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone <this-repo> && cd dsh-plugin-weixin
|
|
58
|
+
node bin/install.mjs # 默认装到 $DSH_PROFILE(或 desktop)
|
|
59
|
+
node bin/install.mjs --profile web # 装到别的 profile
|
|
60
|
+
node bin/install.mjs --uninstall
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
脚本做三件事:
|
|
64
|
+
|
|
65
|
+
1. 把包(含 `vendor/`)放进 `<profile>/node_modules/dsh-weixin`;
|
|
66
|
+
2. 在 `<profile>/package.json` 的 `dsh.profile.bundles` 里追加 `dsh-weixin`;
|
|
67
|
+
3. 追加一行 `dsh-weixin` 到 `<profile>/cordis.patch.yml`(幂等,已存在则刷新那一行)。
|
|
68
|
+
|
|
69
|
+
同时会把两个**可选**依赖(`qrcode-terminal`、`silk-wasm`)搬到 profile 的 `node_modules`——
|
|
70
|
+
不搬的话它们只在这个仓库里可见,装到 profile 后会静默降级。
|
|
71
|
+
|
|
72
|
+
重启 DSH 后生效。
|
|
73
|
+
|
|
74
|
+
## 登录
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node bin/login.js # 终端打印二维码,手机微信扫码
|
|
78
|
+
node bin/login.js --account work # 多账号时指定 id
|
|
79
|
+
node bin/login.js --force # 已有账号时强制重新扫码
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
凭证写入 `~/.dsh/weixin/openclaw-weixin/accounts/<id>.json`,权限 `0600`,**只在本机**。
|
|
83
|
+
`bin/login.js` 不接收密码,也不会把 token 打印到日志(vendor 自带 redact)。
|
|
84
|
+
|
|
85
|
+
### 在 DSH 设置页登录
|
|
86
|
+
|
|
87
|
+
「设置 → 插件 → dsh-plugin-weixin」里也能直接扫码,不用开终端:
|
|
88
|
+
|
|
89
|
+
- 点「扫码登录微信」后,卡片显示**真正的二维码图片**(host 端把 iLink 返回的登录链接渲染成
|
|
90
|
+
PNG 再回传,客户端不需要任何二维码库);
|
|
91
|
+
- 二维码旁边有「在浏览器打开」和「复制链接」——iLink 返回的 `qrcode_img_content` 其实是一个
|
|
92
|
+
**登录链接**(`https://liteapp.weixin.qq.com/q/...`),不是图片;链接在浏览器里打开同样能扫码;
|
|
93
|
+
- 需要验证码时卡片会切到输入框,提交后继续轮询;二维码过期会自动刷新(最多 3 次)。
|
|
94
|
+
|
|
95
|
+
### 退出登录与重新绑定
|
|
96
|
+
|
|
97
|
+
「退出登录」只删除**本机**凭证。iLink 没有服务端解绑接口,机器人仍挂在微信侧(手机微信里仍显示
|
|
98
|
+
「已绑定」),所以:
|
|
99
|
+
|
|
100
|
+
- 退出登录后卡片显示「未登录」是**如实显示,不是状态不实时**——本机确实已经没有任何凭证。
|
|
101
|
+
卡片的状态由本地账号文件决定,手机上的「已绑定」说的是服务端绑定关系,两者本来就不是一回事;
|
|
102
|
+
- 如果你在这次退出登录之前用设置页扫码,那个流程**从来没有走通过**(轮询一直在 `poll(null)`,
|
|
103
|
+
见 `docs/progress.md`)。也就是说:扫码后卡片停在二维码页、扫码结果无人处理,与手机显示
|
|
104
|
+
「已绑定」无关。修复后重新扫码才会真正走到服务端;
|
|
105
|
+
- 走到服务端之后还有一种情况:服务端仍认为这台机器绑定着同一个机器人时,会回 `binded_redirect`
|
|
106
|
+
并**拒绝下发新凭证**(此支语义取自 vendor 源码;vendor 的 CLI 把它读成幂等成功
|
|
107
|
+
「已连接过此 OpenClaw,无需重复连接」,但那只在**本机仍持有 token** 时才成立,退出登录后 token
|
|
108
|
+
已经没了,本机也没有任何接口能解绑);
|
|
109
|
+
- 卡片会把这种情况单独报成「需要处理」,并给出唯一可行的动作:**先在手机微信里解除该机器人的绑定**,
|
|
110
|
+
然后重新扫码。
|
|
111
|
+
|
|
112
|
+
即:退出登录 = 本机忘记;要重新绑定 = 手机上先解绑。
|
|
113
|
+
|
|
114
|
+
### 扫码成功后不需要重载 DSH
|
|
115
|
+
|
|
116
|
+
插件刚加载时如果本机还没有凭证(全新安装,或刚退出登录),收消息的 `getUpdates` 长轮询在那时
|
|
117
|
+
是**无法启动**的。设置页扫码成功后,登录会通知宿主立刻为**刚写入的凭证**把这条循环拉起来,
|
|
118
|
+
所以「扫码 → 重载 DSH → 才通」这个多余步骤已经没有了。
|
|
119
|
+
|
|
120
|
+
如果手机微信仍提示「暂时无法连接 openclaw」,按顺序看四处:
|
|
121
|
+
|
|
122
|
+
1. 插件日志里有没有 `listening as account <id>`——没有就是循环没起来;
|
|
123
|
+
2. 有没有 `settings login service unavailable`——有就说明登录服务没装上,扫码**能**成功、
|
|
124
|
+
凭证**会**写盘,但没有任何人在听「登录已完成」,循环不会切到新凭证(历史上这一行曾把
|
|
125
|
+
`ctx.plugin()` 的误用吞掉,症状与「循环没起来」完全相同);
|
|
126
|
+
3. 有没有 `no Weixin account is saved`(凭证不在)或 `is already being listened to`(重复触发,正常);
|
|
127
|
+
4. 凭证是否被服务端拒绝:`getUpdates errcode=-14` 表示登录已过期,需要重新扫码。
|
|
128
|
+
|
|
129
|
+
换绑到新机器人(token 变了)时,插件会**先停掉旧的长轮询再启动新的**:同一凭证上跑两条长轮询会让
|
|
130
|
+
服务端把入站消息**分摊**给两条循环,而不是各收一份。
|
|
131
|
+
|
|
132
|
+
## 授权(重要)
|
|
133
|
+
|
|
134
|
+
微信文本**不是**授权凭据。因此:
|
|
135
|
+
|
|
136
|
+
- `allowedSenders` 默认**空**,等于拒绝所有人——没配白名单的插件什么都不驱动;
|
|
137
|
+
- 不在白名单的发信人只记一条 warn,不创建会话、不回消息;
|
|
138
|
+
- 真实的文件修改/命令执行仍由 DSH 自己的权限层与确认流程决定,本插件不旁路。
|
|
139
|
+
|
|
140
|
+
## 配置
|
|
141
|
+
|
|
142
|
+
`cordis.patch.yml` 里那行的 `config`:
|
|
143
|
+
|
|
144
|
+
| 字段 | 默认 | 说明 |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `enabled` | `true` | 关闭即不启动长轮询 |
|
|
147
|
+
| `accountId` | `''` | 空 = 只用唯一已保存账号 |
|
|
148
|
+
| `cwd` | `''` | Agent 工作目录;空 = 继承会话默认 |
|
|
149
|
+
| `allowedSenders` | `[]` | 允许驱动 Agent 的微信用户 id |
|
|
150
|
+
| `allowedChats` | `[]` | 会话白名单;空 = 不限制会话 |
|
|
151
|
+
| `maxMessageLength` | `1500` | 单条出站消息字符上限 |
|
|
152
|
+
| `markdownMode` | `'strip'` | 回复的 Markdown 处理:`strip` 转微信可读纯文本,`off` 原样发送 |
|
|
153
|
+
| `progressAfterMs` | `45000` | 超时前发一条进度提示 |
|
|
154
|
+
| `maxTurnWaitMs` | `1200000` | 等待一轮的上限 |
|
|
155
|
+
| `typingIndicator` | `true` | 有 typing ticket 时显示“正在输入” |
|
|
156
|
+
| `mediaEnabled` | `true` | 把回复里引用的本地文件作为附件发出(出站) |
|
|
157
|
+
| `maxMediaBytes` | `104857600`(100MB) | 出站附件大小上限 |
|
|
158
|
+
| `maxInboundMediaBytes` | `20971520`(20MB) | 入站附件下载大小上限 |
|
|
159
|
+
| `cdnBaseUrl` | 空(未配置) | 入站下载与出站上传的 CDN 根。留空表示「这里没配」:会先用账号记录里的 `cdnBaseUrl`,再退回上游默认 `https://novac2c.cdn.weixin.qq.com/c2c` |
|
|
160
|
+
| `asr.modelDir` | `''` | 语音模型目录;空 = `~/.dsh/weixin/asr-models`。设置页的输入框会覆盖它(并写进 `<stateDir>/asr-config.json`) |
|
|
161
|
+
|
|
162
|
+
改完配置重载插件即可。
|
|
163
|
+
|
|
164
|
+
## 入站媒体(微信发来的图片/文件/语音/视频)
|
|
165
|
+
|
|
166
|
+
用户发来的媒体会**先过白名单和去重,再**从 CDN 下载——未授权的发信人不会让本机对 CDN 发起任何请求
|
|
167
|
+
(`test/e2e.test.js` 里有一条断言守着这点)。下载后按 AES-128-ECB 解密,存到
|
|
168
|
+
`<stateDir>/media/inbound/`(默认 `~/.dsh/weixin/media/inbound/`),再把**本地绝对路径 + 用哪个工具读**
|
|
169
|
+
拼进交给 Agent 的 prompt:图片提示 `read_image`,文件/视频提示 `read`。微信图片带的 caption 会一并
|
|
170
|
+
保留,所以 Agent 既有图也有问题。
|
|
171
|
+
|
|
172
|
+
两个安全要点:
|
|
173
|
+
|
|
174
|
+
- **对端给的文件名绝不用来拼路径。** 文件名先取 basename、去掉分隔符与 `.`/`..` 段,再加随机前缀,
|
|
175
|
+
写盘用 `wx` 独占模式。`../../etc/passwd` 只会变成 `a1b2c3d4-passwd` 落在收件目录里,逃不出去。
|
|
176
|
+
- **大小先按 `Content-Length` 判、读完再按实际字节判一遍。** 头部可以缺、也可以撒谎,两道都要过。
|
|
177
|
+
|
|
178
|
+
失败**不静默**:下载失败会把具体原因回给用户(如 `CDN returned 403`、`30.0MB,超过 20.0MB 上限`),
|
|
179
|
+
而不是笼统的「暂不支持」。真正未知的 item 类型(如 `TOOL_CALL_*`)才保留「暂不支持」提示。
|
|
180
|
+
|
|
181
|
+
语音是微信 SILK 格式。插件会尝试用上游的 `silkToWav` 转成 WAV;`silk-wasm` 现在是
|
|
182
|
+
`optionalDependencies` 里的一项(已装则可用;卸载或安装失败不影响插件加载,会走降级路径)。
|
|
183
|
+
`silk-wasm` 缺失时按上游行为把原始 `.silk` 存下来并把路径交给 Agent,回复中说明这是未转码的
|
|
184
|
+
SILK。
|
|
185
|
+
|
|
186
|
+
### 语音识别(ASR)
|
|
187
|
+
|
|
188
|
+
转成 WAV 之后可以再交给一个常驻的 ASR 桥进程,真正做到「听懂」。桥是一个**行协议**子进程:
|
|
189
|
+
stdin 每行一个 `{"wav":"/绝对路径.wav"}`,stdout 每行一个 `{"ok":true,"text":"..."}` 或
|
|
190
|
+
`{"ok":false,"error":"..."}`。进程**常驻复用**——每条语音起一个新 Python 进程要付 15–25 秒冷启动,
|
|
191
|
+
不可接受。
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
asr:
|
|
195
|
+
enabled: true
|
|
196
|
+
command: /path/to/your/venv/bin/python3 # 你自己的 Python 环境
|
|
197
|
+
args: [/path/to/your/asr-server.py] # 一个按行收发 JSON 的转写服务
|
|
198
|
+
timeoutMs: 60000
|
|
199
|
+
modelDir: /path/to/models # 模型目录(设置页里也能改,见下)
|
|
200
|
+
env:
|
|
201
|
+
HF_HOME: /path/to/your/model-cache # 模型缓存放哪(可能很大)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 模型目录可以自定义
|
|
205
|
+
|
|
206
|
+
设置页的「语音模型」区块里,目录是一个**可编辑输入框**:
|
|
207
|
+
|
|
208
|
+
- 改完点「设置目录」,host 端会 `mkdir -p` 并记住这个路径,重启后仍然生效
|
|
209
|
+
(写在 `<stateDir>/asr-config.json`);
|
|
210
|
+
- 该目录同时驱动**下载**和**转写**两端:下载的模型落到 `<目录>/<repo>`,
|
|
211
|
+
转写时 `HF_HOME` 与 `DSH_WEIXIN_ASR_MODEL` 都指向这个目录,所以下载完就能直接用,
|
|
212
|
+
不用再去改配置文件;
|
|
213
|
+
- 配置里的 `asr.env.HF_HOME` / `asr.env.DSH_WEIXIN_ASR_MODEL` 优先级更高——
|
|
214
|
+
手写过的值不会被界面覆盖。
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
**本仓库不含 ASR 服务端。** 插件只带客户端:它按上表调用你的 `command`,把 WAV 路径写进 stdin,
|
|
218
|
+
从 stdout 逐行读 JSON 响应。服务端要自己提供(约 30 行 Python),任何能完成
|
|
219
|
+
「读一行 `{"audio_path": "..."}` → 写一行 `{"ok": true, "text": "..."}`」的进程都可以,
|
|
220
|
+
模型可以用 `mlx-whisper` / `faster-whisper` / 云端 API——插件不关心。
|
|
221
|
+
|
|
222
|
+
之所以不内置:模型约 670MB,且 Apple Silicon 上的最优方案(MLX)不跨平台。把它塞进仓库会让
|
|
223
|
+
绝大多数用户下载几百 MB 却用不上。默认 `asr.enabled: false`,不配就完全没有这条链路。
|
|
224
|
+
|
|
225
|
+
`command` 为空即关闭识别,语音只作为文件交付。三条降级互不影响,Agent 收到的说明能区分
|
|
226
|
+
「没装转码模块」「转码了但桥失败」「识别出来但是空的」:
|
|
227
|
+
|
|
228
|
+
| 情况 | 交付给 Agent 的内容 |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| 识别成功 | `【语音转写】` + 用户原话(明确标为「均按用户原话处理」) |
|
|
231
|
+
| 识别为空 | 明说未识别出内容,并**要求不得臆测**——空结果不许编 |
|
|
232
|
+
| 桥失败/超时 | 仍给出 WAV 路径,附失败原因 |
|
|
233
|
+
| 无 `silk-wasm` | 给出 `.silk` 路径,说明未转码 |
|
|
234
|
+
|
|
235
|
+
桥在请求中途崩溃会被自动重启,只影响当前这一条(该条降级),后续请求照常。`timeoutMs` 是
|
|
236
|
+
**兜底**而不是延迟目标——冷启动加载模型可以到几十秒,所以默认 60s;热调用实测约 294ms。
|
|
237
|
+
|
|
238
|
+
> 本机实测:`2ee80de6-voice.silk`(3830 字节)→ WAV 113324 字节 → 转写「你是谁」。
|
|
239
|
+
> 模型缓存在 `HF_HOME` 指定的位置(stationData 卷),**不要**让它落到系统盘。
|
|
240
|
+
|
|
241
|
+
## Markdown 处理
|
|
242
|
+
|
|
243
|
+
微信不渲染 Markdown,`**加粗**`、`##### 标题`、`` 会原样显示。出站链路因此先把回复
|
|
244
|
+
转成纯文本,**再**按 `maxMessageLength` 分片——顺序不能反,否则分片长度会按未转换的文本计算而超限。
|
|
245
|
+
|
|
246
|
+
`markdownMode` 为 `strip`(默认)时复用 upstream 的 `StreamingMarkdownFilter`
|
|
247
|
+
(`vendor/openclaw-weixin/messaging/markdown-filter.js`),规则完全与上游一致:
|
|
248
|
+
|
|
249
|
+
| 保留 | 剥离(标记去掉、内容保留) |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `**加粗**`、`___加粗___` | CJK 内容的斜体/粗斜体标记 |
|
|
252
|
+
| 代码围栏 ```、行内代码 `` ` `` | H5/H6 标题(`#####`、`######`) |
|
|
253
|
+
| 表格 `\|…\|`、分隔线 `---` | 图片 ``(整段移除) |
|
|
254
|
+
| 非 CJK 内容的斜体 | |
|
|
255
|
+
|
|
256
|
+
空白字符(空行、行尾换行、CRLF)逐字节保留。`markdownMode: 'off'` 时原样发送,便于排障与对照。
|
|
257
|
+
|
|
258
|
+
> 注意:`` 同时是**出站附件**语法。开启 `mediaEnabled` 时该引用会先交给媒体层上传,
|
|
259
|
+
> 文本里只剩 alt 文本;`stripMediaReferences` 已在媒体步骤处理掉的部分不会再进入 Markdown 过滤器。
|
|
260
|
+
|
|
261
|
+
## 开发
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
npm test # node --test "test/**/*.test.js"
|
|
265
|
+
node bin/login.js # 真实扫码登录
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
本地 dev 需要把 DSH 的 `@deepseek-ai/{schemastery,cosmokit,dsh-llm}` 软链到 `node_modules/@deepseek-ai/`
|
|
269
|
+
(它们在 profile 里,不是公开 npm 包):
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
mkdir -p node_modules/@deepseek-ai
|
|
273
|
+
ln -sfn ~/.dsh/profiles/desktop/node_modules/@deepseek-ai/schemastery node_modules/@deepseek-ai/schemastery
|
|
274
|
+
ln -sfn ~/.dsh/profiles/desktop/node_modules/@deepseek-ai/cosmokit node_modules/@deepseek-ai/cosmokit
|
|
275
|
+
ln -sfn ~/.dsh/profiles/node_modules/@deepseek-ai/dsh-llm node_modules/@deepseek-ai/dsh-llm
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## 文档
|
|
279
|
+
|
|
280
|
+
- [实施方案](docs/plan.md)
|
|
281
|
+
- [移植审查清单](docs/hermes-port-audit.md)
|
|
282
|
+
- [进度记录](docs/progress.md)
|
|
283
|
+
- [安全策略](docs/security.md)
|
|
284
|
+
- [第三方代码](THIRD-PARTY-NOTICES.md)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## @tencent-weixin/openclaw-weixin (vendored)
|
|
4
|
+
|
|
5
|
+
This project vendors a trimmed copy of the WeChat iLink transport from
|
|
6
|
+
[`Tencent/openclaw-weixin`](https://github.com/Tencent/openclaw-weixin), npm package
|
|
7
|
+
`@tencent-weixin/openclaw-weixin@2.4.10`, under `vendor/openclaw-weixin/`.
|
|
8
|
+
|
|
9
|
+
**License: MIT.** Upstream license text is kept verbatim at
|
|
10
|
+
[vendor/openclaw-weixin/LICENSE](vendor/openclaw-weixin/LICENSE):
|
|
11
|
+
|
|
12
|
+
> Tencent is pleased to support the open source community by making openclaw-weixin available.
|
|
13
|
+
> Copyright (C) 2026 Tencent. All rights reserved.
|
|
14
|
+
> openclaw-weixin is licensed under the MIT.
|
|
15
|
+
|
|
16
|
+
### What was vendored
|
|
17
|
+
|
|
18
|
+
Compiled ESM JavaScript from the published tarball's `dist/src/`, with `.map` files and
|
|
19
|
+
`sourceMappingURL` comments removed:
|
|
20
|
+
|
|
21
|
+
- `api/api.js`, `api/types.js` — the iLink HTTP transport (`getUpdates`, `sendMessage`,
|
|
22
|
+
`getConfig`, `sendTyping`, `getUploadUrl`, `notifyStart`/`notifyStop`) and uint64-safe JSON parsing.
|
|
23
|
+
- `auth/accounts.js`, `auth/login-qr.js`, `auth/pairing.js` — local 0600 account store and QR login.
|
|
24
|
+
- `storage/state-dir.js`, `storage/sync-buf.js` — state dir resolution and the long-poll sync buffer.
|
|
25
|
+
- `util/logger.js`, `util/redact.js` — logging and secret redaction.
|
|
26
|
+
- `util/random.js` — `generateId` / `tempFileName` (used for client ids and temp media names).
|
|
27
|
+
- `cdn/aes-ecb.js` — AES-128-ECB encrypt/decrypt and PKCS7-padded ciphertext size.
|
|
28
|
+
- `cdn/cdn-url.js` — CDN upload/download URL construction and the `full_url` fallback switch.
|
|
29
|
+
- `cdn/pic-decrypt.js` — downloads CDN bytes and AES-128-ECB decrypts them (both `aes_key`
|
|
30
|
+
encodings: base64 of 16 raw bytes for images, base64 of a 32-char hex string for
|
|
31
|
+
file/voice/video).
|
|
32
|
+
- `cdn/cdn-upload.js` — `uploadBufferToCdn`: POSTs ciphertext to the CDN, retries server errors
|
|
33
|
+
(3 attempts), aborts immediately on 4xx, returns `x-encrypted-param`.
|
|
34
|
+
- `cdn/upload.js` — the high-level upload pipeline (`uploadFileToWeixin` for images,
|
|
35
|
+
`uploadVideoToWeixin`, `uploadFileAttachmentToWeixin`) plus `downloadRemoteImageToTemp`.
|
|
36
|
+
- `media/mime.js` — filename → MIME and MIME/URL → extension mapping.
|
|
37
|
+
- `media/media-download.js` — `downloadMediaFromItem`: per-item-type download + decrypt + save
|
|
38
|
+
for inbound image / voice / file / video.
|
|
39
|
+
- `media/silk-transcode.js` — SILK → WAV transcode via the optional `silk-wasm` dependency;
|
|
40
|
+
returns `null` (caller keeps the raw SILK) when `silk-wasm` is not installed. `silk-wasm`
|
|
41
|
+
is NOT vendored and NOT a dependency — see below.
|
|
42
|
+
- `messaging/markdown-filter.js` — `StreamingMarkdownFilter`, the character-level state machine
|
|
43
|
+
that turns streamed Markdown into WeChat-readable plain text.
|
|
44
|
+
- `messaging/send-media.js` — `sendWeixinMediaFile`, which routes a local file by MIME to
|
|
45
|
+
video / image / file upload + send.
|
|
46
|
+
- `messaging/send.js` — `sendMessageWeixin`, `sendMessageItemWeixin` and the media senders
|
|
47
|
+
(`sendImageMessageWeixin`, `sendVideoMessageWeixin`, `sendFileMessageWeixin`). Vendored
|
|
48
|
+
because `messaging/send-media.js` imports it.
|
|
49
|
+
- `messaging/partial-quote.js` — `resolvePartialQuote`, needed by `messaging/inbound.js`
|
|
50
|
+
(see "What was changed", item 4).
|
|
51
|
+
- `messaging/quote-store.js` — the outbound/inbound quote cache, kept because
|
|
52
|
+
`auth/accounts.js` imports `deleteQuoteCacheForAccount` and `messaging/send.js` imports
|
|
53
|
+
`getQuoteStore`.
|
|
54
|
+
|
|
55
|
+
Slash commands, the progress/notice senders, the OpenClaw channel plugin wrapper and the
|
|
56
|
+
monitor/dispatch layer are NOT vendored. `messaging/inbound.js` is also NOT vendored — the one
|
|
57
|
+
helper `messaging/send.js` needs from it is reproduced in the shim (see "What was changed",
|
|
58
|
+
item 4).
|
|
59
|
+
|
|
60
|
+
### What was changed
|
|
61
|
+
|
|
62
|
+
The only modifications are mechanical de-coupling from the OpenClaw host and one import-site
|
|
63
|
+
rewrite, so the vendored code runs under DSH. Every vendored file in `cdn/`, `media/` and
|
|
64
|
+
`messaging/` is otherwise byte-identical to the published `dist/src/` build:
|
|
65
|
+
|
|
66
|
+
1. Three imports from `openclaw/plugin-sdk/*` were rewritten to
|
|
67
|
+
`vendor/openclaw-weixin/openclaw-shim.js`:
|
|
68
|
+
- `normalizeAccountId` (from `account-id`) → trim + lowercase.
|
|
69
|
+
- `mutateConfigFile` (from `config-mutation`) → async no-op. DSH configuration lives in
|
|
70
|
+
`cordis.patch.yml`, not in an OpenClaw config file, so there is nothing to write back.
|
|
71
|
+
- `resolvePreferredOpenClawTmpDir` (from `temp-path`) → `process.env.DSH_WEIXIN_LOG_DIR`
|
|
72
|
+
or `~/.dsh/weixin/logs`, so logs land under DSH's state dir.
|
|
73
|
+
2. A minimal `vendor/openclaw-weixin/package.json` was added. It is load-bearing, not
|
|
74
|
+
cosmetic: `api.js` reads `pkg.ilink_appid` for the `iLink-App-Id` header and
|
|
75
|
+
`pkg.version` for `iLink-App-ClientVersion` via `buildClientVersion()`.
|
|
76
|
+
3. Credential location is redirected at runtime by `src/state.js` (sets
|
|
77
|
+
`OPENCLAW_STATE_DIR`), so accounts are stored under `~/.dsh/weixin/` instead of
|
|
78
|
+
`~/.openclaw/`.
|
|
79
|
+
4. One *package-internal* import was rewritten to the same shim:
|
|
80
|
+
`messaging/send.js` imported `getMediaLabel` from `messaging/inbound.js`, and
|
|
81
|
+
`inbound.js` is not vendored. It is not vendorable in isolation — its transitive
|
|
82
|
+
closure is `util/logger.js`, `util/random.js`, `api/types.js`, `storage/state-dir.js`,
|
|
83
|
+
`messaging/partial-quote.js`, `messaging/quote-store.js`, plus it exports the whole
|
|
84
|
+
inbound `MsgContext` / quote-resolution layer that nothing vendored uses. So
|
|
85
|
+
`messaging/send.js` now imports `getMediaLabel` from
|
|
86
|
+
`vendor/openclaw-weixin/openclaw-shim.js`, which reproduces the function verbatim.
|
|
87
|
+
`messaging/partial-quote.js` is vendored alongside it for when the inbound layer is
|
|
88
|
+
wired up. This is a change of *import site only* for one four-case label function; it
|
|
89
|
+
touches no protocol, crypto or authorization logic.
|
|
90
|
+
|
|
91
|
+
No protocol, crypto or authorization logic was altered.
|
|
92
|
+
|
|
93
|
+
### Optional runtime dependency: silk-wasm
|
|
94
|
+
|
|
95
|
+
`media/silk-transcode.js` transcodes inbound voice messages from SILK to WAV. Upstream
|
|
96
|
+
loads `silk-wasm@^3.7.1` (a devDependency there) through a lazily evaluated dynamic
|
|
97
|
+
`import("silk-wasm")`, and returns `null` when the import fails. That import is left
|
|
98
|
+
byte-for-byte as upstream ships it — no shim, no rewriting:
|
|
99
|
+
|
|
100
|
+
- The specifier is dynamic, so nothing fails at module load time.
|
|
101
|
+
- With `silk-wasm` absent, `silkToWav` logs a warning and returns `null`, and
|
|
102
|
+
`media/media-download.js` saves the raw SILK buffer instead (labelled `audio/silk`).
|
|
103
|
+
Inbound voice therefore still works, just without a WAV container.
|
|
104
|
+
- Installing `silk-wasm` into this project enables WAV output with no code change.
|
|
105
|
+
|
|
106
|
+
`silk-wasm` is a normal npm dependency, not vendored.
|
|
107
|
+
|
|
108
|
+
## qrcode-terminal
|
|
109
|
+
|
|
110
|
+
`qrcode-terminal@0.12.0` (MIT) — used only by `bin/login.js` to render the login QR code
|
|
111
|
+
in the terminal. It is a normal npm dependency, not vendored.
|