@modelzen/feishu-codex-bridge 0.3.0-win.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/dist/cli.js +301 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,7 @@
33
33
  - **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、诊断、重连。
34
34
  - **稳定隔离**:每会话独立 app-server 进程;卡死有 watchdog(默认 120s)→ 终止 → 回收,异常不波及其他群。
35
35
  - **本地加密密钥库**:飞书应用密钥用 AES-256-GCM 存在 `~/.feishu-codex-bridge/`,不入仓库、不进环境变量。
36
- - **可常驻**:macOS 下可注册成 launchd 后台服务,开机自启。
36
+ - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。
37
37
 
38
38
  ---
39
39
 
@@ -41,6 +41,7 @@
41
41
 
42
42
  | 依赖 | 说明 | 获取方式 |
43
43
  |------|------|----------|
44
+ | **操作系统** | **macOS / Windows** 均支持;Linux·WSL 为 best-effort(已实现 systemd,未广泛实测) | — |
44
45
  | **Node.js ≥ 20** | 运行时 | <https://nodejs.org> 或 `nvm install 20` |
45
46
  | **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或装 Codex.app,或用 `CODEX_BIN` 指向已有二进制 |
46
47
  | **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | `codex login` |
@@ -78,7 +79,7 @@ feishu-codex-bridge run
78
79
  ### 3. 后台 daemon(`start` —— 日常这么跑)
79
80
 
80
81
  ```bash
81
- feishu-codex-bridge start # 装 launchd 并启动:开机自启、崩溃自动拉起、关终端照跑
82
+ feishu-codex-bridge start # 装系统后台服务并启动:开机/登录自启、崩溃自动拉起、关终端照跑
82
83
  feishu-codex-bridge status # 状态 / pid / 日志路径 / 上次退出码
83
84
  feishu-codex-bridge logs -f # 跟踪日志
84
85
  feishu-codex-bridge restart # 重启
@@ -90,7 +91,9 @@ feishu-codex-bridge update # 更新到最新版(npm i -g)并自动重启
90
91
 
91
92
  `start` 会**先在当前终端完成 init**(没配置则扫码),并**阻塞到授权完成**——权限全部开通、且你确认已订阅事件/发布版本——才真正装服务,绝不会装一个收不到消息的空壳。daemon 体跑的就是 `run`。
92
93
 
93
- > ⚠️ **后台 daemon 必须全局安装(`npm i -g`),不要用 npx**:launchd plist 里硬编码了 CLI 路径,而 npx 的临时缓存(`~/.npm/_npx/...`)会被清理,缓存一没服务就起不来。前台 `run` npx 没问题(单次进程)。
94
+ > 🖥 **各平台后台机制**:macOS = launchd 用户服务;**Windows = 登录自启(写 `HKCU\…\Run`,隐藏启动,全程免管理员)**;Linux·WSL = systemd 用户单元(`systemctl --user`,需要 `loginctl enable-linger` 才能登出后续跑;WSL 还需在 `/etc/wsl.conf` `[boot] systemd=true`,否则用前台 `run`)。三者命令一致(`start`/`status`/`stop`/`restart`/`logs`),状态/日志路径统一。
95
+
96
+ > ⚠️ **后台服务必须全局安装(`npm i -g`),不要用 npx**:服务里硬编码了 CLI 路径,而 npx 的临时缓存(`~/.npm/_npx/...`)会被清理,缓存一没服务就起不来。前台 `run` 用 npx 没问题(单次进程)。
94
97
 
95
98
  ### 4. 多飞书机器人(可选)
96
99
 
@@ -198,7 +201,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
198
201
 
199
202
  ```
200
203
  feishu-codex-bridge run 前台启动(没配置先扫码 init;Ctrl+C 优雅退出)
201
- feishu-codex-bridge start 后台 daemon 启动(装 launchd 开机自启;阻塞到授权完成)
204
+ feishu-codex-bridge start 后台 daemon 启动(装系统后台服务、开机/登录自启;阻塞到授权完成)
202
205
  feishu-codex-bridge stop|restart|status|logs 后台 daemon 生命周期
203
206
  feishu-codex-bridge update 更新到最新版并自动重启 daemon(--check 只查不装)
204
207
  feishu-codex-bridge bot init|list|use|rm 多飞书机器人:注册 / 列表 / 切当前 / 移除
@@ -229,7 +232,7 @@ src/
229
232
  config/ 加密密钥库、密钥解析、配置存储、多机器人注册表、scope 清单、路径
230
233
  core/ watchdog、单实例锁、日志
231
234
  cli/ commander 命令(run / start / stop / restart / status / logs / update / bot / doctor / secrets)
232
- service/ launchd 后台服务
235
+ service/ 后台服务适配器(launchd / Windows 登录自启 / systemd)+ 跨平台 spawn
233
236
  ```
234
237
 
235
238
  架构与实现细节见 [`docs/design/feishu-codex-bridge-design.md`](docs/design/feishu-codex-bridge-design.md) 与 [`docs/design/implementation-plan.md`](docs/design/implementation-plan.md)。
package/dist/cli.js CHANGED
@@ -228,6 +228,21 @@ function findBot(reg, nameOrAppId) {
228
228
  function currentBot(reg) {
229
229
  return reg.current ? reg.bots.find((b) => b.appId === reg.current) : void 0;
230
230
  }
231
+ function activeBots(reg) {
232
+ const configured = reg.bots.some((b) => b.active !== void 0);
233
+ if (configured) return reg.bots.filter((b) => b.active === true);
234
+ const cur = currentBot(reg);
235
+ return cur ? [cur] : [];
236
+ }
237
+ async function setActiveBots(appIds) {
238
+ const reg = await loadBots();
239
+ const want = new Set(appIds);
240
+ for (const b of reg.bots) b.active = want.has(b.appId);
241
+ const firstActive = reg.bots.find((b) => b.active);
242
+ if (firstActive) reg.current = firstActive.appId;
243
+ await saveBots(reg);
244
+ return reg;
245
+ }
231
246
  async function addBot(entry) {
232
247
  const reg = await loadBots();
233
248
  reg.bots = reg.bots.filter((b) => b.appId !== entry.appId);
@@ -236,11 +251,6 @@ async function addBot(entry) {
236
251
  await saveBots(reg);
237
252
  return reg;
238
253
  }
239
- async function setCurrent(appId) {
240
- const reg = await loadBots();
241
- reg.current = appId;
242
- await saveBots(reg);
243
- }
244
254
  async function removeBot(appId) {
245
255
  const reg = await loadBots();
246
256
  reg.bots = reg.bots.filter((b) => b.appId !== appId);
@@ -1026,8 +1036,14 @@ function ensureCodex() {
1026
1036
  async function ensureOnboarded(opts = {}) {
1027
1037
  if (!ensureCodex()) return null;
1028
1038
  const reg = await ensureRegistry();
1029
- const entry = currentBot(reg);
1039
+ const entry = opts.bot ? findBot(reg, opts.bot) : currentBot(reg);
1030
1040
  if (!entry) {
1041
+ if (opts.bot) {
1042
+ console.error(
1043
+ `\u2717 \u627E\u4E0D\u5230\u673A\u5668\u4EBA\u300C${opts.bot}\u300D\u3002\u7528 \`feishu-codex-bridge bot list\` \u67E5\u770B\u5DF2\u6CE8\u518C\u7684\u673A\u5668\u4EBA\u3002`
1044
+ );
1045
+ return null;
1046
+ }
1031
1047
  if (!opts.allowCreate) {
1032
1048
  console.error("\u2717 \u5C1A\u672A\u914D\u7F6E\u4EFB\u4F55\u98DE\u4E66\u673A\u5668\u4EBA\u3002\u8BF7\u5148\u8FD0\u884C `feishu-codex-bridge bot init`\uFF08\u6216\u524D\u53F0 `run`\uFF09\u626B\u7801\u521B\u5EFA\u3002");
1033
1049
  return null;
@@ -5956,6 +5972,94 @@ async function startBridge(opts) {
5956
5972
  return { channel, shutdown };
5957
5973
  }
5958
5974
 
5975
+ // src/bot/supervisor.ts
5976
+ var BACKOFF_MIN_MS = 1e3;
5977
+ var BACKOFF_MAX_MS = 3e4;
5978
+ var HEALTHY_UPTIME_MS = 6e4;
5979
+ var SHUTDOWN_GRACE_MS = 8e3;
5980
+ async function runSupervisor(bots) {
5981
+ const cliEntry = process.argv[1];
5982
+ if (!cliEntry) throw new Error("supervisor: \u65E0\u6CD5\u89E3\u6790 CLI \u5165\u53E3\uFF08process.argv[1] \u4E3A\u7A7A\uFF09");
5983
+ const childEnv = { ...process.env };
5984
+ delete childEnv[SERVICE_ENV_FLAG];
5985
+ let shuttingDown = false;
5986
+ const children = bots.map((bot2) => ({ bot: bot2, backoffMs: BACKOFF_MIN_MS, startedAt: 0 }));
5987
+ console.log(`
5988
+ \u6B63\u5728\u542F\u52A8 ${bots.length} \u4E2A\u673A\u5668\u4EBA\uFF08\u5404\u81EA\u72EC\u7ACB\u8FDB\u7A0B\uFF09\uFF1A`);
5989
+ for (const b of bots) console.log(` \u2022 ${b.name} (${b.appId}) [${b.tenant}]`);
5990
+ console.log("Ctrl+C \u9000\u51FA\uFF08\u5173\u95ED\u5168\u90E8\uFF09\u3002\n");
5991
+ const prefixPipe = (name, src, dst) => {
5992
+ if (!src) return;
5993
+ let buf = "";
5994
+ src.setEncoding("utf8");
5995
+ src.on("data", (chunk) => {
5996
+ buf += chunk;
5997
+ let nl;
5998
+ while ((nl = buf.indexOf("\n")) >= 0) {
5999
+ const line = buf.slice(0, nl);
6000
+ buf = buf.slice(nl + 1);
6001
+ dst.write(`\x1B[2m[${name}]\x1B[0m ${line}
6002
+ `);
6003
+ }
6004
+ });
6005
+ src.on("end", () => {
6006
+ if (buf) dst.write(`\x1B[2m[${name}]\x1B[0m ${buf}
6007
+ `);
6008
+ });
6009
+ };
6010
+ const spawnChild = (c) => {
6011
+ c.startedAt = Date.now();
6012
+ const proc = spawnProcess(process.execPath, [cliEntry, "run", "--bot", c.bot.appId], {
6013
+ stdio: ["ignore", "pipe", "pipe"],
6014
+ env: childEnv
6015
+ });
6016
+ c.proc = proc;
6017
+ log.info("supervisor", "child-start", { bot: c.bot.name, appId: c.bot.appId, pid: proc.pid ?? null });
6018
+ prefixPipe(c.bot.name, proc.stdout, process.stdout);
6019
+ prefixPipe(c.bot.name, proc.stderr, process.stderr);
6020
+ proc.on("exit", (code, signal) => {
6021
+ c.proc = void 0;
6022
+ if (shuttingDown) return;
6023
+ const uptime = Date.now() - c.startedAt;
6024
+ if (uptime >= HEALTHY_UPTIME_MS) c.backoffMs = BACKOFF_MIN_MS;
6025
+ const wait = c.backoffMs;
6026
+ c.backoffMs = Math.min(c.backoffMs * 2, BACKOFF_MAX_MS);
6027
+ log.warn("supervisor", "child-exit", { bot: c.bot.name, code, signal, restartInMs: wait });
6028
+ console.error(
6029
+ `\x1B[2m[${c.bot.name}]\x1B[0m \u8FDB\u7A0B\u9000\u51FA\uFF08code=${code ?? signal ?? "?"}\uFF09\uFF0C${Math.round(wait / 1e3)}s \u540E\u91CD\u542F\u2026`
6030
+ );
6031
+ c.restartTimer = setTimeout(() => spawnChild(c), wait);
6032
+ });
6033
+ proc.on("error", (err) => {
6034
+ log.fail("supervisor", err, { bot: c.bot.name, phase: "spawn" });
6035
+ });
6036
+ };
6037
+ for (const c of children) spawnChild(c);
6038
+ await new Promise((resolve7) => {
6039
+ const stop = (sig) => {
6040
+ if (shuttingDown) return;
6041
+ shuttingDown = true;
6042
+ console.log(`
6043
+ \u6536\u5230 ${sig}\uFF0C\u6B63\u5728\u5173\u95ED\u5168\u90E8\u673A\u5668\u4EBA\u2026`);
6044
+ for (const c of children) {
6045
+ if (c.restartTimer) clearTimeout(c.restartTimer);
6046
+ c.proc?.kill("SIGTERM");
6047
+ }
6048
+ const deadline = Date.now() + SHUTDOWN_GRACE_MS;
6049
+ const poll = setInterval(() => {
6050
+ const alive = children.filter((c) => c.proc && !c.proc.killed);
6051
+ if (alive.length === 0 || Date.now() >= deadline) {
6052
+ clearInterval(poll);
6053
+ for (const c of alive) c.proc?.kill("SIGKILL");
6054
+ resolve7();
6055
+ }
6056
+ }, 200);
6057
+ };
6058
+ for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => stop(sig));
6059
+ });
6060
+ process.exit(0);
6061
+ }
6062
+
5959
6063
  // src/core/single-instance.ts
5960
6064
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
5961
6065
  import { dirname as dirname11 } from "path";
@@ -6003,8 +6107,20 @@ function acquireSingleInstanceLock(appId) {
6003
6107
  }
6004
6108
 
6005
6109
  // src/cli/commands/run.ts
6006
- async function runRun() {
6007
- const ready = await ensureOnboarded({ allowCreate: true });
6110
+ async function runRun(botName) {
6111
+ if (botName) {
6112
+ await runSingle(botName);
6113
+ return;
6114
+ }
6115
+ const active = activeBots(await loadBots());
6116
+ if (active.length > 1) {
6117
+ await runSupervisor(active);
6118
+ return;
6119
+ }
6120
+ await runSingle(active[0]?.name);
6121
+ }
6122
+ async function runSingle(botName) {
6123
+ const ready = await ensureOnboarded({ allowCreate: !botName, bot: botName });
6008
6124
  if (!ready) {
6009
6125
  process.exitCode = 1;
6010
6126
  return;
@@ -6047,14 +6163,37 @@ async function runRun() {
6047
6163
 
6048
6164
  // src/cli/commands/daemon.ts
6049
6165
  async function runStart() {
6050
- const ready = await ensureOnboarded({ allowCreate: true });
6051
- if (!ready) {
6052
- process.exitCode = 1;
6053
- return;
6054
- }
6055
- if (!await confirmReadyForDaemon(ready)) {
6056
- process.exitCode = 1;
6057
- return;
6166
+ const active = activeBots(await loadBots());
6167
+ if (active.length === 0) {
6168
+ const ready = await ensureOnboarded({ allowCreate: true });
6169
+ if (!ready) {
6170
+ process.exitCode = 1;
6171
+ return;
6172
+ }
6173
+ if (!await confirmReadyForDaemon(ready)) {
6174
+ process.exitCode = 1;
6175
+ return;
6176
+ }
6177
+ } else {
6178
+ if (active.length > 1) {
6179
+ console.log(`
6180
+ \u540E\u53F0\u670D\u52A1\u5C06\u6258\u7BA1 ${active.length} \u4E2A\u673A\u5668\u4EBA\uFF08supervisor \u591A\u8FDB\u7A0B\uFF0C\u5404\u81EA\u72EC\u7ACB\u8FDB\u7A0B\uFF09\uFF1A`);
6181
+ for (const b of active) console.log(` \u2022 ${b.name} (${b.appId}) [${b.tenant}]`);
6182
+ console.log("");
6183
+ }
6184
+ for (const bot2 of active) {
6185
+ if (active.length > 1) console.log(`
6186
+ \u2500\u2500\u2500\u2500 \u673A\u5668\u4EBA\u300C${bot2.name}\u300D(${bot2.appId}) \u2500\u2500\u2500\u2500`);
6187
+ const ready = await ensureOnboarded({ bot: bot2.appId });
6188
+ if (!ready) {
6189
+ process.exitCode = 1;
6190
+ return;
6191
+ }
6192
+ if (!await confirmReadyForDaemon(ready)) {
6193
+ process.exitCode = 1;
6194
+ return;
6195
+ }
6196
+ }
6058
6197
  }
6059
6198
  const status = await getServiceAdapter().install();
6060
6199
  console.log(installedNote());
@@ -6151,6 +6290,98 @@ async function runUpdate(opts = {}) {
6151
6290
 
6152
6291
  // src/cli/commands/bot.ts
6153
6292
  import { rm as rm5 } from "fs/promises";
6293
+
6294
+ // src/cli/checkbox.ts
6295
+ import { emitKeypressEvents } from "readline";
6296
+ var ESC = "\x1B";
6297
+ var HIDE_CURSOR = `${ESC}[?25l`;
6298
+ var SHOW_CURSOR = `${ESC}[?25h`;
6299
+ var ALT_SCREEN_ON = `${ESC}[?1049h`;
6300
+ var ALT_SCREEN_OFF = `${ESC}[?1049l`;
6301
+ var HOME_AND_CLEAR = `${ESC}[H${ESC}[2J`;
6302
+ async function checkboxSelect(title, items) {
6303
+ const input2 = process.stdin;
6304
+ const output = process.stdout;
6305
+ if (!input2.isTTY) throw new Error("checkboxSelect requires an interactive TTY");
6306
+ const checked = items.map((it) => Boolean(it.checked));
6307
+ let cursor = 0;
6308
+ const frame = () => {
6309
+ const lines = [title, ""];
6310
+ items.forEach((it, i) => {
6311
+ const box = checked[i] ? "\x1B[32m[x]\x1B[0m" : "[ ]";
6312
+ const pointer = i === cursor ? "\x1B[36m>\x1B[0m" : " ";
6313
+ const label = i === cursor ? `\x1B[1m${it.label}\x1B[0m` : it.label;
6314
+ const hint = it.hint ? ` \x1B[2m${it.hint}\x1B[0m` : "";
6315
+ lines.push(`${pointer} ${box} ${label}${hint}`);
6316
+ });
6317
+ const n = checked.filter(Boolean).length;
6318
+ lines.push("");
6319
+ lines.push(`\x1B[2m${n} \u4E2A\u5DF2\u52FE\u9009 \xB7 \u2191\u2193 \u79FB\u52A8 \xB7 \u7A7A\u683C\u52FE\u9009 \xB7 a \u5168\u9009 \xB7 \u56DE\u8F66\u786E\u8BA4 \xB7 q \u53D6\u6D88\x1B[0m`);
6320
+ return lines.join("\r\n");
6321
+ };
6322
+ const redraw = () => {
6323
+ output.write(HOME_AND_CLEAR + frame());
6324
+ };
6325
+ emitKeypressEvents(input2);
6326
+ const wasRaw = Boolean(input2.isRaw);
6327
+ let restored = false;
6328
+ const restore = () => {
6329
+ if (restored) return;
6330
+ restored = true;
6331
+ output.write(`${SHOW_CURSOR}${ALT_SCREEN_OFF}`);
6332
+ };
6333
+ input2.setRawMode?.(true);
6334
+ input2.resume();
6335
+ output.write(`${ALT_SCREEN_ON}${HIDE_CURSOR}`);
6336
+ process.once("exit", restore);
6337
+ redraw();
6338
+ return await new Promise((resolve7) => {
6339
+ const cleanup = () => {
6340
+ input2.off("keypress", onKey);
6341
+ input2.setRawMode?.(wasRaw);
6342
+ input2.pause();
6343
+ process.off("exit", restore);
6344
+ restore();
6345
+ };
6346
+ const onKey = (_str, key) => {
6347
+ const name = key?.name;
6348
+ if (key?.ctrl && name === "c" || name === "escape" || name === "q") {
6349
+ cleanup();
6350
+ resolve7(null);
6351
+ return;
6352
+ }
6353
+ if (name === "return" || name === "enter") {
6354
+ cleanup();
6355
+ resolve7(checked.flatMap((on, i) => on ? [i] : []));
6356
+ return;
6357
+ }
6358
+ if (name === "up" || name === "k") {
6359
+ cursor = (cursor - 1 + items.length) % items.length;
6360
+ redraw();
6361
+ return;
6362
+ }
6363
+ if (name === "down" || name === "j") {
6364
+ cursor = (cursor + 1) % items.length;
6365
+ redraw();
6366
+ return;
6367
+ }
6368
+ if (name === "space") {
6369
+ checked[cursor] = !checked[cursor];
6370
+ redraw();
6371
+ return;
6372
+ }
6373
+ if (name === "a") {
6374
+ const allOn = checked.every(Boolean);
6375
+ for (let i = 0; i < checked.length; i++) checked[i] = !allOn;
6376
+ redraw();
6377
+ return;
6378
+ }
6379
+ };
6380
+ input2.on("keypress", onKey);
6381
+ });
6382
+ }
6383
+
6384
+ // src/cli/commands/bot.ts
6154
6385
  async function runBotInit(name) {
6155
6386
  if (!ensureCodex()) {
6156
6387
  process.exitCode = 1;
@@ -6165,7 +6396,7 @@ async function runBotInit(name) {
6165
6396
  console.log(" 1) \u4E8B\u4EF6\u4E0E\u56DE\u8C03 \u2192 \u957F\u8FDE\u63A5 \u2192 \u8BA2\u9605\uFF1Aim.message.receive_v1 / card.action.trigger / application.bot.menu_v6");
6166
6397
  console.log(" \uFF08\u53EF\u9009\uFF09\u300C\u52A0\u8FDB\u5DF2\u6709\u7FA4\u300D\u529F\u80FD\u518D\u8BA2\u9605\uFF1Aim.chat.member.bot.added_v1 / im.chat.member.bot.deleted_v1");
6167
6398
  console.log(" 2) \u521B\u5EFA\u5E76\u53D1\u5E03\u5E94\u7528\u7248\u672C");
6168
- console.log("\n`bot list` \u67E5\u770B\u5168\u90E8\uFF1B`bot use <\u540D>` \u5207\u6362\u5F53\u524D\uFF1B`run` \u524D\u53F0\u8DD1 / `start` \u540E\u53F0\u5E38\u9A7B\u3002\n");
6399
+ console.log("\n`bot list` \u67E5\u770B\u5168\u90E8\uFF1B`bot use` \u52FE\u9009\u8981\u540C\u65F6\u8FDE\u63A5\u7684\u673A\u5668\u4EBA\uFF1B`run` \u524D\u53F0\u8DD1 / `start` \u540E\u53F0\u5E38\u9A7B\u3002\n");
6169
6400
  }
6170
6401
  async function runBotList() {
6171
6402
  const reg = await loadBots();
@@ -6173,27 +6404,65 @@ async function runBotList() {
6173
6404
  console.log("\uFF08\u8FD8\u6CA1\u6709\u6CE8\u518C\u4EFB\u4F55\u98DE\u4E66\u673A\u5668\u4EBA\u3002\u8FD0\u884C `feishu-codex-bridge bot init` \u521B\u5EFA\u3002\uFF09");
6174
6405
  return;
6175
6406
  }
6407
+ const active = new Set(activeBots(reg).map((b) => b.appId));
6176
6408
  console.log("\n\u5DF2\u6CE8\u518C\u7684\u98DE\u4E66\u673A\u5668\u4EBA\uFF1A\n");
6177
6409
  for (const b of reg.bots) {
6178
- const cur = b.appId === reg.current ? "\u{1F449}" : " ";
6179
- console.log(`${cur} ${b.name.padEnd(16)} ${b.appId} [${b.tenant}]${b.botName ? ` ${b.botName}` : ""}`);
6410
+ const mark = active.has(b.appId) ? "\u2705" : "\u2B1C";
6411
+ console.log(`${mark} ${b.name.padEnd(16)} ${b.appId} [${b.tenant}]${b.botName ? ` ${b.botName}` : ""}`);
6180
6412
  }
6181
- console.log("\n\u{1F449} = \u5F53\u524D\u9009\u4E2D\uFF08run / start \u542F\u52A8\u7684\u5C31\u662F\u5B83\uFF09\u3002`bot use <\u540D>` \u5207\u6362\u3002\n");
6413
+ console.log("\n\x1B[2m`bot use` \u52FE\u9009\u8981\u540C\u65F6\u8FDE\u63A5\u7684\u673A\u5668\u4EBA\uFF0C\u6216 `bot use <\u540D> [\u540D\u2026]` \u76F4\u63A5\u6307\u5B9A\u3002\x1B[0m\n");
6182
6414
  }
6183
- async function runBotUse(name) {
6415
+ async function runBotUse(names) {
6184
6416
  const reg = await loadBots();
6185
- const bot2 = findBot(reg, name);
6186
- if (!bot2) {
6187
- console.error(`\u2717 \u627E\u4E0D\u5230\u673A\u5668\u4EBA\u300C${name}\u300D\u3002\u5DF2\u6CE8\u518C\uFF1A${botNames(reg.bots)}`);
6417
+ if (reg.bots.length === 0) {
6418
+ console.error("\u2717 \u8FD8\u6CA1\u6709\u6CE8\u518C\u4EFB\u4F55\u98DE\u4E66\u673A\u5668\u4EBA\u3002\u5148 `feishu-codex-bridge bot init` \u521B\u5EFA\u3002");
6188
6419
  process.exitCode = 1;
6189
6420
  return;
6190
6421
  }
6191
- if (reg.current === bot2.appId) {
6192
- console.log(`\u300C${bot2.name}\u300D\u5DF2\u7ECF\u662F\u5F53\u524D\u673A\u5668\u4EBA\u3002`);
6422
+ let appIds;
6423
+ if (names.length > 0) {
6424
+ const resolved = [];
6425
+ const unknown = [];
6426
+ for (const n of names) {
6427
+ const b = findBot(reg, n);
6428
+ if (!b) unknown.push(n);
6429
+ else if (!resolved.includes(b.appId)) resolved.push(b.appId);
6430
+ }
6431
+ if (unknown.length) {
6432
+ console.error(`\u2717 \u627E\u4E0D\u5230\u673A\u5668\u4EBA\uFF1A${unknown.join(", ")}\u3002\u5DF2\u6CE8\u518C\uFF1A${botNames(reg.bots)}`);
6433
+ process.exitCode = 1;
6434
+ return;
6435
+ }
6436
+ appIds = resolved;
6437
+ } else {
6438
+ if (!process.stdin.isTTY) {
6439
+ const cur = activeBots(reg).map((b) => b.name).join(", ") || "\uFF08\u7A7A\uFF09";
6440
+ console.log(`\u5F53\u524D\u6D3B\u8DC3\u673A\u5668\u4EBA\uFF1A${cur}`);
6441
+ console.log("\uFF08\u975E\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u65E0\u6CD5\u5F39\u52FE\u9009\u6846\u3002\u7528 `bot use <\u540D> [\u540D\u2026]` \u76F4\u63A5\u6307\u5B9A\u8981\u540C\u65F6\u8FDE\u63A5\u7684\u673A\u5668\u4EBA\u3002\uFF09");
6442
+ return;
6443
+ }
6444
+ const activeSet = new Set(activeBots(reg).map((b) => b.appId));
6445
+ const items = reg.bots.map((b) => ({
6446
+ label: b.name,
6447
+ hint: `${b.appId} [${b.tenant}]${b.botName ? ` ${b.botName}` : ""}`,
6448
+ checked: activeSet.has(b.appId)
6449
+ }));
6450
+ const picked = await checkboxSelect("\u9009\u62E9\u8981\u540C\u65F6\u8FDE\u63A5\u7684\u673A\u5668\u4EBA\uFF08\u7A7A\u683C\u52FE\u9009\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09\uFF1A", items);
6451
+ if (picked === null) {
6452
+ console.log("\u5DF2\u53D6\u6D88\uFF0C\u672A\u6539\u52A8\u3002");
6453
+ return;
6454
+ }
6455
+ appIds = picked.map((i) => reg.bots[i]?.appId).filter((id) => Boolean(id));
6456
+ }
6457
+ await setActiveBots(appIds);
6458
+ const chosen = appIds.map((id) => reg.bots.find((b) => b.appId === id)?.name ?? id);
6459
+ if (chosen.length === 0) {
6460
+ console.log("\u2713 \u5DF2\u6E05\u7A7A\u6D3B\u8DC3\u673A\u5668\u4EBA\u2014\u2014`run` / `start` \u6682\u4E0D\u8FDE\u63A5\u4EFB\u4F55 bot\u3002`bot use` \u91CD\u65B0\u52FE\u9009\u3002");
6193
6461
  return;
6194
6462
  }
6195
- await setCurrent(bot2.appId);
6196
- console.log(`\u2713 \u5F53\u524D\u673A\u5668\u4EBA \u2192 \u300C${bot2.name}\u300D(${bot2.appId})\u3002\u524D\u53F0 \`run\` \u76F4\u63A5\u751F\u6548\uFF1B\u540E\u53F0\u8BF7 \`restart\`\u3002`);
6463
+ console.log(
6464
+ `\u2713 \u6D3B\u8DC3\u673A\u5668\u4EBA\uFF08${chosen.length} \u4E2A\uFF09\u2192 ${chosen.join(", ")}\u3002\u524D\u53F0\u91CD\u8DD1 \`run\` \u751F\u6548${chosen.length > 1 ? "\uFF08\u591A\u8FDB\u7A0B\u6258\u7BA1\uFF09" : ""}\uFF1B\u540E\u53F0\u8BF7 \`restart\`\u3002`
6465
+ );
6197
6466
  }
6198
6467
  async function runBotRm(name) {
6199
6468
  const reg = await loadBots();
@@ -6278,8 +6547,8 @@ function readStdin() {
6278
6547
  // src/cli/index.ts
6279
6548
  var program = new Command();
6280
6549
  program.name("feishu-codex-bridge").description("\u628A\u98DE\u4E66/Lark \u6865\u63A5\u5230\u672C\u673A Codex\uFF08\u9879\u76EE=\u7FA4, \u8BDD\u9898=\u4F1A\u8BDD\uFF09").version(bridgeVersion());
6281
- program.command("run").description("\u524D\u53F0\u542F\u52A8 bot\uFF08\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF1BCtrl+C \u4F18\u96C5\u9000\u51FA\uFF09").action(async () => {
6282
- await runRun();
6550
+ program.command("run").description("\u524D\u53F0\u542F\u52A8\u6D3B\u8DC3\u673A\u5668\u4EBA\uFF08\u591A\u4E2A\u5219\u5404\u81EA\u72EC\u7ACB\u8FDB\u7A0B\uFF1B\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF1BCtrl+C \u4F18\u96C5\u9000\u51FA\uFF09").option("--bot <name>", "\u53EA\u542F\u52A8\u6307\u5B9A\u7684\u4E00\u4E2A\u673A\u5668\u4EBA\uFF08\u540D\u5B57\u6216 appId\uFF09").action(async (options) => {
6551
+ await runRun(options.bot);
6283
6552
  });
6284
6553
  program.command("start").description("\u540E\u53F0 daemon \u542F\u52A8\uFF08\u88C5 launchd \u5F00\u673A\u81EA\u542F\uFF1B\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF09").action(async () => {
6285
6554
  await runStart();
@@ -6306,8 +6575,8 @@ bot.command("init [name]").description("\u6CE8\u518C\u4E00\u4E2A\u98DE\u4E66\u67
6306
6575
  bot.command("list").description("\u5217\u51FA\u5DF2\u6CE8\u518C\u7684\u98DE\u4E66\u673A\u5668\u4EBA").action(async () => {
6307
6576
  await runBotList();
6308
6577
  });
6309
- bot.command("use <name>").description("\u9009\u62E9 run / start \u542F\u52A8\u65F6\u4F7F\u7528\u7684\u673A\u5668\u4EBA").action(async (name) => {
6310
- await runBotUse(name);
6578
+ bot.command("use [names...]").description("\u52FE\u9009/\u6307\u5B9A\u8981\u540C\u65F6\u8FDE\u63A5\u7684\u673A\u5668\u4EBA\uFF08\u591A\u9009\uFF09\uFF1B\u65E0\u53C2\u6570\u5F39\u4EA4\u4E92\u5F0F\u52FE\u9009\u6846").action(async (names) => {
6579
+ await runBotUse(names ?? []);
6311
6580
  });
6312
6581
  bot.command("rm <name>").description("\u79FB\u9664\u4E00\u4E2A\u673A\u5668\u4EBA\u914D\u7F6E").action(async (name) => {
6313
6582
  await runBotRm(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.0-win.1",
3
+ "version": "0.3.1",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {