@lijian-ui/dsh-im-gateway 0.1.1 → 0.2.0

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/README.en.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](./README.md)
4
4
 
5
- > A multi-channel IM gateway plugin for **DeepSeek Harness (dsh)** — connect DingTalk, QQ and WeChat (iLink) bots to your harness agent, with QR-scan binding and streaming replies.
5
+ > A multi-channel IM gateway plugin for **DeepSeek Harness (dsh)** — connect DingTalk, QQ and WeChat (iLink) bots to your harness agent, with QR-scan binding, streaming replies, tool approval, interactive questions, long-text splitting, multi-segment merge, and bilingual UI.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@lijian-ui/dsh-im-gateway)](https://www.npmjs.com/package/@lijian-ui/dsh-im-gateway)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -11,14 +11,32 @@ English | [简体中文](./README.md)
11
11
 
12
12
  ## Features
13
13
 
14
+ ### Channels & Core
15
+
14
16
  - **Unified gateway service** — one plugin, three channels. Every channel routes through a single `ctx.imGateway` core: session management, slash commands, streaming, status broadcast.
15
17
  - **DingTalk** — outbound WebSocket stream connection, group + single chats, @-mention filtering, **AI card streaming** (real-time incremental replies), slash commands.
16
18
  - **QQ** — WebSocket gateway (official `qqbot-nodejs` SDK), private (c2c) + group chats, **QR-scan bot binding** (no manual console setup), **streaming messages** (c2c).
17
19
  - **WeChat (iLink)** — official iLink long-poll protocol, **QR-scan login + pairing code**, single-chat only, media (AES-128-ECB CDN) in/out.
18
20
  - **Multi-bot instances** — the same channel type can appear many times (e.g. two DingTalk bots), each with its own credentials.
19
- - **Built-in slash commands** — `/help`, `/model`, `/status`, `/new`, `/reset`, `/stop` … (see below).
20
21
  - **Settings UI** — a full web settings page ("IM 通道") rendered inside the official dsh web UI; QR-scan binding happens right there.
21
22
  - **Streaming replies** — DingTalk AI Card, QQ stream_messages; plain-text fallback when a channel doesn't support streaming.
23
+ - **Instance lock** — file-based `DSH_HOME` lock prevents multiple instances from corrupting the session log.
24
+
25
+ ### Interaction Enhancements
26
+
27
+ - **Tool approval bridge** — when the agent calls a tool requiring approval, reply "approve" or "reject" directly in IM; timeout falls back to dsh's native approval system.
28
+ - **Interactive question bridge** — when the agent calls `ask_user_question`, the question is pushed to IM; reply with option numbers/text to answer. Races against the Web UI — first answer wins.
29
+ - **Long reply splitting** — replies exceeding the channel's single-message limit are auto-split at sentence boundaries with `(1/3)` segment prefixes, sent as separate messages.
30
+ - **Multi-segment input merge** — consecutive messages are auto-merged into one (configurable timeout window); use `..` to continue merging, `!!` to submit immediately.
31
+ - **File-sending tool** — the agent can call `im_send_file` to send workspace files (images/videos/documents) directly to the current IM session.
32
+ - **Bilingual UI** — set `language: 'zh' | 'en'` to switch all user-visible replies between Chinese and English.
33
+
34
+ ### Session Management
35
+
36
+ - **Built-in slash commands** — `/help`, `/model`, `/status`, `/new`, `/reset`, `/stop`, `/sessions`, `/continue`, `/workspaces`, `/workspace` (see below).
37
+ - **Session list & continue** — `/sessions` lists recent sessions (with titles); `/continue <id>` resumes a specific session.
38
+ - **Workspace management** — `/workspaces` lists all workspaces with session counts; `/workspace <path>` switches workspace (resets the current session; next message creates a new session in the new workspace).
39
+ - **User allowlist** — configure `allowAllUsers` or `allowedUserIds` to control who can use the bot.
22
40
 
23
41
  ---
24
42
 
@@ -38,7 +56,7 @@ The npm package ships pre-built `lib/` — **no build authorization needed** (`a
38
56
 
39
57
  ```bash
40
58
  npm pack @lijian-ui/dsh-im-gateway
41
- dsh plugin --profile web add ./dsh-im-gateway-0.1.0.tgz
59
+ dsh plugin --profile web add ./dsh-im-gateway-0.1.1.tgz
42
60
  ```
43
61
 
44
62
  ### From GitHub
@@ -83,6 +101,32 @@ Sent to the bot in any IM channel:
83
101
  | `/status` | Channel / cwd / current model / agent state |
84
102
  | `/new` `/reset` `/clear` | Start a fresh conversation |
85
103
  | `/stop` | Abort the current reply |
104
+ | `/sessions` | List the 10 most recent sessions (current session marked, with titles) |
105
+ | `/continue <session-id>` | Continue a specific session (use `/sessions` for IDs) |
106
+ | `/workspaces` | List all workspaces (sorted by recent activity, with session counts) |
107
+ | `/workspace <path>` | Switch to the specified workspace (resets current session; next message creates a new session in the new workspace) |
108
+ | `/workspace` | Without arguments, same as `/workspaces` |
109
+
110
+ ### Multi-segment Input Suffixes
111
+
112
+ | Suffix | Effect |
113
+ | --- | --- |
114
+ | (none) | Enters the merge window, waits for more messages (auto-submits after 3s by default) |
115
+ | `..` | Continue merging: add this message to the buffer, keep waiting |
116
+ | `!!` | Submit now: merge the buffer + this message and send to the agent immediately |
117
+
118
+ ### Approval Replies
119
+
120
+ When the agent calls a tool that requires approval, reply directly:
121
+
122
+ | Reply | Effect |
123
+ | --- | --- |
124
+ | `approve` / `yes` / `y` / `allow` | Allow the tool execution |
125
+ | `reject` / `no` / `n` / `deny` | Block the tool execution |
126
+
127
+ (Chinese equivalents: `批准` / `同意` for approve, `拒绝` for reject.)
128
+
129
+ After timeout, the request falls back to dsh's native approval system.
86
130
 
87
131
  ---
88
132
 
@@ -92,6 +136,14 @@ Everything is editable from the settings UI; the underlying schema lives in `~/.
92
136
 
93
137
  ```yaml
94
138
  im-gateway:
139
+ language: zh # UI language: zh (Chinese) | en (English)
140
+ approvalTimeoutSecs: 120 # Tool approval timeout (seconds)
141
+ questionTimeoutSecs: 600 # Interactive question timeout (seconds)
142
+ mergeTimeoutSecs: 3 # Multi-segment input merge window (seconds)
143
+ allowAllUsers: false # Allow all users (dev only)
144
+ allowedUserIds: # Allowlist: { channelId: string[] } or use '*' for any channel
145
+ "*":
146
+ - user-abc
95
147
  channels:
96
148
  - id: dingtalk-main
97
149
  type: dingtalk
@@ -103,6 +155,21 @@ im-gateway:
103
155
  # callbackBaseUrl, appId, botAppId, baseUrl, botId, cdnBaseUrl, pollIntervalMs...
104
156
  ```
105
157
 
158
+ ### Gateway-level Config
159
+
160
+ | Field | Default | Description |
161
+ | --- | --- | --- |
162
+ | `language` | `zh` | UI language (`zh` Chinese / `en` English); affects all user-visible replies |
163
+ | `streamThrottleMs` | `800` | Throttle interval for streaming pushes (ms) |
164
+ | `slashCommands` | `true` | Enable slash commands |
165
+ | `approvalTimeoutSecs` | `120` | Tool approval IM wait timeout (seconds); falls back to dsh native approval on timeout |
166
+ | `questionTimeoutSecs` | `600` | Interactive question IM wait timeout (seconds); falls back to Web UI on timeout |
167
+ | `mergeTimeoutSecs` | `3` | Multi-segment input merge window (seconds); consecutive messages merged into one |
168
+ | `allowAllUsers` | `false` | Allow all users (dev only; do not enable in production) |
169
+ | `allowedUserIds` | `{}` | Allowlist; key is channelId (`*` matches any), value is array of user IDs |
170
+
171
+ ### Channel-level Config
172
+
106
173
  | Field | Applies to | Meaning |
107
174
  | --- | --- | --- |
108
175
  | `clientId` / `clientSecret` | dingtalk | DingTalk app key / secret (Stream mode) |
@@ -119,22 +186,39 @@ IM client ──► channel adapter (dingtalk / qq / weixin)
119
186
  │ ImInboundMessage
120
187
 
121
188
  ctx.imGateway (core)
189
+ │ multi-segment merge → allowlist check → approval/question intercept → slash commands
122
190
  │ ensureSession → agent.followup
123
191
 
124
192
  dsh harness agent (LLM loop)
125
193
  │ session events (turn/start, assistant/chunk, tool/call, turn/end)
126
194
 
127
- streaming reply adapter.beginStream/streamText/endStream
195
+ EventDispatcher → streaming reply / splitting / tool hints
128
196
  │ (AI Card / stream_messages / plain text fallback)
129
197
 
130
198
  IM client
131
199
  ```
132
200
 
133
- - **Host half** (node): `src/index.ts` (apply), `src/gateway/` (core + slash commands), `src/channels/` (dingtalk / qq / weixin + protocol helpers), `src/remote.ts` (Typert RPC for the settings UI), `src/sync.ts` (channel reload on config save).
201
+ ### Module Structure
202
+
203
+ | Module | Responsibility |
204
+ | --- | --- |
205
+ | `im-gateway.ts` | Core service ImGatewayService: session management, message routing, tool registration |
206
+ | `events.ts` | EventDispatcher: SessionEvent → IM channel operations (streaming, splitting, tool hints) |
207
+ | `commands.ts` | CommandHandler: slash commands (/help /reset /model /status /stop /sessions /continue /workspaces /workspace) |
208
+ | `stream.ts` | StreamThrottle: batch text-delta and push at intervals |
209
+ | `approval.ts` | ApprovalBroker: tool approval bridge, holds approval/request pending IM reply |
210
+ | `questions.ts` | QuestionBroker: interactive question bridge, holds ask_user_question pending IM reply |
211
+ | `split.ts` | splitText: long-text splitting at sentence boundaries with segment prefixes |
212
+ | `merge.ts` | SessionMerger: multi-segment input merge, supports `..` / `!!` control suffixes |
213
+ | `i18n.ts` | Translator: Chinese/English bilingual translation table |
214
+ | `instance-lock.ts` | acquireInstanceLock: DSH_HOME file lock, prevents concurrent session log corruption |
215
+ | `types.ts` | Interface definitions: ImChannelAdapter, ImGatewayConfig, ImGateway, etc. |
216
+
217
+ - **Host half** (node): `src/index.ts` (apply), `src/gateway/` (core + above modules), `src/channels/` (dingtalk / qq / weixin + protocol helpers), `src/remote.ts` (Typert RPC for the settings UI), `src/sync.ts` (channel reload on config save).
134
218
  - **Client half** (browser): `src/client/` — settings page "IM 通道" (add/edit modal + QR-scan login + status dots).
135
219
  - **Multi-bot**: `channels` is an array; the same `type` may appear multiple times.
136
220
 
137
- ### Extension points
221
+ ### Extension Points
138
222
 
139
223
  Third parties can register their own channel without forking:
140
224
 
@@ -145,6 +229,20 @@ class MyChannelAdapter implements ImChannelAdapter { /* ... */ }
145
229
  ctx.imGateway.registerChannel(myAdapter)
146
230
  ```
147
231
 
232
+ `ImChannelAdapter` interface optional methods:
233
+
234
+ | Method | Description |
235
+ | --- | --- |
236
+ | `sendText(convId, text)` | **Required**. Send a plain-text message |
237
+ | `sendMedia(convId, filePath, caption?)` | Optional. Send a file/image/video (used by im_send_file tool) |
238
+ | `beginStream(convId)` | Optional. Open a streaming reply (called on first text delta) |
239
+ | `streamText(convId, text)` | Optional. Streaming overwrite update (throttled) |
240
+ | `endStream(convId, fullText)` | Optional. Close a streaming reply (called on turn/end) |
241
+ | `updateCard(convId, text)` | Optional. Legacy single-shot card update |
242
+ | `authorizes(userId)` | Optional. Channel-local authorization check (return false to block) |
243
+ | `maxMessageChars` | Optional. Max chars per message (default 4000, used for splitting) |
244
+ | `label` | Optional. Channel display name (used in question receipts) |
245
+
148
246
  ---
149
247
 
150
248
  ## Development
@@ -156,6 +254,7 @@ npm install
156
254
  npm run build # tsdown → lib/
157
255
  npm run watch # rebuild on save
158
256
  npm run typecheck
257
+ npm test # node --test tests/*.test.mjs
159
258
  ```
160
259
 
161
260
  Local link into a dsh profile:
@@ -166,6 +265,17 @@ dsh plugin --profile web add ./ # install from this directory (link)
166
265
 
167
266
  > **Windows note**: the dsh subprocess loads `lib/index.js` from `package.json` `main` — after editing `src/`, always `npm run build` then restart the dsh process (its require cache keeps the old module).
168
267
 
268
+ ### Tests
269
+
270
+ Tests use Node.js's built-in test runner (`node:test`), in the `tests/` directory:
271
+
272
+ | Test file | Module covered | Tests |
273
+ | --- | --- | --- |
274
+ | `approval.test.mjs` | ApprovalBroker | 8 |
275
+ | `questions.test.mjs` | QuestionBroker + parseQuestionReply + formatQuestionPrompt | 8 |
276
+ | `split.test.mjs` | splitText | 8 |
277
+ | `merge.test.mjs` | SessionMerger + stripControlSuffix | 9 |
278
+
169
279
  ---
170
280
 
171
281
  ## Troubleshooting
@@ -173,6 +283,10 @@ dsh plugin --profile web add ./ # install from this directory (link)
173
283
  - **No logs from the plugin** — cordis buffers `ctx.logger.*` in memory by default. The plugin registers a console exporter on apply, so logs appear in the dsh subprocess stderr (desktop shells prefix them with `[dsh]`).
174
284
  - **QQ client shows "连接中" (connecting) forever** — streaming was opened too early or never closed. This plugin opens the stream on the first assistant text delta and always closes it on `turn/end` (fixed in 0.1.x).
175
285
  - **Chat works but replies are not streaming** — the channel fell back to plain text (e.g. QQ group chats don't support `stream_messages`; WeChat has no streaming concept). This is by design.
286
+ - **Replies split into multiple messages** — auto-splitting kicks in when text exceeds the channel's `maxMessageChars` limit, with `(1/3)` prefixes. This is by design, not a bug.
287
+ - **Multiple messages got merged** — consecutive messages within the 3s merge window are combined into one. Send `!!` to submit immediately, or increase `mergeTimeoutSecs`.
288
+ - **Approval/question timed out** — increase `approvalTimeoutSecs` / `questionTimeoutSecs`. On timeout, the request automatically falls back to the Web UI.
289
+ - **Some text still in Chinese after switching to English** — separators in `formatAnswerSummary` (`、` `;`) and `(空)` are intentionally fixed as they are format symbols, not natural language.
176
290
 
177
291
  ---
178
292
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](./README.en.md) | 简体中文
4
4
 
5
- > 为 **DeepSeek Harness (dsh)** 提供多 IM 通道接入的网关插件:钉钉 / QQ / 个人微信,支持扫码绑定与流式回复。
5
+ > 为 **DeepSeek Harness (dsh)** 提供多 IM 通道接入的网关插件:钉钉 / QQ / 个人微信,支持扫码绑定、流式回复、工具审批、交互提问、长文本分片、多段合并、双语界面。
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@lijian-ui/dsh-im-gateway)](https://www.npmjs.com/package/@lijian-ui/dsh-im-gateway)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -11,14 +11,32 @@
11
11
 
12
12
  ## 功能特性
13
13
 
14
+ ### 通道与核心
15
+
14
16
  - **统一网关服务** — 一个插件、三个通道。所有通道都汇聚到单一的 `ctx.imGateway` 核心:会话管理、斜杠命令、流式回复、状态广播。
15
17
  - **钉钉** — 出站 WebSocket 长连接,群聊 + 单聊,@ 提及过滤,**AI 卡片流式输出**(实时增量回复),斜杠命令。
16
18
  - **QQ** — WebSocket 网关(官方 `qqbot-nodejs` SDK),私聊(c2c)+ 群聊,**扫码绑定机器人**(免去开放平台手动创建),**流式消息**(c2c)。
17
19
  - **个人微信(iLink)** — 官方 iLink 长轮询协议,**扫码登录 + 配对码**,仅单聊,媒体(AES-128-ECB CDN)收发。
18
20
  - **多机器人实例** — 同一通道类型可配置多个实例(例如两个钉钉机器人),各自独立凭据。
19
- - **内置斜杠命令** — `/help`、`/model`、`/status`、`/new`、`/reset`、`/stop` 等(见下文)。
20
21
  - **设置页 UI** — 在官方 dsh web UI 内渲染完整的设置页(「IM 通道」),扫码绑定就在这里完成。
21
22
  - **流式回复** — 钉钉 AI 卡片、QQ stream_messages;渠道不支持流式时自动回退纯文本。
23
+ - **单例锁** — 通过 `DSH_HOME` 文件锁防止多个实例并发写坏 session log。
24
+
25
+ ### 交互增强
26
+
27
+ - **工具审批桥** — agent 调用需要审批的工具时,在 IM 里直接回复「批准」或「拒绝」即可放行/拦截,超时自动委托回 dsh 原生审批体系。
28
+ - **交互提问桥** — agent 调用 `ask_user_question` 时,问题同步推送到 IM,用户回复选项编号/文字即可作答,与 Web 端竞速第一答生效。
29
+ - **长回复分片** — 超过渠道单条上限的回复自动按句号/换行切分,带 `(1/3)` 分段前缀,逐条发送。
30
+ - **多段输入合并** — 用户连续发多条消息时自动合并为一条(可配超时窗口);`..` 续传合并、`!!` 立即提交。
31
+ - **文件发送工具** — agent 可调用 `im_send_file` 工具把工作区文件(图片/视频/文档)直接发送到当前 IM 会话。
32
+ - **双语界面** — 配置 `language: 'zh' | 'en'` 切换所有用户可见回复的语言。
33
+
34
+ ### 会话管理
35
+
36
+ - **内置斜杠命令** — `/help`、`/model`、`/status`、`/new`、`/reset`、`/stop`、`/sessions`、`/continue`、`/workspaces`、`/workspace`(见下文)。
37
+ - **会话列表与继续** — `/sessions` 列出最近会话(含标题),`/continue <id>` 恢复指定会话。
38
+ - **工作区管理** — `/workspaces` 列出所有工作区及会话数,`/workspace <路径>` 切换工作区(自动重置当前会话,下次消息在新工作区创建新会话)。
39
+ - **用户白名单** — 配置 `allowAllUsers` 或 `allowedUserIds` 精确控制谁可以使用机器人。
22
40
 
23
41
  ---
24
42
 
@@ -38,7 +56,7 @@ npm 包自带预构建的 `lib/` — **无需构建授权**(不需要 `allowBu
38
56
 
39
57
  ```bash
40
58
  npm pack @lijian-ui/dsh-im-gateway
41
- dsh plugin --profile web add ./dsh-im-gateway-0.1.0.tgz
59
+ dsh plugin --profile web add ./dsh-im-gateway-0.1.1.tgz
42
60
  ```
43
61
 
44
62
  ### 从 GitHub 安装
@@ -83,6 +101,30 @@ dsh --profile web # 启动后浏览器打开设置 → 「IM
83
101
  | `/status` | 通道 / cwd / 当前模型 / agent 状态 |
84
102
  | `/new` `/reset` `/clear` | 开启全新会话 |
85
103
  | `/stop` | 中止当前回复 |
104
+ | `/sessions` | 列出最近 10 个会话(标记当前会话,显示标题) |
105
+ | `/continue <会话id>` | 继续指定会话(用 `/sessions` 查看可用 id) |
106
+ | `/workspaces` | 列出所有工作区(按最近活动排序,显示会话数) |
107
+ | `/workspace <路径>` | 切换到指定工作区(重置当前会话,下次消息在新工作区创建新会话) |
108
+ | `/workspace` | 不带参数时等同于 `/workspaces` |
109
+
110
+ ### 多段输入控制后缀
111
+
112
+ | 后缀 | 说明 |
113
+ | --- | --- |
114
+ | (无) | 进入合并窗口,等待后续消息(默认 3 秒超时后自动提交) |
115
+ | `..` | 续传合并:把本条加入缓冲,继续等待 |
116
+ | `!!` | 立即提交:把缓冲 + 本条合并后马上发给 agent |
117
+
118
+ ### 审批回复
119
+
120
+ 当 agent 调用需要审批的工具时,直接回复:
121
+
122
+ | 回复 | 效果 |
123
+ | --- | --- |
124
+ | `批准` / `同意` / `yes` / `y` / `allow` | 放行工具执行 |
125
+ | `拒绝` / `no` / `n` / `reject` / `deny` | 拦截工具执行 |
126
+
127
+ 超时后自动委托回 dsh 原生审批体系。
86
128
 
87
129
  ---
88
130
 
@@ -92,6 +134,14 @@ dsh --profile web # 启动后浏览器打开设置 → 「IM
92
134
 
93
135
  ```yaml
94
136
  im-gateway:
137
+ language: zh # 界面语言:zh(中文)| en(英文)
138
+ approvalTimeoutSecs: 120 # 工具审批超时(秒)
139
+ questionTimeoutSecs: 600 # 交互提问超时(秒)
140
+ mergeTimeoutSecs: 3 # 多段输入合并窗口(秒)
141
+ allowAllUsers: false # 全局放行所有用户(仅开发用)
142
+ allowedUserIds: # 白名单:{ channelId: string[] } 或用 '*' 匹配任意渠道
143
+ "*":
144
+ - user-abc
95
145
  channels:
96
146
  - id: dingtalk-main
97
147
  type: dingtalk
@@ -103,6 +153,21 @@ im-gateway:
103
153
  # callbackBaseUrl, appId, botAppId, baseUrl, botId, cdnBaseUrl, pollIntervalMs...
104
154
  ```
105
155
 
156
+ ### 网关级配置
157
+
158
+ | 字段 | 默认值 | 含义 |
159
+ | --- | --- | --- |
160
+ | `language` | `zh` | 界面语言(`zh` 中文 / `en` 英文),影响所有用户可见回复 |
161
+ | `streamThrottleMs` | `800` | 流式推送节流间隔(毫秒) |
162
+ | `slashCommands` | `true` | 是否启用斜杠命令 |
163
+ | `approvalTimeoutSecs` | `120` | 工具审批 IM 等待超时(秒),超时后委托回 dsh 原生审批 |
164
+ | `questionTimeoutSecs` | `600` | 交互提问 IM 等待超时(秒),超时后转回 Web 端 |
165
+ | `mergeTimeoutSecs` | `3` | 多段输入合并窗口(秒),用户连续发消息时合并为一条 |
166
+ | `allowAllUsers` | `false` | 全局放行所有用户(仅开发用,生产环境勿开) |
167
+ | `allowedUserIds` | `{}` | 白名单;key 为 channelId(`*` 匹配任意),value 为用户 ID 数组 |
168
+
169
+ ### 通道级配置
170
+
106
171
  | 字段 | 适用渠道 | 含义 |
107
172
  | --- | --- | --- |
108
173
  | `clientId` / `clientSecret` | dingtalk | 钉钉应用 key / secret(Stream 模式) |
@@ -119,18 +184,35 @@ IM 客户端 ──► 通道适配器 (dingtalk / qq / weixin)
119
184
  │ ImInboundMessage
120
185
 
121
186
  ctx.imGateway(核心)
187
+ │ 多段合并 → 白名单检查 → 审批/提问拦截 → 斜杠命令
122
188
  │ ensureSession → agent.followup
123
189
 
124
190
  dsh harness agent(LLM 循环)
125
191
  │ 会话事件 (turn/start, assistant/chunk, tool/call, turn/end)
126
192
 
127
- 流式回复适配器 beginStream/streamText/endStream
193
+ EventDispatcher流式回复 / 分片 / 工具提示
128
194
  │ (AI 卡片 / stream_messages / 纯文本回退)
129
195
 
130
196
  IM 客户端
131
197
  ```
132
198
 
133
- - **Host 半**(node):`src/index.ts`(apply)、`src/gateway/`(核心 + 斜杠命令)、`src/channels/`(dingtalk / qq / weixin + 协议助手)、`src/remote.ts`(设置页的 Typert RPC)、`src/sync.ts`(保存配置后热重载通道)。
199
+ ### 模块结构
200
+
201
+ | 模块 | 职责 |
202
+ | --- | --- |
203
+ | `im-gateway.ts` | 核心服务 ImGatewayService:会话管理、消息路由、工具注册 |
204
+ | `events.ts` | EventDispatcher:SessionEvent → IM 渠道操作(流式、分片、工具提示) |
205
+ | `commands.ts` | CommandHandler:斜杠命令处理(/help /reset /model /status /stop /sessions /continue /workspaces /workspace) |
206
+ | `stream.ts` | StreamThrottle:流式节流器,攒批 text-delta 后按间隔推送 |
207
+ | `approval.ts` | ApprovalBroker:工具审批桥,挂起 approval/request 等待 IM 回复 |
208
+ | `questions.ts` | QuestionBroker:交互提问桥,挂起 ask_user_question 等待 IM 回复 |
209
+ | `split.ts` | splitText:长文本分片,按句号/换行切分,带分段前缀 |
210
+ | `merge.ts` | SessionMerger:多段输入合并,支持 `..` / `!!` 控制后缀 |
211
+ | `i18n.ts` | Translator:中英文双语翻译表 |
212
+ | `instance-lock.ts` | acquireInstanceLock:DSH_HOME 文件锁,防止并发写坏 session log |
213
+ | `types.ts` | 接口定义:ImChannelAdapter、ImGatewayConfig、ImGateway 等 |
214
+
215
+ - **Host 半**(node):`src/index.ts`(apply)、`src/gateway/`(核心 + 上述模块)、`src/channels/`(dingtalk / qq / weixin + 协议助手)、`src/remote.ts`(设置页的 Typert RPC)、`src/sync.ts`(保存配置后热重载通道)。
134
216
  - **Client 半**(浏览器):`src/client/` — 设置页「IM 通道」(添加/编辑弹窗 + 扫码登录 + 状态点)。
135
217
  - **多机器人**:`channels` 是数组,同一 `type` 可多次出现。
136
218
 
@@ -145,6 +227,20 @@ class MyChannelAdapter implements ImChannelAdapter { /* ... */ }
145
227
  ctx.imGateway.registerChannel(myAdapter)
146
228
  ```
147
229
 
230
+ `ImChannelAdapter` 接口可选方法:
231
+
232
+ | 方法 | 说明 |
233
+ | --- | --- |
234
+ | `sendText(convId, text)` | **必需**。发送纯文本消息 |
235
+ | `sendMedia(convId, filePath, caption?)` | 可选。发送文件/图片/视频(im_send_file 工具使用) |
236
+ | `beginStream(convId)` | 可选。开启流式回复(首个文本增量时调用) |
237
+ | `streamText(convId, text)` | 可选。流式覆盖更新(节流推送) |
238
+ | `endStream(convId, fullText)` | 可选。结束流式回复(turn/end 时调用) |
239
+ | `updateCard(convId, text)` | 可选。遗留单次卡片更新 |
240
+ | `authorizes(userId)` | 可选。渠道本地授权检查(返回 false 拦截) |
241
+ | `maxMessageChars` | 可选。单条消息字符上限(默认 4000,用于分片) |
242
+ | `label` | 可选。渠道显示名称(用于提问回执) |
243
+
148
244
  ---
149
245
 
150
246
  ## 开发
@@ -156,6 +252,7 @@ npm install
156
252
  npm run build # tsdown → lib/
157
253
  npm run watch # 保存自动重编译
158
254
  npm run typecheck
255
+ npm test # node --test tests/*.test.mjs
159
256
  ```
160
257
 
161
258
  本地 link 进 dsh profile:
@@ -166,6 +263,17 @@ dsh plugin --profile web add ./ # 从本目录安装(link)
166
263
 
167
264
  > **Windows 注意**:dsh 子进程从 `package.json` 的 `main` 加载 `lib/index.js` — 修改 `src/` 后必须 `npm run build` 再重启 dsh 进程(它的 require 缓存会保留旧模块)。
168
265
 
266
+ ### 测试
267
+
268
+ 测试使用 Node.js 内置测试运行器(`node:test`),位于 `tests/` 目录:
269
+
270
+ | 测试文件 | 覆盖模块 | 测试数 |
271
+ | --- | --- | --- |
272
+ | `approval.test.mjs` | ApprovalBroker | 8 |
273
+ | `questions.test.mjs` | QuestionBroker + parseQuestionReply + formatQuestionPrompt | 8 |
274
+ | `split.test.mjs` | splitText | 8 |
275
+ | `merge.test.mjs` | SessionMerger + stripControlSuffix | 9 |
276
+
169
277
  ---
170
278
 
171
279
  ## 常见问题
@@ -173,6 +281,10 @@ dsh plugin --profile web add ./ # 从本目录安装(link)
173
281
  - **插件没有任何日志** — cordis 默认把 `ctx.logger.*` 缓存进内存。本插件在 apply 时注册了 console exporter,日志会出现在 dsh 子进程 stderr(桌面壳会加 `[dsh]` 前缀)。
174
282
  - **QQ 客户端一直显示「连接中」** — 流式开得太早或没收干净。本插件在**第一个文本增量**时才开流,并在 `turn/end` 无条件收流(0.1.x 已修复)。
175
283
  - **能对话但不流式** — 渠道回退到了纯文本(例如 QQ 群聊不支持 `stream_messages`;微信本身没有流式概念)。这是设计行为。
284
+ - **回复被截断成多条** — 超过渠道 `maxMessageChars` 上限时自动分片,带 `(1/3)` 前缀。这是设计行为,不是 bug。
285
+ - **多段消息被合并了** — 默认 3 秒合并窗口内连续发的消息会合并为一条。发 `!!` 立即提交,或调大 `mergeTimeoutSecs`。
286
+ - **审批/提问超时了** — 调大 `approvalTimeoutSecs` / `questionTimeoutSecs`。超时后会自动委托回 Web 端。
287
+ - **切换英文后部分文本仍是中文** — `formatAnswerSummary` 中的分隔符(`、` `;`)和 `(空)` 目前固定中文,因为它们是格式符号而非自然语言。
176
288
 
177
289
  ---
178
290