botmux 2.64.0 → 2.65.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 (208) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +3 -3
  3. package/dist/adapters/backend/herdr-backend.d.ts +8 -1
  4. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/herdr-backend.js +15 -2
  6. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts +17 -1
  8. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  9. package/dist/adapters/backend/tmux-backend.js +25 -4
  10. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  11. package/dist/adapters/backend/types.d.ts +14 -0
  12. package/dist/adapters/backend/types.d.ts.map +1 -1
  13. package/dist/adapters/backend/types.js.map +1 -1
  14. package/dist/adapters/backend/zellij-backend.d.ts +12 -1
  15. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  16. package/dist/adapters/backend/zellij-backend.js +25 -8
  17. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  18. package/dist/bot-registry.d.ts +39 -0
  19. package/dist/bot-registry.d.ts.map +1 -1
  20. package/dist/bot-registry.js +30 -0
  21. package/dist/bot-registry.js.map +1 -1
  22. package/dist/cli/send-dispatch.d.ts +23 -0
  23. package/dist/cli/send-dispatch.d.ts.map +1 -0
  24. package/dist/cli/send-dispatch.js +23 -0
  25. package/dist/cli/send-dispatch.js.map +1 -0
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +141 -58
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +8 -6
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +8 -6
  32. package/dist/config.js.map +1 -1
  33. package/dist/core/ask-broker.d.ts +33 -0
  34. package/dist/core/ask-broker.d.ts.map +1 -1
  35. package/dist/core/ask-broker.js +58 -0
  36. package/dist/core/ask-broker.js.map +1 -1
  37. package/dist/core/ask-hook/claude-code.d.ts.map +1 -1
  38. package/dist/core/ask-hook/claude-code.js +15 -9
  39. package/dist/core/ask-hook/claude-code.js.map +1 -1
  40. package/dist/core/ask-hook/codex.d.ts.map +1 -1
  41. package/dist/core/ask-hook/codex.js +2 -1
  42. package/dist/core/ask-hook/codex.js.map +1 -1
  43. package/dist/core/ask-hook/opencode.d.ts.map +1 -1
  44. package/dist/core/ask-hook/opencode.js +9 -6
  45. package/dist/core/ask-hook/opencode.js.map +1 -1
  46. package/dist/core/ask-hook/types.d.ts +3 -1
  47. package/dist/core/ask-hook/types.d.ts.map +1 -1
  48. package/dist/core/ask-types.d.ts +13 -6
  49. package/dist/core/ask-types.d.ts.map +1 -1
  50. package/dist/core/ask-types.js.map +1 -1
  51. package/dist/core/command-handler.d.ts.map +1 -1
  52. package/dist/core/command-handler.js +255 -4
  53. package/dist/core/command-handler.js.map +1 -1
  54. package/dist/core/daemon-heartbeat.d.ts +15 -0
  55. package/dist/core/daemon-heartbeat.d.ts.map +1 -0
  56. package/dist/core/daemon-heartbeat.js +83 -0
  57. package/dist/core/daemon-heartbeat.js.map +1 -0
  58. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  59. package/dist/core/dashboard-ipc-server.js +80 -33
  60. package/dist/core/dashboard-ipc-server.js.map +1 -1
  61. package/dist/core/dispatch.d.ts +1 -23
  62. package/dist/core/dispatch.d.ts.map +1 -1
  63. package/dist/core/dispatch.js +1 -17
  64. package/dist/core/dispatch.js.map +1 -1
  65. package/dist/core/idle-worker-sweeper.d.ts +13 -0
  66. package/dist/core/idle-worker-sweeper.d.ts.map +1 -0
  67. package/dist/core/idle-worker-sweeper.js +42 -0
  68. package/dist/core/idle-worker-sweeper.js.map +1 -0
  69. package/dist/core/maintenance-schedule.d.ts +34 -0
  70. package/dist/core/maintenance-schedule.d.ts.map +1 -0
  71. package/dist/core/maintenance-schedule.js +72 -0
  72. package/dist/core/maintenance-schedule.js.map +1 -0
  73. package/dist/core/maintenance.d.ts +43 -0
  74. package/dist/core/maintenance.d.ts.map +1 -0
  75. package/dist/core/maintenance.js +160 -0
  76. package/dist/core/maintenance.js.map +1 -0
  77. package/dist/core/reply-target.d.ts +23 -0
  78. package/dist/core/reply-target.d.ts.map +1 -0
  79. package/dist/core/reply-target.js +47 -0
  80. package/dist/core/reply-target.js.map +1 -0
  81. package/dist/core/restart-report.d.ts +49 -0
  82. package/dist/core/restart-report.d.ts.map +1 -0
  83. package/dist/core/restart-report.js +98 -0
  84. package/dist/core/restart-report.js.map +1 -0
  85. package/dist/core/scheduler.d.ts.map +1 -1
  86. package/dist/core/scheduler.js +20 -0
  87. package/dist/core/scheduler.js.map +1 -1
  88. package/dist/core/session-manager.d.ts +26 -10
  89. package/dist/core/session-manager.d.ts.map +1 -1
  90. package/dist/core/session-manager.js +104 -26
  91. package/dist/core/session-manager.js.map +1 -1
  92. package/dist/core/session-marker.d.ts +13 -0
  93. package/dist/core/session-marker.d.ts.map +1 -0
  94. package/dist/core/session-marker.js +55 -0
  95. package/dist/core/session-marker.js.map +1 -0
  96. package/dist/core/types.d.ts +20 -1
  97. package/dist/core/types.d.ts.map +1 -1
  98. package/dist/core/types.js.map +1 -1
  99. package/dist/core/worker-budget.d.ts +19 -0
  100. package/dist/core/worker-budget.d.ts.map +1 -0
  101. package/dist/core/worker-budget.js +50 -0
  102. package/dist/core/worker-budget.js.map +1 -0
  103. package/dist/core/worker-pool.d.ts +5 -2
  104. package/dist/core/worker-pool.d.ts.map +1 -1
  105. package/dist/core/worker-pool.js +105 -12
  106. package/dist/core/worker-pool.js.map +1 -1
  107. package/dist/daemon.d.ts.map +1 -1
  108. package/dist/daemon.js +243 -38
  109. package/dist/daemon.js.map +1 -1
  110. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  111. package/dist/dashboard/web/bot-defaults.js +118 -0
  112. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  113. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  114. package/dist/dashboard/web/i18n.js +44 -0
  115. package/dist/dashboard/web/i18n.js.map +1 -1
  116. package/dist/dashboard/web/settings.d.ts.map +1 -1
  117. package/dist/dashboard/web/settings.js +84 -13
  118. package/dist/dashboard/web/settings.js.map +1 -1
  119. package/dist/dashboard-web/app.js +568 -503
  120. package/dist/dashboard.js +87 -7
  121. package/dist/dashboard.js.map +1 -1
  122. package/dist/global-config.d.ts +46 -0
  123. package/dist/global-config.d.ts.map +1 -1
  124. package/dist/global-config.js +115 -0
  125. package/dist/global-config.js.map +1 -1
  126. package/dist/i18n/en.d.ts.map +1 -1
  127. package/dist/i18n/en.js +72 -1
  128. package/dist/i18n/en.js.map +1 -1
  129. package/dist/i18n/zh.d.ts.map +1 -1
  130. package/dist/i18n/zh.js +72 -1
  131. package/dist/i18n/zh.js.map +1 -1
  132. package/dist/im/lark/ask-card.d.ts.map +1 -1
  133. package/dist/im/lark/ask-card.js +15 -1
  134. package/dist/im/lark/ask-card.js.map +1 -1
  135. package/dist/im/lark/card-builder.d.ts +17 -0
  136. package/dist/im/lark/card-builder.d.ts.map +1 -1
  137. package/dist/im/lark/card-builder.js +146 -0
  138. package/dist/im/lark/card-builder.js.map +1 -1
  139. package/dist/im/lark/card-handler.d.ts.map +1 -1
  140. package/dist/im/lark/card-handler.js +119 -4
  141. package/dist/im/lark/card-handler.js.map +1 -1
  142. package/dist/im/lark/client.d.ts +3 -2
  143. package/dist/im/lark/client.d.ts.map +1 -1
  144. package/dist/im/lark/client.js +28 -2
  145. package/dist/im/lark/client.js.map +1 -1
  146. package/dist/im/lark/event-dispatcher.d.ts +7 -17
  147. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  148. package/dist/im/lark/event-dispatcher.js +170 -50
  149. package/dist/im/lark/event-dispatcher.js.map +1 -1
  150. package/dist/im/lark/message-parser.d.ts +1 -0
  151. package/dist/im/lark/message-parser.d.ts.map +1 -1
  152. package/dist/im/lark/message-parser.js +1 -0
  153. package/dist/im/lark/message-parser.js.map +1 -1
  154. package/dist/im/lark/reply-mode-command.d.ts +2 -0
  155. package/dist/im/lark/reply-mode-command.d.ts.map +1 -0
  156. package/dist/im/lark/reply-mode-command.js +102 -0
  157. package/dist/im/lark/reply-mode-command.js.map +1 -0
  158. package/dist/services/bot-config-store.d.ts +144 -0
  159. package/dist/services/bot-config-store.d.ts.map +1 -0
  160. package/dist/services/bot-config-store.js +241 -0
  161. package/dist/services/bot-config-store.js.map +1 -0
  162. package/dist/services/card-prefs-store.d.ts +5 -0
  163. package/dist/services/card-prefs-store.d.ts.map +1 -1
  164. package/dist/services/card-prefs-store.js +47 -0
  165. package/dist/services/card-prefs-store.js.map +1 -1
  166. package/dist/services/chat-reply-mode-store.d.ts +28 -0
  167. package/dist/services/chat-reply-mode-store.d.ts.map +1 -0
  168. package/dist/services/chat-reply-mode-store.js +115 -0
  169. package/dist/services/chat-reply-mode-store.js.map +1 -0
  170. package/dist/services/hook-runner.d.ts +43 -0
  171. package/dist/services/hook-runner.d.ts.map +1 -0
  172. package/dist/services/hook-runner.js +394 -0
  173. package/dist/services/hook-runner.js.map +1 -0
  174. package/dist/services/restart-intent-store.d.ts +26 -0
  175. package/dist/services/restart-intent-store.d.ts.map +1 -0
  176. package/dist/services/restart-intent-store.js +84 -0
  177. package/dist/services/restart-intent-store.js.map +1 -0
  178. package/dist/services/session-lifecycle-hooks.d.ts +10 -0
  179. package/dist/services/session-lifecycle-hooks.d.ts.map +1 -0
  180. package/dist/services/session-lifecycle-hooks.js +66 -0
  181. package/dist/services/session-lifecycle-hooks.js.map +1 -0
  182. package/dist/services/session-store.d.ts +6 -0
  183. package/dist/services/session-store.d.ts.map +1 -1
  184. package/dist/services/session-store.js +25 -0
  185. package/dist/services/session-store.js.map +1 -1
  186. package/dist/skills/definitions.d.ts.map +1 -1
  187. package/dist/skills/definitions.js +28 -3
  188. package/dist/skills/definitions.js.map +1 -1
  189. package/dist/types.d.ts +33 -0
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/utils/install-info.d.ts +13 -0
  192. package/dist/utils/install-info.d.ts.map +1 -0
  193. package/dist/utils/install-info.js +56 -0
  194. package/dist/utils/install-info.js.map +1 -0
  195. package/dist/utils/listen-with-probe.d.ts +26 -0
  196. package/dist/utils/listen-with-probe.d.ts.map +1 -0
  197. package/dist/utils/listen-with-probe.js +64 -0
  198. package/dist/utils/listen-with-probe.js.map +1 -0
  199. package/dist/utils/web-terminal-listen.d.ts +30 -0
  200. package/dist/utils/web-terminal-listen.d.ts.map +1 -0
  201. package/dist/utils/web-terminal-listen.js +81 -0
  202. package/dist/utils/web-terminal-listen.js.map +1 -0
  203. package/dist/worker.js +71 -44
  204. package/dist/worker.js.map +1 -1
  205. package/dist/workflows/definition.d.ts +30 -30
  206. package/dist/workflows/events/payloads.d.ts +4 -4
  207. package/dist/workflows/events/schema.d.ts +156 -156
  208. package/package.json +1 -1
@@ -11,9 +11,9 @@ import * as sessionStore from '../services/session-store.js';
11
11
  import * as scheduleStore from '../services/schedule-store.js';
12
12
  import * as scheduler from './scheduler.js';
13
13
  import { scanMultipleProjects, describeProjectDir } from '../services/project-scanner.js';
14
- import { buildRepoSelectCard, buildAdoptSelectCard, buildCodexAppThreadSelectCard, buildSessionClosedCard, buildSlashListCard, getCliDisplayName } from '../im/lark/card-builder.js';
14
+ import { buildRepoSelectCard, buildAdoptSelectCard, buildCodexAppThreadSelectCard, buildSessionClosedCard, buildSlashListCard, getCliDisplayName, buildConfigCard } from '../im/lark/card-builder.js';
15
15
  import { createCliAdapterSync } from '../adapters/cli/registry.js';
16
- import { deleteMessage, sendMessage, listChatBotMembers, resolveUserUnionId, getChatModeStrict } from '../im/lark/client.js';
16
+ import { deleteMessage, sendMessage, sendUserMessage, listChatBotMembers, resolveUserUnionId, getChatModeStrict } from '../im/lark/client.js';
17
17
  import { chatAppLink, normalizeBrand } from '../im/lark/lark-hosts.js';
18
18
  import { claimPairing } from '../services/pairing-store.js';
19
19
  import { logger } from '../utils/logger.js';
@@ -26,6 +26,8 @@ import { discoverAdoptableZellijSessions, validateZellijAdoptTarget } from './ze
26
26
  import { listCodexAppThreads } from '../services/codex-app-threads.js';
27
27
  import { generateAuthUrl, getTokenStatus } from '../utils/user-token.js';
28
28
  import { bindOncall, unbindOncall, getOncallStatus } from '../services/oncall-store.js';
29
+ import { CONFIG_FIELDS, findConfigField, settableFieldKeys, parseBooleanValue, applyConfigField, setBotAllowedUsers, getConfigSnapshot, getConfigCardData, } from '../services/bot-config-store.js';
30
+ import { resolveCliId, findInvalidAllowedUserEntries } from '../setup/bot-config-editor.js';
29
31
  import { publishAttentionPatch, announcePendingRepoSession } from './session-activity.js';
30
32
  import { setCardMode } from '../services/card-mode-store.js';
31
33
  import { invalidWorkingDirs } from '../utils/working-dir.js';
@@ -34,7 +36,7 @@ import { getBotCapability, setBotCapability, clearBotCapability } from '../servi
34
36
  import { sessionKey, sessionAnchorId } from './types.js';
35
37
  import { t, localeForBot } from '../i18n/index.js';
36
38
  // ─── Exported constants ──────────────────────────────────────────────────────
37
- export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help', '/cd', '/repo', '/schedule', '/role', '/pair', '/login', '/adopt', '/detach', '/disconnect', '/oncall', '/group', '/g', '/relay', '/card', '/list-slash-command', '/slash']);
39
+ export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help', '/cd', '/repo', '/schedule', '/role', '/botconfig', '/pair', '/login', '/adopt', '/detach', '/disconnect', '/oncall', '/group', '/g', '/relay', '/card', '/list-slash-command', '/slash']);
38
40
  /**
39
41
  * Daemon commands that act on the chat itself rather than opening a
40
42
  * conversation. `/group` (`/g`) just creates a Lark group and replies once —
@@ -43,7 +45,7 @@ export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help'
43
45
  * card buttons routable, but for these that record is a phantom conversation
44
46
  * that pollutes the dashboard's session list. Handle them without a session.
45
47
  */
46
- export const SESSIONLESS_DAEMON_COMMANDS = new Set(['/group', '/g', '/list-slash-command', '/slash']);
48
+ export const SESSIONLESS_DAEMON_COMMANDS = new Set(['/group', '/g', '/list-slash-command', '/slash', '/botconfig']);
47
49
  /**
48
50
  * Slash commands that are forwarded verbatim to the underlying CLI (e.g.
49
51
  * Claude Code's `/compact`, `/model`, `/usage`). The daemon does NOT handle
@@ -469,6 +471,241 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
469
471
  // Unrecognized format
470
472
  await sessionReply(rootId, t('schedule.parse_failed', undefined, loc));
471
473
  }
474
+ // ─── Config command ──────────────────────────────────────────────────────────
475
+ function configEffectNote(effect, loc) {
476
+ return effect === 'immediate'
477
+ ? t('cmd.config.effect_immediate', undefined, loc)
478
+ : t('cmd.config.effect_next_session', undefined, loc);
479
+ }
480
+ /** `/botconfig zh|en`(及常见别名)→ 卡片显示语言;非语言参数 → undefined(按子命令走)。 */
481
+ function cardLocaleArg(sub) {
482
+ if (!sub)
483
+ return undefined;
484
+ if (sub === 'zh' || sub === 'cn' || sub === '中文' || sub === '中')
485
+ return 'zh';
486
+ if (sub === 'en' || sub === 'english' || sub === '英文' || sub === '英')
487
+ return 'en';
488
+ return undefined;
489
+ }
490
+ function buildConfigHelp(loc) {
491
+ const fields = CONFIG_FIELDS.map(f => `• ${f.key} — ${f.hint}`).join('\n');
492
+ return t('cmd.config.help', { fields }, loc);
493
+ }
494
+ function buildConfigSnapshot(larkAppId, loc) {
495
+ const snap = getConfigSnapshot(larkAppId);
496
+ if (!snap.ok)
497
+ return t('cmd.config.no_bot', undefined, loc);
498
+ const lines = snap.rows.map(r => `• ${r.key} = ${r.value}`).join('\n');
499
+ return t('cmd.config.snapshot', {
500
+ cli: snap.info.cliId,
501
+ brand: snap.info.brand,
502
+ admins: snap.info.resolvedAdmins,
503
+ dirs: snap.info.workingDirs.join(', ') || '∅',
504
+ fields: lines,
505
+ }, loc);
506
+ }
507
+ /**
508
+ * `/botconfig set allowedUsers ...` —— 动信任根的敏感路径,与普通字段分开:
509
+ * 末尾的 `确认`/`confirm` 才真正落盘;缺确认 → 回显预览要求二次确认。
510
+ * 非法条目(裸邮箱前缀等)先挡;防自锁 / 解析空由 {@link setBotAllowedUsers} 兜底。
511
+ */
512
+ async function applyAllowedUsersSet(tokens, rootId, larkAppId, senderId, deps, loc) {
513
+ const reply = (c) => deps.sessionReply(rootId, c, undefined, larkAppId);
514
+ let list = [...tokens];
515
+ let confirmed = false;
516
+ if (list.length && /^(confirm|确认|yes|--yes)$/i.test(list[list.length - 1])) {
517
+ confirmed = true;
518
+ list = list.slice(0, -1);
519
+ }
520
+ const entries = list.join(' ').split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
521
+ if (entries.length === 0) {
522
+ await reply(t('cmd.config.allow_usage', undefined, loc));
523
+ return;
524
+ }
525
+ const invalid = findInvalidAllowedUserEntries(entries);
526
+ if (invalid.length) {
527
+ await reply(t('cmd.config.allow_invalid', { items: invalid.join(', ') }, loc));
528
+ return;
529
+ }
530
+ if (!confirmed) {
531
+ await reply(t('cmd.config.allow_confirm', { list: entries.join(', ') }, loc));
532
+ return;
533
+ }
534
+ const r = await setBotAllowedUsers(larkAppId, entries, senderId);
535
+ if (!r.ok) {
536
+ if (r.reason === 'self_lockout') {
537
+ await reply(t('cmd.config.allow_lockout', undefined, loc));
538
+ return;
539
+ }
540
+ if (r.reason === 'empty_resolved') {
541
+ await reply(t('cmd.config.allow_empty', undefined, loc));
542
+ return;
543
+ }
544
+ await reply(t('cmd.config.write_failed', { reason: r.reason }, loc));
545
+ return;
546
+ }
547
+ await reply(t('cmd.config.allow_ok', { count: r.resolved.length, total: r.raw.length }, loc));
548
+ }
549
+ /**
550
+ * `/botconfig` —— owner/allowedUsers 远程改本 bot 运营字段。sessionless:只认 larkAppId,
551
+ * 不需活跃会话。严格 admin 闸(拒绝开放模式 bot),写盘 + 内存热更新,无需重启。
552
+ */
553
+ async function handleConfigCommand(message, rootId, larkAppId, deps) {
554
+ const loc = localeForBot(larkAppId);
555
+ const reply = (c) => deps.sessionReply(rootId, c, undefined, larkAppId);
556
+ const senderId = message.senderId;
557
+ // Admin 闸:严格限定 allowedUsers,**拒绝开放模式**(无 allowlist 的 bot 没有可
558
+ // 授权的 owner,不能凭聊天改配置)。上游 canOperate 对开放模式 / 兄弟 bot 也放行,
559
+ // 改配置比一般 daemon 命令敏感,这里收紧到「本 bot 的 allowedUsers」。
560
+ let bot;
561
+ try {
562
+ bot = getBot(larkAppId);
563
+ }
564
+ catch {
565
+ await reply(t('cmd.config.no_bot', undefined, loc));
566
+ return;
567
+ }
568
+ const admins = bot.resolvedAllowedUsers;
569
+ if (admins.length === 0) {
570
+ await reply(t('cmd.config.no_owner', undefined, loc));
571
+ return;
572
+ }
573
+ if (!senderId || !admins.includes(senderId)) {
574
+ await reply(t('cmd.config.not_admin', undefined, loc));
575
+ return;
576
+ }
577
+ const trimmed = message.content.replace(/^\/botconfig\s*/i, '').trim();
578
+ const parts = trimmed ? trimmed.split(/\s+/) : [];
579
+ const sub = parts[0]?.toLowerCase();
580
+ // 裸 /botconfig → 交互配置卡片;`/botconfig zh|en` → 指定卡片显示语言(覆盖 bot 默认)。
581
+ const cardLoc = cardLocaleArg(sub);
582
+ if (!sub || cardLoc) {
583
+ const renderLoc = cardLoc ?? loc;
584
+ let modelChoices = [];
585
+ try {
586
+ modelChoices = createCliAdapterSync(bot.config.cliId, bot.config.cliPathOverride).modelChoices ?? [];
587
+ }
588
+ catch { /* 无候选 → 不渲染 model 下拉 */ }
589
+ const data = getConfigCardData(larkAppId, modelChoices);
590
+ if (!data) {
591
+ await reply(buildConfigHelp(renderLoc));
592
+ return;
593
+ }
594
+ const cardJson = buildConfigCard(data, renderLoc);
595
+ // 始终把卡片**私信**给 owner,群里不留任何回复:
596
+ // • 私聊(单发给 bot)→ sendUserMessage 落在当前私聊 = 直接返回配置;
597
+ // • 群 / 话题群 → 卡片落在 owner 私聊,群内不产生「话题回复」、也只他可见。
598
+ // 不再依赖 getChatModeStrict(它会偶发 500 → 误判)。
599
+ // 私信失败(owner 从未与 bot 开过单聊等):**绝不**把整张配置卡回退到会话内——
600
+ // 在群/话题群里那会让 owner-only 的运营配置卡全员可见(按钮虽仍重验 admin 无法提权,
601
+ // 但卡片本身就违背「始终私信」意图)。只回一句简短文字引导去单聊后重试。
602
+ try {
603
+ await sendUserMessage(larkAppId, senderId, cardJson, 'interactive');
604
+ }
605
+ catch {
606
+ await reply(t('cmd.config.card_dm_failed', undefined, renderLoc));
607
+ }
608
+ return;
609
+ }
610
+ if (sub === 'help' || sub === '帮助') {
611
+ await reply(buildConfigHelp(loc));
612
+ return;
613
+ }
614
+ if (sub === 'get' || sub === 'show' || sub === 'list' || sub === '查看') {
615
+ await reply(buildConfigSnapshot(larkAppId, loc));
616
+ return;
617
+ }
618
+ if (sub === 'set' || sub === 'unset') {
619
+ const fieldKey = parts[1];
620
+ if (!fieldKey) {
621
+ await reply(t('cmd.config.set_usage', undefined, loc));
622
+ return;
623
+ }
624
+ const spec = findConfigField(fieldKey);
625
+ if (!spec) {
626
+ await reply(t('cmd.config.unknown_field', { field: fieldKey, fields: settableFieldKeys().join(', ') }, loc));
627
+ return;
628
+ }
629
+ if (sub === 'unset') {
630
+ if (!spec.clearable) {
631
+ await reply(t('cmd.config.not_clearable', { field: spec.key }, loc));
632
+ return;
633
+ }
634
+ const r = await applyConfigField(larkAppId, spec, null);
635
+ if (!r.ok) {
636
+ await reply(t('cmd.config.write_failed', { reason: r.reason }, loc));
637
+ return;
638
+ }
639
+ await reply(t('cmd.config.unset_ok', { field: spec.key, old: r.oldText, effect: configEffectNote(r.effect, loc) }, loc));
640
+ return;
641
+ }
642
+ // set
643
+ if (spec.kind === 'allowedUsers') {
644
+ await applyAllowedUsersSet(parts.slice(2), rootId, larkAppId, senderId, deps, loc);
645
+ return;
646
+ }
647
+ const rawValue = parts.slice(2).join(' ').trim();
648
+ if (!rawValue) {
649
+ await reply(t('cmd.config.value_required', { field: spec.key }, loc));
650
+ return;
651
+ }
652
+ let value;
653
+ switch (spec.kind) {
654
+ case 'boolean': {
655
+ const b = parseBooleanValue(rawValue);
656
+ if (b === undefined) {
657
+ await reply(t('cmd.config.invalid_bool', { field: spec.key, value: rawValue }, loc));
658
+ return;
659
+ }
660
+ value = b;
661
+ break;
662
+ }
663
+ case 'enum': {
664
+ const v = rawValue.toLowerCase();
665
+ if (!spec.enumValues?.includes(v)) {
666
+ await reply(t('cmd.config.invalid_enum', { field: spec.key, values: (spec.enumValues ?? []).join('|') }, loc));
667
+ return;
668
+ }
669
+ value = v;
670
+ break;
671
+ }
672
+ case 'cli': {
673
+ try {
674
+ const id = resolveCliId(rawValue);
675
+ if (!id) {
676
+ await reply(t('cmd.config.value_required', { field: spec.key }, loc));
677
+ return;
678
+ }
679
+ value = id;
680
+ }
681
+ catch (e) {
682
+ await reply(t('cmd.config.invalid_cli', { msg: e?.message ?? String(e) }, loc));
683
+ return;
684
+ }
685
+ break;
686
+ }
687
+ case 'dir': {
688
+ const v = validateWorkingDir(rawValue, loc);
689
+ if (!v.ok) {
690
+ await reply(v.error);
691
+ return;
692
+ }
693
+ value = rawValue; // 存原始(保留 ~),与 workingDir 落盘一致;使用处再 expandHome
694
+ break;
695
+ }
696
+ default: // 'string'
697
+ value = rawValue;
698
+ }
699
+ const r = await applyConfigField(larkAppId, spec, value);
700
+ if (!r.ok) {
701
+ await reply(t('cmd.config.write_failed', { reason: r.reason }, loc));
702
+ return;
703
+ }
704
+ await reply(t('cmd.config.set_ok', { field: spec.key, old: r.oldText, new: r.newText, effect: configEffectNote(r.effect, loc) }, loc));
705
+ return;
706
+ }
707
+ await reply(t('cmd.config.unknown_sub', { sub }, loc));
708
+ }
472
709
  // ─── Main command handler ────────────────────────────────────────────────────
473
710
  /**
474
711
  * Handle `/card` (owner-only). Resolves the active session itself, so off/on
@@ -846,6 +1083,16 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
846
1083
  logger.info(`[${logTag}] Role command handled`);
847
1084
  break;
848
1085
  }
1086
+ case '/botconfig': {
1087
+ const appId = larkAppId ?? ds?.larkAppId;
1088
+ if (!appId) {
1089
+ await sessionReply(rootId, t('cmd.config.no_bot', undefined, loc));
1090
+ break;
1091
+ }
1092
+ await handleConfigCommand(message, rootId, appId, deps);
1093
+ logger.info(`[${logTag}] Config command handled`);
1094
+ break;
1095
+ }
849
1096
  case '/pair': {
850
1097
  const code = message.content.replace(/^\/pair\s*/, '').trim();
851
1098
  if (!larkAppId) {
@@ -1755,6 +2002,10 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1755
2002
  t('help.grant', undefined, loc),
1756
2003
  t('help.revoke', undefined, loc),
1757
2004
  '',
2005
+ t('help.heading_config', undefined, loc),
2006
+ t('help.config_get', undefined, loc),
2007
+ t('help.config_set', undefined, loc),
2008
+ '',
1758
2009
  t('help.heading_group', undefined, loc),
1759
2010
  t('help.group', undefined, loc),
1760
2011
  '',