botmux 2.26.0 → 2.27.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 +48 -46
- package/README.md +47 -46
- package/dist/cli.js +259 -40
- package/dist/cli.js.map +1 -1
- package/dist/setup/bots-store.d.ts +3 -0
- package/dist/setup/bots-store.d.ts.map +1 -0
- package/dist/setup/bots-store.js +24 -0
- package/dist/setup/bots-store.js.map +1 -0
- package/dist/setup/lark-scopes.json +301 -0
- package/dist/setup/register-app.d.ts +46 -0
- package/dist/setup/register-app.d.ts.map +1 -0
- package/dist/setup/register-app.js +87 -0
- package/dist/setup/register-app.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +115 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -0
- package/dist/setup/verify-permissions.js +207 -0
- package/dist/setup/verify-permissions.js.map +1 -0
- package/package.json +5 -3
package/README.en.md
CHANGED
|
@@ -71,78 +71,71 @@ Compared to OpenClaw-style approaches built on Agent SDKs:
|
|
|
71
71
|
|
|
72
72
|
## 5-Minute Setup
|
|
73
73
|
|
|
74
|
+
> 💡 **TL;DR**: run `botmux setup` and pick "scan-to-create" to finish Steps 1+2 in one shot (the official `@larksuiteoapi/node-sdk` device flow gives you the AppID/AppSecret). PersonalAgent apps come with event subscriptions and bot capability pre-configured, so only Step 4 (permissions) + Step 5 (optional redirect URL) + Step 6 (publish) require browser clicks; the setup wizard writes a JSON file with a one-line clipboard copy command and prints deep-links to each remaining step.
|
|
75
|
+
|
|
74
76
|
### Step 1: Create a Lark App
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
**Recommended**: `botmux setup` → pick "1) Scan-to-create app". Scan with the Lark mobile app and the AppID/AppSecret are persisted automatically; no manual browser navigation. Falls back to manual paste on cancel/timeout/network error.
|
|
79
|
+
|
|
80
|
+
> ⚠️ **Currently only Feishu (feishu.cn) tenants are supported.** If scan detects a Lark international (larksuite.com) tenant, setup aborts — the daemon runtime (Lark Client/WSClient/event-dispatcher) hasn't been wired up for the `larksuite.com` domain yet, so accepting Lark credentials would land users in a half-working state. A follow-up PR will add full Lark support.
|
|
81
|
+
|
|
82
|
+
**Manual**: go to the [Lark Open Platform](https://open.larkoffice.com/app) and click "Create Custom App".
|
|
77
83
|
|
|
78
84
|

|
|
79
85
|
|
|
80
86
|
### Step 2: Get Credentials
|
|
81
87
|
|
|
88
|
+
> The scan-to-create path completes this step automatically; skip to Step 3.
|
|
89
|
+
|
|
82
90
|
Open the app details page → "Credentials & Basic Info", and copy the **App ID** and **App Secret**.
|
|
83
91
|
|
|
84
92
|

|
|
85
93
|
|
|
86
|
-
### Step 3:
|
|
87
|
-
|
|
88
|
-
Go to "Permissions & Scopes" → "Batch Import/Export", and paste the following JSON to import all permissions at once:
|
|
89
|
-
|
|
90
|
-

|
|
91
|
-
|
|
92
|
-
<details>
|
|
93
|
-
<summary>Click to expand batch import JSON</summary>
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"scopes": {
|
|
98
|
-
"tenant": [
|
|
99
|
-
"contact:user.base:readonly",
|
|
100
|
-
"contact:user.id:readonly",
|
|
101
|
-
"im:chat:read",
|
|
102
|
-
"im:chat.members:bot_access",
|
|
103
|
-
"im:chat.members:read",
|
|
104
|
-
"im:message",
|
|
105
|
-
"im:message:readonly",
|
|
106
|
-
"im:message:send_as_bot",
|
|
107
|
-
"im:message:update",
|
|
108
|
-
"im:message.group_at_msg",
|
|
109
|
-
"im:message.group_at_msg:readonly",
|
|
110
|
-
"im:message.group_msg",
|
|
111
|
-
"im:message.p2p_msg:readonly",
|
|
112
|
-
"im:message.reactions:write_only",
|
|
113
|
-
"im:resource"
|
|
114
|
-
]
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
</details>
|
|
119
|
-
|
|
120
|
-
### Step 4: Install & Start botmux
|
|
94
|
+
### Step 3: Install & Start botmux
|
|
121
95
|
|
|
122
96
|
```bash
|
|
123
97
|
# Install
|
|
124
98
|
npm install -g botmux
|
|
125
99
|
|
|
126
|
-
# Interactive setup —
|
|
100
|
+
# Interactive setup — pick "1) Scan-to-create app" or "2) Paste AppID/Secret manually".
|
|
101
|
+
# Credentials are validated with a tenant_access_token call before bots.json is written.
|
|
102
|
+
# At the end of setup the wizard writes the full scope JSON to ~/.botmux/lark-scopes.json
|
|
103
|
+
# and prints a one-line clipboard copy command for your platform.
|
|
127
104
|
botmux setup
|
|
128
105
|
|
|
129
|
-
# Start (
|
|
106
|
+
# Start (if you ever need to verify the event subscription, Lark requires the daemon to be running so it can detect the WebSocket connection)
|
|
107
|
+
# Re-validates credentials before forking workers; missing scopes only WARN, do not block the daemon.
|
|
130
108
|
botmux start
|
|
131
109
|
```
|
|
132
110
|
|
|
133
|
-
### Step
|
|
111
|
+
### Step 4: Add Permissions
|
|
134
112
|
|
|
135
|
-
|
|
113
|
+
Run the copy-to-clipboard command setup printed, then go to "Permissions & Scopes" → "Batch Import/Export" and paste. Submit for review — visibility "only me" auto-approves.
|
|
136
114
|
|
|
137
|
-
|
|
115
|
+

|
|
138
116
|
|
|
139
|
-
|
|
117
|
+
The full JSON lives at `~/.botmux/lark-scopes.json` (also tracked in-repo at [src/setup/lark-scopes.json](src/setup/lark-scopes.json), kept in sync with the internal wiki, covers ~290 tenant + user scopes).
|
|
140
118
|
|
|
141
|
-
|
|
119
|
+
```bash
|
|
120
|
+
# macOS
|
|
121
|
+
cat ~/.botmux/lark-scopes.json | pbcopy
|
|
122
|
+
# Linux X
|
|
123
|
+
cat ~/.botmux/lark-scopes.json | xclip -selection clipboard
|
|
124
|
+
# Wayland
|
|
125
|
+
cat ~/.botmux/lark-scopes.json | wl-copy
|
|
126
|
+
```
|
|
142
127
|
|
|
143
|
-
|
|
128
|
+
> Scan-created PersonalAgent apps have `im.message.receive_v1` + `card.action.trigger` subscribed and the bot capability enabled out of the box, per botmux maintainer testing. Lark hasn't documented this as stable behavior, so **if the bot receives no messages at all after setup**, see "Step 8: Troubleshoot — bot not receiving messages" below for a manual fallback.
|
|
144
129
|
|
|
145
|
-
|
|
130
|
+
### Step 5: Add Redirect URL (optional)
|
|
131
|
+
|
|
132
|
+
If you plan to use `/login` inside Lark to let botmux act on your behalf for docs / calendar / wiki / sheets, add a redirect URL under "Security Settings" → "Redirect URL":
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
http://127.0.0.1:9768/callback
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Skip this step if you only need bot messaging.
|
|
146
139
|
|
|
147
140
|
### Step 6: Publish the App
|
|
148
141
|
|
|
@@ -158,7 +151,16 @@ Go to "Version Management & Release", click "Create Version" and publish. Set av
|
|
|
158
151
|
|
|
159
152
|

|
|
160
153
|
|
|
161
|
-
### Step 8:
|
|
154
|
+
### Step 8: Troubleshoot — bot not receiving messages (fallback)
|
|
155
|
+
|
|
156
|
+
PersonalAgent apps come with event subscriptions and bot capability pre-configured; in normal cases you don't touch this. If the bot **receives no messages at all** after setup (not even DMs), verify these two settings:
|
|
157
|
+
|
|
158
|
+
- **Event subscription**: Open Platform → your app → Events & Callbacks → should be subscribed to `im.message.receive_v1` + `card.action.trigger`. If missing, add them manually. Subscription mode must be "Receive via persistent connection" (WebSocket), and the botmux daemon must be running.
|
|
159
|
+
- **Bot capability**: Open Platform → your app → Features → Bot should be enabled (it is by default). Adjust name/avatar if needed.
|
|
160
|
+
|
|
161
|
+
After verifying, restart: `botmux restart`.
|
|
162
|
+
|
|
163
|
+
### Step 9: Enable Boot-time Autostart (recommended)
|
|
162
164
|
|
|
163
165
|
Once the bot is sending and receiving messages cleanly, run:
|
|
164
166
|
|
package/README.md
CHANGED
|
@@ -173,78 +173,70 @@ CLI 进入 botmux 会话时自动获得 `~/.botmux/bin` 在 PATH 中,以及一
|
|
|
173
173
|
|
|
174
174
|
## 5 分钟快速接入
|
|
175
175
|
|
|
176
|
+
> 💡 **TL;DR**:跑 `botmux setup` 选「扫码建应用」一步完成 Step 1+2(拿 AppID/AppSecret)。PersonalAgent 应用建出来时事件订阅和 bot 能力都已默认配好,只剩 Step 4 权限申请 + Step 5(按需)重定向 URL + Step 6 发版三步要在浏览器手动点;setup 完成后会自动写 JSON 文件 + 打印一键复制命令 + 各步骤的深链。
|
|
177
|
+
|
|
176
178
|
### Step 1: 创建飞书应用
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
**推荐路径**:`botmux setup` 选「1) 扫码建应用」,飞书扫码完成后自动落盘 AppID/AppSecret,无需手动浏览器创建。底层走 `@larksuiteoapi/node-sdk` 的官方 device flow。
|
|
181
|
+
|
|
182
|
+
> ⚠️ **目前仅支持飞书 (feishu.cn) 租户**。扫码检测到 Lark 国际版 (larksuite.com) 会中止 setup —— daemon runtime (Lark Client/WSClient/event-dispatcher 等) 需要一并接入 lark 域,会在单独 PR 跟进。
|
|
183
|
+
|
|
184
|
+
**手动路径**:打开 [飞书开放平台](https://open.larkoffice.com/app),点击「创建企业自建应用」。
|
|
179
185
|
|
|
180
186
|

|
|
181
187
|
|
|
182
188
|
### Step 2: 获取凭证
|
|
183
189
|
|
|
190
|
+
> 扫码路径自动完成此步,可直接跳到 Step 3。
|
|
191
|
+
|
|
184
192
|
进入应用详情 →「凭证与基础信息」,复制 **App ID** 和 **App Secret**。
|
|
185
193
|
|
|
186
194
|

|
|
187
195
|
|
|
188
|
-
### Step 3:
|
|
189
|
-
|
|
190
|
-
进入「权限管理」→「批量导入/导出权限」,粘贴以下 JSON 一次性导入所有权限:
|
|
191
|
-
|
|
192
|
-

|
|
193
|
-
|
|
194
|
-
<details>
|
|
195
|
-
<summary>点击展开批量导入 JSON</summary>
|
|
196
|
-
|
|
197
|
-
```json
|
|
198
|
-
{
|
|
199
|
-
"scopes": {
|
|
200
|
-
"tenant": [
|
|
201
|
-
"contact:user.base:readonly",
|
|
202
|
-
"contact:user.id:readonly",
|
|
203
|
-
"im:chat:read",
|
|
204
|
-
"im:chat.members:bot_access",
|
|
205
|
-
"im:chat.members:read",
|
|
206
|
-
"im:message",
|
|
207
|
-
"im:message:readonly",
|
|
208
|
-
"im:message:send_as_bot",
|
|
209
|
-
"im:message:update",
|
|
210
|
-
"im:message.group_at_msg",
|
|
211
|
-
"im:message.group_at_msg:readonly",
|
|
212
|
-
"im:message.group_msg",
|
|
213
|
-
"im:message.p2p_msg:readonly",
|
|
214
|
-
"im:message.reactions:write_only",
|
|
215
|
-
"im:resource"
|
|
216
|
-
]
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
</details>
|
|
221
|
-
|
|
222
|
-
### Step 4: 安装 & 启动 botmux
|
|
196
|
+
### Step 3: 安装 & 启动 botmux
|
|
223
197
|
|
|
224
198
|
```bash
|
|
225
199
|
# 安装
|
|
226
200
|
npm install -g botmux
|
|
227
201
|
|
|
228
|
-
# 交互式配置 —
|
|
202
|
+
# 交互式配置 — 选「1) 扫码建应用」或「2) 手动粘 AppID/Secret」
|
|
203
|
+
# 凭证拿到后自动取一次 tenant_access_token 校验,通过才落盘 bots.json
|
|
204
|
+
# setup 末尾会把完整权限 JSON 写到 ~/.botmux/lark-scopes.json 并打印一键复制命令
|
|
229
205
|
botmux setup
|
|
230
206
|
|
|
231
|
-
#
|
|
207
|
+
# 启动(如果之后需要确认事件订阅,飞书后台会要求 daemon 已在跑才能识别长连接)
|
|
208
|
+
# start 前再校验一次凭证;权限未配齐不会阻塞 daemon,只 WARN
|
|
232
209
|
botmux start
|
|
233
210
|
```
|
|
234
211
|
|
|
235
|
-
### Step
|
|
212
|
+
### Step 4: 添加权限
|
|
236
213
|
|
|
237
|
-
|
|
214
|
+
setup 完成后,按 terminal 提示的一键复制命令把权限 JSON 复制到剪贴板,进入「权限管理」→「批量导入/导出权限」粘贴 → 提交审批。可用性范围选「仅自己可见」会自动通过:
|
|
238
215
|
|
|
239
|
-
|
|
216
|
+

|
|
240
217
|
|
|
241
|
-
|
|
218
|
+
完整 JSON 已经写到 `~/.botmux/lark-scopes.json`,源仓库版本在 [src/setup/lark-scopes.json](src/setup/lark-scopes.json)(与本仓库内部 wiki 文档同步,覆盖 tenant + user 双套域 ≈ 290 项)。
|
|
242
219
|
|
|
243
|
-
|
|
220
|
+
```bash
|
|
221
|
+
# macOS
|
|
222
|
+
cat ~/.botmux/lark-scopes.json | pbcopy
|
|
223
|
+
# Linux X
|
|
224
|
+
cat ~/.botmux/lark-scopes.json | xclip -selection clipboard
|
|
225
|
+
# Wayland
|
|
226
|
+
cat ~/.botmux/lark-scopes.json | wl-copy
|
|
227
|
+
```
|
|
244
228
|
|
|
245
|
-
|
|
229
|
+
> 扫码建出来的 PersonalAgent 应用,botmux 维护者实测默认已订阅 `im.message.receive_v1` + `card.action.trigger` 并开通 bot 能力,所以主线流程不再要求手动配。但飞书没在公开文档里承诺这是稳定行为,**如果配好后机器人完全收不到消息**,参见下方「Step 8: 机器人收不到消息时的自查」。
|
|
246
230
|
|
|
247
|
-
|
|
231
|
+
### Step 5: 添加重定向 URL(按需)
|
|
232
|
+
|
|
233
|
+
如果之后要在飞书里 `/login` 让 botmux 以你的身份调云文档/日历/Wiki 等 API,进入「安全设置」→「重定向 URL」填入:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
http://127.0.0.1:9768/callback
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
只用 bot 收发消息的话这一步可以跳过。
|
|
248
240
|
|
|
249
241
|
### Step 6: 发版
|
|
250
242
|
|
|
@@ -260,7 +252,16 @@ botmux start
|
|
|
260
252
|
|
|
261
253
|

|
|
262
254
|
|
|
263
|
-
### Step 8:
|
|
255
|
+
### Step 8: 机器人收不到消息时的自查(fallback)
|
|
256
|
+
|
|
257
|
+
PersonalAgent 默认配好事件订阅 + bot 能力,正常情况下不用动。如果按上面步骤走完 bot **完全收不到任何消息**(连私聊都不回),分别确认这两项:
|
|
258
|
+
|
|
259
|
+
- **事件订阅**:开放平台 → 你的应用 → 事件与回调 → 应当订阅 `im.message.receive_v1` + `card.action.trigger`(默认已订阅,如缺失就手动添加)。订阅方式必须是「使用长连接接收事件」(WebSocket),且 botmux daemon 已经在跑。
|
|
260
|
+
- **机器人能力**:开放平台 → 你的应用 → 应用功能 → 机器人 应当已开通(默认开通),名字/头像可以改。
|
|
261
|
+
|
|
262
|
+
确认后重启 daemon:`botmux restart`。
|
|
263
|
+
|
|
264
|
+
### Step 9: 开机自启(推荐)
|
|
264
265
|
|
|
265
266
|
确认机器人能正常收发消息之后,跑一次:
|
|
266
267
|
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd)
|
|
18
18
|
*/
|
|
19
19
|
import { execSync, spawnSync, spawn } from 'node:child_process';
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
|
|
20
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -26,6 +26,7 @@ import { createRequire } from 'node:module';
|
|
|
26
26
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
27
27
|
import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart } from './autostart.js';
|
|
28
28
|
import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
29
|
+
import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
|
|
29
30
|
import { logger } from './utils/logger.js';
|
|
30
31
|
import { firstPositional } from './cli/arg-utils.js';
|
|
31
32
|
// CLI subcommands (send/thread/bots/list/etc) print JSON to stdout for
|
|
@@ -144,28 +145,189 @@ function ask(rl, question) {
|
|
|
144
145
|
return new Promise(resolve => rl.question(question, resolve));
|
|
145
146
|
}
|
|
146
147
|
// ─── Setup helpers ──────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(' - im:message.group_at_msg (群消息)');
|
|
152
|
-
console.log(' - im:resource (文件下载)');
|
|
153
|
-
console.log(' - im:chat (群信息)');
|
|
154
|
-
console.log(' - contact:user.base:readonly (用户信息)\n');
|
|
155
|
-
console.log('启用事件订阅 (WebSocket 模式):');
|
|
156
|
-
console.log(' - im.message.receive_v1');
|
|
157
|
-
console.log(' - card.action.trigger\n');
|
|
148
|
+
// Thin wrapper around setup/bots-store.writeBotsJsonAtomic so call-sites keep
|
|
149
|
+
// the same name without passing BOTS_JSON_FILE explicitly each time.
|
|
150
|
+
function writeBotsJsonAtomic(bots) {
|
|
151
|
+
writeBotsAtomic(BOTS_JSON_FILE, bots);
|
|
158
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* 从 bot 配置里取 brand. 旧的 bots.json (1.0 之前) 没这个字段, default 到 feishu
|
|
155
|
+
* 保留向后兼容. cmdStart 凭证校验 + printRemainingSteps 深链都靠它选 host.
|
|
156
|
+
*/
|
|
157
|
+
function botBrand(b) {
|
|
158
|
+
return b?.brand === 'lark' ? 'lark' : 'feishu';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 把 botmux 推荐的完整 scope JSON (从 src/setup/lark-scopes.json) 写到
|
|
162
|
+
* 用户配置目录, 同时给出跨平台一键复制命令. JSON 长 (293 项, 297 行),
|
|
163
|
+
* terminal 直接打印用户也复制不了, 写文件 + pbcopy/xclip 才是顺手的姿势.
|
|
164
|
+
*
|
|
165
|
+
* Returns: 写出的 JSON 文件绝对路径.
|
|
166
|
+
*/
|
|
167
|
+
function writeScopesJsonToConfigDir() {
|
|
168
|
+
// build script 会把 src/setup/lark-scopes.json copy 到 dist/setup/.
|
|
169
|
+
// dist 模式下 __dirname 是 dist/, 找 ./setup/lark-scopes.json; dev (tsx)
|
|
170
|
+
// 模式找 src/setup/lark-scopes.json 在源码同目录也成立.
|
|
171
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
172
|
+
const srcCandidates = [
|
|
173
|
+
join(here, 'setup', 'lark-scopes.json'),
|
|
174
|
+
join(here, '..', 'src', 'setup', 'lark-scopes.json'),
|
|
175
|
+
];
|
|
176
|
+
let scopesPath = srcCandidates[0];
|
|
177
|
+
for (const p of srcCandidates) {
|
|
178
|
+
if (existsSync(p)) {
|
|
179
|
+
scopesPath = p;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const destPath = join(CONFIG_DIR, 'lark-scopes.json');
|
|
184
|
+
copyFileSync(scopesPath, destPath);
|
|
185
|
+
return destPath;
|
|
186
|
+
}
|
|
187
|
+
function printCopyHint(filePath) {
|
|
188
|
+
console.log(' 一键复制 JSON 到剪贴板:');
|
|
189
|
+
console.log(` macOS: cat ${filePath} | pbcopy`);
|
|
190
|
+
console.log(` Linux: cat ${filePath} | xclip -selection clipboard`);
|
|
191
|
+
console.log(` Wayland: cat ${filePath} | wl-copy`);
|
|
192
|
+
console.log(` 远程 SSH: scp 拉到本地, 或 less ${filePath} 终端选中复制`);
|
|
193
|
+
}
|
|
194
|
+
function printRemainingSteps(appId, brand) {
|
|
195
|
+
// 数据源: 飞书内部 wiki UBOXwH01CixfxfkqxUpcKgvQnsg "[Botmux] 5分钟创建一个
|
|
196
|
+
// 真正好用的飞书助理" 的"权限申请"段, 加 botmux 维护者实测确认 PersonalAgent
|
|
197
|
+
// 应用扫码建出来时已经默认订阅 im.message.receive_v1 / card.action.trigger
|
|
198
|
+
// 事件并开通 bot 能力. 但 lark-channel-bridge README 当前仍要求用户手动补
|
|
199
|
+
// 事件订阅, 跟我们结论不一致 — 不排除飞书最近升级了 PersonalAgent 预配 (那个
|
|
200
|
+
// README 是旧版), 也不排除存在租户/版本差异让某些用户的 PersonalAgent 没预配.
|
|
201
|
+
//
|
|
202
|
+
// 折中: 主线流程只列"必须手动"的两步 (权限 + 重定向 URL), 末尾再给"如果
|
|
203
|
+
// bot 收不到消息" 的兜底 fallback 链接, 让用户能自查事件订阅 / bot 能力.
|
|
204
|
+
const host = brand === 'lark' ? 'open.larksuite.com' : 'open.feishu.cn';
|
|
205
|
+
const home = `https://${host}/app/${appId}`;
|
|
206
|
+
let scopesJsonPath = '';
|
|
207
|
+
try {
|
|
208
|
+
scopesJsonPath = writeScopesJsonToConfigDir();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// 不应阻止 setup 完成, 只 WARN
|
|
212
|
+
console.log(`\n⚠️ 写权限 JSON 失败 (${err.message}), 请手动从仓库源码 src/setup/lark-scopes.json 拷.`);
|
|
213
|
+
}
|
|
214
|
+
console.log('\n⚠️ 扫码 / 粘贴只完成了"建应用 + 拿凭证". 还有这些步骤要在开放平台浏览器里点:\n');
|
|
215
|
+
console.log(' 1. 申请权限 (一次性导入完整 JSON 提交审批)');
|
|
216
|
+
console.log(` 深链: ${home}/auth → 进入「权限管理」→「批量导入/导出权限」→ 粘贴 → 提交`);
|
|
217
|
+
if (scopesJsonPath) {
|
|
218
|
+
console.log(` 权限 JSON: ${scopesJsonPath}`);
|
|
219
|
+
printCopyHint(scopesJsonPath);
|
|
220
|
+
}
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(' 2. 添加重定向 URL (用于 botmux 内 `/login` 拿用户 UAT 调云文档/日历等)');
|
|
223
|
+
console.log(` 深链: ${home}/safe → 进入「安全设置」→「重定向 URL」`);
|
|
224
|
+
console.log(' 填入: http://127.0.0.1:9768/callback');
|
|
225
|
+
console.log(' 不打算用 `/login` 跨用户调 API 的话, 这一步可以跳过.\n');
|
|
226
|
+
console.log(' 完成后 `botmux start` (或 `botmux restart`),启动检查不会卡住,');
|
|
227
|
+
console.log(' 缺权限只 WARN,去开放平台补齐后 daemon 自动恢复。\n');
|
|
228
|
+
// Fallback 自查清单 — 维护者实测 PersonalAgent 默认配好下面两项, 但飞书
|
|
229
|
+
// 没承诺过这是稳定行为. 收不到消息时让用户能自查.
|
|
230
|
+
console.log(' ─── 如果机器人配置好后收不到消息, 自查下面两点 ───');
|
|
231
|
+
console.log(' a. 事件订阅: PersonalAgent 默认订阅 im.message.receive_v1 + card.action.trigger,');
|
|
232
|
+
console.log(` 如缺失, 请到 ${home}/dev-config/event-sub 手动添加`);
|
|
233
|
+
console.log(' b. 机器人能力: PersonalAgent 默认已开通,');
|
|
234
|
+
console.log(` 如缺失, 请到 ${home}/feature/bot 启用 (应用功能 → 机器人)`);
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 让用户选"扫码建应用"还是"手动粘 AppID/Secret".
|
|
239
|
+
*
|
|
240
|
+
* 默认走扫码: 调 SDK `registerApp` → 拿 client_id/client_secret. 失败 (用户拒绝/
|
|
241
|
+
* 超时/网络/取消) 一律降级到手动, 不阻塞流程.
|
|
242
|
+
*
|
|
243
|
+
* Codex review 边界:
|
|
244
|
+
* - secret 不进 argv / 日志 / 错误链 (registerApp 内部 safeMsg 已做; 手动模式下
|
|
245
|
+
* AppSecret 通过 rl.question 异步读取, 不会出现在 process.argv)
|
|
246
|
+
* - 任何失败都返回结构化对象, 不抛 (调用方根据 ok=false 回退)
|
|
247
|
+
*/
|
|
248
|
+
async function obtainCredentials(rl) {
|
|
249
|
+
console.log('── 飞书应用建立 ──\n');
|
|
250
|
+
console.log('1) 扫码建应用(推荐,一步拿到 AppID/Secret,需要飞书 App 扫码)');
|
|
251
|
+
console.log('2) 手动粘 AppID/Secret(已经在开放平台创建好应用了)\n');
|
|
252
|
+
const choice = (await ask(rl, '选择 [1]: ')).trim();
|
|
253
|
+
if (choice !== '2') {
|
|
254
|
+
// 动态导入避免冷启动加载 SDK
|
|
255
|
+
const { tryRegisterApp } = await import('./setup/register-app.js');
|
|
256
|
+
const result = await tryRegisterApp();
|
|
257
|
+
if (result.ok) {
|
|
258
|
+
// Lark 国际版需要 daemon 链路全程走 larksuite.com 域 (Client domain /
|
|
259
|
+
// WSClient / event-dispatcher 的 fetch URL / scope 深链 host). 当前
|
|
260
|
+
// botmux runtime 这几处都硬编码 feishu.cn, 所以即使扫码成功了也无法
|
|
261
|
+
// 真正跑起来. 干净做法是 setup 阶段就拒绝, 让用户用 feishu 租户. 单
|
|
262
|
+
// 独 PR 完整接入 lark 后再去掉这个分支.
|
|
263
|
+
if (result.brand === 'lark') {
|
|
264
|
+
console.log(`\n❌ 检测到 Lark 国际版 (larksuite.com) 租户。`);
|
|
265
|
+
console.log(` botmux 当前 daemon 运行链路仅支持飞书 (feishu.cn) 租户,`);
|
|
266
|
+
console.log(` Lark 国际版完整接入会在单独 PR 跟进 (BotConfig / Client domain /`);
|
|
267
|
+
console.log(` WSClient / event-dispatcher 等需要一并支持).`);
|
|
268
|
+
console.log(` 请用飞书 (feishu.cn) 租户重试 setup。\n`);
|
|
269
|
+
return { ok: false, reason: 'lark_unsupported' };
|
|
270
|
+
}
|
|
271
|
+
console.log(`\n✅ 应用创建成功`);
|
|
272
|
+
console.log(` App ID: ${result.appId}`);
|
|
273
|
+
console.log(` 租户类型: ${result.brand}`);
|
|
274
|
+
return { ok: true, appId: result.appId, appSecret: result.appSecret, brand: result.brand };
|
|
275
|
+
}
|
|
276
|
+
console.log(`\n⚠️ 扫码失败 (${result.error}): ${result.message}`);
|
|
277
|
+
if (result.error === 'aborted') {
|
|
278
|
+
// 用户主动取消整个 setup, 不再问手动 fallback
|
|
279
|
+
return { ok: false, reason: 'cancelled' };
|
|
280
|
+
}
|
|
281
|
+
console.log(' 降级到手动输入 AppID/Secret。\n');
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log('\n请在浏览器打开 https://open.feishu.cn/app 创建应用,然后回来粘 ID/Secret。\n');
|
|
285
|
+
}
|
|
286
|
+
// 手动 fallback. 不再提问租户类型 — 当前 daemon runtime 只支持 feishu,
|
|
287
|
+
// 让用户选 lark 是误导. 等 lark 完整接入再加回来.
|
|
288
|
+
const appId = (await ask(rl, 'AppID (cli_xxx): ')).trim();
|
|
289
|
+
const appSecret = (await ask(rl, 'AppSecret: ')).trim();
|
|
290
|
+
if (!appId || !appSecret) {
|
|
291
|
+
console.log('\n❌ AppID/AppSecret 不能为空,setup 中止。');
|
|
292
|
+
return { ok: false, reason: 'cancelled' };
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, appId, appSecret, brand: 'feishu' };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 收集一个机器人完整配置 (凭证 + CLI/工作目录/allowedUsers).
|
|
298
|
+
*
|
|
299
|
+
* 顺序: 拿凭证 → tenant_access_token 验证 → 通过才返回 bot 对象. 验证失败
|
|
300
|
+
* 直接返回 null, 调用方负责"不写 bots.json". Codex review 边界 #2.
|
|
301
|
+
*/
|
|
159
302
|
async function promptBotConfig(rl) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
303
|
+
const creds = await obtainCredentials(rl);
|
|
304
|
+
if (!creds.ok)
|
|
305
|
+
return null;
|
|
306
|
+
// 凭证立刻验证. 通不过不写 bots.json.
|
|
307
|
+
console.log('\n校验凭证(取 tenant_access_token)…');
|
|
308
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
309
|
+
const v = await validateCredentials(creds.appId, creds.appSecret, creds.brand);
|
|
310
|
+
if (!v.ok) {
|
|
311
|
+
console.log(`\n❌ 凭证校验失败 (${v.error}): ${v.message}`);
|
|
312
|
+
console.log(' 不写 bots.json。请重新运行 botmux setup。');
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
console.log('✅ 凭证有效(tenant_access_token 已成功获取)\n');
|
|
316
|
+
console.log('支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) gemini 6) opencode');
|
|
163
317
|
const cliChoice = await ask(rl, 'CLI 适配器 [1]: ');
|
|
164
318
|
const cliIdMap = { '1': 'claude-code', '2': 'aiden', '3': 'coco', '4': 'codex', '5': 'gemini', '6': 'opencode' };
|
|
165
319
|
const cliId = cliIdMap[cliChoice] ?? (cliChoice || 'claude-code');
|
|
166
320
|
const workingDir = await ask(rl, '默认工作目录 [~]: ');
|
|
167
321
|
const allowedUsers = await ask(rl, '允许的用户 (邮箱或 open_id,逗号分隔,留空=不限制): ');
|
|
168
|
-
|
|
322
|
+
// brand 必须持久化: cmdStart 的 validate / event-dispatcher 走的 deep link
|
|
323
|
+
// 都看这个字段; 不写就只能硬编码 feishu, lark 租户用户会被打成凭证无效.
|
|
324
|
+
// 为了向后兼容 (旧 bots.json 没 brand 字段), reader 应当 default 到 'feishu'.
|
|
325
|
+
const bot = {
|
|
326
|
+
larkAppId: creds.appId,
|
|
327
|
+
larkAppSecret: creds.appSecret,
|
|
328
|
+
brand: creds.brand,
|
|
329
|
+
cliId,
|
|
330
|
+
};
|
|
169
331
|
if (workingDir)
|
|
170
332
|
bot.workingDir = workingDir;
|
|
171
333
|
if (allowedUsers)
|
|
@@ -203,18 +365,25 @@ function parseDotEnvToBotConfig() {
|
|
|
203
365
|
bot.projectScanDir = vars.PROJECT_SCAN_DIR;
|
|
204
366
|
return bot;
|
|
205
367
|
}
|
|
206
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* 收集一个机器人配置并写盘 (单机器人 fresh install / 重新配置).
|
|
370
|
+
*
|
|
371
|
+
* 失败路径 (扫码取消 / 凭证校验不通过): 不创建任何配置文件, 不动旧 .env.
|
|
372
|
+
* Codex review 边界 #2: 中途失败一律不留半截 JSON.
|
|
373
|
+
*/
|
|
207
374
|
async function writeSingleBotConfig() {
|
|
208
|
-
console.log('── 飞书应用配置 ──\n');
|
|
209
|
-
printLarkPermissions();
|
|
210
375
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
211
376
|
const bot = await promptBotConfig(rl);
|
|
212
377
|
rl.close();
|
|
213
|
-
|
|
378
|
+
if (!bot)
|
|
379
|
+
return false;
|
|
380
|
+
writeBotsJsonAtomic([bot]);
|
|
214
381
|
console.log(`\n✅ 配置已写入: ${BOTS_JSON_FILE}`);
|
|
215
|
-
|
|
216
|
-
console.log(
|
|
382
|
+
printRemainingSteps(bot.larkAppId, botBrand(bot));
|
|
383
|
+
console.log(`下一步:`);
|
|
384
|
+
console.log(` 1. botmux start 启动 daemon`);
|
|
217
385
|
console.log(` 2. botmux autostart enable 注册开机自启(推荐:${process.platform === 'darwin' ? 'mac launchd' : process.platform === 'linux' ? 'linux user systemd' : '当前平台暂不支持'},无需 sudo)`);
|
|
386
|
+
return true;
|
|
218
387
|
}
|
|
219
388
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
220
389
|
async function cmdSetup() {
|
|
@@ -235,26 +404,36 @@ async function cmdSetup() {
|
|
|
235
404
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
236
405
|
const action = await ask(rl, '操作: 1) 添加新机器人 2) 重新配置 (1/2) [1]: ');
|
|
237
406
|
if (action === '2') {
|
|
238
|
-
renameSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
239
|
-
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak\n`);
|
|
240
407
|
console.log('\n── 重新配置 ──\n');
|
|
241
|
-
printLarkPermissions();
|
|
242
408
|
const newBot = await promptBotConfig(rl);
|
|
243
409
|
rl.close();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
410
|
+
if (!newBot) {
|
|
411
|
+
console.log('\n⚠️ setup 中止,旧配置保留不动。');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Codex review #1: 先 copyFileSync 备份, 再原子写新文件. 之前先 rename
|
|
415
|
+
// 旧文件再 write, 一旦 write 失败 (磁盘/权限/进程被 kill) 用户就丢了
|
|
416
|
+
// bots.json. copy 之后写失败旧文件原地不动, .bak 是无害的同名副本.
|
|
417
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
418
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
419
|
+
writeBotsJsonAtomic([newBot]);
|
|
420
|
+
console.log(`✅ 配置已写入: ${BOTS_JSON_FILE}`);
|
|
421
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
422
|
+
console.log(`下一步: botmux restart\n`);
|
|
247
423
|
return;
|
|
248
424
|
}
|
|
249
425
|
console.log('\n── 添加新机器人 ──\n');
|
|
250
|
-
printLarkPermissions();
|
|
251
426
|
const newBot = await promptBotConfig(rl);
|
|
252
427
|
rl.close();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
428
|
+
if (!newBot) {
|
|
429
|
+
console.log('\n⚠️ setup 中止,bots.json 不动。');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
writeBotsJsonAtomic([...bots, newBot]);
|
|
433
|
+
console.log(`\n✅ 已添加机器人 ${newBot.larkAppId},共 ${bots.length + 1} 个`);
|
|
256
434
|
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
257
|
-
|
|
435
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
436
|
+
console.log(`下一步: botmux restart\n`);
|
|
258
437
|
}
|
|
259
438
|
else if (hasEnv) {
|
|
260
439
|
// --- Single-bot mode (.env exists) ---
|
|
@@ -263,9 +442,11 @@ async function cmdSetup() {
|
|
|
263
442
|
const action = await ask(rl, '操作: 1) 添加新机器人 2) 覆盖当前配置 (1/2): ');
|
|
264
443
|
if (action === '2') {
|
|
265
444
|
rl.close();
|
|
266
|
-
await writeSingleBotConfig();
|
|
267
|
-
|
|
268
|
-
|
|
445
|
+
const ok = await writeSingleBotConfig();
|
|
446
|
+
if (ok) {
|
|
447
|
+
renameSync(ENV_FILE, ENV_FILE + '.bak');
|
|
448
|
+
console.log(` 旧 .env 已备份: ${ENV_FILE}.bak`);
|
|
449
|
+
}
|
|
269
450
|
return;
|
|
270
451
|
}
|
|
271
452
|
// Migrate .env → bots.json
|
|
@@ -278,16 +459,20 @@ async function cmdSetup() {
|
|
|
278
459
|
}
|
|
279
460
|
console.log(`\n当前机器人: ${existingBot.larkAppId} (${existingBot.cliId ?? 'claude-code'})`);
|
|
280
461
|
console.log('\n── 添加新机器人 ──\n');
|
|
281
|
-
printLarkPermissions();
|
|
282
462
|
const newBot = await promptBotConfig(rl);
|
|
283
463
|
rl.close();
|
|
284
|
-
|
|
285
|
-
|
|
464
|
+
if (!newBot) {
|
|
465
|
+
console.log('\n⚠️ setup 中止,.env 和 bots.json 都不动。');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// 写新文件成功后才备份 .env. 失败不动两边.
|
|
469
|
+
writeBotsJsonAtomic([existingBot, newBot]);
|
|
286
470
|
renameSync(ENV_FILE, ENV_FILE + '.bak');
|
|
287
471
|
console.log(`\n✅ 已迁移到多机器人配置`);
|
|
288
472
|
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
289
473
|
console.log(` 旧配置已备份: ${ENV_FILE}.bak`);
|
|
290
|
-
|
|
474
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
475
|
+
console.log(`下一步: botmux restart\n`);
|
|
291
476
|
}
|
|
292
477
|
else {
|
|
293
478
|
// --- Fresh install ---
|
|
@@ -379,6 +564,40 @@ async function cmdStart() {
|
|
|
379
564
|
ensureConfigDir();
|
|
380
565
|
preflightNodeSanity();
|
|
381
566
|
await ensureSystemDependencies();
|
|
567
|
+
// 启动前快速校验每个 bot 的凭证. Codex review 边界 #5: 凭证无效是
|
|
568
|
+
// 唯一应该阻塞 start 的情况; scope/event 缺失在 daemon 起来后用 WARN
|
|
569
|
+
// + 私信处理 (event-dispatcher.checkRequiredScopes).
|
|
570
|
+
//
|
|
571
|
+
// 失败时打印明确的 appId 前缀和错误码, 不打印 secret, 不 spawn pm2 进程.
|
|
572
|
+
const botsForCheck = loadBotsJson();
|
|
573
|
+
if (botsForCheck.length > 0) {
|
|
574
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
575
|
+
const invalid = [];
|
|
576
|
+
for (const b of botsForCheck) {
|
|
577
|
+
if (!b.larkAppId || !b.larkAppSecret) {
|
|
578
|
+
invalid.push({ appId: b.larkAppId || '(空 appId)', reason: 'larkAppId/larkAppSecret 缺失' });
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const v = await validateCredentials(b.larkAppId, b.larkAppSecret, botBrand(b));
|
|
582
|
+
if (!v.ok) {
|
|
583
|
+
if (v.error === 'invalid_credentials') {
|
|
584
|
+
invalid.push({ appId: b.larkAppId, reason: v.message });
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// network / unknown — 不应该拦下启动, 走 WARN
|
|
588
|
+
console.warn(`⚠️ [${b.larkAppId}] 启动前凭证验证未成功(${v.error}): ${v.message}`);
|
|
589
|
+
console.warn(` daemon 仍会启动;启动后 dispatcher 会自行重试。`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (invalid.length > 0) {
|
|
594
|
+
console.error('\n❌ 以下机器人凭证无效,botmux start 中止:\n');
|
|
595
|
+
for (const e of invalid)
|
|
596
|
+
console.error(` - ${e.appId}: ${e.reason}`);
|
|
597
|
+
console.error('\n 修复方式: 运行 `botmux setup` 选 "重新配置" 重新走扫码/手动流程。');
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
382
601
|
cleanupLegacyPm2();
|
|
383
602
|
const cfg = ecosystemConfig();
|
|
384
603
|
runPm2(['start', cfg]);
|