botmux 2.99.0 → 2.101.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 +33 -7
- package/README.md +33 -7
- package/dist/adapters/backend/sandbox.d.ts +33 -4
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +90 -29
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/cli/hermes.d.ts.map +1 -1
- package/dist/adapters/cli/hermes.js +6 -0
- package/dist/adapters/cli/hermes.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +241 -13
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +8 -6
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +10 -3
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +12 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +10 -3
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +659 -154
- package/dist/cli.js.map +1 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +26 -36
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +37 -2
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +87 -17
- 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 +6 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +24 -8
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +6 -0
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +3 -1
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +24 -3
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +14 -4
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +5 -19
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard-web/app.js +40 -33
- package/dist/dashboard-web/chunks/{bot-defaults-page-3EV7K6LJ.js → bot-defaults-page-HOGLABBC.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-NASYW5FD.js → chunk-ECXPBQ3J.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-MR57RUVP.js +31 -0
- package/dist/dashboard-web/chunks/{connectors-XPZKEHF6.js → connectors-UGTF4Q4I.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-C54PVKYP.js → groups-page-IKTCMQ4A.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-BBQYR7WY.js → insights-page-7CCSHEKM.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-5PJCAMID.js → overview-page-JOJW65P5.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-APUDLPVM.js → roles-page-E2VN2DLV.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-WG2UKHSO.js → schedules-6QIHJIHX.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-QN2TARF2.js +238 -0
- package/dist/dashboard-web/chunks/{settings-page-OSBZNCXL.js → settings-page-WWTXB577.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-Y3VUAOYG.js → skills-page-2FAUPQ6H.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-CRH3WZPH.js → team-federation-page-PTBQQSFS.js} +1 -1
- package/dist/dashboard-web/chunks/{workflows-UB7OPYGI.js → workflows-ZNBB5KFV.js} +1 -1
- package/dist/dashboard.js +86 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +2 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -2
- package/dist/im/lark/card-builder.js +2 -2
- package/dist/im/lark/event-dispatcher.d.ts +8 -3
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +45 -9
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/platform/bind.d.ts.map +1 -1
- package/dist/platform/bind.js +36 -15
- package/dist/platform/bind.js.map +1 -1
- package/dist/platform/binding.d.ts +5 -0
- package/dist/platform/binding.d.ts.map +1 -1
- package/dist/platform/binding.js +11 -0
- package/dist/platform/binding.js.map +1 -1
- package/dist/platform/platform-http.d.ts +11 -0
- package/dist/platform/platform-http.d.ts.map +1 -0
- package/dist/platform/platform-http.js +41 -0
- package/dist/platform/platform-http.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +14 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js +40 -3
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/bot-union-ids-store.d.ts +9 -0
- package/dist/services/bot-union-ids-store.d.ts.map +1 -0
- package/dist/services/bot-union-ids-store.js +59 -0
- package/dist/services/bot-union-ids-store.js.map +1 -0
- package/dist/services/coco-transcript.d.ts.map +1 -1
- package/dist/services/coco-transcript.js +59 -64
- package/dist/services/coco-transcript.js.map +1 -1
- package/dist/services/jsonl-cursor.d.ts +12 -0
- package/dist/services/jsonl-cursor.d.ts.map +1 -1
- package/dist/services/jsonl-cursor.js +63 -4
- package/dist/services/jsonl-cursor.js.map +1 -1
- package/dist/services/opencode-paths.d.ts +7 -0
- package/dist/services/opencode-paths.d.ts.map +1 -0
- package/dist/services/opencode-paths.js +18 -0
- package/dist/services/opencode-paths.js.map +1 -0
- package/dist/services/platform-team-store.d.ts +37 -0
- package/dist/services/platform-team-store.d.ts.map +1 -0
- package/dist/services/platform-team-store.js +120 -0
- package/dist/services/platform-team-store.js.map +1 -0
- package/dist/services/team-groups-store.d.ts +14 -1
- package/dist/services/team-groups-store.d.ts.map +1 -1
- package/dist/services/team-groups-store.js +28 -1
- package/dist/services/team-groups-store.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts +6 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +18 -15
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/interactive-select.d.ts +22 -0
- package/dist/setup/interactive-select.d.ts.map +1 -1
- package/dist/setup/interactive-select.js +101 -7
- package/dist/setup/interactive-select.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +42 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +107 -0
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/setup-args.d.ts +77 -0
- package/dist/setup/setup-args.d.ts.map +1 -0
- package/dist/setup/setup-args.js +272 -0
- package/dist/setup/setup-args.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +12 -2
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js +4 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/chunk-JXLHRRJD.js +0 -31
- package/dist/dashboard-web/chunks/sessions-page-F3U5NFPD.js +0 -238
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* botmux setup — interactive first-time configuration
|
|
7
7
|
* botmux setup --no-open-platform-auto — skip Feishu Open Platform automation
|
|
8
|
+
* botmux setup list|add|edit|remove — scripted (non-TUI) bot management, see `botmux setup help`
|
|
8
9
|
* botmux start — start daemon (pm2)
|
|
9
10
|
* botmux stop — stop daemon
|
|
10
11
|
* botmux restart [--include-pm2] — restart daemon (optionally restart PM2 God too)
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
import { execSync, execFileSync, spawnSync, spawn } from 'node:child_process';
|
|
22
23
|
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
|
|
23
24
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
24
|
-
import { join, dirname, basename } from 'node:path';
|
|
25
|
+
import { join, dirname, basename, resolve } from 'node:path';
|
|
25
26
|
import { homedir } from 'node:os';
|
|
26
27
|
import { fileURLToPath } from 'node:url';
|
|
27
28
|
import { createInterface } from 'node:readline';
|
|
@@ -35,10 +36,11 @@ import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
|
35
36
|
import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
|
|
36
37
|
import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, assertOwnerWhenChatGroups, findInvalidAllowedUserEntries, hasOwnerEntry, } from './setup/bot-config-editor.js';
|
|
37
38
|
import { resolveCliSelection, selectionKeyForBot } from './setup/cli-selection.js';
|
|
38
|
-
import {
|
|
39
|
+
import { buildBotFromAddFlags, editInputFromFlags, isScriptedSetupInvocation, maskAppSecret, parseSetupCommand, SETUP_CLI_USAGE, } from './setup/setup-args.js';
|
|
40
|
+
import { interactiveSelect, pickChoice, pickCliSelection } from './setup/interactive-select.js';
|
|
39
41
|
import { buildPreset, serializePreset, presetFilename } from './setup/agent-preset.js';
|
|
40
42
|
import { logger } from './utils/logger.js';
|
|
41
|
-
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
43
|
+
import { expandHomePath, invalidWorkingDirs } from './utils/working-dir.js';
|
|
42
44
|
import { firstPositional } from './cli/arg-utils.js';
|
|
43
45
|
import { dispatchPrimaryMessage, findStdinAliasAttachment, sendFileAttachments } from './cli/send-dispatch.js';
|
|
44
46
|
import { buildPm2SpawnCommand } from './cli/pm2-command.js';
|
|
@@ -240,6 +242,22 @@ function ensureBotWorkingDirsExist(bot, context = 'workingDir') {
|
|
|
240
242
|
console.log(' 请先创建目录,或重新填写一个已存在的工作目录。');
|
|
241
243
|
return false;
|
|
242
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* 固定默认目录(defaultWorkingDir)写盘前的存在性校验。运行时 daemon 对无效
|
|
247
|
+
* defaultWorkingDir 只是 WARN 后回退弹仓库选择卡,用户很难察觉配置根本没生效,
|
|
248
|
+
* 所以 setup 侧必须在写盘前就挡下来。未配置视为通过。
|
|
249
|
+
*/
|
|
250
|
+
function ensureBotDefaultWorkingDirExists(bot) {
|
|
251
|
+
const raw = typeof bot.defaultWorkingDir === 'string' ? bot.defaultWorkingDir.trim() : '';
|
|
252
|
+
if (!raw)
|
|
253
|
+
return true;
|
|
254
|
+
const missing = missingDirResolved(raw);
|
|
255
|
+
if (!missing)
|
|
256
|
+
return true;
|
|
257
|
+
console.log(`\n❌ 固定默认目录不存在或不是目录: ${missing}`);
|
|
258
|
+
console.log(' 请先创建目录,或改用仓库选择卡片模式。');
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
243
261
|
function ensureUniqueBotProcessNames(bots) {
|
|
244
262
|
try {
|
|
245
263
|
assertUniqueBotProcessNames(bots, PM2_NAME);
|
|
@@ -490,10 +508,78 @@ async function finishOpenPlatformSetup(appId, brand) {
|
|
|
490
508
|
printRemainingSteps(appId, brand);
|
|
491
509
|
}
|
|
492
510
|
/**
|
|
493
|
-
*
|
|
511
|
+
* 「选择已有应用」路径:复用/扫码飞书 Web 登录态 → 拉当前账号可见的自建应用
|
|
512
|
+
* 列表 → 交互选择 → 自动读取该应用的 AppSecret。仅支持飞书 (feishu.cn) 租户
|
|
513
|
+
* (Web console 机制所限)。
|
|
494
514
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
515
|
+
* 失败返回区分两类,调用方据此导航:
|
|
516
|
+
* - back — 用户主动退出(列表 Esc / 放弃手动粘 secret)→ 回「飞书应用来源」
|
|
517
|
+
* - failed — 技术性失败(登录 / 列表 / console 访问)→ 提示后回「飞书应用来源」
|
|
518
|
+
*/
|
|
519
|
+
async function pickExistingAppCredentials(rl) {
|
|
520
|
+
const { prepareFeishuWebSession, createOpenPlatformApiClient, listOpenPlatformApps, fetchOpenPlatformAppSecret, } = await import('./setup/open-platform-automation.js');
|
|
521
|
+
console.log('\n获取飞书 Web 登录态(复用上次登录,过期则需重新扫码)…');
|
|
522
|
+
const prepared = await prepareFeishuWebSession({
|
|
523
|
+
onQrCode: (info) => {
|
|
524
|
+
process.stderr.write('\n请用飞书 App 扫码登录,以读取你创建过的应用列表:\n\n');
|
|
525
|
+
process.stderr.write(`${info.qrText}\n`);
|
|
526
|
+
},
|
|
527
|
+
onStatus: (message) => { process.stderr.write(`${message}\n`); },
|
|
528
|
+
});
|
|
529
|
+
if (!prepared.ok) {
|
|
530
|
+
console.log(`⚠️ 飞书 Web 登录失败 (${prepared.reason}): ${prepared.message}`);
|
|
531
|
+
return { ok: false, reason: 'failed' };
|
|
532
|
+
}
|
|
533
|
+
const clientRes = await createOpenPlatformApiClient(prepared.cookies);
|
|
534
|
+
if (!clientRes.ok) {
|
|
535
|
+
console.log(`⚠️ 开放平台访问失败 (${clientRes.reason}): ${clientRes.message}`);
|
|
536
|
+
return { ok: false, reason: 'failed' };
|
|
537
|
+
}
|
|
538
|
+
let apps;
|
|
539
|
+
try {
|
|
540
|
+
apps = await listOpenPlatformApps(clientRes.client);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
console.log(`⚠️ 拉取应用列表失败: ${err?.message ?? String(err)}`);
|
|
544
|
+
return { ok: false, reason: 'failed' };
|
|
545
|
+
}
|
|
546
|
+
if (apps.length === 0) {
|
|
547
|
+
console.log('⚠️ 当前账号名下没有可选的自建应用。');
|
|
548
|
+
return { ok: false, reason: 'failed' };
|
|
549
|
+
}
|
|
550
|
+
// 已在 bots.json 里的应用打标——可以重复选(比如换机器重配),但要让人知道。
|
|
551
|
+
const configured = new Set(loadBotsJson().map(b => b?.larkAppId));
|
|
552
|
+
const idx = await pickChoice(rl, {
|
|
553
|
+
title: '选择已有应用',
|
|
554
|
+
items: apps.map(a => ({
|
|
555
|
+
label: a.name,
|
|
556
|
+
hint: `${a.clientId}${configured.has(a.clientId) ? ' · 已在 bots.json' : ''}`,
|
|
557
|
+
})),
|
|
558
|
+
footer: 'Esc 返回上一步',
|
|
559
|
+
});
|
|
560
|
+
if (idx === null)
|
|
561
|
+
return { ok: false, reason: 'back' };
|
|
562
|
+
const app = apps[idx];
|
|
563
|
+
try {
|
|
564
|
+
const appSecret = await fetchOpenPlatformAppSecret(clientRes.client, app.clientId);
|
|
565
|
+
console.log(`✅ 已选择 ${app.name} (${app.clientId}),AppSecret 已自动获取`);
|
|
566
|
+
return { ok: true, appId: app.clientId, appSecret, brand: 'feishu' };
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
console.log(`⚠️ 自动读取 AppSecret 失败: ${err?.message ?? String(err)}`);
|
|
570
|
+
const manual = (await ask(rl, `请手动粘贴 ${app.clientId} 的 AppSecret(留空返回上一步): `)).trim();
|
|
571
|
+
if (!manual)
|
|
572
|
+
return { ok: false, reason: 'back' };
|
|
573
|
+
return { ok: true, appId: app.clientId, appSecret: manual, brand: 'feishu' };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 拿应用凭证:扫码创建新应用 / 选择已有应用 / 手动输入,三选一。
|
|
578
|
+
*
|
|
579
|
+
* 导航语义(TTY):子界面 Esc / 主动放弃一律**返回「飞书应用来源」菜单**,
|
|
580
|
+
* 只有在来源菜单本身 Esc(或扫码时 Ctrl-C)才取消整个 setup;技术性失败
|
|
581
|
+
* 提示后同样回到来源菜单,让用户改走其他方式。非 TTY 没有 Esc,保持
|
|
582
|
+
* 旧的「失败降级手动输入」直落语义,避免菜单循环在管道输入下打转。
|
|
497
583
|
*
|
|
498
584
|
* Codex review 边界:
|
|
499
585
|
* - secret 不进 argv / 日志 / 错误链 (registerApp 内部 safeMsg 已做; 手动模式下
|
|
@@ -501,54 +587,90 @@ async function finishOpenPlatformSetup(appId, brand) {
|
|
|
501
587
|
* - 任何失败都返回结构化对象, 不抛 (调用方根据 ok=false 回退)
|
|
502
588
|
*/
|
|
503
589
|
async function obtainCredentials(rl) {
|
|
504
|
-
|
|
505
|
-
console.log('
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
590
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
591
|
+
console.log('── 飞书应用 ──\n');
|
|
592
|
+
for (;;) {
|
|
593
|
+
const method = await pickChoice(rl, {
|
|
594
|
+
title: '飞书应用来源',
|
|
595
|
+
items: [
|
|
596
|
+
{ label: '扫码创建新应用(推荐)', hint: '飞书 App 扫码,自动创建并拿到 AppID/Secret' },
|
|
597
|
+
{ label: '选择已有应用', hint: '飞书 Web 登录列出你创建过的应用,自动取 AppID/Secret(仅飞书租户)' },
|
|
598
|
+
{ label: '手动输入 AppID/Secret', hint: '已在开放平台创建好应用' },
|
|
599
|
+
],
|
|
600
|
+
defaultIndex: 0,
|
|
601
|
+
footer: 'Esc 取消 setup',
|
|
602
|
+
});
|
|
603
|
+
if (method === null)
|
|
604
|
+
return { ok: false, reason: 'cancelled' };
|
|
605
|
+
if (method === 0) {
|
|
606
|
+
// 动态导入避免冷启动加载 SDK
|
|
607
|
+
const { tryRegisterApp } = await import('./setup/register-app.js');
|
|
608
|
+
const result = await tryRegisterApp();
|
|
609
|
+
if (result.ok) {
|
|
610
|
+
// brand 由扫码 device flow 的 tenant_brand 自动识别(registerApp 内部已
|
|
611
|
+
// 切到对应域名轮询)。feishu / lark 都直接落盘——daemon 链路全程从
|
|
612
|
+
// BotConfig.brand 派生 host(Client / WSClient domain、裸 fetch、深链)。
|
|
613
|
+
console.log(`\n✅ 应用创建成功`);
|
|
614
|
+
console.log(` App ID: ${result.appId}`);
|
|
615
|
+
console.log(` 租户类型: ${result.brand === 'lark' ? 'Lark 国际版 (larksuite.com)' : '飞书 (feishu.cn)'}`);
|
|
616
|
+
if (result.userOpenId) {
|
|
617
|
+
console.log(` 扫码人 open_id: ${result.userOpenId}(将默认作为 allowedUsers)`);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
ok: true,
|
|
621
|
+
appId: result.appId,
|
|
622
|
+
appSecret: result.appSecret,
|
|
623
|
+
brand: result.brand,
|
|
624
|
+
userOpenId: result.userOpenId,
|
|
625
|
+
};
|
|
521
626
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
627
|
+
console.log(`\n⚠️ 扫码失败 (${result.error}): ${result.message}`);
|
|
628
|
+
if (result.error === 'aborted') {
|
|
629
|
+
// 用户主动取消整个 setup, 不再问手动 fallback
|
|
630
|
+
return { ok: false, reason: 'cancelled' };
|
|
631
|
+
}
|
|
632
|
+
if (interactive) {
|
|
633
|
+
console.log(' 已返回「飞书应用来源」,可重试或改走其他方式。\n');
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
console.log(' 降级到手动输入 AppID/Secret。\n');
|
|
637
|
+
}
|
|
638
|
+
if (method === 1) {
|
|
639
|
+
const existing = await pickExistingAppCredentials(rl);
|
|
640
|
+
if (existing.ok)
|
|
641
|
+
return existing;
|
|
642
|
+
if (interactive) {
|
|
643
|
+
// back(Esc / 主动放弃)静默回菜单;failed 已打印过原因,补一句导航。
|
|
644
|
+
if (existing.reason === 'failed')
|
|
645
|
+
console.log(' 已返回「飞书应用来源」,可重试或改走其他方式。\n');
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
console.log(' 降级到手动输入 AppID/Secret。\n');
|
|
649
|
+
}
|
|
650
|
+
// 手动输入(method 2;非 TTY 下也是 0/1 失败后的直落兜底):扫码路径已用
|
|
651
|
+
// tenant_brand 自动识别;手动路径没有这个信号,兜底让用户手选租户类型
|
|
652
|
+
// (决定建应用 / 运行时的域名)。
|
|
653
|
+
const brandIdx = await pickChoice(rl, {
|
|
654
|
+
title: '租户类型',
|
|
655
|
+
items: [
|
|
656
|
+
{ label: '飞书(中国版)', hint: 'open.feishu.cn' },
|
|
657
|
+
{ label: 'Lark(国际版)', hint: 'open.larksuite.com' },
|
|
658
|
+
],
|
|
659
|
+
defaultIndex: 0,
|
|
660
|
+
footer: 'Esc 返回上一步',
|
|
661
|
+
});
|
|
662
|
+
if (brandIdx === null && interactive)
|
|
663
|
+
continue; // Esc → 回「飞书应用来源」
|
|
664
|
+
const brand = brandIdx === 1 ? 'lark' : 'feishu';
|
|
665
|
+
console.log(`\n请在浏览器打开 ${larkHosts(brand).openApi}/app 创建应用,然后回来粘 ID/Secret。\n`);
|
|
666
|
+
const appId = (await ask(rl, 'AppID (cli_xxx): ')).trim();
|
|
667
|
+
const appSecret = (await ask(rl, 'AppSecret: ')).trim();
|
|
668
|
+
if (!appId || !appSecret) {
|
|
669
|
+
console.log('\n❌ AppID/AppSecret 不能为空,setup 中止。');
|
|
533
670
|
return { ok: false, reason: 'cancelled' };
|
|
534
671
|
}
|
|
535
|
-
|
|
672
|
+
return { ok: true, appId, appSecret, brand };
|
|
536
673
|
}
|
|
537
|
-
// 手动 fallback / 手动选项:扫码路径已用 tenant_brand 自动识别;手动路径
|
|
538
|
-
// 没有这个信号,兜底让用户手选租户类型(决定建应用 / 运行时的域名)。
|
|
539
|
-
console.log('\n租户类型:');
|
|
540
|
-
console.log(' 1) 飞书(中国版,open.feishu.cn)');
|
|
541
|
-
console.log(' 2) Lark(国际版,open.larksuite.com)');
|
|
542
|
-
const brandChoice = (await ask(rl, '选择 [1]: ')).trim();
|
|
543
|
-
const brand = brandChoice === '2' ? 'lark' : 'feishu';
|
|
544
|
-
console.log(`\n请在浏览器打开 ${larkHosts(brand).openApi}/app 创建应用,然后回来粘 ID/Secret。\n`);
|
|
545
|
-
const appId = (await ask(rl, 'AppID (cli_xxx): ')).trim();
|
|
546
|
-
const appSecret = (await ask(rl, 'AppSecret: ')).trim();
|
|
547
|
-
if (!appId || !appSecret) {
|
|
548
|
-
console.log('\n❌ AppID/AppSecret 不能为空,setup 中止。');
|
|
549
|
-
return { ok: false, reason: 'cancelled' };
|
|
550
|
-
}
|
|
551
|
-
return { ok: true, appId, appSecret, brand };
|
|
552
674
|
}
|
|
553
675
|
/**
|
|
554
676
|
* 用新应用自身凭证验证扫码链路拿到的 open_id。
|
|
@@ -623,11 +745,17 @@ async function promptBotConfig(rl) {
|
|
|
623
745
|
console.log('✅ 凭证有效(tenant_access_token 已成功获取)\n');
|
|
624
746
|
// CLI 适配器:可搜索的级联选择器(选 Aiden 可进 × Claude / × Codex,aiden 网关)。
|
|
625
747
|
// 非交互终端自动回退为序号 / ID 文本输入。
|
|
748
|
+
// Esc = 中止 setup(不写盘)。新建流程的必答题没有"上一步"可退,绝不静默
|
|
749
|
+
// 替用户选默认——扫码建出的应用可事后用「选择已有应用」找回,不会丢。
|
|
626
750
|
const selKey = await pickCliSelection(rl, { title: '选择 CLI 适配器' });
|
|
751
|
+
if (selKey === null) {
|
|
752
|
+
console.log('\n已取消(Esc),setup 中止,不写任何配置。');
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
627
755
|
let cliId;
|
|
628
756
|
let wrapperCli;
|
|
629
757
|
try {
|
|
630
|
-
const sel = resolveCliSelection(selKey
|
|
758
|
+
const sel = resolveCliSelection(selKey);
|
|
631
759
|
cliId = sel.cliId;
|
|
632
760
|
wrapperCli = sel.wrapperCli;
|
|
633
761
|
}
|
|
@@ -636,16 +764,53 @@ async function promptBotConfig(rl) {
|
|
|
636
764
|
console.log(' 不写 bots.json。请重新运行 botmux setup。');
|
|
637
765
|
return null;
|
|
638
766
|
}
|
|
639
|
-
|
|
767
|
+
// 新话题工作目录:两种模式二选一。旧问法只问「默认工作目录」但写的是
|
|
768
|
+
// workingDir——那只是仓库选择卡片的扫描根,新话题照样弹卡,误导性强;
|
|
769
|
+
// 真正「直接进目录、不弹卡」的是 defaultWorkingDir,现在显式让用户选。
|
|
770
|
+
// 「固定默认目录」放首位当推荐默认:大量用户的真实诉求是"新话题直接进目录",
|
|
771
|
+
// 弹卡模式作为多仓库场景的进阶选项。
|
|
772
|
+
const dirMode = await pickChoice(rl, {
|
|
773
|
+
title: '新话题工作目录',
|
|
774
|
+
items: [
|
|
775
|
+
{ label: '固定默认目录(推荐)', hint: '新话题直接在指定目录启动、不弹卡片' },
|
|
776
|
+
{ label: '仓库选择卡片', hint: '新话题先弹卡片,从扫描到的 git 仓库中选一个再启动' },
|
|
777
|
+
],
|
|
778
|
+
defaultIndex: 0,
|
|
779
|
+
footer: 'Esc 取消 setup · 之后可用 /config 或 botmux setup edit 修改',
|
|
780
|
+
});
|
|
781
|
+
// Esc = 中止 setup,不静默套用推荐默认(非 TTY 留空走 defaultIndex,不受影响)。
|
|
782
|
+
if (dirMode === null) {
|
|
783
|
+
console.log('\n已取消(Esc),setup 中止,不写任何配置。');
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
let workingDir;
|
|
787
|
+
let defaultWorkingDir;
|
|
788
|
+
if (dirMode === 1) {
|
|
789
|
+
const raw = await ask(rl, '仓库扫描根目录(卡片会列出其下的 git 仓库,逗号分隔多个)[~]: ');
|
|
790
|
+
workingDir = raw.trim() || '~';
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
// 存在性校验循环——运行时 daemon 对无效 defaultWorkingDir 只会静默回退
|
|
794
|
+
// 弹卡,setup 阶段必须挡住。留空默认 ~(一定存在,回车即通过)。
|
|
795
|
+
for (;;) {
|
|
796
|
+
const dir = (await ask(rl, '默认工作目录(新话题直接在此目录启动)[~]: ')).trim() || '~';
|
|
797
|
+
if (ensureBotDefaultWorkingDirExists({ defaultWorkingDir: dir })) {
|
|
798
|
+
defaultWorkingDir = dir;
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
640
803
|
const bot = {
|
|
641
804
|
larkAppId: creds.appId,
|
|
642
805
|
larkAppSecret: creds.appSecret,
|
|
643
806
|
cliId,
|
|
644
807
|
// aiden × claude/codex 等启动前缀;普通 CLI 不写此字段。
|
|
645
808
|
...(wrapperCli ? { wrapperCli } : {}),
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
809
|
+
// 仓库选择模式总是写 workingDir(留空用 '~'),用户手动编辑 bots.json 时
|
|
810
|
+
// 一眼能看到字段在哪儿;固定默认目录模式只写 defaultWorkingDir,扫描根
|
|
811
|
+
// 回退默认 ~,bots.json 不留多余字段。
|
|
812
|
+
...(workingDir ? { workingDir } : {}),
|
|
813
|
+
...(defaultWorkingDir ? { defaultWorkingDir } : {}),
|
|
649
814
|
};
|
|
650
815
|
// brand 落盘:只在国际版 (lark) 时写字段,feishu 留空——保持旧 bots.json 干净,
|
|
651
816
|
// 且 botBrand()/normalizeBrand() 读不到时 default 到 feishu,向后兼容。
|
|
@@ -673,7 +838,7 @@ async function promptBotConfig(rl) {
|
|
|
673
838
|
else {
|
|
674
839
|
bot.allowedUsers = await promptRequiredOwner(rl);
|
|
675
840
|
}
|
|
676
|
-
if (!ensureBotWorkingDirsExist(bot, '
|
|
841
|
+
if (!ensureBotWorkingDirsExist(bot, '仓库扫描根目录'))
|
|
677
842
|
return null;
|
|
678
843
|
return normalizeBotConfig(bot);
|
|
679
844
|
}
|
|
@@ -712,6 +877,29 @@ function formatBotConfigTable(bots) {
|
|
|
712
877
|
const render = (cells) => ' ' + cells.map((cell, i) => padEndDisplay(cell, widths[i])).join(' ');
|
|
713
878
|
return [render(headers), ...rows.map(render)].join('\n');
|
|
714
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* 从 bots 列表交互选择一个机器人,返回下标;取消 / 找不到返回 undefined。
|
|
882
|
+
* TTY 用可搜索选择器;非 TTY 保持旧文本语义(进程名 / AppID——见
|
|
883
|
+
* parseBotSelection 上的注释,刻意不接受裸序号,避免 off-by-one 歧义)。
|
|
884
|
+
*/
|
|
885
|
+
async function pickBotSelection(rl, bots, title) {
|
|
886
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
887
|
+
const selected = await ask(rl, '选择机器人(进程名 或 AppID): ');
|
|
888
|
+
return parseBotSelection(selected, bots);
|
|
889
|
+
}
|
|
890
|
+
const idx = await interactiveSelect({
|
|
891
|
+
title,
|
|
892
|
+
items: bots.map((b, i) => ({
|
|
893
|
+
label: botProcessName(b, i, PM2_NAME),
|
|
894
|
+
hint: `${b?.larkAppId ?? ''} · ${b?.cliId ?? 'claude-code'}`,
|
|
895
|
+
})),
|
|
896
|
+
footer: 'Esc 返回操作菜单',
|
|
897
|
+
});
|
|
898
|
+
if (idx === null)
|
|
899
|
+
return undefined;
|
|
900
|
+
console.log(` ✔ ${title}: ${botProcessName(bots[idx], idx, PM2_NAME)}`);
|
|
901
|
+
return idx;
|
|
902
|
+
}
|
|
715
903
|
async function promptEditBotConfig(rl, bot) {
|
|
716
904
|
console.log('\n字段留空表示保留当前值;可选字段输入 - 表示清空。\n');
|
|
717
905
|
const input = {};
|
|
@@ -768,11 +956,37 @@ async function promptEditBotConfig(rl, bot) {
|
|
|
768
956
|
'留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
|
|
769
957
|
]);
|
|
770
958
|
input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
959
|
+
// 新话题工作目录:模式二选一(与 promptBotConfig 的新建流程同款问法)。
|
|
960
|
+
const currentDirMode = bot.defaultWorkingDir
|
|
961
|
+
? `固定默认目录: ${bot.defaultWorkingDir}`
|
|
962
|
+
: `仓库选择卡片,扫描根: ${bot.workingDir ?? '~'}`;
|
|
963
|
+
const dirMode = await pickChoice(rl, {
|
|
964
|
+
title: '新话题工作目录',
|
|
965
|
+
items: [
|
|
966
|
+
{ label: '保留当前配置', hint: currentDirMode },
|
|
967
|
+
{ label: '固定默认目录', hint: '新话题直接在指定目录启动、不弹卡片' },
|
|
968
|
+
{ label: '仓库选择卡片', hint: '新话题先弹卡片选 git 仓库;下一问填卡片的扫描根目录' },
|
|
969
|
+
],
|
|
970
|
+
defaultIndex: 0,
|
|
971
|
+
});
|
|
972
|
+
if (dirMode === 1) {
|
|
973
|
+
printInputHelp('固定默认目录', [
|
|
974
|
+
'新话题直接在此目录启动、不弹仓库选择卡片。',
|
|
975
|
+
'留空保留当前值;输入 - 清空并回到仓库选择卡片模式。',
|
|
976
|
+
]);
|
|
977
|
+
input.defaultWorkingDir = await ask(rl, `固定默认目录 [${formatOptionalValue(bot.defaultWorkingDir)}]: `);
|
|
978
|
+
}
|
|
979
|
+
else if (dirMode === 2) {
|
|
980
|
+
printInputHelp('仓库扫描根目录', [
|
|
981
|
+
'仓库选择卡片会列出这些目录下的 git 仓库,支持逗号分隔多个。',
|
|
982
|
+
'留空保留当前值;输入 - 清空并回到默认 ~。',
|
|
983
|
+
]);
|
|
984
|
+
input.workingDir = await ask(rl, `仓库扫描根目录 [${formatOptionalValue(bot.workingDir)}]: `);
|
|
985
|
+
if (bot.defaultWorkingDir) {
|
|
986
|
+
console.log(' 已切回仓库选择卡片模式,原固定默认目录将被清空。');
|
|
987
|
+
input.defaultWorkingDir = '-';
|
|
988
|
+
}
|
|
989
|
+
}
|
|
776
990
|
printInputHelp('允许的用户', [
|
|
777
991
|
'可选。限制哪些飞书用户可以操作机器人,支持完整邮箱(如 alice@example.com)、union_id(on_xxx)或 open_id(ou_xxx),多个值用逗号分隔。',
|
|
778
992
|
'注意:必须是完整邮箱,邮箱前缀(如 alice)无法解析、会被丢弃。',
|
|
@@ -852,6 +1066,247 @@ async function writeSingleBotConfig() {
|
|
|
852
1066
|
console.log(` 2. botmux autostart enable 注册开机自启(推荐:${process.platform === 'darwin' ? 'mac launchd' : process.platform === 'linux' ? 'linux user systemd' : process.platform === 'win32' ? 'Windows Task Scheduler' : '当前平台暂不支持'},无需 sudo)`);
|
|
853
1067
|
return true;
|
|
854
1068
|
}
|
|
1069
|
+
// ─── Scripted (non-TUI) setup ────────────────────────────────────────────────
|
|
1070
|
+
/** 脚本化 setup 统一失败出口:--json 输出结构化错误到 stdout,退出码 1。 */
|
|
1071
|
+
function failSetupScripted(json, message) {
|
|
1072
|
+
if (json)
|
|
1073
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
1074
|
+
else
|
|
1075
|
+
console.error(`❌ ${message}`);
|
|
1076
|
+
process.exitCode = 1;
|
|
1077
|
+
}
|
|
1078
|
+
/** 某个(可能带 ~ 前缀的)路径若不存在/不是目录,返回展开后的绝对路径;合法返回 null。 */
|
|
1079
|
+
function missingDirResolved(raw) {
|
|
1080
|
+
const resolved = resolve(expandHomePath(raw));
|
|
1081
|
+
try {
|
|
1082
|
+
if (statSync(resolved).isDirectory())
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
catch { /* not a dir */ }
|
|
1086
|
+
return resolved;
|
|
1087
|
+
}
|
|
1088
|
+
/** workingDir / workingDirs / defaultWorkingDir 里所有无效目录(脚本化模式一次性报全)。 */
|
|
1089
|
+
function invalidBotDirs(bot) {
|
|
1090
|
+
const invalid = [...invalidWorkingDirs(bot)];
|
|
1091
|
+
const raw = typeof bot.defaultWorkingDir === 'string' ? bot.defaultWorkingDir.trim() : '';
|
|
1092
|
+
if (raw) {
|
|
1093
|
+
const missing = missingDirResolved(raw);
|
|
1094
|
+
if (missing)
|
|
1095
|
+
invalid.push(missing);
|
|
1096
|
+
}
|
|
1097
|
+
return invalid;
|
|
1098
|
+
}
|
|
1099
|
+
/** list/add/edit 的 JSON 输出视图:bot 条目 + 进程名,secret 脱敏(stdout 可能被贴进聊天/日志)。 */
|
|
1100
|
+
function botJsonView(bot, index) {
|
|
1101
|
+
return {
|
|
1102
|
+
processName: botProcessName(bot, index, PM2_NAME),
|
|
1103
|
+
...bot,
|
|
1104
|
+
larkAppSecret: maskAppSecret(bot?.larkAppSecret),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* `botmux setup list|add|edit|remove` — 脚本化(非 TUI)bot 管理。
|
|
1109
|
+
* 给 coding agent / 脚本一个字段级稳定接口,不依赖交互问答顺序(管道喂数字
|
|
1110
|
+
* 的老姿势在问题序列变化时会静默错位)。校验口径与 TUI 一致:目录存在性、
|
|
1111
|
+
* owner 必填、凭证变更时的 tenant_access_token 校验,任一失败不写盘。
|
|
1112
|
+
*/
|
|
1113
|
+
async function cmdSetupScripted(argv) {
|
|
1114
|
+
const wantsJson = argv.includes('--json');
|
|
1115
|
+
let cmd;
|
|
1116
|
+
try {
|
|
1117
|
+
cmd = parseSetupCommand(argv);
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
failSetupScripted(wantsJson, err?.message ?? String(err));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (cmd.action === 'help') {
|
|
1124
|
+
console.log(SETUP_CLI_USAGE);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
ensureConfigDir();
|
|
1128
|
+
const bots = loadBotsJson();
|
|
1129
|
+
if (cmd.action === 'list') {
|
|
1130
|
+
if (cmd.json) {
|
|
1131
|
+
console.log(JSON.stringify(bots.map((b, i) => botJsonView(b, i)), null, 2));
|
|
1132
|
+
}
|
|
1133
|
+
else if (bots.length === 0) {
|
|
1134
|
+
console.log('尚未配置机器人。运行 botmux setup(交互式)或 botmux setup add 添加。');
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
console.log(formatBotConfigTable(bots));
|
|
1138
|
+
console.log('\n完整字段用 --json 查看(secret 脱敏;明文只在 ~/.botmux/bots.json)。');
|
|
1139
|
+
}
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (cmd.action === 'add') {
|
|
1143
|
+
let bot;
|
|
1144
|
+
try {
|
|
1145
|
+
bot = buildBotFromAddFlags(cmd.flags);
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
failSetupScripted(cmd.json, err?.message ?? String(err));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// 单机器人 .env 老配置:与 TUI「添加新机器人」一致,先迁移进 bots.json 再追加。
|
|
1152
|
+
let existing = bots;
|
|
1153
|
+
let migratedEnv = false;
|
|
1154
|
+
if (!existsSync(BOTS_JSON_FILE) && existsSync(ENV_FILE)) {
|
|
1155
|
+
const legacy = parseDotEnvToBotConfig();
|
|
1156
|
+
if (legacy.larkAppId && legacy.larkAppSecret) {
|
|
1157
|
+
existing = [legacy];
|
|
1158
|
+
migratedEnv = true;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (existing.some(b => b?.larkAppId === bot.larkAppId)) {
|
|
1162
|
+
failSetupScripted(cmd.json, `AppID ${bot.larkAppId} 已存在,修改请用 botmux setup edit ${bot.larkAppId}。`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const badDirs = invalidBotDirs(bot);
|
|
1166
|
+
if (badDirs.length > 0) {
|
|
1167
|
+
failSetupScripted(cmd.json, `目录不存在或不是目录: ${badDirs.join(', ')}。请先创建,未写入配置。`);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
// 凭证校验与 TUI 同口径:换不到 tenant_access_token 一律不写盘。
|
|
1171
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
1172
|
+
const v = await validateCredentials(bot.larkAppId, bot.larkAppSecret, botBrand(bot));
|
|
1173
|
+
if (!v.ok) {
|
|
1174
|
+
failSetupScripted(cmd.json, `凭证校验失败 (${v.error}): ${v.message}`);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
writeBotsJsonAtomic([...existing, bot]);
|
|
1178
|
+
if (migratedEnv)
|
|
1179
|
+
renameSync(ENV_FILE, ENV_FILE + '.bak');
|
|
1180
|
+
// 开放平台自动配置(权限导入/发版)需要扫码,脚本化模式默认跳过、显式 opt-in。
|
|
1181
|
+
if (cmd.openPlatformAuto) {
|
|
1182
|
+
await finishOpenPlatformSetup(bot.larkAppId, botBrand(bot));
|
|
1183
|
+
}
|
|
1184
|
+
const index = existing.length;
|
|
1185
|
+
if (cmd.json) {
|
|
1186
|
+
console.log(JSON.stringify({
|
|
1187
|
+
ok: true,
|
|
1188
|
+
action: 'add',
|
|
1189
|
+
bot: botJsonView(bot, index),
|
|
1190
|
+
botsFile: BOTS_JSON_FILE,
|
|
1191
|
+
envMigrated: migratedEnv || undefined,
|
|
1192
|
+
openPlatform: cmd.openPlatformAuto ? 'attempted' : 'skipped',
|
|
1193
|
+
next: 'botmux restart',
|
|
1194
|
+
}, null, 2));
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
console.log(`✅ 已添加机器人 ${botProcessName(bot, index, PM2_NAME)} (${bot.larkAppId}),共 ${index + 1} 个`);
|
|
1198
|
+
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
1199
|
+
if (migratedEnv)
|
|
1200
|
+
console.log(` 旧 .env 已迁移并备份: ${ENV_FILE}.bak`);
|
|
1201
|
+
if (!cmd.openPlatformAuto) {
|
|
1202
|
+
console.log(' 已跳过开放平台自动配置(权限导入/发版)。需要时加 --open-platform-auto(要扫码),或运行交互式 botmux setup。');
|
|
1203
|
+
}
|
|
1204
|
+
console.log('下一步: botmux restart');
|
|
1205
|
+
}
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (cmd.action === 'edit') {
|
|
1209
|
+
const index = parseBotSelection(cmd.selector, bots);
|
|
1210
|
+
if (index === undefined) {
|
|
1211
|
+
failSetupScripted(cmd.json, `找不到机器人 "${cmd.selector}"(接受进程名 botmux-N 或 AppID,botmux setup list 可查)。`);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const original = bots[index];
|
|
1215
|
+
let edited;
|
|
1216
|
+
let modelCleared = false;
|
|
1217
|
+
try {
|
|
1218
|
+
const input = editInputFromFlags(cmd.flags);
|
|
1219
|
+
if (Object.keys(input).length === 0) {
|
|
1220
|
+
throw new Error('edit 至少需要一个字段参数(如 --cli codex)。查看用法:botmux setup help');
|
|
1221
|
+
}
|
|
1222
|
+
// 切换 CLI 强制清空旧 model(与 TUI 同理:旧值属于上一个 CLI,套用会 spawn 报错)。
|
|
1223
|
+
const nextCliId = input.cliChoice ? resolveCliId(input.cliChoice) : undefined;
|
|
1224
|
+
if (nextCliId && nextCliId !== (original.cliId ?? 'claude-code') && original.model && input.model === undefined) {
|
|
1225
|
+
input.model = null;
|
|
1226
|
+
modelCleared = true;
|
|
1227
|
+
}
|
|
1228
|
+
edited = applyBotConfigEdits(original, input);
|
|
1229
|
+
assertOwnerWhenChatGroups(edited);
|
|
1230
|
+
}
|
|
1231
|
+
catch (err) {
|
|
1232
|
+
failSetupScripted(cmd.json, err?.message ?? String(err));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const badDirs = invalidBotDirs(edited);
|
|
1236
|
+
if (badDirs.length > 0) {
|
|
1237
|
+
failSetupScripted(cmd.json, `目录不存在或不是目录: ${badDirs.join(', ')}。配置未修改。`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const appIdChanged = edited.larkAppId !== original.larkAppId;
|
|
1241
|
+
if (appIdChanged && bots.some((b, i) => i !== index && b?.larkAppId === edited.larkAppId)) {
|
|
1242
|
+
failSetupScripted(cmd.json, `AppID ${edited.larkAppId} 已被另一个机器人使用,配置未修改。`);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (appIdChanged || edited.larkAppSecret !== original.larkAppSecret) {
|
|
1246
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
1247
|
+
const v = await validateCredentials(edited.larkAppId, edited.larkAppSecret, botBrand(edited));
|
|
1248
|
+
if (!v.ok) {
|
|
1249
|
+
failSetupScripted(cmd.json, `凭证校验失败 (${v.error}): ${v.message}。配置未修改。`);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const nextBots = bots.slice();
|
|
1254
|
+
nextBots[index] = edited;
|
|
1255
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
1256
|
+
writeBotsJsonAtomic(nextBots);
|
|
1257
|
+
const changed = [...new Set([...Object.keys(original), ...Object.keys(edited)])]
|
|
1258
|
+
.filter(k => JSON.stringify(original[k]) !== JSON.stringify(edited[k]));
|
|
1259
|
+
if (cmd.json) {
|
|
1260
|
+
console.log(JSON.stringify({
|
|
1261
|
+
ok: true,
|
|
1262
|
+
action: 'edit',
|
|
1263
|
+
bot: botJsonView(edited, index),
|
|
1264
|
+
changed,
|
|
1265
|
+
modelCleared: modelCleared || undefined,
|
|
1266
|
+
backup: BOTS_JSON_FILE + '.bak',
|
|
1267
|
+
next: 'botmux restart',
|
|
1268
|
+
}, null, 2));
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
console.log(`✅ 已更新机器人 ${botProcessName(edited, index, PM2_NAME)} (${edited.larkAppId})`);
|
|
1272
|
+
console.log(` 变更字段: ${changed.join(', ') || '(无实际变化)'}`);
|
|
1273
|
+
if (modelCleared)
|
|
1274
|
+
console.log(' ⚠️ 已切换 CLI,原 model 字段已清空(需要时用 --model 或 /config 重设)。');
|
|
1275
|
+
if (appIdChanged)
|
|
1276
|
+
console.log(' ⚠️ LARK_APP_ID 已变更:历史会话/群聊状态不迁移,新应用可能需重新配置开放平台权限。');
|
|
1277
|
+
console.log(` 旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
1278
|
+
console.log('下一步: botmux restart');
|
|
1279
|
+
}
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
// remove
|
|
1283
|
+
if (!cmd.yes) {
|
|
1284
|
+
failSetupScripted(cmd.json, '非交互删除需要显式 --yes 确认。');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const result = removeBotConfig(bots, cmd.selector);
|
|
1288
|
+
if (!result) {
|
|
1289
|
+
failSetupScripted(cmd.json, `找不到机器人 "${cmd.selector}"(接受进程名 botmux-N 或 AppID,botmux setup list 可查)。`);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
1293
|
+
writeBotsJsonAtomic(result.bots);
|
|
1294
|
+
if (cmd.json) {
|
|
1295
|
+
console.log(JSON.stringify({
|
|
1296
|
+
ok: true,
|
|
1297
|
+
action: 'remove',
|
|
1298
|
+
removed: botJsonView(result.removed, result.index),
|
|
1299
|
+
remaining: result.bots.length,
|
|
1300
|
+
backup: BOTS_JSON_FILE + '.bak',
|
|
1301
|
+
next: 'botmux restart',
|
|
1302
|
+
}, null, 2));
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
console.log(`✅ 已删除机器人 ${botProcessName(result.removed, result.index, PM2_NAME)} (${result.removed.larkAppId}),剩余 ${result.bots.length} 个`);
|
|
1306
|
+
console.log(` 旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
1307
|
+
console.log('下一步: botmux restart');
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
855
1310
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
856
1311
|
async function cmdSetup() {
|
|
857
1312
|
ensureConfigDir();
|
|
@@ -867,123 +1322,166 @@ async function cmdSetup() {
|
|
|
867
1322
|
console.log(formatBotConfigTable(bots));
|
|
868
1323
|
console.log('');
|
|
869
1324
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
rl
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
console.log(`下一步: botmux restart\n`);
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
if (action === '3') {
|
|
891
|
-
console.log('\n── 编辑现有机器人 ──\n');
|
|
892
|
-
const selected = await ask(rl, '选择机器人(进程名 或 AppID): ');
|
|
893
|
-
const index = parseBotSelection(selected, bots);
|
|
894
|
-
if (index === undefined) {
|
|
1325
|
+
// 交互模式下子界面(选机器人)Esc = 返回本操作菜单;非 TTY 无 Esc,保持
|
|
1326
|
+
// 「无效选择即报错退出」旧语义,避免管道输入在循环里打转。
|
|
1327
|
+
const interactiveMenus = process.stdin.isTTY && process.stdout.isTTY;
|
|
1328
|
+
for (;;) {
|
|
1329
|
+
const action = await pickChoice(rl, {
|
|
1330
|
+
title: '操作',
|
|
1331
|
+
items: [
|
|
1332
|
+
{ label: '添加新机器人' },
|
|
1333
|
+
{ label: '编辑现有机器人' },
|
|
1334
|
+
{ label: '删除机器人' },
|
|
1335
|
+
// 「重新配置」= 丢弃全部现有配置重建,低频且有破坏性,压轴放最后。
|
|
1336
|
+
{ label: '重新配置', hint: '丢弃现有配置,重建为单机器人配置' },
|
|
1337
|
+
],
|
|
1338
|
+
defaultIndex: 0,
|
|
1339
|
+
footer: 'Esc 退出',
|
|
1340
|
+
});
|
|
1341
|
+
if (action === null) {
|
|
895
1342
|
rl.close();
|
|
896
|
-
console.log('\n
|
|
1343
|
+
console.log('\n已取消。');
|
|
897
1344
|
return;
|
|
898
1345
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
edited = await promptEditBotConfig(rl, original);
|
|
903
|
-
}
|
|
904
|
-
catch (err) {
|
|
1346
|
+
if (action === 3) {
|
|
1347
|
+
console.log('\n── 重新配置 ──\n');
|
|
1348
|
+
const newBot = await promptBotConfig(rl);
|
|
905
1349
|
rl.close();
|
|
906
|
-
|
|
1350
|
+
if (!newBot) {
|
|
1351
|
+
console.log('\n⚠️ setup 中止,旧配置保留不动。');
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
// Codex review #1: 先 copyFileSync 备份, 再原子写新文件. 之前先 rename
|
|
1355
|
+
// 旧文件再 write, 一旦 write 失败 (磁盘/权限/进程被 kill) 用户就丢了
|
|
1356
|
+
// bots.json. copy 之后写失败旧文件原地不动, .bak 是无害的同名副本.
|
|
1357
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
1358
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
1359
|
+
writeBotsJsonAtomic([newBot]);
|
|
1360
|
+
console.log(`✅ 配置已写入: ${BOTS_JSON_FILE}`);
|
|
1361
|
+
await finishOpenPlatformSetup(newBot.larkAppId, botBrand(newBot));
|
|
1362
|
+
console.log(`下一步: botmux restart\n`);
|
|
907
1363
|
return;
|
|
908
1364
|
}
|
|
909
|
-
if (
|
|
1365
|
+
if (action === 1) {
|
|
1366
|
+
console.log('\n── 编辑现有机器人 ──\n');
|
|
1367
|
+
const index = await pickBotSelection(rl, bots, '选择要编辑的机器人');
|
|
1368
|
+
if (index === undefined) {
|
|
1369
|
+
if (interactiveMenus) {
|
|
1370
|
+
console.log(' 已返回操作菜单。\n');
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
rl.close();
|
|
1374
|
+
console.log('\n❌ 未选择机器人,配置未修改。');
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const original = bots[index];
|
|
1378
|
+
let edited;
|
|
1379
|
+
try {
|
|
1380
|
+
edited = await promptEditBotConfig(rl, original);
|
|
1381
|
+
}
|
|
1382
|
+
catch (err) {
|
|
1383
|
+
rl.close();
|
|
1384
|
+
console.log(`\n❌ 编辑失败: ${err?.message ?? String(err)}`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (!ensureBotWorkingDirsExist(edited, '仓库扫描根目录') || !ensureBotDefaultWorkingDirExists(edited)) {
|
|
1388
|
+
rl.close();
|
|
1389
|
+
console.log(' 配置未修改。');
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// 凭证字段有变化时, 像 promptBotConfig 一样跑一次 tenant_access_token
|
|
1393
|
+
// 校验. 失败不写盘——避免编辑后 typo 一个字符, daemon 重启时才发现.
|
|
1394
|
+
// (cmdRestart 不校验凭证, 只 cmdStart 校验, 所以编辑路径必须自己兜.)
|
|
1395
|
+
const appIdChanged = edited.larkAppId !== original.larkAppId;
|
|
1396
|
+
const appSecretChanged = edited.larkAppSecret !== original.larkAppSecret;
|
|
1397
|
+
if (appIdChanged || appSecretChanged) {
|
|
1398
|
+
console.log('\n校验新凭证(取 tenant_access_token)…');
|
|
1399
|
+
const { validateCredentials } = await import('./setup/verify-permissions.js');
|
|
1400
|
+
const v = await validateCredentials(edited.larkAppId, edited.larkAppSecret, botBrand(edited));
|
|
1401
|
+
if (!v.ok) {
|
|
1402
|
+
rl.close();
|
|
1403
|
+
console.log(`\n❌ 凭证校验失败 (${v.error}): ${v.message}`);
|
|
1404
|
+
console.log(' 配置未修改。请重新运行 botmux setup → 编辑现有机器人。');
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
console.log('✅ 凭证有效\n');
|
|
1408
|
+
}
|
|
910
1409
|
rl.close();
|
|
911
|
-
|
|
1410
|
+
const nextBots = bots.slice();
|
|
1411
|
+
nextBots[index] = edited;
|
|
1412
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
1413
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
1414
|
+
writeBotsJsonAtomic(nextBots);
|
|
1415
|
+
console.log(`✅ 已更新机器人 ${botProcessName(edited, index, PM2_NAME)} (${edited.larkAppId})`);
|
|
1416
|
+
// appId 切换 = 换了一个飞书应用, 新 appId 大概率需要重新申请权限 + 配重定向 URL.
|
|
1417
|
+
// 把 printRemainingSteps 的深链端给用户, 比 README 警告里那句"历史数据不迁移"更可操作.
|
|
1418
|
+
if (appIdChanged) {
|
|
1419
|
+
await finishOpenPlatformSetup(edited.larkAppId, botBrand(edited));
|
|
1420
|
+
}
|
|
1421
|
+
console.log(`下一步: botmux restart\n`);
|
|
912
1422
|
return;
|
|
913
1423
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const v = await validateCredentials(edited.larkAppId, edited.larkAppSecret, botBrand(edited));
|
|
923
|
-
if (!v.ok) {
|
|
1424
|
+
if (action === 2) {
|
|
1425
|
+
console.log('\n── 删除机器人 ──\n');
|
|
1426
|
+
const delIndex = await pickBotSelection(rl, bots, '选择要删除的机器人');
|
|
1427
|
+
if (delIndex === undefined) {
|
|
1428
|
+
if (interactiveMenus) {
|
|
1429
|
+
console.log(' 已返回操作菜单。\n');
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
924
1432
|
rl.close();
|
|
925
|
-
console.log(
|
|
926
|
-
console.log(' 配置未修改。请重新运行 botmux setup → 编辑现有机器人。');
|
|
1433
|
+
console.log('\n❌ 未选择机器人,配置未修改。');
|
|
927
1434
|
return;
|
|
928
1435
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const nextBots = bots.slice();
|
|
933
|
-
nextBots[index] = edited;
|
|
934
|
-
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
935
|
-
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
936
|
-
writeBotsJsonAtomic(nextBots);
|
|
937
|
-
console.log(`✅ 已更新机器人 ${botProcessName(edited, index, PM2_NAME)} (${edited.larkAppId})`);
|
|
938
|
-
// appId 切换 = 换了一个飞书应用, 新 appId 大概率需要重新申请权限 + 配重定向 URL.
|
|
939
|
-
// 把 printRemainingSteps 的深链端给用户, 比 README 警告里那句"历史数据不迁移"更可操作.
|
|
940
|
-
if (appIdChanged) {
|
|
941
|
-
await finishOpenPlatformSetup(edited.larkAppId, botBrand(edited));
|
|
942
|
-
}
|
|
943
|
-
console.log(`下一步: botmux restart\n`);
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
if (action === '4') {
|
|
947
|
-
console.log('\n── 删除机器人 ──\n');
|
|
948
|
-
const selected = await ask(rl, '选择机器人(进程名 或 AppID): ');
|
|
949
|
-
const result = removeBotConfig(bots, selected);
|
|
950
|
-
if (!result) {
|
|
1436
|
+
const nextBots = bots.slice();
|
|
1437
|
+
const [removed] = nextBots.splice(delIndex, 1);
|
|
1438
|
+
const confirm = (await ask(rl, `确认删除 ${botProcessName(removed, delIndex, PM2_NAME)} (${removed.larkAppId})? (y/N): `)).trim().toLowerCase();
|
|
951
1439
|
rl.close();
|
|
952
|
-
|
|
1440
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
1441
|
+
console.log('\n已取消,配置未修改。');
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
copyFileSync(BOTS_JSON_FILE, BOTS_JSON_FILE + '.bak');
|
|
1445
|
+
console.log(`旧配置已备份: ${BOTS_JSON_FILE}.bak`);
|
|
1446
|
+
writeBotsJsonAtomic(nextBots);
|
|
1447
|
+
console.log(`✅ 已删除机器人 ${botProcessName(removed, delIndex, PM2_NAME)} (${removed.larkAppId})`);
|
|
1448
|
+
console.log(`下一步: botmux restart\n`);
|
|
953
1449
|
return;
|
|
954
1450
|
}
|
|
955
|
-
|
|
1451
|
+
console.log('\n── 添加新机器人 ──\n');
|
|
1452
|
+
const newBot = await promptBotConfig(rl);
|
|
956
1453
|
rl.close();
|
|
957
|
-
if (
|
|
958
|
-
console.log('\n
|
|
1454
|
+
if (!newBot) {
|
|
1455
|
+
console.log('\n⚠️ setup 中止,bots.json 不动。');
|
|
959
1456
|
return;
|
|
960
1457
|
}
|
|
961
|
-
|
|
962
|
-
console.log(
|
|
963
|
-
|
|
964
|
-
|
|
1458
|
+
writeBotsJsonAtomic([...bots, newBot]);
|
|
1459
|
+
console.log(`\n✅ 已添加机器人 ${newBot.larkAppId},共 ${bots.length + 1} 个`);
|
|
1460
|
+
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
1461
|
+
await finishOpenPlatformSetup(newBot.larkAppId, botBrand(newBot));
|
|
965
1462
|
console.log(`下一步: botmux restart\n`);
|
|
966
1463
|
return;
|
|
967
1464
|
}
|
|
968
|
-
console.log('\n── 添加新机器人 ──\n');
|
|
969
|
-
const newBot = await promptBotConfig(rl);
|
|
970
|
-
rl.close();
|
|
971
|
-
if (!newBot) {
|
|
972
|
-
console.log('\n⚠️ setup 中止,bots.json 不动。');
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
writeBotsJsonAtomic([...bots, newBot]);
|
|
976
|
-
console.log(`\n✅ 已添加机器人 ${newBot.larkAppId},共 ${bots.length + 1} 个`);
|
|
977
|
-
console.log(` 配置文件: ${BOTS_JSON_FILE}`);
|
|
978
|
-
await finishOpenPlatformSetup(newBot.larkAppId, botBrand(newBot));
|
|
979
|
-
console.log(`下一步: botmux restart\n`);
|
|
980
1465
|
}
|
|
981
1466
|
else if (hasEnv) {
|
|
982
1467
|
// --- Single-bot mode (.env exists) ---
|
|
983
1468
|
console.log(`当前使用单机器人配置: ${ENV_FILE}`);
|
|
984
1469
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
985
|
-
const action = await
|
|
986
|
-
|
|
1470
|
+
const action = await pickChoice(rl, {
|
|
1471
|
+
title: '操作',
|
|
1472
|
+
items: [
|
|
1473
|
+
{ label: '添加新机器人', hint: '迁移 .env 到 bots.json 多机器人配置' },
|
|
1474
|
+
{ label: '覆盖当前配置' },
|
|
1475
|
+
],
|
|
1476
|
+
defaultIndex: 0,
|
|
1477
|
+
footer: 'Esc 退出',
|
|
1478
|
+
});
|
|
1479
|
+
if (action === null) {
|
|
1480
|
+
rl.close();
|
|
1481
|
+
console.log('\n已取消。');
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (action === 1) {
|
|
987
1485
|
rl.close();
|
|
988
1486
|
const ok = await writeSingleBotConfig();
|
|
989
1487
|
if (ok) {
|
|
@@ -5591,9 +6089,16 @@ switch (command) {
|
|
|
5591
6089
|
case '-v':
|
|
5592
6090
|
console.log(getVersion());
|
|
5593
6091
|
break;
|
|
5594
|
-
case 'setup':
|
|
5595
|
-
|
|
6092
|
+
case 'setup': {
|
|
6093
|
+
// 带子命令(list/add/edit/remove/help)走脚本化非 TUI 模式;空参数 / 纯
|
|
6094
|
+
// flag(如 --no-open-platform-auto)保持原交互 TUI,向后兼容。
|
|
6095
|
+
const setupArgs = process.argv.slice(3);
|
|
6096
|
+
if (isScriptedSetupInvocation(setupArgs))
|
|
6097
|
+
await cmdSetupScripted(setupArgs);
|
|
6098
|
+
else
|
|
6099
|
+
await cmdSetup();
|
|
5596
6100
|
break;
|
|
6101
|
+
}
|
|
5597
6102
|
case 'start':
|
|
5598
6103
|
await cmdStart();
|
|
5599
6104
|
break;
|