botmux 2.55.0 → 2.56.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.
Files changed (103) hide show
  1. package/dist/adapters/backend/herdr-backend.d.ts +80 -0
  2. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -0
  3. package/dist/adapters/backend/herdr-backend.js +519 -0
  4. package/dist/adapters/backend/herdr-backend.js.map +1 -0
  5. package/dist/adapters/backend/session-backend-selector.d.ts +2 -2
  6. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  7. package/dist/adapters/backend/session-backend-selector.js +19 -1
  8. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  9. package/dist/adapters/backend/tmux-pipe-backend.d.ts +29 -0
  10. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  11. package/dist/adapters/backend/tmux-pipe-backend.js +51 -4
  12. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  13. package/dist/adapters/backend/types.d.ts +7 -0
  14. package/dist/adapters/backend/types.d.ts.map +1 -1
  15. package/dist/adapters/backend/types.js.map +1 -1
  16. package/dist/adapters/cli/codex.d.ts.map +1 -1
  17. package/dist/adapters/cli/codex.js +11 -0
  18. package/dist/adapters/cli/codex.js.map +1 -1
  19. package/dist/bot-registry.d.ts +2 -1
  20. package/dist/bot-registry.d.ts.map +1 -1
  21. package/dist/bot-registry.js.map +1 -1
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +262 -15
  24. package/dist/cli.js.map +1 -1
  25. package/dist/config.d.ts +2 -1
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +13 -3
  28. package/dist/config.js.map +1 -1
  29. package/dist/core/command-handler.d.ts.map +1 -1
  30. package/dist/core/command-handler.js +12 -7
  31. package/dist/core/command-handler.js.map +1 -1
  32. package/dist/core/pending-response.d.ts +16 -1
  33. package/dist/core/pending-response.d.ts.map +1 -1
  34. package/dist/core/pending-response.js +19 -1
  35. package/dist/core/pending-response.js.map +1 -1
  36. package/dist/core/session-discovery.d.ts +20 -4
  37. package/dist/core/session-discovery.d.ts.map +1 -1
  38. package/dist/core/session-discovery.js +228 -72
  39. package/dist/core/session-discovery.js.map +1 -1
  40. package/dist/core/session-manager.d.ts +2 -1
  41. package/dist/core/session-manager.d.ts.map +1 -1
  42. package/dist/core/session-manager.js +77 -45
  43. package/dist/core/session-manager.js.map +1 -1
  44. package/dist/core/types.d.ts +8 -1
  45. package/dist/core/types.d.ts.map +1 -1
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/core/worker-pool.d.ts.map +1 -1
  48. package/dist/core/worker-pool.js +74 -62
  49. package/dist/core/worker-pool.js.map +1 -1
  50. package/dist/daemon.js +4 -4
  51. package/dist/daemon.js.map +1 -1
  52. package/dist/i18n/en.d.ts.map +1 -1
  53. package/dist/i18n/en.js +1 -0
  54. package/dist/i18n/en.js.map +1 -1
  55. package/dist/i18n/zh.d.ts.map +1 -1
  56. package/dist/i18n/zh.js +1 -0
  57. package/dist/i18n/zh.js.map +1 -1
  58. package/dist/im/lark/card-builder.d.ts +2 -1
  59. package/dist/im/lark/card-builder.d.ts.map +1 -1
  60. package/dist/im/lark/card-builder.js +19 -2
  61. package/dist/im/lark/card-builder.js.map +1 -1
  62. package/dist/im/lark/card-handler.d.ts.map +1 -1
  63. package/dist/im/lark/card-handler.js +5 -3
  64. package/dist/im/lark/card-handler.js.map +1 -1
  65. package/dist/im/lark/event-dispatcher.js +3 -3
  66. package/dist/im/lark/event-dispatcher.js.map +1 -1
  67. package/dist/setup/agent-preset.d.ts +78 -0
  68. package/dist/setup/agent-preset.d.ts.map +1 -0
  69. package/dist/setup/agent-preset.js +127 -0
  70. package/dist/setup/agent-preset.js.map +1 -0
  71. package/dist/setup/bot-config-editor.js +2 -2
  72. package/dist/setup/bot-config-editor.js.map +1 -1
  73. package/dist/setup/ensure-herdr-integrations.d.ts +26 -0
  74. package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -0
  75. package/dist/setup/ensure-herdr-integrations.js +127 -0
  76. package/dist/setup/ensure-herdr-integrations.js.map +1 -0
  77. package/dist/setup/ensure-herdr.d.ts +12 -0
  78. package/dist/setup/ensure-herdr.d.ts.map +1 -0
  79. package/dist/setup/ensure-herdr.js +70 -0
  80. package/dist/setup/ensure-herdr.js.map +1 -0
  81. package/dist/setup/ensure-opus.d.ts +13 -0
  82. package/dist/setup/ensure-opus.d.ts.map +1 -0
  83. package/dist/setup/ensure-opus.js +64 -0
  84. package/dist/setup/ensure-opus.js.map +1 -0
  85. package/dist/setup/ensure-tmux.d.ts +13 -0
  86. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  87. package/dist/setup/ensure-tmux.js +4 -4
  88. package/dist/setup/ensure-tmux.js.map +1 -1
  89. package/dist/setup/index.d.ts +4 -0
  90. package/dist/setup/index.d.ts.map +1 -1
  91. package/dist/setup/index.js +78 -1
  92. package/dist/setup/index.js.map +1 -1
  93. package/dist/types.d.ts +14 -2
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/utils/transient-snapshot.d.ts +3 -3
  96. package/dist/utils/transient-snapshot.js +3 -3
  97. package/dist/worker.js +251 -179
  98. package/dist/worker.js.map +1 -1
  99. package/dist/workflows/attempt-resume.d.ts +2 -1
  100. package/dist/workflows/attempt-resume.d.ts.map +1 -1
  101. package/dist/workflows/attempt-resume.js.map +1 -1
  102. package/dist/workflows/definition.d.ts +22 -22
  103. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -31,13 +31,14 @@ import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart }
31
31
  import { tmuxEnv } from './setup/ensure-tmux.js';
32
32
  import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
33
33
  import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, assertOwnerWhenChatGroups, findInvalidAllowedUserEntries, hasOwnerEntry, } from './setup/bot-config-editor.js';
34
+ import { buildPreset, serializePreset, presetFilename } from './setup/agent-preset.js';
34
35
  import { createCliAdapterSync } from './adapters/cli/registry.js';
35
36
  import { logger } from './utils/logger.js';
36
37
  import { invalidWorkingDirs } from './utils/working-dir.js';
37
38
  import { firstPositional } from './cli/arg-utils.js';
38
39
  import { formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-list-output.js';
39
40
  import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, } from './utils/bot-routing.js';
40
- import { isLocale, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
41
+ import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
41
42
  import { readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
42
43
  // Resolve the CLI's UI locale once from the global config file, so subsequent
43
44
  // CLI output (and any t() callers that don't pass an explicit locale) honour
@@ -748,8 +749,8 @@ async function promptEditBotConfig(rl, bot) {
748
749
  input.model = cliChanged && result === undefined ? null : result;
749
750
  }
750
751
  printInputHelp('会话后端 backendType', [
751
- '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;zellij 为实验后端(pty-under-zellij,需 zellij >= 0.44)。',
752
- '留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / zellij。',
752
+ '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。',
753
+ '留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
753
754
  ]);
754
755
  input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `);
755
756
  printInputHelp('默认工作目录', [
@@ -2210,6 +2211,12 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
2210
2211
  create-group --bot <name> [--bot ...] [--name "群名"]
2211
2212
  用指定 bot 起新群;详见 \`botmux create-group --help\`
2212
2213
 
2214
+ 预设分享(导出某 bot 的可分享配置给同事,绝不含密钥):
2215
+ preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]
2216
+ 导出 cliId/model/角色/能力标签 + 接入指引;
2217
+ 默认 team 级角色,--from-chat 取某群角色内容;
2218
+ 缺省写 ./<name或appid>.botmux-preset.json,--out - 走 stdout
2219
+
2213
2220
  配置目录: ~/.botmux/
2214
2221
  文档: https://github.com/deepcoldy/botmux
2215
2222
  `);
@@ -2285,6 +2292,26 @@ function argValue(args, ...flags) {
2285
2292
  function argFlag(args, flag) {
2286
2293
  return args.includes(flag);
2287
2294
  }
2295
+ /**
2296
+ * True when `flag` is present but lacks a usable value — i.e. it's the last
2297
+ * token, is followed by another flag, or was given as `--flag=` (empty). Lets
2298
+ * callers surface a friendly error instead of silently falling back to a
2299
+ * default (e.g. treating a value-less `--from-chat` as "no chat"). `allowDash`
2300
+ * permits a bare `-` value (used by `--out -` to mean stdout).
2301
+ */
2302
+ function flagPresentButValueMissing(args, flag, allowDash = false) {
2303
+ const i = args.findIndex(a => a === flag || a.startsWith(flag + '='));
2304
+ if (i < 0)
2305
+ return false; // absent entirely — not "missing a value"
2306
+ if (args[i].startsWith(flag + '='))
2307
+ return args[i].slice(flag.length + 1) === '';
2308
+ const next = args[i + 1];
2309
+ if (next === undefined)
2310
+ return true;
2311
+ if (next.startsWith('-'))
2312
+ return !(allowDash && next === '-');
2313
+ return false;
2314
+ }
2288
2315
  /** Extract positional args, skipping --flag and the value that follows it
2289
2316
  * (for --flag <value> style). --flag=value style is self-contained.
2290
2317
  * `booleanFlags` lists flags that take no value — without this hint the
@@ -2643,9 +2670,10 @@ function argValues(args, ...flags) {
2643
2670
  }
2644
2671
  // Card v2 body builder helpers — extracted to im/lark/md-card.ts so the
2645
2672
  // daemon's bridge fallback path can produce identical cards. cmdSend
2646
- // keeps using `buildCardBodyElements` and `hasMarkdown` from there.
2673
+ // keeps using `buildCardBodyElements` from there.
2674
+ import { buildMentionedPendingResponseCard } from './im/lark/card-builder.js';
2647
2675
  import { buildCardBodyElements, brandFooterSegment } from './im/lark/md-card.js';
2648
- import { claimPendingResponseCard, markPendingResponseCardPatchedIfCurrent, mergePendingResponseState } from './core/pending-response.js';
2676
+ import { COMPLETED_REACTION_EMOJI_TYPE, claimPendingResponseCard, markPendingResponseCardPatchedIfCurrent, mergePendingResponseState, shouldMarkPendingAsMentionedSend, shouldPatchPendingOnExplicitSend } from './core/pending-response.js';
2649
2677
  import { resolveBrandLabel } from './bot-registry.js';
2650
2678
  import { config } from './config.js';
2651
2679
  import { resolveQuoteTarget, validateMentionDecision } from './services/send-policy.js';
@@ -2843,7 +2871,7 @@ async function cmdSend(rest) {
2843
2871
  registerBot(cfg);
2844
2872
  }
2845
2873
  catch { /* */ }
2846
- const { sendMessage, replyMessage, uploadImage, uploadFile, deleteMessage, MessageWithdrawnError } = await import('./im/lark/client.js');
2874
+ const { sendMessage, replyMessage, uploadImage, uploadFile, updateMessage, addReaction, MessageWithdrawnError } = await import('./im/lark/client.js');
2847
2875
  const appId = s.larkAppId;
2848
2876
  // Effective target chat for top-level mode (defaults to session's chat)
2849
2877
  const targetChatId = overrideChatId ?? s.chatId;
@@ -2914,16 +2942,50 @@ async function cmdSend(rest) {
2914
2942
  catch { /* best-effort: marker miss only causes a redundant fallback message */ }
2915
2943
  };
2916
2944
  const shouldRecordBridgeMarker = !sendTopLevel && !overrideChatId && !sendInto;
2945
+ let hasNotificationMentionsForPending = mentionArgs.length > 0;
2917
2946
  const dispatchOrPatchPending = async (content, msgType) => {
2918
- const pendingCardId = msgType === 'interactive' ? claimPendingResponseCard(s) : undefined;
2919
- const sentId = await dispatchPrimary(content, msgType);
2920
- const latest = pendingCardId ? loadSessionFresh(s) : undefined;
2921
- if (pendingCardId && latest?.pendingResponseCardId === pendingCardId) {
2922
- deleteMessage(appId, pendingCardId)
2923
- .then(() => { markPendingResponseCardPatchedIfCurrent(latest, pendingCardId); saveSession(latest); })
2924
- .catch((err) => logger.warn(`[send:${sid.substring(0, 8)}] failed to withdraw pending card after explicit send: ${err?.message ?? err}`));
2925
- }
2926
- return sentId;
2947
+ const pendingCardId = shouldPatchPendingOnExplicitSend(s, { msgType, sendTopLevel, overrideChatId: !!overrideChatId, sendInto: !!sendInto, hasNotificationMentions: hasNotificationMentionsForPending })
2948
+ ? claimPendingResponseCard(s)
2949
+ : undefined;
2950
+ if (!pendingCardId) {
2951
+ const sentId = await dispatchPrimary(content, msgType);
2952
+ if (shouldMarkPendingAsMentionedSend({ msgType, sendTopLevel, overrideChatId: !!overrideChatId, sendInto: !!sendInto, hasNotificationMentions: hasNotificationMentionsForPending })) {
2953
+ const stalePendingCardId = claimPendingResponseCard(s);
2954
+ const latest = stalePendingCardId ? loadSessionFresh(s) : undefined;
2955
+ if (stalePendingCardId && latest?.pendingResponseCardId === stalePendingCardId) {
2956
+ updateMessage(appId, stalePendingCardId, buildMentionedPendingResponseCard(localeForBot(appId)))
2957
+ .then(() => {
2958
+ if (markPendingResponseCardPatchedIfCurrent(latest, stalePendingCardId))
2959
+ saveSession(latest);
2960
+ })
2961
+ .catch((err) => logger.warn(`[send:${sid.substring(0, 8)}] failed to mark pending card after mentioned send: ${err?.message ?? err}`));
2962
+ }
2963
+ }
2964
+ return sentId;
2965
+ }
2966
+ const latest = loadSessionFresh(s);
2967
+ if (latest?.pendingResponseCardId !== pendingCardId)
2968
+ return dispatchPrimary(content, msgType);
2969
+ try {
2970
+ await updateMessage(appId, pendingCardId, content);
2971
+ if (markPendingResponseCardPatchedIfCurrent(latest, pendingCardId)) {
2972
+ saveSession(latest);
2973
+ if (latest.quoteTargetId) {
2974
+ addReaction(appId, latest.quoteTargetId, COMPLETED_REACTION_EMOJI_TYPE)
2975
+ .catch((err) => logger.warn(`[send:${sid.substring(0, 8)}] failed to add completion reaction to ${latest.quoteTargetId}: ${err?.message ?? err}`));
2976
+ }
2977
+ }
2978
+ return pendingCardId;
2979
+ }
2980
+ catch (err) {
2981
+ if (err instanceof MessageWithdrawnError) {
2982
+ logger.warn(`[send:${sid.substring(0, 8)}] pending card withdrawn before explicit send patch; sending a new reply`);
2983
+ markPendingResponseCardPatchedIfCurrent(latest, pendingCardId);
2984
+ saveSession(latest);
2985
+ return dispatchPrimary(content, msgType);
2986
+ }
2987
+ throw err;
2988
+ }
2927
2989
  };
2928
2990
  // Quote chain (普通群): the primary message replies to the turn's target so
2929
2991
  // Lark renders a 引用 chain. --quote overrides, --no-quote opts out. Thread
@@ -3066,6 +3128,7 @@ async function cmdSend(rest) {
3066
3128
  }
3067
3129
  catch { /* best-effort */ }
3068
3130
  const explicitKnownBotMention = hasKnownBotMention(text, mentions, botEntries, crossRef, appId);
3131
+ hasNotificationMentionsForPending ||= explicitKnownBotMention;
3069
3132
  const knownBotOpenIds = knownBotOpenIdsFromCrossRef(crossRef, botEntries, appId);
3070
3133
  // --no-mention 显式不 @ 任何人 → 连 footer 的"发送给/cc"寻址 <at> 也清空,
3071
3134
  // 否则 footer 仍会 @ 人,与 --no-mention 语义和"未@任何人"输出自相矛盾
@@ -4190,6 +4253,168 @@ function cmdLang(args) {
4190
4253
  console.log(`✅ Set global lang → ${target}.`);
4191
4254
  console.log(`Run \`botmux restart\` for changes to take effect.`);
4192
4255
  }
4256
+ // ─── botmux preset ────────────────────────────────────────────────────────────
4257
+ /**
4258
+ * `botmux preset <sub>` dispatcher. Currently only `export`.
4259
+ */
4260
+ async function cmdPreset(sub, rest) {
4261
+ switch (sub) {
4262
+ case 'export':
4263
+ await cmdPresetExport(rest);
4264
+ break;
4265
+ default:
4266
+ console.error('用法: botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]');
4267
+ process.exit(1);
4268
+ }
4269
+ }
4270
+ /**
4271
+ * `botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]`
4272
+ *
4273
+ * Export a bot's **shareable, secret-free** preset (cliId / model / team role /
4274
+ * capability + an embedded guide) so a teammate's agent can self-configure a
4275
+ * matching bot. Never emits credentials or deployment fields — see
4276
+ * agent-preset.ts:buildPreset for the allow-list guarantee.
4277
+ *
4278
+ * Role source: team-level by default; `--from-chat <chatId>` exports that
4279
+ * group's role content instead (the chatId itself is dropped). Both role and
4280
+ * capability resolve under the effective data dir: this fn sets
4281
+ * `SESSION_DATA_DIR ??= resolveDataDir()` (SESSION_DATA_DIR → ~/.botmux
4282
+ * breadcrumb → default), and reads it via config.session.dataDir's lazy getter —
4283
+ * correct in agent sessions and bare-shell runs alike.
4284
+ */
4285
+ async function cmdPresetExport(rest) {
4286
+ process.env.SESSION_DATA_DIR ??= resolveDataDir();
4287
+ const USAGE = '用法: botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]';
4288
+ const selection = firstPositional(rest, ['--from-chat', '--out']);
4289
+ if (!selection) {
4290
+ console.error(USAGE);
4291
+ console.error(' <bot> 进程名 (botmux-xxx) 或 larkAppId');
4292
+ process.exit(1);
4293
+ return;
4294
+ }
4295
+ const bots = loadBotsJson();
4296
+ if (bots.length === 0) {
4297
+ console.error('❌ 没有可用的 bot:未找到 bots.json 或其中为空。先跑 `botmux setup`。');
4298
+ process.exit(1);
4299
+ return;
4300
+ }
4301
+ const idx = parseBotSelection(selection, bots);
4302
+ if (idx === undefined) {
4303
+ console.error(`❌ 找不到 bot "${selection}"。可选:`);
4304
+ bots.forEach((b, i) => {
4305
+ const appId = typeof b.larkAppId === 'string' ? b.larkAppId : '(无 larkAppId)';
4306
+ console.error(` - ${botProcessName(b, i)} (${appId})`);
4307
+ });
4308
+ process.exit(1);
4309
+ return;
4310
+ }
4311
+ const bot = bots[idx];
4312
+ const appId = typeof bot.larkAppId === 'string' ? bot.larkAppId : '';
4313
+ if (!appId) {
4314
+ console.error(`❌ bot "${selection}" 缺少 larkAppId,无法解析角色/能力。`);
4315
+ process.exit(1);
4316
+ return;
4317
+ }
4318
+ if (!bot.cliId || typeof bot.cliId !== 'string') {
4319
+ console.error(`❌ bot "${selection}" 缺少 cliId,无法导出预设。`);
4320
+ process.exit(1);
4321
+ return;
4322
+ }
4323
+ // Fail loudly when a flag was given without a value, instead of silently
4324
+ // exporting as if it weren't passed (e.g. a value-less `--from-chat` would
4325
+ // otherwise quietly fall back to the team role).
4326
+ if (flagPresentButValueMissing(rest, '--from-chat')) {
4327
+ console.error('❌ --from-chat 需要一个 chatId(如 oc_xxx)。');
4328
+ console.error(USAGE);
4329
+ process.exit(1);
4330
+ return;
4331
+ }
4332
+ if (flagPresentButValueMissing(rest, '--out', true)) {
4333
+ console.error('❌ --out 需要一个文件路径,或用 `--out -` 输出到 stdout。');
4334
+ console.error(USAGE);
4335
+ process.exit(1);
4336
+ return;
4337
+ }
4338
+ const fromChat = argValue(rest, '--from-chat');
4339
+ const out = argValue(rest, '--out');
4340
+ const skipConfirm = argFlag(rest, '--yes') || argFlag(rest, '-y');
4341
+ // capability + role read the SAME data dir. config.session.dataDir is a lazy
4342
+ // getter, so the SESSION_DATA_DIR set at the top of this fn (= resolveDataDir())
4343
+ // is honored — correct for both agent sessions AND bare-shell runs (no longer
4344
+ // the frozen packaged default).
4345
+ const dataDir = config.session.dataDir;
4346
+ const { resolveTeamRoleFile, resolveRoleFile } = await import('./core/role-resolver.js');
4347
+ const { getBotCapability } = await import('./services/bot-profile-store.js');
4348
+ let teamRole;
4349
+ if (fromChat) {
4350
+ teamRole = resolveRoleFile(appId, fromChat);
4351
+ if (teamRole === null) {
4352
+ console.error(`⚠️ 群 ${fromChat} 下没有为该 bot 配置角色;导出将不含 teamRole(仍含 cliId/model/capability)。`);
4353
+ }
4354
+ }
4355
+ else {
4356
+ teamRole = resolveTeamRoleFile(appId);
4357
+ if (teamRole === null) {
4358
+ console.error('⚠️ 该 bot 没有 team 级角色;导出将不含 teamRole。可加 `--from-chat <chatId>` 导出某群的角色内容。');
4359
+ }
4360
+ }
4361
+ const capability = getBotCapability(dataDir, appId);
4362
+ const sourceName = typeof bot.name === 'string' && bot.name.trim() ? bot.name.trim() : undefined;
4363
+ const preset = buildPreset({
4364
+ cliId: bot.cliId,
4365
+ model: typeof bot.model === 'string' ? bot.model : undefined,
4366
+ teamRole,
4367
+ capability,
4368
+ sourceName,
4369
+ });
4370
+ const json = serializePreset(preset);
4371
+ // Confirm before writing — the role may carry internal info. --yes skips.
4372
+ if (!skipConfirm) {
4373
+ if (!process.stdin.isTTY) {
4374
+ console.error('❌ 角色内容可能含内部信息,导出前需确认;非交互环境(如 agent 调用)请加 `--yes` 跳过确认。');
4375
+ process.exit(1);
4376
+ return;
4377
+ }
4378
+ if (teamRole || capability) {
4379
+ console.error('\n即将导出以下内容,请确认不含敏感/内部信息:');
4380
+ console.error('────────────────────────────────────────');
4381
+ if (teamRole)
4382
+ console.error(`[角色 teamRole]\n${teamRole}`);
4383
+ if (capability)
4384
+ console.error(`[能力标签 capability] ${capability}`);
4385
+ console.error('────────────────────────────────────────');
4386
+ }
4387
+ else {
4388
+ console.error('\n(无角色 / 能力标签内容,仅导出 cliId/model)');
4389
+ }
4390
+ // Prompt on stderr so a piped stdout (--out -) stays clean.
4391
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
4392
+ const answer = (await ask(rl, '确认导出?输入 y 继续,其它取消: ')).trim().toLowerCase();
4393
+ rl.close();
4394
+ if (answer !== 'y' && answer !== 'yes') {
4395
+ console.error('已取消,未写入任何文件。');
4396
+ process.exit(1);
4397
+ return;
4398
+ }
4399
+ }
4400
+ // stdout mode: the JSON must own stdout; all chatter goes to stderr.
4401
+ if (out === '-') {
4402
+ process.stdout.write(json);
4403
+ console.error('✅ 已输出到 stdout。本文件不含任何密钥(larkAppId/secret/allowedUsers 等均未包含)。');
4404
+ return;
4405
+ }
4406
+ const outPath = out ?? `./${presetFilename(sourceName, appId)}`;
4407
+ try {
4408
+ writeFileSync(outPath, json, 'utf-8');
4409
+ }
4410
+ catch (err) {
4411
+ console.error(`❌ 写入 ${outPath} 失败: ${err?.message ?? String(err)}`);
4412
+ process.exit(1);
4413
+ return;
4414
+ }
4415
+ console.error(`✅ 已导出预设到 ${outPath}`);
4416
+ console.error(' 本文件不含任何密钥(larkAppId/secret/allowedUsers/workingDir 等均未包含),可安全分享。');
4417
+ }
4193
4418
  // ─── Main ────────────────────────────────────────────────────────────────────
4194
4419
  function getVersion() {
4195
4420
  const pkgPath = join(PKG_ROOT, 'package.json');
@@ -4318,6 +4543,25 @@ async function cmdVoiceSetup(args) {
4318
4543
  mergeGlobalConfig({ voice: voice });
4319
4544
  console.log('\n✅ 已写入 voice 配置。`botmux restart` 后,配了语音的机器人回复卡片底部会出现「🔊 语音总结」按钮。');
4320
4545
  console.log(' 查看:`botmux voice status` 关闭:`botmux voice disable`');
4546
+ // 语音合成产物要编码成飞书语音气泡用的 opus,依赖系统的 opusenc(opus-tools)。
4547
+ // 缺了就当场帮用户装(沿用 ensure-tmux 的包管理器/sudo 机制)。
4548
+ const { ensureOpusTools, probeOpusenc } = await import('./setup/ensure-opus.js');
4549
+ if (!probeOpusenc()) {
4550
+ console.log('\n⚠️ 未检测到 opus 编码器(opus-tools)——语音合成需要它把音频转成飞书语音格式。');
4551
+ const yes = (await ask(rl, '现在自动安装 opus-tools?(Y/n): ')).trim().toLowerCase();
4552
+ if (yes === '' || yes === 'y' || yes === 'yes') {
4553
+ const r = await ensureOpusTools();
4554
+ if (r.installed)
4555
+ console.log(`✅ opus-tools 就绪${r.version ? `(${r.version})` : ''}`);
4556
+ else {
4557
+ console.log(`未能自动安装:${r.reason ?? ''}`);
4558
+ console.log(`请手动安装后再用语音:${r.manualCommand ?? 'apt-get install -y opus-tools / brew install opus-tools'}`);
4559
+ }
4560
+ }
4561
+ else {
4562
+ console.log('已跳过。记得手动安装:Debian/Ubuntu `sudo apt-get install -y opus-tools`,macOS `brew install opus-tools`。');
4563
+ }
4564
+ }
4321
4565
  }
4322
4566
  finally {
4323
4567
  rl.close();
@@ -4401,6 +4645,9 @@ switch (command) {
4401
4645
  case 'bots':
4402
4646
  await cmdBots(process.argv[3] ?? 'list', process.argv.slice(4));
4403
4647
  break;
4648
+ case 'preset':
4649
+ await cmdPreset(process.argv[3] ?? '', process.argv.slice(4));
4650
+ break;
4404
4651
  case 'history':
4405
4652
  await cmdHistory(process.argv.slice(3));
4406
4653
  break;