botmux 2.26.0 → 2.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +50 -46
- package/README.md +49 -46
- package/dist/cli.js +279 -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/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,209 @@ 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
|
+
// 环境感知: SSH/headless 没有 X server, xclip 一定报 "Can't open display".
|
|
189
|
+
// 这种场景下"剪贴板"在用户本地 (运行 SSH 客户端的那台机器), 远程机上能做的:
|
|
190
|
+
// - 直接 cat, 让用户在本地 terminal 鼠标选中 (SSH 选中即写本地剪贴板)
|
|
191
|
+
// - OSC 52: terminal app 代写本地剪贴板, iTerm2 / kitty / WezTerm /
|
|
192
|
+
// Alacritty / tmux 1.5+ 都支持, gnome-terminal / Terminal.app 不支持
|
|
193
|
+
// 检测 DISPLAY (X11) 或 WAYLAND_DISPLAY 都没有, 或 SSH_* 环境变量存在
|
|
194
|
+
// → 当作 SSH 场景, 不推荐 xclip / pbcopy.
|
|
195
|
+
const isSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY);
|
|
196
|
+
const hasLocalGui = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY) && !isSsh;
|
|
197
|
+
const isMacLocal = process.platform === 'darwin' && !isSsh;
|
|
198
|
+
console.log(' 把 JSON 内容拷到本地剪贴板, 然后到飞书"批量导入/导出权限"页粘贴:');
|
|
199
|
+
if (isMacLocal) {
|
|
200
|
+
console.log(` macOS 本地: cat ${filePath} | pbcopy`);
|
|
201
|
+
}
|
|
202
|
+
else if (hasLocalGui) {
|
|
203
|
+
console.log(` Linux 本地 (X 服务器): cat ${filePath} | xclip -selection clipboard`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// SSH / headless: 鼠标选中是最稳的, OSC 52 作为高级选项
|
|
207
|
+
console.log(` SSH 终端鼠标选中复制: cat ${filePath}`);
|
|
208
|
+
console.log(' (终端把选中的字符直接写到你本地剪贴板, 不依赖远端剪贴板工具)');
|
|
209
|
+
console.log(` 或 OSC 52 (兼容 iTerm2 / kitty / WezTerm / Alacritty / tmux 1.5+):`);
|
|
210
|
+
console.log(` base64 -w0 < ${filePath} | awk 'BEGIN{printf "\\033]52;c;"}{printf "%s",$0}END{printf "\\a"}'`);
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
function printRemainingSteps(appId, brand) {
|
|
215
|
+
// 数据源: 飞书内部 wiki UBOXwH01CixfxfkqxUpcKgvQnsg "[Botmux] 5分钟创建一个
|
|
216
|
+
// 真正好用的飞书助理" 的"权限申请"段, 加 botmux 维护者实测确认 PersonalAgent
|
|
217
|
+
// 应用扫码建出来时已经默认订阅 im.message.receive_v1 / card.action.trigger
|
|
218
|
+
// 事件并开通 bot 能力. 但 lark-channel-bridge README 当前仍要求用户手动补
|
|
219
|
+
// 事件订阅, 跟我们结论不一致 — 不排除飞书最近升级了 PersonalAgent 预配 (那个
|
|
220
|
+
// README 是旧版), 也不排除存在租户/版本差异让某些用户的 PersonalAgent 没预配.
|
|
221
|
+
//
|
|
222
|
+
// 折中: 主线流程只列"必须手动"的两步 (权限 + 重定向 URL), 末尾再给"如果
|
|
223
|
+
// bot 收不到消息" 的兜底 fallback 链接, 让用户能自查事件订阅 / bot 能力.
|
|
224
|
+
const host = brand === 'lark' ? 'open.larksuite.com' : 'open.feishu.cn';
|
|
225
|
+
const home = `https://${host}/app/${appId}`;
|
|
226
|
+
let scopesJsonPath = '';
|
|
227
|
+
try {
|
|
228
|
+
scopesJsonPath = writeScopesJsonToConfigDir();
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// 不应阻止 setup 完成, 只 WARN
|
|
232
|
+
console.log(`\n⚠️ 写权限 JSON 失败 (${err.message}), 请手动从仓库源码 src/setup/lark-scopes.json 拷.`);
|
|
233
|
+
}
|
|
234
|
+
console.log('\n⚠️ 扫码 / 粘贴只完成了"建应用 + 拿凭证". 还有这些步骤要在开放平台浏览器里点:\n');
|
|
235
|
+
console.log(' 1. 申请权限 (一次性导入完整 JSON 提交审批)');
|
|
236
|
+
console.log(` 深链: ${home}/auth → 进入「权限管理」→「批量导入/导出权限」→ 粘贴 → 提交`);
|
|
237
|
+
if (scopesJsonPath) {
|
|
238
|
+
console.log(` 权限 JSON: ${scopesJsonPath}`);
|
|
239
|
+
printCopyHint(scopesJsonPath);
|
|
240
|
+
}
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(' 2. 添加重定向 URL (用于 botmux 内 `/login` 拿用户 UAT 调云文档/日历等)');
|
|
243
|
+
console.log(` 深链: ${home}/safe → 进入「安全设置」→「重定向 URL」`);
|
|
244
|
+
console.log(' 填入: http://127.0.0.1:9768/callback');
|
|
245
|
+
console.log(' 不打算用 `/login` 跨用户调 API 的话, 这一步可以跳过.\n');
|
|
246
|
+
console.log(' 完成后 `botmux start` (或 `botmux restart`),启动检查不会卡住,');
|
|
247
|
+
console.log(' 缺权限只 WARN,去开放平台补齐后 daemon 自动恢复。\n');
|
|
248
|
+
// Fallback 自查清单 — 维护者实测 PersonalAgent 默认配好下面两项, 但飞书
|
|
249
|
+
// 没承诺过这是稳定行为. 收不到消息时让用户能自查.
|
|
250
|
+
console.log(' ─── 如果机器人配置好后收不到消息, 自查下面两点 ───');
|
|
251
|
+
console.log(' a. 事件订阅: PersonalAgent 默认订阅 im.message.receive_v1 + card.action.trigger,');
|
|
252
|
+
console.log(` 如缺失, 请到 ${home}/dev-config/event-sub 手动添加`);
|
|
253
|
+
console.log(' b. 机器人能力: PersonalAgent 默认已开通,');
|
|
254
|
+
console.log(` 如缺失, 请到 ${home}/feature/bot 启用 (应用功能 → 机器人)`);
|
|
255
|
+
console.log('');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 让用户选"扫码建应用"还是"手动粘 AppID/Secret".
|
|
259
|
+
*
|
|
260
|
+
* 默认走扫码: 调 SDK `registerApp` → 拿 client_id/client_secret. 失败 (用户拒绝/
|
|
261
|
+
* 超时/网络/取消) 一律降级到手动, 不阻塞流程.
|
|
262
|
+
*
|
|
263
|
+
* Codex review 边界:
|
|
264
|
+
* - secret 不进 argv / 日志 / 错误链 (registerApp 内部 safeMsg 已做; 手动模式下
|
|
265
|
+
* AppSecret 通过 rl.question 异步读取, 不会出现在 process.argv)
|
|
266
|
+
* - 任何失败都返回结构化对象, 不抛 (调用方根据 ok=false 回退)
|
|
267
|
+
*/
|
|
268
|
+
async function obtainCredentials(rl) {
|
|
269
|
+
console.log('── 飞书应用建立 ──\n');
|
|
270
|
+
console.log('1) 扫码建应用(推荐,一步拿到 AppID/Secret,需要飞书 App 扫码)');
|
|
271
|
+
console.log('2) 手动粘 AppID/Secret(已经在开放平台创建好应用了)\n');
|
|
272
|
+
const choice = (await ask(rl, '选择 [1]: ')).trim();
|
|
273
|
+
if (choice !== '2') {
|
|
274
|
+
// 动态导入避免冷启动加载 SDK
|
|
275
|
+
const { tryRegisterApp } = await import('./setup/register-app.js');
|
|
276
|
+
const result = await tryRegisterApp();
|
|
277
|
+
if (result.ok) {
|
|
278
|
+
// Lark 国际版需要 daemon 链路全程走 larksuite.com 域 (Client domain /
|
|
279
|
+
// WSClient / event-dispatcher 的 fetch URL / scope 深链 host). 当前
|
|
280
|
+
// botmux runtime 这几处都硬编码 feishu.cn, 所以即使扫码成功了也无法
|
|
281
|
+
// 真正跑起来. 干净做法是 setup 阶段就拒绝, 让用户用 feishu 租户. 单
|
|
282
|
+
// 独 PR 完整接入 lark 后再去掉这个分支.
|
|
283
|
+
if (result.brand === 'lark') {
|
|
284
|
+
console.log(`\n❌ 检测到 Lark 国际版 (larksuite.com) 租户。`);
|
|
285
|
+
console.log(` botmux 当前 daemon 运行链路仅支持飞书 (feishu.cn) 租户,`);
|
|
286
|
+
console.log(` Lark 国际版完整接入会在单独 PR 跟进 (BotConfig / Client domain /`);
|
|
287
|
+
console.log(` WSClient / event-dispatcher 等需要一并支持).`);
|
|
288
|
+
console.log(` 请用飞书 (feishu.cn) 租户重试 setup。\n`);
|
|
289
|
+
return { ok: false, reason: 'lark_unsupported' };
|
|
290
|
+
}
|
|
291
|
+
console.log(`\n✅ 应用创建成功`);
|
|
292
|
+
console.log(` App ID: ${result.appId}`);
|
|
293
|
+
console.log(` 租户类型: ${result.brand}`);
|
|
294
|
+
return { ok: true, appId: result.appId, appSecret: result.appSecret, brand: result.brand };
|
|
295
|
+
}
|
|
296
|
+
console.log(`\n⚠️ 扫码失败 (${result.error}): ${result.message}`);
|
|
297
|
+
if (result.error === 'aborted') {
|
|
298
|
+
// 用户主动取消整个 setup, 不再问手动 fallback
|
|
299
|
+
return { ok: false, reason: 'cancelled' };
|
|
300
|
+
}
|
|
301
|
+
console.log(' 降级到手动输入 AppID/Secret。\n');
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.log('\n请在浏览器打开 https://open.feishu.cn/app 创建应用,然后回来粘 ID/Secret。\n');
|
|
305
|
+
}
|
|
306
|
+
// 手动 fallback. 不再提问租户类型 — 当前 daemon runtime 只支持 feishu,
|
|
307
|
+
// 让用户选 lark 是误导. 等 lark 完整接入再加回来.
|
|
308
|
+
const appId = (await ask(rl, 'AppID (cli_xxx): ')).trim();
|
|
309
|
+
const appSecret = (await ask(rl, 'AppSecret: ')).trim();
|
|
310
|
+
if (!appId || !appSecret) {
|
|
311
|
+
console.log('\n❌ AppID/AppSecret 不能为空,setup 中止。');
|
|
312
|
+
return { ok: false, reason: 'cancelled' };
|
|
313
|
+
}
|
|
314
|
+
return { ok: true, appId, appSecret, brand: 'feishu' };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 收集一个机器人完整配置 (凭证 + CLI/工作目录/allowedUsers).
|
|
318
|
+
*
|
|
319
|
+
* 顺序: 拿凭证 → tenant_access_token 验证 → 通过才返回 bot 对象. 验证失败
|
|
320
|
+
* 直接返回 null, 调用方负责"不写 bots.json". Codex review 边界 #2.
|
|
321
|
+
*/
|
|
159
322
|
async function promptBotConfig(rl) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
323
|
+
const creds = await obtainCredentials(rl);
|
|
324
|
+
if (!creds.ok)
|
|
325
|
+
return null;
|
|
326
|
+
// 凭证立刻验证. 通不过不写 bots.json.
|
|
327
|
+
console.log('\n校验凭证(取 tenant_access_token)…');
|
|
328
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
329
|
+
const v = await validateCredentials(creds.appId, creds.appSecret, creds.brand);
|
|
330
|
+
if (!v.ok) {
|
|
331
|
+
console.log(`\n❌ 凭证校验失败 (${v.error}): ${v.message}`);
|
|
332
|
+
console.log(' 不写 bots.json。请重新运行 botmux setup。');
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
console.log('✅ 凭证有效(tenant_access_token 已成功获取)\n');
|
|
336
|
+
console.log('支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) gemini 6) opencode');
|
|
163
337
|
const cliChoice = await ask(rl, 'CLI 适配器 [1]: ');
|
|
164
338
|
const cliIdMap = { '1': 'claude-code', '2': 'aiden', '3': 'coco', '4': 'codex', '5': 'gemini', '6': 'opencode' };
|
|
165
339
|
const cliId = cliIdMap[cliChoice] ?? (cliChoice || 'claude-code');
|
|
166
340
|
const workingDir = await ask(rl, '默认工作目录 [~]: ');
|
|
167
341
|
const allowedUsers = await ask(rl, '允许的用户 (邮箱或 open_id,逗号分隔,留空=不限制): ');
|
|
168
|
-
|
|
342
|
+
// brand 必须持久化: cmdStart 的 validate / event-dispatcher 走的 deep link
|
|
343
|
+
// 都看这个字段; 不写就只能硬编码 feishu, lark 租户用户会被打成凭证无效.
|
|
344
|
+
// 为了向后兼容 (旧 bots.json 没 brand 字段), reader 应当 default 到 'feishu'.
|
|
345
|
+
const bot = {
|
|
346
|
+
larkAppId: creds.appId,
|
|
347
|
+
larkAppSecret: creds.appSecret,
|
|
348
|
+
brand: creds.brand,
|
|
349
|
+
cliId,
|
|
350
|
+
};
|
|
169
351
|
if (workingDir)
|
|
170
352
|
bot.workingDir = workingDir;
|
|
171
353
|
if (allowedUsers)
|
|
@@ -203,18 +385,25 @@ function parseDotEnvToBotConfig() {
|
|
|
203
385
|
bot.projectScanDir = vars.PROJECT_SCAN_DIR;
|
|
204
386
|
return bot;
|
|
205
387
|
}
|
|
206
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* 收集一个机器人配置并写盘 (单机器人 fresh install / 重新配置).
|
|
390
|
+
*
|
|
391
|
+
* 失败路径 (扫码取消 / 凭证校验不通过): 不创建任何配置文件, 不动旧 .env.
|
|
392
|
+
* Codex review 边界 #2: 中途失败一律不留半截 JSON.
|
|
393
|
+
*/
|
|
207
394
|
async function writeSingleBotConfig() {
|
|
208
|
-
console.log('── 飞书应用配置 ──\n');
|
|
209
|
-
printLarkPermissions();
|
|
210
395
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
211
396
|
const bot = await promptBotConfig(rl);
|
|
212
397
|
rl.close();
|
|
213
|
-
|
|
398
|
+
if (!bot)
|
|
399
|
+
return false;
|
|
400
|
+
writeBotsJsonAtomic([bot]);
|
|
214
401
|
console.log(`\n✅ 配置已写入: ${BOTS_JSON_FILE}`);
|
|
215
|
-
|
|
216
|
-
console.log(
|
|
402
|
+
printRemainingSteps(bot.larkAppId, botBrand(bot));
|
|
403
|
+
console.log(`下一步:`);
|
|
404
|
+
console.log(` 1. botmux start 启动 daemon`);
|
|
217
405
|
console.log(` 2. botmux autostart enable 注册开机自启(推荐:${process.platform === 'darwin' ? 'mac launchd' : process.platform === 'linux' ? 'linux user systemd' : '当前平台暂不支持'},无需 sudo)`);
|
|
406
|
+
return true;
|
|
218
407
|
}
|
|
219
408
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
220
409
|
async function cmdSetup() {
|
|
@@ -235,26 +424,36 @@ async function cmdSetup() {
|
|
|
235
424
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
236
425
|
const action = await ask(rl, '操作: 1) 添加新机器人 2) 重新配置 (1/2) [1]: ');
|
|
237
426
|
if (action === '2') {
|
|
238
|
-
renameSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
239
|
-
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak\n`);
|
|
240
427
|
console.log('\n── 重新配置 ──\n');
|
|
241
|
-
printLarkPermissions();
|
|
242
428
|
const newBot = await promptBotConfig(rl);
|
|
243
429
|
rl.close();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
430
|
+
if (!newBot) {
|
|
431
|
+
console.log('\n⚠️ setup 中止,旧配置保留不动。');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Codex review #1: 先 copyFileSync 备份, 再原子写新文件. 之前先 rename
|
|
435
|
+
// 旧文件再 write, 一旦 write 失败 (磁盘/权限/进程被 kill) 用户就丢了
|
|
436
|
+
// bots.json. copy 之后写失败旧文件原地不动, .bak 是无害的同名副本.
|
|
437
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
438
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
439
|
+
writeBotsJsonAtomic([newBot]);
|
|
440
|
+
console.log(`✅ 配置已写入: ${BOTS_JSON_FILE}`);
|
|
441
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
442
|
+
console.log(`下一步: botmux restart\n`);
|
|
247
443
|
return;
|
|
248
444
|
}
|
|
249
445
|
console.log('\n── 添加新机器人 ──\n');
|
|
250
|
-
printLarkPermissions();
|
|
251
446
|
const newBot = await promptBotConfig(rl);
|
|
252
447
|
rl.close();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
448
|
+
if (!newBot) {
|
|
449
|
+
console.log('\n⚠️ setup 中止,bots.json 不动。');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
writeBotsJsonAtomic([...bots, newBot]);
|
|
453
|
+
console.log(`\n✅ 已添加机器人 ${newBot.larkAppId},共 ${bots.length + 1} 个`);
|
|
256
454
|
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
257
|
-
|
|
455
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
456
|
+
console.log(`下一步: botmux restart\n`);
|
|
258
457
|
}
|
|
259
458
|
else if (hasEnv) {
|
|
260
459
|
// --- Single-bot mode (.env exists) ---
|
|
@@ -263,9 +462,11 @@ async function cmdSetup() {
|
|
|
263
462
|
const action = await ask(rl, '操作: 1) 添加新机器人 2) 覆盖当前配置 (1/2): ');
|
|
264
463
|
if (action === '2') {
|
|
265
464
|
rl.close();
|
|
266
|
-
await writeSingleBotConfig();
|
|
267
|
-
|
|
268
|
-
|
|
465
|
+
const ok = await writeSingleBotConfig();
|
|
466
|
+
if (ok) {
|
|
467
|
+
renameSync(ENV_FILE, ENV_FILE + '.bak');
|
|
468
|
+
console.log(` 旧 .env 已备份: ${ENV_FILE}.bak`);
|
|
469
|
+
}
|
|
269
470
|
return;
|
|
270
471
|
}
|
|
271
472
|
// Migrate .env → bots.json
|
|
@@ -278,16 +479,20 @@ async function cmdSetup() {
|
|
|
278
479
|
}
|
|
279
480
|
console.log(`\n当前机器人: ${existingBot.larkAppId} (${existingBot.cliId ?? 'claude-code'})`);
|
|
280
481
|
console.log('\n── 添加新机器人 ──\n');
|
|
281
|
-
printLarkPermissions();
|
|
282
482
|
const newBot = await promptBotConfig(rl);
|
|
283
483
|
rl.close();
|
|
284
|
-
|
|
285
|
-
|
|
484
|
+
if (!newBot) {
|
|
485
|
+
console.log('\n⚠️ setup 中止,.env 和 bots.json 都不动。');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// 写新文件成功后才备份 .env. 失败不动两边.
|
|
489
|
+
writeBotsJsonAtomic([existingBot, newBot]);
|
|
286
490
|
renameSync(ENV_FILE, ENV_FILE + '.bak');
|
|
287
491
|
console.log(`\n✅ 已迁移到多机器人配置`);
|
|
288
492
|
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
289
493
|
console.log(` 旧配置已备份: ${ENV_FILE}.bak`);
|
|
290
|
-
|
|
494
|
+
printRemainingSteps(newBot.larkAppId, botBrand(newBot));
|
|
495
|
+
console.log(`下一步: botmux restart\n`);
|
|
291
496
|
}
|
|
292
497
|
else {
|
|
293
498
|
// --- Fresh install ---
|
|
@@ -379,6 +584,40 @@ async function cmdStart() {
|
|
|
379
584
|
ensureConfigDir();
|
|
380
585
|
preflightNodeSanity();
|
|
381
586
|
await ensureSystemDependencies();
|
|
587
|
+
// 启动前快速校验每个 bot 的凭证. Codex review 边界 #5: 凭证无效是
|
|
588
|
+
// 唯一应该阻塞 start 的情况; scope/event 缺失在 daemon 起来后用 WARN
|
|
589
|
+
// + 私信处理 (event-dispatcher.checkRequiredScopes).
|
|
590
|
+
//
|
|
591
|
+
// 失败时打印明确的 appId 前缀和错误码, 不打印 secret, 不 spawn pm2 进程.
|
|
592
|
+
const botsForCheck = loadBotsJson();
|
|
593
|
+
if (botsForCheck.length > 0) {
|
|
594
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
595
|
+
const invalid = [];
|
|
596
|
+
for (const b of botsForCheck) {
|
|
597
|
+
if (!b.larkAppId || !b.larkAppSecret) {
|
|
598
|
+
invalid.push({ appId: b.larkAppId || '(空 appId)', reason: 'larkAppId/larkAppSecret 缺失' });
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const v = await validateCredentials(b.larkAppId, b.larkAppSecret, botBrand(b));
|
|
602
|
+
if (!v.ok) {
|
|
603
|
+
if (v.error === 'invalid_credentials') {
|
|
604
|
+
invalid.push({ appId: b.larkAppId, reason: v.message });
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// network / unknown — 不应该拦下启动, 走 WARN
|
|
608
|
+
console.warn(`⚠️ [${b.larkAppId}] 启动前凭证验证未成功(${v.error}): ${v.message}`);
|
|
609
|
+
console.warn(` daemon 仍会启动;启动后 dispatcher 会自行重试。`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (invalid.length > 0) {
|
|
614
|
+
console.error('\n❌ 以下机器人凭证无效,botmux start 中止:\n');
|
|
615
|
+
for (const e of invalid)
|
|
616
|
+
console.error(` - ${e.appId}: ${e.reason}`);
|
|
617
|
+
console.error('\n 修复方式: 运行 `botmux setup` 选 "重新配置" 重新走扫码/手动流程。');
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
382
621
|
cleanupLegacyPm2();
|
|
383
622
|
const cfg = ecosystemConfig();
|
|
384
623
|
runPm2(['start', cfg]);
|