botmux 2.28.0 → 2.29.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 +6 -3
- package/README.md +6 -3
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +26 -28
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +3 -4
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +14 -13
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +2 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +5 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +6 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.js +317 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -1
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts +2 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +139 -161
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +52 -32
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +40 -17
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +17 -16
- package/dist/daemon.js.map +1 -1
- package/dist/global-config.d.ts +15 -0
- package/dist/global-config.d.ts.map +1 -0
- package/dist/global-config.js +73 -0
- package/dist/global-config.js.map +1 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +251 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/index.d.ts +33 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +74 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +4 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +5 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/i18n/zh.d.ts +6 -0
- package/dist/i18n/zh.d.ts.map +1 -0
- package/dist/i18n/zh.js +254 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +10 -9
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +58 -53
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +44 -51
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +4 -2
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +1 -7
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +11 -3
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/index-daemon.js +10 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts +44 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -0
- package/dist/setup/bot-config-editor.js +170 -0
- package/dist/setup/bot-config-editor.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +20 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -27,8 +27,20 @@ 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
29
|
import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
|
|
30
|
+
import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, } from './setup/bot-config-editor.js';
|
|
30
31
|
import { logger } from './utils/logger.js';
|
|
31
32
|
import { firstPositional } from './cli/arg-utils.js';
|
|
33
|
+
import { isLocale, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
|
|
34
|
+
import { readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
|
|
35
|
+
// Resolve the CLI's UI locale once from the global config file, so subsequent
|
|
36
|
+
// CLI output (and any t() callers that don't pass an explicit locale) honour
|
|
37
|
+
// the user's chosen language. Daemon entrypoint sets this separately for the
|
|
38
|
+
// daemon process.
|
|
39
|
+
{
|
|
40
|
+
const cfg = readGlobalConfig();
|
|
41
|
+
if (cfg.lang)
|
|
42
|
+
setDefaultLocale(cfg.lang);
|
|
43
|
+
}
|
|
32
44
|
// CLI subcommands (send/thread/bots/list/etc) print JSON to stdout for
|
|
33
45
|
// callers to parse. Transitive logger.info calls from shared modules would
|
|
34
46
|
// corrupt that stream, so the CLI process runs silent by default. DEBUG=1
|
|
@@ -91,17 +103,29 @@ function runPm2(args, inherit = true, home = PM2_HOME) {
|
|
|
91
103
|
function loadBotsJson() {
|
|
92
104
|
if (existsSync(BOTS_JSON_FILE)) {
|
|
93
105
|
try {
|
|
94
|
-
return
|
|
106
|
+
return parseBotConfigsJson(readFileSync(BOTS_JSON_FILE, 'utf-8'), BOTS_JSON_FILE);
|
|
95
107
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(`❌ ${err?.message ?? String(err)}`);
|
|
110
|
+
process.exit(1);
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
return [];
|
|
101
114
|
}
|
|
115
|
+
function ensureUniqueBotProcessNames(bots) {
|
|
116
|
+
try {
|
|
117
|
+
assertUniqueBotProcessNames(bots, PM2_NAME);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`❌ ${err?.message ?? String(err)}`);
|
|
121
|
+
console.error(' 请修改 bots.json 中的 name,确保进程名唯一。');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
102
125
|
function ecosystemConfig() {
|
|
103
126
|
const daemonScript = join(PKG_ROOT, 'dist', 'index-daemon.js');
|
|
104
127
|
const bots = loadBotsJson();
|
|
128
|
+
ensureUniqueBotProcessNames(bots);
|
|
105
129
|
const baseApp = {
|
|
106
130
|
script: daemonScript,
|
|
107
131
|
cwd: CONFIG_DIR,
|
|
@@ -113,7 +137,7 @@ function ecosystemConfig() {
|
|
|
113
137
|
};
|
|
114
138
|
const apps = bots.map((_bot, i) => ({
|
|
115
139
|
...baseApp,
|
|
116
|
-
name:
|
|
140
|
+
name: botProcessName(_bot, i, PM2_NAME),
|
|
117
141
|
error_file: join(LOG_DIR, `daemon-${i}-error.log`),
|
|
118
142
|
out_file: join(LOG_DIR, `daemon-${i}-out.log`),
|
|
119
143
|
env: { SESSION_DATA_DIR: DATA_DIR, BOTMUX_BOT_INDEX: String(i) },
|
|
@@ -145,10 +169,18 @@ function ask(rl, question) {
|
|
|
145
169
|
return new Promise(resolve => rl.question(question, resolve));
|
|
146
170
|
}
|
|
147
171
|
// ─── Setup helpers ──────────────────────────────────────────────────────────
|
|
172
|
+
function printInputHelp(title, lines) {
|
|
173
|
+
console.log(`\n${title}`);
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
console.log(` ${line}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
148
178
|
// Thin wrapper around setup/bots-store.writeBotsJsonAtomic so call-sites keep
|
|
149
179
|
// the same name without passing BOTS_JSON_FILE explicitly each time.
|
|
150
180
|
function writeBotsJsonAtomic(bots) {
|
|
151
|
-
|
|
181
|
+
const normalized = bots.map(bot => normalizeBotConfig(bot));
|
|
182
|
+
ensureUniqueBotProcessNames(normalized);
|
|
183
|
+
writeBotsAtomic(BOTS_JSON_FILE, normalized);
|
|
152
184
|
}
|
|
153
185
|
/**
|
|
154
186
|
* 从 bot 配置里取 brand. 旧的 bots.json (1.0 之前) 没这个字段, default 到 feishu
|
|
@@ -330,8 +362,15 @@ async function promptBotConfig(rl) {
|
|
|
330
362
|
console.log('✅ 凭证有效(tenant_access_token 已成功获取)\n');
|
|
331
363
|
console.log('支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode');
|
|
332
364
|
const cliChoice = await ask(rl, 'CLI 适配器 [1]: ');
|
|
333
|
-
|
|
334
|
-
|
|
365
|
+
let cliId;
|
|
366
|
+
try {
|
|
367
|
+
cliId = resolveCliId(cliChoice) ?? 'claude-code';
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
console.log(`\n❌ ${err?.message ?? String(err)}`);
|
|
371
|
+
console.log(' 不写 bots.json。请重新运行 botmux setup。');
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
335
374
|
const workingDir = await ask(rl, '默认工作目录 [~]: ');
|
|
336
375
|
// 不再持久化 brand 字段: setup 阶段 brand=lark 直接被 obtainCredentials 中止,
|
|
337
376
|
// 落盘的永远是 'feishu', 写进配置是死字段. 等 lark 完整接入再加回来, 那时
|
|
@@ -350,7 +389,90 @@ async function promptBotConfig(rl) {
|
|
|
350
389
|
// 字段即可. 手动 fallback 场景没 open_id, 字段直接不写 (== 不限制).
|
|
351
390
|
if (creds.userOpenId)
|
|
352
391
|
bot.allowedUsers = [creds.userOpenId];
|
|
353
|
-
return bot;
|
|
392
|
+
return normalizeBotConfig(bot);
|
|
393
|
+
}
|
|
394
|
+
function formatOptionalValue(v) {
|
|
395
|
+
if (Array.isArray(v))
|
|
396
|
+
return v.join(',');
|
|
397
|
+
if (typeof v === 'string' && v)
|
|
398
|
+
return v;
|
|
399
|
+
return '未设置';
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 把 bots.json 渲染成对齐的小表格. 不带行号——进程名 (botmux-N) 已经
|
|
403
|
+
* 是唯一可寻址的标识, 行号 + 进程名后缀 1-based / 0-based 并列容易引
|
|
404
|
+
* 起 off-by-one 误解 (用户曾踩过 "1. botmux-0" 这种排版).
|
|
405
|
+
*
|
|
406
|
+
* 选择机器人时直接输完整进程名 (botmux-N / botmux-custom) 或 AppID,
|
|
407
|
+
* parseBotSelection 不再接受裸数字, 避免又冒出 "序号到底是几" 的歧义.
|
|
408
|
+
*/
|
|
409
|
+
function formatBotConfigTable(bots) {
|
|
410
|
+
if (bots.length === 0)
|
|
411
|
+
return '';
|
|
412
|
+
const headers = ['进程名', 'App ID', 'CLI'];
|
|
413
|
+
const rows = bots.map((b, i) => [
|
|
414
|
+
botProcessName(b, i, PM2_NAME),
|
|
415
|
+
String(b?.larkAppId ?? ''),
|
|
416
|
+
String(b?.cliId ?? 'claude-code'),
|
|
417
|
+
]);
|
|
418
|
+
const widths = headers.map((h, c) => Math.max(displayWidth(h), ...rows.map(r => displayWidth(r[c]))));
|
|
419
|
+
const render = (cells) => ' ' + cells.map((cell, i) => padEndDisplay(cell, widths[i])).join(' ');
|
|
420
|
+
return [render(headers), ...rows.map(render)].join('\n');
|
|
421
|
+
}
|
|
422
|
+
async function promptEditBotConfig(rl, bot) {
|
|
423
|
+
console.log('\n字段留空表示保留当前值;可选字段输入 - 表示清空。\n');
|
|
424
|
+
const input = {};
|
|
425
|
+
printInputHelp('botmux status 显示名称', [
|
|
426
|
+
'可选。用于本机进程名,方便在 botmux status / logs 中识别机器人。',
|
|
427
|
+
'留空保留当前值;输入 - 清空自定义名称并恢复 botmux-<序号>。',
|
|
428
|
+
]);
|
|
429
|
+
input.name = await ask(rl, `botmux status 显示名称 [${formatOptionalValue(bot.name)}]: `);
|
|
430
|
+
printInputHelp('LARK_APP_ID', [
|
|
431
|
+
'飞书开放平台应用的 App ID。修改后,这个配置项会切到另一个飞书应用。',
|
|
432
|
+
'留空保留当前值;修改会二次确认,因为历史会话和群聊状态不会自动迁移。',
|
|
433
|
+
]);
|
|
434
|
+
input.larkAppId = await ask(rl, `LARK_APP_ID [${bot.larkAppId}]: `);
|
|
435
|
+
printInputHelp('LARK_APP_SECRET', [
|
|
436
|
+
'当前 App ID 对应的 App Secret。只更新密钥时填写这一项即可。',
|
|
437
|
+
'留空保留当前值。',
|
|
438
|
+
]);
|
|
439
|
+
input.larkAppSecret = await ask(rl, `LARK_APP_SECRET [保留当前值]: `);
|
|
440
|
+
console.log('\n支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode');
|
|
441
|
+
printInputHelp('CLI 适配器', [
|
|
442
|
+
'选择 botmux 需要套用哪一种 CLI 参数协议和会话恢复方式。',
|
|
443
|
+
'留空保留当前值;可以输入序号,也可以直接输入适配器 ID。',
|
|
444
|
+
]);
|
|
445
|
+
input.cliChoice = await ask(rl, `CLI 适配器 [${bot.cliId ?? 'claude-code'}]: `);
|
|
446
|
+
printInputHelp('CLI 可执行文件路径覆盖', [
|
|
447
|
+
'可选。CLI 入口的绝对路径,用于在原 CLI 外面套一层 wrapper / router。',
|
|
448
|
+
'典型场景:ccr (Claude Code Router) / claude-w / aiden-x-claude 等自定义入口。',
|
|
449
|
+
'留空保留当前值;输入 - 清空覆盖,回到 PATH 查 cliId 对应的默认二进制。',
|
|
450
|
+
]);
|
|
451
|
+
input.cliPathOverride = await ask(rl, `CLI 可执行文件路径覆盖 [${formatOptionalValue(bot.cliPathOverride)}]: `);
|
|
452
|
+
printInputHelp('会话后端 backendType', [
|
|
453
|
+
'可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着。',
|
|
454
|
+
'留空保留当前值;输入 - 回到自动检测;只接受 pty 或 tmux。',
|
|
455
|
+
]);
|
|
456
|
+
input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `);
|
|
457
|
+
printInputHelp('默认工作目录', [
|
|
458
|
+
'可选。新会话默认进入的目录,支持逗号分隔多个候选目录。',
|
|
459
|
+
'留空保留当前值;输入 - 清空并回到默认 ~。',
|
|
460
|
+
]);
|
|
461
|
+
input.workingDir = await ask(rl, `默认工作目录 [${formatOptionalValue(bot.workingDir)}]: `);
|
|
462
|
+
printInputHelp('允许的用户', [
|
|
463
|
+
'可选。限制哪些飞书用户可以操作机器人,支持邮箱前缀或 open_id,多个值用逗号分隔。',
|
|
464
|
+
'留空保留当前值;输入 - 清空限制。',
|
|
465
|
+
]);
|
|
466
|
+
input.allowedUsers = await ask(rl, `允许的用户 [${formatOptionalValue(bot.allowedUsers)}]: `);
|
|
467
|
+
const edited = applyBotConfigEdits(bot, input);
|
|
468
|
+
if (edited.larkAppId !== bot.larkAppId) {
|
|
469
|
+
console.log('\n⚠️ LARK_APP_ID 变更后,旧 appId 下的历史会话/群聊状态数据不会自动迁移。');
|
|
470
|
+
const confirm = (await ask(rl, `确认将 LARK_APP_ID 从 ${bot.larkAppId} 改为 ${edited.larkAppId}? (y/N): `)).trim().toLowerCase();
|
|
471
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
472
|
+
edited.larkAppId = bot.larkAppId;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return edited;
|
|
354
476
|
}
|
|
355
477
|
/** Parse .env file to extract bot config for migration to bots.json */
|
|
356
478
|
function parseDotEnvToBotConfig() {
|
|
@@ -371,16 +493,14 @@ function parseDotEnvToBotConfig() {
|
|
|
371
493
|
};
|
|
372
494
|
if (vars.CLI_ID)
|
|
373
495
|
bot.cliId = vars.CLI_ID;
|
|
374
|
-
if (vars.CLI_PATH)
|
|
375
|
-
bot.cliPathOverride = vars.CLI_PATH;
|
|
496
|
+
if (vars.CLI_PATH?.trim())
|
|
497
|
+
bot.cliPathOverride = vars.CLI_PATH.trim();
|
|
376
498
|
if (vars.BACKEND_TYPE)
|
|
377
499
|
bot.backendType = vars.BACKEND_TYPE;
|
|
378
500
|
if (vars.WORKING_DIR)
|
|
379
501
|
bot.workingDir = vars.WORKING_DIR;
|
|
380
502
|
if (vars.ALLOWED_USERS)
|
|
381
503
|
bot.allowedUsers = vars.ALLOWED_USERS.split(',').map((s) => s.trim()).filter(Boolean);
|
|
382
|
-
if (vars.PROJECT_SCAN_DIR)
|
|
383
|
-
bot.projectScanDir = vars.PROJECT_SCAN_DIR;
|
|
384
504
|
return bot;
|
|
385
505
|
}
|
|
386
506
|
/**
|
|
@@ -413,14 +533,12 @@ async function cmdSetup() {
|
|
|
413
533
|
console.log(`数据目录: ${DATA_DIR}\n`);
|
|
414
534
|
if (hasBots) {
|
|
415
535
|
// --- Multi-bot mode (bots.json exists) ---
|
|
416
|
-
const bots =
|
|
417
|
-
console.log(`已配置 ${bots.length}
|
|
418
|
-
|
|
419
|
-
console.log(` ${i + 1}. ${bots[i].larkAppId} (${bots[i].cliId ?? 'claude-code'})`);
|
|
420
|
-
}
|
|
536
|
+
const bots = loadBotsJson();
|
|
537
|
+
console.log(`已配置 ${bots.length} 个机器人:\n`);
|
|
538
|
+
console.log(formatBotConfigTable(bots));
|
|
421
539
|
console.log('');
|
|
422
540
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
423
|
-
const action = await ask(rl, '操作: 1) 添加新机器人 2) 重新配置 (1/2) [1]: ');
|
|
541
|
+
const action = await ask(rl, '操作: 1) 添加新机器人 2) 重新配置 3) 编辑现有机器人 4) 删除机器人 (1/2/3/4) [1]: ');
|
|
424
542
|
if (action === '2') {
|
|
425
543
|
console.log('\n── 重新配置 ──\n');
|
|
426
544
|
const newBot = await promptBotConfig(rl);
|
|
@@ -440,6 +558,79 @@ async function cmdSetup() {
|
|
|
440
558
|
console.log(`下一步: botmux restart\n`);
|
|
441
559
|
return;
|
|
442
560
|
}
|
|
561
|
+
if (action === '3') {
|
|
562
|
+
console.log('\n── 编辑现有机器人 ──\n');
|
|
563
|
+
const selected = await ask(rl, '选择机器人(进程名 或 AppID): ');
|
|
564
|
+
const index = parseBotSelection(selected, bots);
|
|
565
|
+
if (index === undefined) {
|
|
566
|
+
rl.close();
|
|
567
|
+
console.log('\n❌ 找不到指定机器人,配置未修改。');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const original = bots[index];
|
|
571
|
+
let edited;
|
|
572
|
+
try {
|
|
573
|
+
edited = await promptEditBotConfig(rl, original);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
rl.close();
|
|
577
|
+
console.log(`\n❌ 编辑失败: ${err?.message ?? String(err)}`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// 凭证字段有变化时, 像 promptBotConfig 一样跑一次 tenant_access_token
|
|
581
|
+
// 校验. 失败不写盘——避免编辑后 typo 一个字符, daemon 重启时才发现.
|
|
582
|
+
// (cmdRestart 不校验凭证, 只 cmdStart 校验, 所以编辑路径必须自己兜.)
|
|
583
|
+
const appIdChanged = edited.larkAppId !== original.larkAppId;
|
|
584
|
+
const appSecretChanged = edited.larkAppSecret !== original.larkAppSecret;
|
|
585
|
+
if (appIdChanged || appSecretChanged) {
|
|
586
|
+
console.log('\n校验新凭证(取 tenant_access_token)…');
|
|
587
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
588
|
+
const v = await validateCredentials(edited.larkAppId, edited.larkAppSecret, botBrand(edited));
|
|
589
|
+
if (!v.ok) {
|
|
590
|
+
rl.close();
|
|
591
|
+
console.log(`\n❌ 凭证校验失败 (${v.error}): ${v.message}`);
|
|
592
|
+
console.log(' 配置未修改。请重新运行 botmux setup → 编辑现有机器人。');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
console.log('✅ 凭证有效\n');
|
|
596
|
+
}
|
|
597
|
+
rl.close();
|
|
598
|
+
const nextBots = bots.slice();
|
|
599
|
+
nextBots[index] = edited;
|
|
600
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
601
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
602
|
+
writeBotsJsonAtomic(nextBots);
|
|
603
|
+
console.log(`✅ 已更新机器人 ${botProcessName(edited, index, PM2_NAME)} (${edited.larkAppId})`);
|
|
604
|
+
// appId 切换 = 换了一个飞书应用, 新 appId 大概率需要重新申请权限 + 配重定向 URL.
|
|
605
|
+
// 把 printRemainingSteps 的深链端给用户, 比 README 警告里那句"历史数据不迁移"更可操作.
|
|
606
|
+
if (appIdChanged) {
|
|
607
|
+
printRemainingSteps(edited.larkAppId, botBrand(edited));
|
|
608
|
+
}
|
|
609
|
+
console.log(`下一步: botmux restart\n`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (action === '4') {
|
|
613
|
+
console.log('\n── 删除机器人 ──\n');
|
|
614
|
+
const selected = await ask(rl, '选择机器人(进程名 或 AppID): ');
|
|
615
|
+
const result = removeBotConfig(bots, selected);
|
|
616
|
+
if (!result) {
|
|
617
|
+
rl.close();
|
|
618
|
+
console.log('\n❌ 找不到指定机器人,配置未修改。');
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const confirm = (await ask(rl, `确认删除 ${botProcessName(result.removed, result.index, PM2_NAME)} (${result.removed.larkAppId})? (y/N): `)).trim().toLowerCase();
|
|
622
|
+
rl.close();
|
|
623
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
624
|
+
console.log('\n已取消,配置未修改。');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
628
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
629
|
+
writeBotsJsonAtomic(result.bots);
|
|
630
|
+
console.log(`✅ 已删除机器人 ${botProcessName(result.removed, result.index, PM2_NAME)} (${result.removed.larkAppId})`);
|
|
631
|
+
console.log(`下一步: botmux restart\n`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
443
634
|
console.log('\n── 添加新机器人 ──\n');
|
|
444
635
|
const newBot = await promptBotConfig(rl);
|
|
445
636
|
rl.close();
|
|
@@ -746,12 +937,12 @@ async function cmdRestart() {
|
|
|
746
937
|
ensureConfigDir();
|
|
747
938
|
preflightNodeSanity();
|
|
748
939
|
await ensureSystemDependencies();
|
|
940
|
+
const cfg = ecosystemConfig();
|
|
749
941
|
cleanupLegacyPm2();
|
|
750
942
|
// Delete all botmux processes (handles both old single-process and new multi-process)
|
|
751
943
|
deleteAllBotmuxProcesses();
|
|
752
944
|
// Wipe abandoned dashboard-daemon descriptors left behind by killed daemons.
|
|
753
945
|
cleanupStaleDaemonDescriptors();
|
|
754
|
-
const cfg = ecosystemConfig();
|
|
755
946
|
runPm2(['start', cfg]);
|
|
756
947
|
if (refreshAutostart({ pkgRoot: PKG_ROOT, configDir: CONFIG_DIR, logDir: LOG_DIR })) {
|
|
757
948
|
console.log(`autostart unit 已同步到当前 Node/cli.js 路径`);
|
|
@@ -819,13 +1010,23 @@ function cmdLogs() {
|
|
|
819
1010
|
? process.argv[process.argv.indexOf('--lines') + 1] || '50'
|
|
820
1011
|
: '50';
|
|
821
1012
|
const bots = loadBotsJson();
|
|
822
|
-
// Support --bot <index> to filter specific bot logs
|
|
1013
|
+
// Support --bot <0-based-index|pm2-name|appId> to filter specific bot logs.
|
|
823
1014
|
const botIdx = process.argv.includes('--bot')
|
|
824
1015
|
? process.argv[process.argv.indexOf('--bot') + 1]
|
|
825
1016
|
: undefined;
|
|
826
1017
|
let target;
|
|
827
1018
|
if (botIdx !== undefined) {
|
|
828
|
-
|
|
1019
|
+
const numericIdx = /^\d+$/.test(botIdx) ? Number(botIdx) : undefined;
|
|
1020
|
+
const selectedIdx = numericIdx === undefined
|
|
1021
|
+
? parseBotSelection(botIdx, bots)
|
|
1022
|
+
: numericIdx >= 0 && numericIdx < bots.length
|
|
1023
|
+
? numericIdx
|
|
1024
|
+
: undefined;
|
|
1025
|
+
target = selectedIdx !== undefined
|
|
1026
|
+
? botProcessName(bots[selectedIdx], selectedIdx, PM2_NAME)
|
|
1027
|
+
: numericIdx !== undefined
|
|
1028
|
+
? `${PM2_NAME}-${botIdx}`
|
|
1029
|
+
: botIdx;
|
|
829
1030
|
}
|
|
830
1031
|
else {
|
|
831
1032
|
// Show all botmux logs via pm2 regex match
|
|
@@ -1633,7 +1834,7 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
1633
1834
|
start 启动 daemon
|
|
1634
1835
|
stop 停止 daemon
|
|
1635
1836
|
restart 重启 daemon(自动恢复活跃会话)
|
|
1636
|
-
logs 查看 daemon 日志(--lines N, --bot <index>)
|
|
1837
|
+
logs 查看 daemon 日志(--lines N, --bot <0-based-index|pm2-name|appId>)
|
|
1637
1838
|
status 查看 daemon 状态
|
|
1638
1839
|
upgrade 升级到最新版本
|
|
1639
1840
|
dashboard 打印新的 Web Dashboard 一次性登录 URL(旧 token 同时失效)
|
|
@@ -1647,6 +1848,9 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
1647
1848
|
autostart enable 注册开机自启(macOS launchd / Linux user systemd,无需 sudo)
|
|
1648
1849
|
autostart disable 注销开机自启
|
|
1649
1850
|
autostart status 查看自启状态
|
|
1851
|
+
lang [zh|en] 切换 UI 语言(无参 = 查看当前设置)
|
|
1852
|
+
--bot N 仅改 bots.json 中第 N 个 bot 的 lang
|
|
1853
|
+
--unset 清除(global 或 --bot N 配合)
|
|
1650
1854
|
|
|
1651
1855
|
定时任务(可在 CLI 会话内自动推断 chat):
|
|
1652
1856
|
schedule list 列出所有任务
|
|
@@ -2591,15 +2795,11 @@ async function cmdBots(sub, rest) {
|
|
|
2591
2795
|
botEntries = JSON.parse(readFileSync(botInfoPath, 'utf-8'));
|
|
2592
2796
|
}
|
|
2593
2797
|
catch { /* */ }
|
|
2594
|
-
const botByCli = new Map();
|
|
2595
|
-
for (const b of botEntries)
|
|
2596
|
-
botByCli.set(b.cliId, b);
|
|
2597
2798
|
try {
|
|
2598
2799
|
const { listChatBotMembers } = await import('./im/lark/client.js');
|
|
2599
2800
|
const chatBots = await listChatBotMembers(appId, s.chatId);
|
|
2600
2801
|
const result = chatBots.map(cb => {
|
|
2601
|
-
|
|
2602
|
-
return { name: cb.displayName, openId: cb.openId, isSelf: info?.larkAppId === appId };
|
|
2802
|
+
return { name: cb.displayName, openId: cb.openId, isSelf: cb.larkAppId === appId };
|
|
2603
2803
|
});
|
|
2604
2804
|
console.log(JSON.stringify({ sessionId: sid, chatId: s.chatId, bots: result, total: result.length }, null, 2));
|
|
2605
2805
|
}
|
|
@@ -2611,6 +2811,93 @@ async function cmdBots(sub, rest) {
|
|
|
2611
2811
|
console.log(JSON.stringify({ sessionId: sid, bots: result, total: result.length, note: `chat query failed: ${err.message}` }, null, 2));
|
|
2612
2812
|
}
|
|
2613
2813
|
}
|
|
2814
|
+
// ─── botmux lang ─────────────────────────────────────────────────────────────
|
|
2815
|
+
/**
|
|
2816
|
+
* `botmux lang [zh|en] [--bot N] [--unset]`
|
|
2817
|
+
*
|
|
2818
|
+
* No arg → print effective locale + per-bot overrides.
|
|
2819
|
+
* `zh|en` → write global `~/.botmux/config.json` (or, with `--bot N`, write
|
|
2820
|
+
* the per-bot `lang` field in `bots.json`).
|
|
2821
|
+
* `--unset` → clear the global config's `lang` (or, with `--bot N`, drop
|
|
2822
|
+
* the per-bot override).
|
|
2823
|
+
*
|
|
2824
|
+
* On any write, hint the user to `botmux restart` so live daemons pick it up.
|
|
2825
|
+
*/
|
|
2826
|
+
function cmdLang(args) {
|
|
2827
|
+
ensureConfigDir();
|
|
2828
|
+
const cfg = readGlobalConfig();
|
|
2829
|
+
const globalLang = cfg.lang;
|
|
2830
|
+
const botFlagIdx = args.indexOf('--bot');
|
|
2831
|
+
const botFlag = botFlagIdx >= 0 ? parseInt(args[botFlagIdx + 1] ?? '', 10) : NaN;
|
|
2832
|
+
const unset = args.includes('--unset');
|
|
2833
|
+
const positional = args.filter((a, i) => {
|
|
2834
|
+
if (a === '--bot')
|
|
2835
|
+
return false;
|
|
2836
|
+
if (i > 0 && args[i - 1] === '--bot')
|
|
2837
|
+
return false;
|
|
2838
|
+
if (a === '--unset')
|
|
2839
|
+
return false;
|
|
2840
|
+
return true;
|
|
2841
|
+
});
|
|
2842
|
+
const target = positional[0]?.toLowerCase();
|
|
2843
|
+
// No-arg → status
|
|
2844
|
+
if (!target && !unset) {
|
|
2845
|
+
const bots = loadBotsJson();
|
|
2846
|
+
const effective = globalLang ?? 'zh';
|
|
2847
|
+
console.log(`Global lang: ${globalLang ?? '(unset, defaults to zh)'}`);
|
|
2848
|
+
console.log(`Effective for CLI: ${effective}`);
|
|
2849
|
+
console.log(`Config file: ${globalConfigPath()}`);
|
|
2850
|
+
if (bots.length > 0) {
|
|
2851
|
+
console.log('\nPer-bot:');
|
|
2852
|
+
bots.forEach((b, i) => {
|
|
2853
|
+
const explicit = isLocale(b.lang) ? b.lang : undefined;
|
|
2854
|
+
const eff = explicit ?? effective;
|
|
2855
|
+
const tag = explicit ? `${explicit} (explicit override)` : `${eff} (inherits global)`;
|
|
2856
|
+
console.log(` ${i}. ${b.larkAppId} → ${tag}`);
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
// Per-bot operations require an existing bots.json index.
|
|
2862
|
+
if (!isNaN(botFlag)) {
|
|
2863
|
+
const bots = loadBotsJson();
|
|
2864
|
+
if (botFlag < 0 || botFlag >= bots.length) {
|
|
2865
|
+
console.error(`--bot index out of range; bots.json has ${bots.length} entry(ies). Use \`botmux lang\` to see indices.`);
|
|
2866
|
+
process.exit(1);
|
|
2867
|
+
}
|
|
2868
|
+
if (unset) {
|
|
2869
|
+
delete bots[botFlag].lang;
|
|
2870
|
+
writeBotsJsonAtomic(bots);
|
|
2871
|
+
console.log(`✅ Cleared per-bot lang for bot ${botFlag} (${bots[botFlag].larkAppId}).`);
|
|
2872
|
+
}
|
|
2873
|
+
else {
|
|
2874
|
+
if (!isLocale(target)) {
|
|
2875
|
+
console.error(`Unknown locale "${target}". Supported: ${SUPPORTED_LOCALES.join(', ')}.`);
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
}
|
|
2878
|
+
bots[botFlag].lang = target;
|
|
2879
|
+
writeBotsJsonAtomic(bots);
|
|
2880
|
+
console.log(`✅ Set bot ${botFlag} (${bots[botFlag].larkAppId}) lang → ${target}.`);
|
|
2881
|
+
}
|
|
2882
|
+
console.log(`Run \`botmux restart\` for changes to take effect.`);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
// Global operations
|
|
2886
|
+
if (unset) {
|
|
2887
|
+
setGlobalLocale(null);
|
|
2888
|
+
console.log(`✅ Cleared global lang (will default to zh).`);
|
|
2889
|
+
console.log(`Run \`botmux restart\` for changes to take effect.`);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (!isLocale(target)) {
|
|
2893
|
+
console.error(`Unknown locale "${target}". Supported: ${SUPPORTED_LOCALES.join(', ')}.`);
|
|
2894
|
+
console.error(`Usage: botmux lang [zh|en] [--bot N] [--unset]`);
|
|
2895
|
+
process.exit(1);
|
|
2896
|
+
}
|
|
2897
|
+
setGlobalLocale(target);
|
|
2898
|
+
console.log(`✅ Set global lang → ${target}.`);
|
|
2899
|
+
console.log(`Run \`botmux restart\` for changes to take effect.`);
|
|
2900
|
+
}
|
|
2614
2901
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
2615
2902
|
function getVersion() {
|
|
2616
2903
|
const pkgPath = join(PKG_ROOT, 'package.json');
|
|
@@ -2682,6 +2969,9 @@ switch (command) {
|
|
|
2682
2969
|
case 'quoted':
|
|
2683
2970
|
await cmdQuoted(process.argv.slice(3));
|
|
2684
2971
|
break;
|
|
2972
|
+
case 'lang':
|
|
2973
|
+
cmdLang(process.argv.slice(3));
|
|
2974
|
+
break;
|
|
2685
2975
|
case 'thread': {
|
|
2686
2976
|
// Removed in favor of `botmux history` (普通群也兼容). Friendly stderr so
|
|
2687
2977
|
// pre-rename scripts/skills surface the rename instead of "unknown command".
|