@colinagent/hulala-bot 0.3.2 → 0.3.3

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.
Files changed (3) hide show
  1. package/README.md +186 -180
  2. package/README.zh-CN.md +390 -0
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -1,117 +1,122 @@
1
1
  # hulala-bot
2
2
 
3
- 把这个机器上的 DeepSeek Harness agent 接到 **Telegram** 和 **飞书 / Lark** 上。手机发一句话,本机的 Host 执行,回复流式回到同一个对话;`/cwd` 切换这个对话挂载哪个会话——和 TUI 里切换会话是同一件事。
3
+ Put the DeepSeek Harness agent running on this machine on **Telegram** and **Feishu / Lark**. Send one sentence from your phone, the local Host runs it, and the answer streams back into the same conversation; `/cwd` changes which session that conversation is mounted to — the same thing switching sessions in the TUI does.
4
4
 
5
- DSH 原生和呼啦啦都用同一种方式接:**装成 Host 插件**,进 profile,随 Host 起落。npm 上显示的就是这个文件。
5
+ Native DSH and Hulala attach the same way: **install it as a Host plugin**, put it in a profile, and it lives and dies with the Host. This file is also the npm page.
6
6
 
7
7
  ---
8
8
 
9
- ## 让 AI 帮你配
9
+ ## Let an AI set it up
10
10
 
11
- 这个文件就是给 AI 看的说明书:命令、配置项、期望输出、报错原文都写在里面。把下面这段发给你的 AI(Claude Code、Cursor、ChatGPT 都行),它会照着做:
11
+ This file is written to be an AI's instruction sheet: commands, configuration fields, expected output and exact error strings are all here. Send this to your assistant (Claude Code, Cursor, ChatGPT — anything that can run commands):
12
12
 
13
13
  ```text
14
- 帮我配置 hulala-bot(一个把 Telegram / 飞书接到本机 DeepSeek Harness 的插件)。
15
- 请先完整读 https://github.com/colinagent/hulala-bot 的 README,然后按顺序执行:
16
-
17
- 1. 检查环境:node -v(>=22.6)、pnpm -v、hulala --version
18
- 2. 按「安装」一节装插件(呼啦啦用 hulala install bot;没有这条命令就用 hulala plugin)
19
- 3. 按「配置」一节写 config.json;凭据只进系统钥匙串,不要让我把 token 贴进聊天,也不要写进 config.json
20
- 4. 重启 Host
21
- 5. 按「验证清单」逐条确认,每一步把实际输出贴给我
22
- 6. 任何一步失败,按「故障排查」的原文匹配原因,不要猜
23
-
24
- 规则:不要发明 README 之外的命令、配置项、权限或事件;缺信息就停下来问我。
14
+ Set up hulala-bot for me (a plugin that connects Telegram / Feishu to the local
15
+ DeepSeek Harness). First read https://github.com/colinagent/hulala-bot in full,
16
+ then work through it in order:
17
+
18
+ 1. Check the environment: node -v (>=22.6), pnpm -v, hulala --version
19
+ 2. Install the plugin as described in "Install" (hulala install bot on Hulala;
20
+ hulala plugin if that command does not exist yet)
21
+ 3. Write config.json as described in "Configure". Credentials go into the OS
22
+ keychain only — never ask me to paste a token into the chat, never write one
23
+ into config.json
24
+ 4. Restart the Host
25
+ 5. Walk the verification checklist and paste the real output of every step
26
+ 6. If a step fails, match the exact text against "Troubleshooting". Do not guess.
27
+
28
+ Rules: invent no commands, config fields, scopes or events beyond this README;
29
+ stop and ask me when something is missing.
25
30
  ```
26
31
 
27
- 给 AI 的三条硬规则,README 里也有对应依据:
32
+ The three rules the README backs up:
28
33
 
29
- 1. **凭据只进钥匙串**,配置文件里只放 `tokenRef` / `appSecretRef`(`<service>/<account>`)。
30
- 2. **允许名单不能为空**:空名单会被拒绝启动,这不是可以跳过的步骤。
31
- 3. **改完配置必须重启 Host**:插件在启动时读取配置,没有热加载。
34
+ 1. **Credentials live in the keychain.** The config file only holds `tokenRef` / `appSecretRef` (`<service>/<account>`).
35
+ 2. **The allowlist cannot be empty.** The bot refuses to start without one; it is not a step that can be skipped.
36
+ 3. **Restart the Host after any config change.** The plugin reads its config at startup; there is no hot reload.
32
37
 
33
38
  ---
34
39
 
35
- ## 它是什么
40
+ ## What it is
36
41
 
37
- **一个 Host 插件**,不 patch DSH 一行代码。它跑在 Host 进程里,直接调宿主服务:
42
+ **One Host plugin** that patches no line of DSH. It runs inside the Host process and calls host services directly:
38
43
 
39
44
  ```
40
- ctx.sessionController.list / create / prompt / follow → 会话与流式
41
- ctx.workspaceController.create → 工作目录
42
- ctx.on('approval/request', (req, next) => …) → 审批
45
+ ctx.sessionController.list / create / prompt / follow → sessions and streaming
46
+ ctx.workspaceController.create → working directory
47
+ ctx.on('approval/request', (req, next) => …) → approvals
43
48
  ```
44
49
 
45
- 没有端口、token、cookie、WebSocket(飞书那条长连接是飞书自己的事件通道,不是本机服务)。会话历史、cwd、模型、权限以 Host 为真源;这里只保存「哪个对话挂了哪个会话」。
50
+ No port, no token exchange, no cookie, no local WebSocket (the Feishu long connection is Feishu's own event channel, not a server on this machine). Sessions, history, cwd, models and permissions stay the Host's; this plugin stores only which conversation is mounted to which session.
46
51
 
47
- **一份代码,两个渠道**:`src/channel.ts` 是渠道契约,`src/telegram/`、`src/lark/` 各自实现,`src/bridge.ts` 的会话逻辑两边共用。两个渠道可以同时开,各自有允许名单和挂载表;Telegram 的对话和飞书的对话是两个独立会话,不合并身份。
52
+ **One code path, two channels**: `src/channel.ts` is the channel contract, `src/telegram/` and `src/lark/` implement it, and the session logic in `src/bridge.ts` is shared. Both channels can run at once, each with its own allowlist and mount table; a Telegram conversation and a Feishu conversation are separate sessions, with no identity merging.
48
53
 
49
54
  ---
50
55
 
51
- ## 安装
56
+ ## Install
52
57
 
53
- 先检查环境:
58
+ Check the environment first:
54
59
 
55
60
  ```sh
56
- node -v # 需要 >= 22.6
57
- pnpm -v # 安装走 pnpm;没有就先装
61
+ node -v # >= 22.6
62
+ pnpm -v # the install goes through pnpm; install it if missing
58
63
  hulala --version
59
64
  ```
60
65
 
61
- ### 呼啦啦
66
+ ### Hulala
62
67
 
63
68
  ```sh
64
69
  hulala install bot
65
- # 老版本没有这条命令时,等价写法:
70
+ # On an older Hulala without that command, the equivalent is:
66
71
  hulala plugin --profile tui add @colinagent/hulala-bot
67
72
  ```
68
73
 
69
- `hulala install bot` 只是把 profile 名(`tui`)和包名填好的快捷方式,安装仍由 DSH 的 `runPluginCommand` 完成。
74
+ `hulala install bot` only fills in the profile name (`tui`) and the package name; the install still runs through DSH's `runPluginCommand`.
70
75
 
71
- ### DSH(Web / Desktop / TUI)
76
+ ### DSH (Web / Desktop / TUI)
72
77
 
73
78
  ```sh
74
79
  dsh plugin --profile tui add @colinagent/hulala-bot
75
80
  ```
76
81
 
77
- **期望输出**:最后一行出现 `profile tui: @deepseek-ai/dsh-base, @colinagent/hulala-bot` 和 `Restart the Host for the change to take effect.`
82
+ **Expected output**: a final `profile tui: @deepseek-ai/dsh-base, @colinagent/hulala-bot` and `Restart the Host for the change to take effect.`
78
83
 
79
- 装完核对:
84
+ Confirm:
80
85
 
81
86
  ```sh
82
- hulala plugin --profile tui list # 依赖里有 @colinagent/hulala-bot
87
+ hulala plugin --profile tui list # @colinagent/hulala-bot is in the dependencies
83
88
  ```
84
89
 
85
- 卸载:`hulala plugin --profile tui remove @colinagent/hulala-bot`,然后重启 Host。
90
+ Uninstall: `hulala plugin --profile tui remove @colinagent/hulala-bot`, then restart the Host.
86
91
 
87
92
  ---
88
93
 
89
- ## 配置
94
+ ## Configure
90
95
 
91
- 配置写在 `$DSH_HOME/config/hulala-bot/config.json`:
96
+ The configuration lives in `$DSH_HOME/config/hulala-bot/config.json`:
92
97
 
93
- | Host | 实际路径 |
98
+ | Host | Actual path |
94
99
  |---|---|
95
- | 呼啦啦 | `~/.hulala/harness/config/hulala-bot/config.json` |
100
+ | Hulala | `~/.hulala/harness/config/hulala-bot/config.json` |
96
101
  | DSH | `~/.dsh/config/hulala-bot/config.json` |
97
102
 
98
- 用 `--init` 写,不要手写整个文件(它会校验、合并、设置 `0600` 权限):
103
+ Write it with `--init` rather than by hand — it validates, merges, and sets `0600`:
99
104
 
100
105
  ```sh
101
- # 呼啦啦
106
+ # Hulala
102
107
  hulala plugin --profile tui exec hulala-bot --init
103
108
 
104
109
  # DSH
105
110
  dsh plugin --profile tui exec hulala-bot --init
106
111
  ```
107
112
 
108
- 有终端时它会逐项提问;没有终端(或加 `--no-input`)时用参数。**两条渠道可以只配一条**,也可以都配。
113
+ With a terminal it asks one question at a time; without one (or with `--no-input`) it takes flags. **Configure one channel or both** — a missing section simply does not start.
109
114
 
110
115
  ### Telegram
111
116
 
112
- 先准备:跟 [@BotFather](https://t.me/BotFather) 说 `/newbot` 拿 token;自己的 user id 可以问 [@userinfobot](https://t.me/userinfobot)。
117
+ Prepare: `/newbot` to [@BotFather](https://t.me/BotFather) for a token; your own user id from [@userinfobot](https://t.me/userinfobot).
113
118
 
114
- token 放系统钥匙串(**不要**写进 config.json):
119
+ Put the token in the OS keychain (**never** in `config.json`):
115
120
 
116
121
  ```bash
117
122
  # macOS
@@ -120,7 +125,7 @@ security add-generic-password -U -s telegram -a bot-token -w '123456:ABC…'
120
125
  secret-tool store --label 'telegram bot' service telegram account bot-token
121
126
  ```
122
127
 
123
- 写配置:
128
+ Write the config:
124
129
 
125
130
  ```sh
126
131
  hulala plugin --profile tui exec hulala-bot --init --no-input \
@@ -129,27 +134,27 @@ hulala plugin --profile tui exec hulala-bot --init --no-input \
129
134
  --default-cwd ~/hulala-workspace/workspace
130
135
  ```
131
136
 
132
- 国内网络需要代理时加 `--proxy http://127.0.0.1:7897`(只支持 HTTP/HTTPS,SOCKS5 会在启动前报错)。
137
+ On a network that cannot reach `api.telegram.org`, add `--proxy http://127.0.0.1:7897` (HTTP/HTTPS only; SOCKS5 is rejected before startup).
133
138
 
134
- 也可以临时用环境变量代替钥匙串:`TELEGRAM_BOT_TOKEN=…`,或配置里的 `tokenEnv` 指定变量名。
139
+ Environment variables can stand in for the keychain: `TELEGRAM_BOT_TOKEN=…`, or point `tokenEnv` at your own variable.
135
140
 
136
- ### 飞书 / Lark
141
+ ### Feishu / Lark
137
142
 
138
- 配置由你在开发者后台完成,插件这边只填 app id 和允许名单。
143
+ You create the app in the developer console; this plugin only needs the app id and an allowlist.
139
144
 
140
- 1. 在 [飞书开放平台](https://open.feishu.cn)(国际版 [Lark](https://open.larksuite.com))建一个**企业自建应用**,拿到 App ID(`cli_` + 16 位十六进制)和 App Secret。
141
- 2. 开启**机器人能力**。
142
- 3. 权限至少:`im:message`、`im:message:send_as_bot`。后台会按实际接口提示还缺哪一项。
143
- 4. **事件与回调 → 订阅方式:使用长连接接收事件**。订阅 `im.message.receive_v1`,并添加卡片回调 `card.action.trigger`。**不要**配 webhook 地址。
144
- 5. 发布版本,把可用范围设成你自己(或团队),然后在飞书里单聊机器人或把它拉进群(群里必须 @ 它)。
145
+ 1. Create a **self-built enterprise app** at [Feishu Open Platform](https://open.feishu.cn) (international: [Lark](https://open.larksuite.com)) and copy the App ID (`cli_` + 16 hex) and App Secret.
146
+ 2. Enable the **bot capability**.
147
+ 3. Scopes: at least `im:message` and `im:message:send_as_bot`. The console tells you which one is still missing for a given API.
148
+ 4. **Events and Callbacks → subscription mode: receive events through a long connection.** Subscribe to `im.message.receive_v1` and add the `card.action.trigger` callback. **Do not** configure a webhook URL.
149
+ 5. Publish a version, set its availability to yourself (or your team), then message the bot or add it to a group (it must be @-mentioned in groups).
145
150
 
146
- App secret 放钥匙串:
151
+ Put the app secret in the keychain:
147
152
 
148
153
  ```bash
149
154
  security add-generic-password -U -s lark -a app-secret -w '…'
150
155
  ```
151
156
 
152
- 写配置:
157
+ Write the config:
153
158
 
154
159
  ```sh
155
160
  hulala plugin --profile tui exec hulala-bot --init --no-input \
@@ -158,38 +163,38 @@ hulala plugin --profile tui exec hulala-bot --init --no-input \
158
163
  --lark-allowed-user ou_0123456789abcdef0123456789abcdef
159
164
  ```
160
165
 
161
- 国际版加 `--lark-domain lark`(Open API 指向 `open.larksuite.com`)。
166
+ International Lark: add `--lark-domain lark` (the Open API moves to `open.larksuite.com`).
162
167
 
163
- **不知道自己的 open id**:先随便给机器人发一条消息,然后看日志里的
168
+ **Finding your open id**: message the bot once and read the log:
164
169
 
165
170
  ```
166
171
  ignored chat=oc_… from=ou_… (not in allowlist)
167
172
  ```
168
173
 
169
- 把 `ou_…` 填进允许名单(重跑 `--init --lark-allowed-user ou_…`),重启 Host。
174
+ Put that `ou_…` into the allowlist (`--init --lark-allowed-user ou_…`) and restart the Host.
170
175
 
171
- ### `--init` 参数
176
+ ### `--init` flags
172
177
 
173
- | 参数 | 说明 |
178
+ | Flag | Meaning |
174
179
  |---|---|
175
- | `--telegram-token-ref <service/account>` | 钥匙串引用,默认示例 `telegram/bot-token` |
176
- | `--telegram-token-env <NAME>` | 改从环境变量读 token |
177
- | `--telegram-allowed-user <id[,id]>` | 允许的 Telegram user id(可重复) |
178
- | `--telegram-allowed-chat <id[,id]>` | 允许的 chat id |
179
- | `--proxy <http://…>` | Telegram 走的 HTTP/HTTPS 代理 |
180
- | `--parse-mode markdown\|plain` | Telegram 回复格式,`markdown` 失败会自动退回纯文本 |
181
- | `--lark-app-id <cli_…>` | 飞书应用 App ID |
182
- | `--lark-app-secret-ref <service/account>` | app secret 的钥匙串引用 |
183
- | `--lark-app-secret-env <NAME>` | 改从环境变量读 app secret |
184
- | `--lark-allowed-user <ou_…[,ou_…]>` | 允许的 open id |
185
- | `--lark-allowed-chat <oc_…[,oc_…]>` | 允许的 chat id |
186
- | `--lark-domain feishu\|lark` | 国内飞书(默认)或国际版 |
187
- | `--default-cwd <path>` | 第一次说话时在哪个目录建会话 |
188
- | `--no-input` | 不提问;缺参数直接报错 |
189
-
190
- `--init` **按渠道合并**:只改它拿到的那条渠道,另一条和已有的允许名单/未知字段原样保留。所以补一个代理、加一个用户都不用重写整个文件。
191
-
192
- ### 文件结构
180
+ | `--telegram-token-ref <service/account>` | Keychain reference; the documented name is `telegram/bot-token` |
181
+ | `--telegram-token-env <NAME>` | Read the token from an environment variable instead |
182
+ | `--telegram-allowed-user <id[,id]>` | Allowed Telegram user ids (repeatable) |
183
+ | `--telegram-allowed-chat <id[,id]>` | Allowed chat ids |
184
+ | `--proxy <http://…>` | HTTP/HTTPS proxy for Telegram |
185
+ | `--parse-mode markdown\|plain` | Telegram formatting; `markdown` falls back to plain text on rejection |
186
+ | `--lark-app-id <cli_…>` | Feishu app id |
187
+ | `--lark-app-secret-ref <service/account>` | Keychain reference for the app secret |
188
+ | `--lark-app-secret-env <NAME>` | Read the app secret from an environment variable |
189
+ | `--lark-allowed-user <ou_…[,ou_…]>` | Allowed open ids |
190
+ | `--lark-allowed-chat <oc_…[,oc_…]>` | Allowed chat ids |
191
+ | `--lark-domain feishu\|lark` | Feishu (default) or international Lark |
192
+ | `--default-cwd <path>` | Where the first message creates its session |
193
+ | `--no-input` | Never prompt; missing values are errors |
194
+
195
+ `--init` **merges per channel**: it rewrites only the channel whose flags it got, and keeps the other channel, existing allowlists and unknown fields. Adding a proxy or one more user never wipes the rest of the file.
196
+
197
+ ### File shape
193
198
 
194
199
  ```json
195
200
  {
@@ -210,179 +215,180 @@ ignored chat=oc_… from=ou_… (not in allowlist)
210
215
  }
211
216
  ```
212
217
 
213
- 顶层:
218
+ Top level:
214
219
 
215
- | 字段 | 说明 |
220
+ | Field | Meaning |
216
221
  |---|---|
217
- | `defaultCwd` | 第一次说话时在哪个目录建会话 |
218
- | `streamIntervalMs` | 两次流式更新的最小间隔,默认 800ms(飞书卡片有频率限制,别调太低) |
222
+ | `defaultCwd` | Where the first message creates its session |
223
+ | `streamIntervalMs` | Minimum gap between two streaming edits, default 800ms (Feishu cards are rate limited — do not go low) |
219
224
 
220
- `telegram`:
225
+ `telegram`:
221
226
 
222
- | 字段 | 说明 |
227
+ | Field | Meaning |
223
228
  |---|---|
224
- | `tokenRef` / `tokenEnv` | 钥匙串引用或环境变量名 |
225
- | `allowedUsers` | Telegram user id;与 `allowedChats` 都为空时拒绝启动 |
226
- | `allowedChats` | chat id(群/频道场景) |
227
- | `parseMode` | `markdown`(默认,失败自动回退)或 `plain` |
228
- | `proxy` | HTTP/HTTPS CONNECT 代理;不读 `HTTPS_PROXY`,只认这一项 |
229
- | `baseUrl` | 只给测试用 |
230
- | `pollTimeoutSeconds` | 长轮询等待秒数,默认 25 |
229
+ | `tokenRef` / `tokenEnv` | Keychain reference or environment variable name |
230
+ | `allowedUsers` | Telegram user ids; startup is refused when this and `allowedChats` are both empty |
231
+ | `allowedChats` | Chat ids (groups/channels) |
232
+ | `parseMode` | `markdown` (default, falls back on rejection) or `plain` |
233
+ | `proxy` | HTTP/HTTPS CONNECT proxy; `HTTPS_PROXY` is not read, this is the only switch |
234
+ | `baseUrl` | Tests only |
235
+ | `pollTimeoutSeconds` | Long-poll hold, default 25 |
231
236
 
232
- `lark`:
237
+ `lark`:
233
238
 
234
- | 字段 | 说明 |
239
+ | Field | Meaning |
235
240
  |---|---|
236
- | `appId` | `cli_` + 16 位十六进制;格式不对在连网前就报错 |
237
- | `appSecretRef` / `appSecretEnv` | 钥匙串引用或环境变量名 |
238
- | `domain` | `feishu`(默认)或 `lark` |
239
- | `allowedUsers` | open id(`ou_…`);与 `allowedChats` 都为空时拒绝启动 |
240
- | `allowedChats` | chat id(`oc_…`) |
241
- | `baseUrl` | 只给测试用 |
241
+ | `appId` | `cli_` + 16 hex; a bad shape fails before any network call |
242
+ | `appSecretRef` / `appSecretEnv` | Keychain reference or environment variable name |
243
+ | `domain` | `feishu` (default) or `lark` |
244
+ | `allowedUsers` | Open ids (`ou_…`); startup is refused when this and `allowedChats` are both empty |
245
+ | `allowedChats` | Chat ids (`oc_…`) |
246
+ | `baseUrl` | Tests only |
242
247
 
243
- 环境变量:`HULALA_BOT_CONFIG`(配置文件路径)、`HULALA_BOT_STATE`(状态目录)、`HULALA_BOT_LOCK_DIR`(锁目录)、`TELEGRAM_BOT_TOKEN`、`LARK_APP_SECRET`。
248
+ Environment variables: `HULALA_BOT_CONFIG` (config path), `HULALA_BOT_STATE` (state directory), `HULALA_BOT_LOCK_DIR` (lock directory), `TELEGRAM_BOT_TOKEN`, `LARK_APP_SECRET`.
244
249
 
245
250
  ---
246
251
 
247
- ## 重启与验证
252
+ ## Restart and verify
248
253
 
249
- 配置和插件都只在 Host 启动时读取,**改完必须重启**:
254
+ The plugin reads both the profile and the config at Host startup, so **restart after any change**:
250
255
 
251
256
  ```sh
252
257
  hulala host stop
253
258
  hulala host start
254
- # DSH:重启你自己的 dsh 进程
259
+ # DSH: restart your own dsh process
255
260
  ```
256
261
 
257
- ### 验证清单
262
+ ### Checklist
258
263
 
259
264
  ```sh
260
- # 1. 插件在 profile 里
265
+ # 1. The plugin is in the profile
261
266
  hulala plugin --profile tui list
262
- # 期望:@deepseek-ai/dsh-base, @colinagent/hulala-bot
267
+ # Expect: @deepseek-ai/dsh-base, @colinagent/hulala-bot
263
268
 
264
- # 2. Host 起来了
269
+ # 2. The Host is up
265
270
  hulala host status
266
- # 期望:"state": "running"
271
+ # Expect: "state": "running"
267
272
 
268
- # 3. 插件真的加载了
273
+ # 3. The plugin actually loaded
269
274
  grep "bot starting inside the Host" ~/.hulala/logs/host.log | tail -1
270
- # 期望:bot starting inside the Host; channels telegram
271
- # (配了飞书就是 channels telegram, lark)
275
+ # Expect: bot starting inside the Host; channels telegram
276
+ # (with Feishu configured: channels telegram, lark)
272
277
 
273
- # 4. 连接锁只有一个持有者
278
+ # 4. Exactly one lock holder per credential
274
279
  ls ~/.hulala-bot/*.lock
275
- # 期望:每条渠道一个 .lock;内容是本机 Host 的 pid
280
+ # Expect: one .lock per channel; contents are the local Host pid
276
281
 
277
- # 5. 给机器人发一句话
278
- # 期望:先出现占位消息,然后被流式替换成回答;TUI 里能看到同一个会话
282
+ # 5. Message the bot
283
+ # Expect: a placeholder appears, then gets replaced by the streaming answer;
284
+ # the same session is visible in the TUI
279
285
  ```
280
286
 
281
- 飞书额外确认:
287
+ Feishu extras:
282
288
 
283
289
  ```sh
284
- # 日志里有没有收到事件(首次配置时会有 ignored … not in allowlist)
290
+ # Watch inbound events (the first run logs "ignored … not in allowlist")
285
291
  tail -f ~/.hulala/harness/config/hulala-bot/logs/bridge-$(date +%F).log
286
292
  ```
287
293
 
288
- 日志位置:`~/.hulala/harness/config/hulala-bot/logs/`(DSH 在 `~/.dsh/config/hulala-bot/logs/`)。
294
+ Logs live in `~/.hulala/harness/config/hulala-bot/logs/` (DSH: `~/.dsh/config/hulala-bot/logs/`).
289
295
 
290
296
  ---
291
297
 
292
- ## 故障排查
298
+ ## Troubleshooting
293
299
 
294
- 先看这两个地方,90% 的问题能直接定位:
300
+ These two places explain most failures:
295
301
 
296
302
  ```sh
297
303
  tail -50 ~/.hulala/harness/config/hulala-bot/logs/bridge-$(date +%F).log
298
304
  grep -i "hulala-bot" ~/.hulala/logs/host.log | tail -20
299
305
  ```
300
306
 
301
- | 症状 / 报错原文 | 原因 | 处理 |
307
+ | Symptom / exact text | Cause | Fix |
302
308
  |---|---|---|
303
- | `no channel configured: add a "telegram" or "lark" section` | 配置文件不存在或两段都没有 | 跑 `--init`,或检查 `HULALA_BOT_CONFIG` 指向的路径 |
304
- | `refusing to start Telegram with an empty allowlist` | 允许名单为空(拒绝是设计行为) | 加 `--telegram-allowed-user` |
305
- | `no Telegram bot token: set tokenRef …` | 钥匙串里没有条目,或引用名写错 | `security find-generic-password -s telegram -a bot-token -w` 确认;名字要和 `tokenRef` 一致 |
306
- | `lark.appId must look like cli_<16 hex>` | App ID 复制错/不是自建应用 | 回开发者后台复制 `cli_` + 16 位十六进制 |
307
- | `no Lark app secret: set lark.appSecretRef …` | 同上,secret 没进钥匙串 | 存 `lark/app-secret`,或设 `LARK_APP_SECRET` |
308
- | `this bot is already connected from another Host (pid …)` | 另一个 Host(DSH 或旧进程)正在用同一 token/app id | 停掉那个进程;确认它已退出后删掉报错里给出的 `.lock` |
309
- | Telegram 报 `getUpdates: Conflict: … (409)` | 两个进程在轮询同一个 bot token | 同上;另外检查有没有旧的独立桥进程 |
310
- | `proxy CONNECT failed: 403` | 代理拒绝 CONNECT | 检查代理是否允许该目标/是否要认证(`http://user:pass@host:port`) |
311
- | `connect ECONNREFUSED 127.0.0.1:7897` | 代理端口没开 | 启动 Clash/Surge,或改 `proxy` |
312
- | 飞书机器人完全不回话 | ① 订阅方式不是「长连接」② 没订阅 `im.message.receive_v1` ③ 没开机器人能力 ④ 可用范围不含你 ⑤ App Secret 错 | 按这五条逐项查;secret 错会在日志里出现 `99991663` 或 `tenant_access_token` 相关错误 |
313
- | 飞书私聊正常、群里不回 | 群里必须 @ 机器人 | @ 它再发;命令前可以带 @ |
314
- | 飞书审批按钮没反应 | 没添加 `card.action.trigger` 回调 | 后台补上这个回调,重启 Host |
315
- | 流式很慢 / 频繁限流 | `streamIntervalMs` 低于平台限制 | 调到 800 以上(飞书卡片更新有频率限制) |
316
- | 回复出现 `☑️ 已处理` | 这个审批已经在另一端处理过 | 正常,不是错误 |
317
- | 日志里 `ignored … (not in allowlist)` | 发送者不在名单里 | 用日志里的 id 补进允许名单并重启 |
318
- | 重启后行为没变 | 插件只在启动时读配置 | 确认进程真的重启了(`hulala host status` 的 pid 变了) |
319
-
320
- 调试用的日志行会带渠道名前缀:`telegram`、`lark`。凭据在写日志前会被替换成 `<secret>`,不会出现在文件里。
309
+ | `no channel configured: add a "telegram" or "lark" section` | No config file, or neither section | Run `--init`, or check what `HULALA_BOT_CONFIG` points at |
310
+ | `refusing to start Telegram with an empty allowlist` | Empty allowlist (refusal is by design) | Add `--telegram-allowed-user` |
311
+ | `no Telegram bot token: set tokenRef …` | No keychain entry, or the reference name differs | Check with `security find-generic-password -s telegram -a bot-token -w`; names must match `tokenRef` |
312
+ | `lark.appId must look like cli_<16 hex>` | Wrong app id pasted, or not a self-built app | Copy `cli_` + 16 hex from the developer console |
313
+ | `no Lark app secret: set lark.appSecretRef …` | Secret not in the keychain | Store `lark/app-secret`, or set `LARK_APP_SECRET` |
314
+ | `this bot is already connected from another Host (pid …)` | Another Host (DSH or a stale process) uses the same token/app id | Stop it; once it is gone, remove the `.lock` path named in the error |
315
+ | Telegram `getUpdates: Conflict: … (409)` | Two processes polling one bot token | Same as above; also look for an old standalone bridge process |
316
+ | `proxy CONNECT failed: 403` | The proxy refused CONNECT | Check the proxy allows the target or needs auth (`http://user:pass@host:port`) |
317
+ | `connect ECONNREFUSED 127.0.0.1:7897` | Nothing listening on the proxy port | Start Clash/Surge, or change `proxy` |
318
+ | Feishu bot never answers | ① subscription mode is not "long connection" ② `im.message.receive_v1` not subscribed ③ bot capability off ④ availability excludes you ⑤ wrong app secret | Check all five; a wrong secret shows up as `99991663` / `tenant_access_token` errors in the log |
319
+ | Feishu DM works, groups do not | The bot must be @-mentioned in groups | Mention it; a leading mention is fine before a command |
320
+ | Feishu approval buttons do nothing | The `card.action.trigger` callback is missing | Add it in the console and restart the Host |
321
+ | Streaming is slow / rate limited | `streamIntervalMs` is below the platform limit | Keep it at 800 or higher (Feishu card updates are rate limited) |
322
+ | A reply says `☑️ 已处理` | That approval was already handled on another surface | Expected, not an error |
323
+ | Log says `ignored … (not in allowlist)` | The sender is not allowlisted | Add that id to the allowlist and restart |
324
+ | Nothing changed after a restart | The plugin only reads config at startup | Confirm the process really restarted (the pid in `hulala host status` changed) |
325
+
326
+ Log lines from channels are prefixed with `telegram` or `lark`. Credentials are replaced with `<secret>` before anything is written.
321
327
 
322
328
  ---
323
329
 
324
- ## 命令
330
+ ## Commands
325
331
 
326
- 两个渠道命令一样:
332
+ The same commands on both channels:
327
333
 
328
- | 命令 | 行为 |
334
+ | Command | Behaviour |
329
335
  |---|---|
330
- | `/cwd` | 列出最近用过的目录,点一个就挂上去 |
331
- | `/cwd <path>` | 换挂载:该目录下最近的会话,没有就新建 |
332
- | `/new` | 在当前目录新建会话 |
333
- | `/sessions` | 最近 10 个会话,`*` 表示当前 |
334
- | `/switch <n>` | 切到第 n 个 |
335
- | `/status` | 会话、目录、运行状态 |
336
- | `/stop` | 停止当前回合 |
337
- | `/help` | 上面这些 |
336
+ | `/cwd` | List recently used directories; tap one to mount it |
337
+ | `/cwd <path>` | Switch mount: newest session under that directory, create one if none |
338
+ | `/new` | Create a session in the current directory |
339
+ | `/sessions` | Last 10 sessions, `*` marks the current one |
340
+ | `/switch <n>` | Switch to the n-th |
341
+ | `/status` | Session, directory, running state |
342
+ | `/stop` | Cancel the running turn |
343
+ | `/help` | All of the above |
338
344
 
339
- 直接发文字就是提问。第一次说话会自动在 `defaultCwd` 里建会话并挂上。
345
+ Plain text is a prompt. The first message creates a session under `defaultCwd` and mounts it.
340
346
 
341
- 审批会以按钮出现(Telegram inline keyboard / 飞书卡片按钮),只有「允许这一次 / 拒绝」。请求里没有命令行文本,只有工具名与理由。
347
+ Approvals arrive as buttons (Telegram inline keyboard / Feishu card actions): "allow once" or "reject". The request carries the tool name and reason, not the command line; showing the exact command would require a `callId` lookup in the session log.
342
348
 
343
349
  ---
344
350
 
345
- ## 两个渠道的差异
351
+ ## How the channels differ
346
352
 
347
- | | Telegram | 飞书 / Lark |
353
+ | | Telegram | Feishu / Lark |
348
354
  |---|---|---|
349
- | 传输 | Bot API 长轮询 | 长连接(WebSocket) |
350
- | 流式 | 反复编辑同一条消息 | 反复 patch 同一张卡片(飞书不能编辑文本消息) |
351
- | 按钮 | inline keyboard | 卡片 `action` + `card.action.trigger` |
352
- | 置顶状态栏 | 有(`📍 <目录>`) | 无;用 `/status` 看 |
353
- | "正在输入" | 有 | 飞书没有这个 API |
354
- | 单条上限 | 4096(内部按 4000 切) | 4000 字符一张卡片,超了续卡片 |
355
- | 群聊 | 直接收 | 必须 @ 机器人 |
355
+ | Transport | Bot API long polling | Long connection (WebSocket) |
356
+ | Streaming | Edits one message | Patches one card (Feishu cannot edit text messages) |
357
+ | Buttons | Inline keyboard | Card `action` + `card.action.trigger` |
358
+ | Pinned status | Yes (`📍 <directory>`) | No; use `/status` |
359
+ | Typing indicator | Yes | Feishu has no such API |
360
+ | Message limit | 4096 (split at 4000) | 4000 chars per card, then another card |
361
+ | Groups | Any message | Must @-mention the bot |
356
362
 
357
- 两个渠道可以同时开:各自挂各自的会话,审批同时出现在两边,先点的生效,另一边显示「已在其他端处理」。
363
+ Both channels can run together: each mounts its own sessions, approvals appear on both, the first answer wins, and the other surface shows that it was handled elsewhere.
358
364
 
359
365
  ---
360
366
 
361
- ## 状态与安全
367
+ ## State and safety
362
368
 
363
- - **状态目录**:`$DSH_HOME/config/hulala-bot/`,里面有 `config.json`、`bindings.json`(挂载表)、`receipts/`(去重账本,24h TTL)、`logs/`,权限 `0600`/`0700`。
364
- - **连接锁**:`~/.hulala-bot/<凭据摘要>.lock`,不在状态目录里,所以 DSH 和呼啦啦同时装插件时也只有一个在连。
365
- - **凭据**:只进钥匙串(或环境变量),日志写之前替换成 `<secret>`。
366
- - **边界**:白名单是唯一口令;消息正文、工具输出、贴出的路径都会经过 Telegram / 飞书的服务器。插件不给 Host 增加权限——它能做的事,TUI 也能做;权限仍由 Host 的 preset 决定。
367
- - **跨端取消**:插件模式下,另一端已经处理过的审批不会从这边被远程收走,只会在下次交互时显示「已在其他端处理」。
369
+ - **State directory**: `$DSH_HOME/config/hulala-bot/` — `config.json`, `bindings.json` (mount table), `receipts/` (de-duplication, 24h TTL), `logs/`; permissions `0600`/`0700`.
370
+ - **Connection lock**: `~/.hulala-bot/<digest>.lock`, outside every state directory, so DSH and Hulala with the same plugin still connect only once.
371
+ - **Credentials**: keychain (or environment) only; redacted as `<secret>` before logging.
372
+ - **Boundary**: the allowlist is the only password; message text, tool output and paths pass through Telegram's or Feishu's servers. The plugin grants the Host no new authority — anything it can do, the TUI can do; permissions still come from the Host's preset.
373
+ - **Cross-surface cancellation**: in plugin mode an approval already handled elsewhere is not withdrawn from this surface; it shows "handled elsewhere" on the next interaction.
368
374
 
369
375
  ---
370
376
 
371
- ## 开发
377
+ ## Development
372
378
 
373
379
  ```bash
374
- node --test "tests/*.test.ts" # 106 个用例
380
+ node --test "tests/*.test.ts" # 107 tests
375
381
  bun run typecheck
376
382
  bun run build # dist/index.mjs + dist/plugin.mjs
377
- node scripts/spike-host.ts <dsh-bin> <dsh-home> <profile> # 对真 Host 跑一轮
383
+ node scripts/spike-host.ts <dsh-bin> <dsh-home> <profile> # one round against a real Host
378
384
  ```
379
385
 
380
- 需要 Node ≥ 22.6。飞书的 REST 用注入的 `fetch` 测,长连接用假 transport 测,测试不需要租户、不需要网络。
386
+ Node ≥ 22.6. Feishu REST is tested against an injected `fetch`, the long connection against a fake transport — no tenant, no network.
381
387
 
382
- 版本约束:`engines.dsh` 声明 DSH 兼容范围(当前 `>=0.2.2-rc.1 <0.3.0`),装载器在 import 之前校验,不满足就跳过并写进 `skippedBundles`;`@deepseek-ai/dsh-*` 必须是 peerDependencies,避免出现第二份注册表。
388
+ Version gate: `engines.dsh` declares the DSH range (`>=0.2.2-rc.1 <0.3.0`); the loader checks before import, skips incompatible bundles and reports them in `skippedBundles`; `@deepseek-ai/dsh-*` must stay peer dependencies so no second registry appears.
383
389
 
384
- 更细的架构与坑:见仓库里的 `AGENTS.md`。
390
+ Deeper architecture and traps: `AGENTS.md` in the repository.
385
391
 
386
- ## 许可
392
+ ## License
387
393
 
388
- GPL-3.0-only。它链接的是 GPL 的 DeepSeek Harness / Hulala,不是专有代码。
394
+ GPL-3.0-only. It links GPL DeepSeek Harness / Hulala, not proprietary code.
@@ -0,0 +1,390 @@
1
+ # hulala-bot
2
+
3
+ [English](https://github.com/colinagent/hulala-bot/blob/main/README.md) | 简体中文
4
+
5
+ 把这个机器上的 DeepSeek Harness agent 接到 **Telegram** 和 **飞书 / Lark** 上。手机发一句话,本机的 Host 执行,回复流式回到同一个对话;`/cwd` 切换这个对话挂载哪个会话——和 TUI 里切换会话是同一件事。
6
+
7
+ DSH 原生和呼啦啦都用同一种方式接:**装成 Host 插件**,进 profile,随 Host 起落。npm 上显示的就是这个文件。
8
+
9
+ ---
10
+
11
+ ## 让 AI 帮你配
12
+
13
+ 这个文件就是给 AI 看的说明书:命令、配置项、期望输出、报错原文都写在里面。把下面这段发给你的 AI(Claude Code、Cursor、ChatGPT 都行),它会照着做:
14
+
15
+ ```text
16
+ 帮我配置 hulala-bot(一个把 Telegram / 飞书接到本机 DeepSeek Harness 的插件)。
17
+ 请先完整读 https://github.com/colinagent/hulala-bot/blob/main/README.zh-CN.md 的说明,然后按顺序执行:
18
+
19
+ 1. 检查环境:node -v(>=22.6)、pnpm -v、hulala --version
20
+ 2. 按「安装」一节装插件(呼啦啦用 hulala install bot;没有这条命令就用 hulala plugin)
21
+ 3. 按「配置」一节写 config.json;凭据只进系统钥匙串,不要让我把 token 贴进聊天,也不要写进 config.json
22
+ 4. 重启 Host
23
+ 5. 按「验证清单」逐条确认,每一步把实际输出贴给我
24
+ 6. 任何一步失败,按「故障排查」的原文匹配原因,不要猜
25
+
26
+ 规则:不要发明 README 之外的命令、配置项、权限或事件;缺信息就停下来问我。
27
+ ```
28
+
29
+ 给 AI 的三条硬规则,README 里也有对应依据:
30
+
31
+ 1. **凭据只进钥匙串**,配置文件里只放 `tokenRef` / `appSecretRef`(`<service>/<account>`)。
32
+ 2. **允许名单不能为空**:空名单会被拒绝启动,这不是可以跳过的步骤。
33
+ 3. **改完配置必须重启 Host**:插件在启动时读取配置,没有热加载。
34
+
35
+ ---
36
+
37
+ ## 它是什么
38
+
39
+ **一个 Host 插件**,不 patch DSH 一行代码。它跑在 Host 进程里,直接调宿主服务:
40
+
41
+ ```
42
+ ctx.sessionController.list / create / prompt / follow → 会话与流式
43
+ ctx.workspaceController.create → 工作目录
44
+ ctx.on('approval/request', (req, next) => …) → 审批
45
+ ```
46
+
47
+ 没有端口、token、cookie、WebSocket(飞书那条长连接是飞书自己的事件通道,不是本机服务)。会话历史、cwd、模型、权限以 Host 为真源;这里只保存「哪个对话挂了哪个会话」。
48
+
49
+ **一份代码,两个渠道**:`src/channel.ts` 是渠道契约,`src/telegram/`、`src/lark/` 各自实现,`src/bridge.ts` 的会话逻辑两边共用。两个渠道可以同时开,各自有允许名单和挂载表;Telegram 的对话和飞书的对话是两个独立会话,不合并身份。
50
+
51
+ ---
52
+
53
+ ## 安装
54
+
55
+ 先检查环境:
56
+
57
+ ```sh
58
+ node -v # 需要 >= 22.6
59
+ pnpm -v # 安装走 pnpm;没有就先装
60
+ hulala --version
61
+ ```
62
+
63
+ ### 呼啦啦
64
+
65
+ ```sh
66
+ hulala install bot
67
+ # 老版本没有这条命令时,等价写法:
68
+ hulala plugin --profile tui add @colinagent/hulala-bot
69
+ ```
70
+
71
+ `hulala install bot` 只是把 profile 名(`tui`)和包名填好的快捷方式,安装仍由 DSH 的 `runPluginCommand` 完成。
72
+
73
+ ### DSH(Web / Desktop / TUI)
74
+
75
+ ```sh
76
+ dsh plugin --profile tui add @colinagent/hulala-bot
77
+ ```
78
+
79
+ **期望输出**:最后一行出现 `profile tui: @deepseek-ai/dsh-base, @colinagent/hulala-bot` 和 `Restart the Host for the change to take effect.`
80
+
81
+ 装完核对:
82
+
83
+ ```sh
84
+ hulala plugin --profile tui list # 依赖里有 @colinagent/hulala-bot
85
+ ```
86
+
87
+ 卸载:`hulala plugin --profile tui remove @colinagent/hulala-bot`,然后重启 Host。
88
+
89
+ ---
90
+
91
+ ## 配置
92
+
93
+ 配置写在 `$DSH_HOME/config/hulala-bot/config.json`:
94
+
95
+ | Host | 实际路径 |
96
+ |---|---|
97
+ | 呼啦啦 | `~/.hulala/harness/config/hulala-bot/config.json` |
98
+ | DSH | `~/.dsh/config/hulala-bot/config.json` |
99
+
100
+ 用 `--init` 写,不要手写整个文件(它会校验、合并、设置 `0600` 权限):
101
+
102
+ ```sh
103
+ # 呼啦啦
104
+ hulala plugin --profile tui exec hulala-bot --init
105
+
106
+ # DSH
107
+ dsh plugin --profile tui exec hulala-bot --init
108
+ ```
109
+
110
+ 有终端时它会逐项提问;没有终端(或加 `--no-input`)时用参数。**两条渠道可以只配一条**,也可以都配。
111
+
112
+ ### Telegram
113
+
114
+ 先准备:跟 [@BotFather](https://t.me/BotFather) 说 `/newbot` 拿 token;自己的 user id 可以问 [@userinfobot](https://t.me/userinfobot)。
115
+
116
+ token 放系统钥匙串(**不要**写进 config.json):
117
+
118
+ ```bash
119
+ # macOS
120
+ security add-generic-password -U -s telegram -a bot-token -w '123456:ABC…'
121
+ # Linux
122
+ secret-tool store --label 'telegram bot' service telegram account bot-token
123
+ ```
124
+
125
+ 写配置:
126
+
127
+ ```sh
128
+ hulala plugin --profile tui exec hulala-bot --init --no-input \
129
+ --telegram-token-ref telegram/bot-token \
130
+ --telegram-allowed-user 123456789 \
131
+ --default-cwd ~/hulala-workspace/workspace
132
+ ```
133
+
134
+ 国内网络需要代理时加 `--proxy http://127.0.0.1:7897`(只支持 HTTP/HTTPS,SOCKS5 会在启动前报错)。
135
+
136
+ 也可以临时用环境变量代替钥匙串:`TELEGRAM_BOT_TOKEN=…`,或配置里的 `tokenEnv` 指定变量名。
137
+
138
+ ### 飞书 / Lark
139
+
140
+ 配置由你在开发者后台完成,插件这边只填 app id 和允许名单。
141
+
142
+ 1. 在 [飞书开放平台](https://open.feishu.cn)(国际版 [Lark](https://open.larksuite.com))建一个**企业自建应用**,拿到 App ID(`cli_` + 16 位十六进制)和 App Secret。
143
+ 2. 开启**机器人能力**。
144
+ 3. 权限至少:`im:message`、`im:message:send_as_bot`。后台会按实际接口提示还缺哪一项。
145
+ 4. **事件与回调 → 订阅方式:使用长连接接收事件**。订阅 `im.message.receive_v1`,并添加卡片回调 `card.action.trigger`。**不要**配 webhook 地址。
146
+ 5. 发布版本,把可用范围设成你自己(或团队),然后在飞书里单聊机器人或把它拉进群(群里必须 @ 它)。
147
+
148
+ App secret 放钥匙串:
149
+
150
+ ```bash
151
+ security add-generic-password -U -s lark -a app-secret -w '…'
152
+ ```
153
+
154
+ 写配置:
155
+
156
+ ```sh
157
+ hulala plugin --profile tui exec hulala-bot --init --no-input \
158
+ --lark-app-id cli_0123456789abcdef \
159
+ --lark-app-secret-ref lark/app-secret \
160
+ --lark-allowed-user ou_0123456789abcdef0123456789abcdef
161
+ ```
162
+
163
+ 国际版加 `--lark-domain lark`(Open API 指向 `open.larksuite.com`)。
164
+
165
+ **不知道自己的 open id**:先随便给机器人发一条消息,然后看日志里的
166
+
167
+ ```
168
+ ignored chat=oc_… from=ou_… (not in allowlist)
169
+ ```
170
+
171
+ 把 `ou_…` 填进允许名单(重跑 `--init --lark-allowed-user ou_…`),重启 Host。
172
+
173
+ ### `--init` 参数
174
+
175
+ | 参数 | 说明 |
176
+ |---|---|
177
+ | `--telegram-token-ref <service/account>` | 钥匙串引用,默认示例 `telegram/bot-token` |
178
+ | `--telegram-token-env <NAME>` | 改从环境变量读 token |
179
+ | `--telegram-allowed-user <id[,id]>` | 允许的 Telegram user id(可重复) |
180
+ | `--telegram-allowed-chat <id[,id]>` | 允许的 chat id |
181
+ | `--proxy <http://…>` | Telegram 走的 HTTP/HTTPS 代理 |
182
+ | `--parse-mode markdown\|plain` | Telegram 回复格式,`markdown` 失败会自动退回纯文本 |
183
+ | `--lark-app-id <cli_…>` | 飞书应用 App ID |
184
+ | `--lark-app-secret-ref <service/account>` | app secret 的钥匙串引用 |
185
+ | `--lark-app-secret-env <NAME>` | 改从环境变量读 app secret |
186
+ | `--lark-allowed-user <ou_…[,ou_…]>` | 允许的 open id |
187
+ | `--lark-allowed-chat <oc_…[,oc_…]>` | 允许的 chat id |
188
+ | `--lark-domain feishu\|lark` | 国内飞书(默认)或国际版 |
189
+ | `--default-cwd <path>` | 第一次说话时在哪个目录建会话 |
190
+ | `--no-input` | 不提问;缺参数直接报错 |
191
+
192
+ `--init` **按渠道合并**:只改它拿到的那条渠道,另一条和已有的允许名单/未知字段原样保留。所以补一个代理、加一个用户都不用重写整个文件。
193
+
194
+ ### 文件结构
195
+
196
+ ```json
197
+ {
198
+ "defaultCwd": "~/hulala-workspace/workspace",
199
+ "streamIntervalMs": 800,
200
+ "telegram": {
201
+ "tokenRef": "telegram/bot-token",
202
+ "allowedUsers": [123456789],
203
+ "parseMode": "markdown",
204
+ "proxy": "http://127.0.0.1:7897"
205
+ },
206
+ "lark": {
207
+ "appId": "cli_0123456789abcdef",
208
+ "appSecretRef": "lark/app-secret",
209
+ "domain": "feishu",
210
+ "allowedUsers": ["ou_0123456789abcdef0123456789abcdef"]
211
+ }
212
+ }
213
+ ```
214
+
215
+ 顶层:
216
+
217
+ | 字段 | 说明 |
218
+ |---|---|
219
+ | `defaultCwd` | 第一次说话时在哪个目录建会话 |
220
+ | `streamIntervalMs` | 两次流式更新的最小间隔,默认 800ms(飞书卡片有频率限制,别调太低) |
221
+
222
+ `telegram`:
223
+
224
+ | 字段 | 说明 |
225
+ |---|---|
226
+ | `tokenRef` / `tokenEnv` | 钥匙串引用或环境变量名 |
227
+ | `allowedUsers` | Telegram user id;与 `allowedChats` 都为空时拒绝启动 |
228
+ | `allowedChats` | chat id(群/频道场景) |
229
+ | `parseMode` | `markdown`(默认,失败自动回退)或 `plain` |
230
+ | `proxy` | HTTP/HTTPS CONNECT 代理;不读 `HTTPS_PROXY`,只认这一项 |
231
+ | `baseUrl` | 只给测试用 |
232
+ | `pollTimeoutSeconds` | 长轮询等待秒数,默认 25 |
233
+
234
+ `lark`:
235
+
236
+ | 字段 | 说明 |
237
+ |---|---|
238
+ | `appId` | `cli_` + 16 位十六进制;格式不对在连网前就报错 |
239
+ | `appSecretRef` / `appSecretEnv` | 钥匙串引用或环境变量名 |
240
+ | `domain` | `feishu`(默认)或 `lark` |
241
+ | `allowedUsers` | open id(`ou_…`);与 `allowedChats` 都为空时拒绝启动 |
242
+ | `allowedChats` | chat id(`oc_…`) |
243
+ | `baseUrl` | 只给测试用 |
244
+
245
+ 环境变量:`HULALA_BOT_CONFIG`(配置文件路径)、`HULALA_BOT_STATE`(状态目录)、`HULALA_BOT_LOCK_DIR`(锁目录)、`TELEGRAM_BOT_TOKEN`、`LARK_APP_SECRET`。
246
+
247
+ ---
248
+
249
+ ## 重启与验证
250
+
251
+ 配置和插件都只在 Host 启动时读取,**改完必须重启**:
252
+
253
+ ```sh
254
+ hulala host stop
255
+ hulala host start
256
+ # DSH:重启你自己的 dsh 进程
257
+ ```
258
+
259
+ ### 验证清单
260
+
261
+ ```sh
262
+ # 1. 插件在 profile 里
263
+ hulala plugin --profile tui list
264
+ # 期望:@deepseek-ai/dsh-base, @colinagent/hulala-bot
265
+
266
+ # 2. Host 起来了
267
+ hulala host status
268
+ # 期望:"state": "running"
269
+
270
+ # 3. 插件真的加载了
271
+ grep "bot starting inside the Host" ~/.hulala/logs/host.log | tail -1
272
+ # 期望:bot starting inside the Host; channels telegram
273
+ # (配了飞书就是 channels telegram, lark)
274
+
275
+ # 4. 连接锁只有一个持有者
276
+ ls ~/.hulala-bot/*.lock
277
+ # 期望:每条渠道一个 .lock;内容是本机 Host 的 pid
278
+
279
+ # 5. 给机器人发一句话
280
+ # 期望:先出现占位消息,然后被流式替换成回答;TUI 里能看到同一个会话
281
+ ```
282
+
283
+ 飞书额外确认:
284
+
285
+ ```sh
286
+ # 日志里有没有收到事件(首次配置时会有 ignored … not in allowlist)
287
+ tail -f ~/.hulala/harness/config/hulala-bot/logs/bridge-$(date +%F).log
288
+ ```
289
+
290
+ 日志位置:`~/.hulala/harness/config/hulala-bot/logs/`(DSH 在 `~/.dsh/config/hulala-bot/logs/`)。
291
+
292
+ ---
293
+
294
+ ## 故障排查
295
+
296
+ 先看这两个地方,90% 的问题能直接定位:
297
+
298
+ ```sh
299
+ tail -50 ~/.hulala/harness/config/hulala-bot/logs/bridge-$(date +%F).log
300
+ grep -i "hulala-bot" ~/.hulala/logs/host.log | tail -20
301
+ ```
302
+
303
+ | 症状 / 报错原文 | 原因 | 处理 |
304
+ |---|---|---|
305
+ | `no channel configured: add a "telegram" or "lark" section` | 配置文件不存在或两段都没有 | 跑 `--init`,或检查 `HULALA_BOT_CONFIG` 指向的路径 |
306
+ | `refusing to start Telegram with an empty allowlist` | 允许名单为空(拒绝是设计行为) | 加 `--telegram-allowed-user` |
307
+ | `no Telegram bot token: set tokenRef …` | 钥匙串里没有条目,或引用名写错 | `security find-generic-password -s telegram -a bot-token -w` 确认;名字要和 `tokenRef` 一致 |
308
+ | `lark.appId must look like cli_<16 hex>` | App ID 复制错/不是自建应用 | 回开发者后台复制 `cli_` + 16 位十六进制 |
309
+ | `no Lark app secret: set lark.appSecretRef …` | 同上,secret 没进钥匙串 | 存 `lark/app-secret`,或设 `LARK_APP_SECRET` |
310
+ | `this bot is already connected from another Host (pid …)` | 另一个 Host(DSH 或旧进程)正在用同一 token/app id | 停掉那个进程;确认它已退出后删掉报错里给出的 `.lock` |
311
+ | Telegram 报 `getUpdates: Conflict: … (409)` | 两个进程在轮询同一个 bot token | 同上;另外检查有没有旧的独立桥进程 |
312
+ | `proxy CONNECT failed: 403` | 代理拒绝 CONNECT | 检查代理是否允许该目标/是否要认证(`http://user:pass@host:port`) |
313
+ | `connect ECONNREFUSED 127.0.0.1:7897` | 代理端口没开 | 启动 Clash/Surge,或改 `proxy` |
314
+ | 飞书机器人完全不回话 | ① 订阅方式不是「长连接」② 没订阅 `im.message.receive_v1` ③ 没开机器人能力 ④ 可用范围不含你 ⑤ App Secret 错 | 按这五条逐项查;secret 错会在日志里出现 `99991663` 或 `tenant_access_token` 相关错误 |
315
+ | 飞书私聊正常、群里不回 | 群里必须 @ 机器人 | @ 它再发;命令前可以带 @ |
316
+ | 飞书审批按钮没反应 | 没添加 `card.action.trigger` 回调 | 后台补上这个回调,重启 Host |
317
+ | 流式很慢 / 频繁限流 | `streamIntervalMs` 低于平台限制 | 调到 800 以上(飞书卡片更新有频率限制) |
318
+ | 回复出现 `☑️ 已处理` | 这个审批已经在另一端处理过 | 正常,不是错误 |
319
+ | 日志里 `ignored … (not in allowlist)` | 发送者不在名单里 | 用日志里的 id 补进允许名单并重启 |
320
+ | 重启后行为没变 | 插件只在启动时读配置 | 确认进程真的重启了(`hulala host status` 的 pid 变了) |
321
+
322
+ 调试用的日志行会带渠道名前缀:`telegram`、`lark`。凭据在写日志前会被替换成 `<secret>`,不会出现在文件里。
323
+
324
+ ---
325
+
326
+ ## 命令
327
+
328
+ 两个渠道命令一样:
329
+
330
+ | 命令 | 行为 |
331
+ |---|---|
332
+ | `/cwd` | 列出最近用过的目录,点一个就挂上去 |
333
+ | `/cwd <path>` | 换挂载:该目录下最近的会话,没有就新建 |
334
+ | `/new` | 在当前目录新建会话 |
335
+ | `/sessions` | 最近 10 个会话,`*` 表示当前 |
336
+ | `/switch <n>` | 切到第 n 个 |
337
+ | `/status` | 会话、目录、运行状态 |
338
+ | `/stop` | 停止当前回合 |
339
+ | `/help` | 上面这些 |
340
+
341
+ 直接发文字就是提问。第一次说话会自动在 `defaultCwd` 里建会话并挂上。
342
+
343
+ 审批会以按钮出现(Telegram inline keyboard / 飞书卡片按钮),只有「允许这一次 / 拒绝」。请求里没有命令行文本,只有工具名与理由。
344
+
345
+ ---
346
+
347
+ ## 两个渠道的差异
348
+
349
+ | | Telegram | 飞书 / Lark |
350
+ |---|---|---|
351
+ | 传输 | Bot API 长轮询 | 长连接(WebSocket) |
352
+ | 流式 | 反复编辑同一条消息 | 反复 patch 同一张卡片(飞书不能编辑文本消息) |
353
+ | 按钮 | inline keyboard | 卡片 `action` + `card.action.trigger` |
354
+ | 置顶状态栏 | 有(`📍 <目录>`) | 无;用 `/status` 看 |
355
+ | "正在输入" | 有 | 飞书没有这个 API |
356
+ | 单条上限 | 4096(内部按 4000 切) | 4000 字符一张卡片,超了续卡片 |
357
+ | 群聊 | 直接收 | 必须 @ 机器人 |
358
+
359
+ 两个渠道可以同时开:各自挂各自的会话,审批同时出现在两边,先点的生效,另一边显示「已在其他端处理」。
360
+
361
+ ---
362
+
363
+ ## 状态与安全
364
+
365
+ - **状态目录**:`$DSH_HOME/config/hulala-bot/`,里面有 `config.json`、`bindings.json`(挂载表)、`receipts/`(去重账本,24h TTL)、`logs/`,权限 `0600`/`0700`。
366
+ - **连接锁**:`~/.hulala-bot/<凭据摘要>.lock`,不在状态目录里,所以 DSH 和呼啦啦同时装插件时也只有一个在连。
367
+ - **凭据**:只进钥匙串(或环境变量),日志写之前替换成 `<secret>`。
368
+ - **边界**:白名单是唯一口令;消息正文、工具输出、贴出的路径都会经过 Telegram / 飞书的服务器。插件不给 Host 增加权限——它能做的事,TUI 也能做;权限仍由 Host 的 preset 决定。
369
+ - **跨端取消**:插件模式下,另一端已经处理过的审批不会从这边被远程收走,只会在下次交互时显示「已在其他端处理」。
370
+
371
+ ---
372
+
373
+ ## 开发
374
+
375
+ ```bash
376
+ node --test "tests/*.test.ts" # 106 个用例
377
+ bun run typecheck
378
+ bun run build # dist/index.mjs + dist/plugin.mjs
379
+ node scripts/spike-host.ts <dsh-bin> <dsh-home> <profile> # 对真 Host 跑一轮
380
+ ```
381
+
382
+ 需要 Node ≥ 22.6。飞书的 REST 用注入的 `fetch` 测,长连接用假 transport 测,测试不需要租户、不需要网络。
383
+
384
+ 版本约束:`engines.dsh` 声明 DSH 兼容范围(当前 `>=0.2.2-rc.1 <0.3.0`),装载器在 import 之前校验,不满足就跳过并写进 `skippedBundles`;`@deepseek-ai/dsh-*` 必须是 peerDependencies,避免出现第二份注册表。
385
+
386
+ 更细的架构与坑:见仓库里的 `AGENTS.md`。
387
+
388
+ ## 许可
389
+
390
+ GPL-3.0-only。它链接的是 GPL 的 DeepSeek Harness / Hulala,不是专有代码。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colinagent/hulala-bot",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "GPL-3.0-only",
5
5
  "type": "module",
6
6
  "engines": {
@@ -52,8 +52,9 @@
52
52
  "dist/index.mjs",
53
53
  "dist/plugin.mjs",
54
54
  "cordis.patch.yml",
55
- "package.json",
56
55
  "README.md",
56
+ "README.zh-CN.md",
57
+ "package.json",
57
58
  "LICENSE"
58
59
  ],
59
60
  "exports": {