botmux 2.94.0 → 2.96.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 (231) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +3 -3
  3. package/dist/adapters/backend/tmux-backend.d.ts +18 -2
  4. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/tmux-backend.js +44 -11
  6. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  7. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  8. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  9. package/dist/adapters/backend/types.d.ts +8 -0
  10. package/dist/adapters/backend/types.d.ts.map +1 -1
  11. package/dist/adapters/backend/types.js.map +1 -1
  12. package/dist/adapters/backend/zellij-backend.js +1 -1
  13. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  14. package/dist/adapters/cli/kimi.d.ts +4 -0
  15. package/dist/adapters/cli/kimi.d.ts.map +1 -0
  16. package/dist/adapters/cli/kimi.js +54 -0
  17. package/dist/adapters/cli/kimi.js.map +1 -0
  18. package/dist/adapters/cli/registry.d.ts +2 -1
  19. package/dist/adapters/cli/registry.d.ts.map +1 -1
  20. package/dist/adapters/cli/registry.js +6 -20
  21. package/dist/adapters/cli/registry.js.map +1 -1
  22. package/dist/adapters/cli/types.d.ts +1 -1
  23. package/dist/adapters/cli/types.d.ts.map +1 -1
  24. package/dist/adapters/hook-installer.d.ts.map +1 -1
  25. package/dist/adapters/hook-installer.js +138 -28
  26. package/dist/adapters/hook-installer.js.map +1 -1
  27. package/dist/bot-registry.d.ts +101 -4
  28. package/dist/bot-registry.d.ts.map +1 -1
  29. package/dist/bot-registry.js +192 -3
  30. package/dist/bot-registry.js.map +1 -1
  31. package/dist/cli/stdin-encoding.d.ts +21 -0
  32. package/dist/cli/stdin-encoding.d.ts.map +1 -1
  33. package/dist/cli/stdin-encoding.js +37 -0
  34. package/dist/cli/stdin-encoding.js.map +1 -1
  35. package/dist/cli.d.ts.map +1 -1
  36. package/dist/cli.js +27 -4
  37. package/dist/cli.js.map +1 -1
  38. package/dist/core/ask-hook/opencode.d.ts +15 -17
  39. package/dist/core/ask-hook/opencode.d.ts.map +1 -1
  40. package/dist/core/ask-hook/opencode.js +17 -19
  41. package/dist/core/ask-hook/opencode.js.map +1 -1
  42. package/dist/core/command-handler.d.ts.map +1 -1
  43. package/dist/core/command-handler.js +29 -3
  44. package/dist/core/command-handler.js.map +1 -1
  45. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  46. package/dist/core/dashboard-ipc-server.js +282 -5
  47. package/dist/core/dashboard-ipc-server.js.map +1 -1
  48. package/dist/core/dashboard-rows.d.ts +2 -0
  49. package/dist/core/dashboard-rows.d.ts.map +1 -1
  50. package/dist/core/dashboard-rows.js +2 -0
  51. package/dist/core/dashboard-rows.js.map +1 -1
  52. package/dist/core/session-discovery.d.ts +9 -0
  53. package/dist/core/session-discovery.d.ts.map +1 -1
  54. package/dist/core/session-discovery.js +24 -0
  55. package/dist/core/session-discovery.js.map +1 -1
  56. package/dist/core/session-manager.d.ts.map +1 -1
  57. package/dist/core/session-manager.js +7 -6
  58. package/dist/core/session-manager.js.map +1 -1
  59. package/dist/core/terminal-url.d.ts.map +1 -1
  60. package/dist/core/terminal-url.js +15 -0
  61. package/dist/core/terminal-url.js.map +1 -1
  62. package/dist/core/trigger-session.d.ts.map +1 -1
  63. package/dist/core/trigger-session.js +101 -9
  64. package/dist/core/trigger-session.js.map +1 -1
  65. package/dist/core/types.d.ts +16 -0
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/core/types.js.map +1 -1
  68. package/dist/core/worker-pool.d.ts +1 -1
  69. package/dist/core/worker-pool.d.ts.map +1 -1
  70. package/dist/core/worker-pool.js +69 -11
  71. package/dist/core/worker-pool.js.map +1 -1
  72. package/dist/daemon.d.ts.map +1 -1
  73. package/dist/daemon.js +14 -11
  74. package/dist/daemon.js.map +1 -1
  75. package/dist/dashboard/aggregator.d.ts +5 -0
  76. package/dist/dashboard/aggregator.d.ts.map +1 -1
  77. package/dist/dashboard/aggregator.js +7 -0
  78. package/dist/dashboard/aggregator.js.map +1 -1
  79. package/dist/dashboard/bot-payload.d.ts +8 -0
  80. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  81. package/dist/dashboard/bot-payload.js +8 -1
  82. package/dist/dashboard/bot-payload.js.map +1 -1
  83. package/dist/dashboard/connector-api.d.ts.map +1 -1
  84. package/dist/dashboard/connector-api.js +14 -3
  85. package/dist/dashboard/connector-api.js.map +1 -1
  86. package/dist/dashboard/session-cleanup.d.ts +1 -0
  87. package/dist/dashboard/session-cleanup.d.ts.map +1 -1
  88. package/dist/dashboard/session-cleanup.js +2 -0
  89. package/dist/dashboard/session-cleanup.js.map +1 -1
  90. package/dist/dashboard/settings-write-applier.d.ts +2 -1
  91. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  92. package/dist/dashboard/settings-write-applier.js +7 -0
  93. package/dist/dashboard/settings-write-applier.js.map +1 -1
  94. package/dist/dashboard/trigger-api.d.ts +4 -0
  95. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  96. package/dist/dashboard/trigger-api.js +20 -0
  97. package/dist/dashboard/trigger-api.js.map +1 -1
  98. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  99. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  100. package/dist/dashboard/web/bot-defaults.js +352 -37
  101. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  102. package/dist/dashboard/web/connectors.d.ts +11 -0
  103. package/dist/dashboard/web/connectors.d.ts.map +1 -1
  104. package/dist/dashboard/web/connectors.js +45 -1
  105. package/dist/dashboard/web/connectors.js.map +1 -1
  106. package/dist/dashboard/web/groups.d.ts +7 -0
  107. package/dist/dashboard/web/groups.d.ts.map +1 -1
  108. package/dist/dashboard/web/groups.js +62 -15
  109. package/dist/dashboard/web/groups.js.map +1 -1
  110. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  111. package/dist/dashboard/web/i18n.js +82 -4
  112. package/dist/dashboard/web/i18n.js.map +1 -1
  113. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  114. package/dist/dashboard/web/sessions.js +149 -5
  115. package/dist/dashboard/web/sessions.js.map +1 -1
  116. package/dist/dashboard/web/settings.d.ts.map +1 -1
  117. package/dist/dashboard/web/settings.js +11 -0
  118. package/dist/dashboard/web/settings.js.map +1 -1
  119. package/dist/dashboard/web/whiteboards.d.ts.map +1 -1
  120. package/dist/dashboard/web/whiteboards.js +14 -5
  121. package/dist/dashboard/web/whiteboards.js.map +1 -1
  122. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  123. package/dist/dashboard/webhook-routes.js +126 -6
  124. package/dist/dashboard/webhook-routes.js.map +1 -1
  125. package/dist/dashboard-web/app.js +814 -725
  126. package/dist/dashboard-web/apple-touch-icon.png +0 -0
  127. package/dist/dashboard-web/favicon.png +0 -0
  128. package/dist/dashboard-web/index.html +2 -0
  129. package/dist/dashboard-web/style.css +47 -0
  130. package/dist/dashboard.js +247 -16
  131. package/dist/dashboard.js.map +1 -1
  132. package/dist/global-config.d.ts +10 -0
  133. package/dist/global-config.d.ts.map +1 -1
  134. package/dist/global-config.js +8 -0
  135. package/dist/global-config.js.map +1 -1
  136. package/dist/i18n/en.d.ts.map +1 -1
  137. package/dist/i18n/en.js +21 -0
  138. package/dist/i18n/en.js.map +1 -1
  139. package/dist/i18n/zh.d.ts.map +1 -1
  140. package/dist/i18n/zh.js +21 -0
  141. package/dist/i18n/zh.js.map +1 -1
  142. package/dist/im/lark/card-builder.d.ts.map +1 -1
  143. package/dist/im/lark/card-builder.js +1 -0
  144. package/dist/im/lark/card-builder.js.map +1 -1
  145. package/dist/im/lark/client.d.ts +12 -0
  146. package/dist/im/lark/client.d.ts.map +1 -1
  147. package/dist/im/lark/client.js +48 -21
  148. package/dist/im/lark/client.js.map +1 -1
  149. package/dist/im/lark/event-dispatcher.d.ts +15 -1
  150. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  151. package/dist/im/lark/event-dispatcher.js +199 -16
  152. package/dist/im/lark/event-dispatcher.js.map +1 -1
  153. package/dist/im/lark/grant-command.js +4 -4
  154. package/dist/im/lark/grant-command.js.map +1 -1
  155. package/dist/im/lark/message-parser.d.ts +40 -1
  156. package/dist/im/lark/message-parser.d.ts.map +1 -1
  157. package/dist/im/lark/message-parser.js +37 -2
  158. package/dist/im/lark/message-parser.js.map +1 -1
  159. package/dist/im/lark/summary-command.d.ts +19 -0
  160. package/dist/im/lark/summary-command.d.ts.map +1 -0
  161. package/dist/im/lark/summary-command.js +276 -0
  162. package/dist/im/lark/summary-command.js.map +1 -0
  163. package/dist/mir-local-runtime.d.ts +40 -0
  164. package/dist/mir-local-runtime.d.ts.map +1 -1
  165. package/dist/mir-local-runtime.js +246 -52
  166. package/dist/mir-local-runtime.js.map +1 -1
  167. package/dist/mir-runner.js +26 -1
  168. package/dist/mir-runner.js.map +1 -1
  169. package/dist/platform/binding.d.ts +6 -0
  170. package/dist/platform/binding.d.ts.map +1 -1
  171. package/dist/platform/binding.js +17 -0
  172. package/dist/platform/binding.js.map +1 -1
  173. package/dist/platform/tunnel-client.d.ts +2 -0
  174. package/dist/platform/tunnel-client.d.ts.map +1 -1
  175. package/dist/platform/tunnel-client.js.map +1 -1
  176. package/dist/services/bot-config-store.d.ts.map +1 -1
  177. package/dist/services/bot-config-store.js +1 -0
  178. package/dist/services/bot-config-store.js.map +1 -1
  179. package/dist/services/card-prefs-store.d.ts +2 -2
  180. package/dist/services/card-prefs-store.d.ts.map +1 -1
  181. package/dist/services/card-prefs-store.js +5 -5
  182. package/dist/services/card-prefs-store.js.map +1 -1
  183. package/dist/services/chat-reply-mode-store.d.ts +6 -4
  184. package/dist/services/chat-reply-mode-store.d.ts.map +1 -1
  185. package/dist/services/chat-reply-mode-store.js +6 -4
  186. package/dist/services/chat-reply-mode-store.js.map +1 -1
  187. package/dist/services/oncall-store.d.ts +25 -0
  188. package/dist/services/oncall-store.d.ts.map +1 -1
  189. package/dist/services/oncall-store.js +56 -0
  190. package/dist/services/oncall-store.js.map +1 -1
  191. package/dist/services/pi-transcript.d.ts +19 -0
  192. package/dist/services/pi-transcript.d.ts.map +1 -0
  193. package/dist/services/pi-transcript.js +222 -0
  194. package/dist/services/pi-transcript.js.map +1 -0
  195. package/dist/services/summary-range-store.d.ts +21 -0
  196. package/dist/services/summary-range-store.d.ts.map +1 -0
  197. package/dist/services/summary-range-store.js +89 -0
  198. package/dist/services/summary-range-store.js.map +1 -0
  199. package/dist/services/transcript-resolver.d.ts.map +1 -1
  200. package/dist/services/transcript-resolver.js +21 -4
  201. package/dist/services/transcript-resolver.js.map +1 -1
  202. package/dist/services/trigger-types.d.ts +14 -2
  203. package/dist/services/trigger-types.d.ts.map +1 -1
  204. package/dist/services/trigger-types.js +15 -1
  205. package/dist/services/trigger-types.js.map +1 -1
  206. package/dist/setup/bot-config-editor.d.ts +7 -0
  207. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  208. package/dist/setup/bot-config-editor.js +35 -0
  209. package/dist/setup/bot-config-editor.js.map +1 -1
  210. package/dist/types.d.ts +21 -1
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/utils/executable.d.ts +3 -0
  213. package/dist/utils/executable.d.ts.map +1 -0
  214. package/dist/utils/executable.js +24 -0
  215. package/dist/utils/executable.js.map +1 -0
  216. package/dist/utils/terminal-favicon.d.ts +2 -0
  217. package/dist/utils/terminal-favicon.d.ts.map +1 -0
  218. package/dist/utils/terminal-favicon.js +5 -0
  219. package/dist/utils/terminal-favicon.js.map +1 -0
  220. package/dist/worker.js +211 -12
  221. package/dist/worker.js.map +1 -1
  222. package/dist/workflows/definition.d.ts +8 -8
  223. package/dist/workflows/events/payloads.d.ts +10 -10
  224. package/dist/workflows/events/schema.d.ts +28 -28
  225. package/dist/workflows/v3/bot-resolve.d.ts +1 -1
  226. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
  227. package/dist/workflows/v3/bot-resolve.js +2 -1
  228. package/dist/workflows/v3/bot-resolve.js.map +1 -1
  229. package/dist/workflows/v3/cli-run.js +2 -2
  230. package/dist/workflows/v3/cli-run.js.map +1 -1
  231. package/package.json +2 -2
Binary file
@@ -4,6 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>botmux dashboard</title>
7
+ <link rel="icon" type="image/png" href="/assets/favicon.png">
8
+ <link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
7
9
  <link rel="stylesheet" href="/assets/style.css">
8
10
  </head>
9
11
  <body>
@@ -919,6 +919,22 @@ button.primary {
919
919
  border: 1px solid var(--border-soft);
920
920
  }
921
921
 
922
+ .session-lock-badge {
923
+ display: inline-flex;
924
+ align-items: center;
925
+ min-height: 22px;
926
+ margin-left: 6px;
927
+ padding: 0 8px;
928
+ border-radius: var(--radius-sm);
929
+ border: 1px solid color-mix(in srgb, var(--warning) 38%, var(--border-soft));
930
+ background: color-mix(in srgb, var(--warning) 10%, var(--surface-muted));
931
+ color: var(--warning);
932
+ font-size: 11px;
933
+ font-weight: 800;
934
+ line-height: 1;
935
+ text-transform: uppercase;
936
+ }
937
+
922
938
  .cli-claude-code { background: #e8f0ff; color: #234fb4; border-color: #c8d9ff; }
923
939
  .cli-codex { background: #fff0df; color: #a14c11; border-color: #ffd8ad; }
924
940
  .cli-codex-app { background: #e9f7ff; color: #11638f; border-color: #bde4fa; }
@@ -1140,6 +1156,10 @@ button.primary {
1140
1156
  background: color-mix(in srgb, var(--accent) 5%, var(--surface-raised));
1141
1157
  }
1142
1158
 
1159
+ .session-card.locked {
1160
+ border-color: color-mix(in srgb, var(--warning) 45%, var(--border));
1161
+ }
1162
+
1143
1163
  @media (prefers-reduced-motion: reduce) {
1144
1164
  .session-card { animation: none; transition: none; }
1145
1165
  .session-board-needs-you h2::before { animation: none; }
@@ -1197,6 +1217,19 @@ button.primary {
1197
1217
  font-size: 12px;
1198
1218
  }
1199
1219
 
1220
+ .session-card-status-group,
1221
+ .drawer-status-line {
1222
+ display: inline-flex;
1223
+ align-items: center;
1224
+ gap: 6px;
1225
+ flex-wrap: wrap;
1226
+ }
1227
+
1228
+ .session-card-status-group .session-lock-badge,
1229
+ .drawer-status-line .session-lock-badge {
1230
+ margin-left: 0;
1231
+ }
1232
+
1200
1233
  .session-signal {
1201
1234
  display: inline-flex;
1202
1235
  align-items: center;
@@ -1487,6 +1520,10 @@ button.primary {
1487
1520
  background: color-mix(in srgb, var(--accent) 4%, var(--surface-raised));
1488
1521
  }
1489
1522
 
1523
+ .kanban-card.locked {
1524
+ border-color: color-mix(in srgb, var(--warning) 45%, var(--border));
1525
+ }
1526
+
1490
1527
  .kanban-card:focus-visible {
1491
1528
  outline: 2px solid var(--accent);
1492
1529
  outline-offset: 1px;
@@ -1873,6 +1910,11 @@ button.primary {
1873
1910
  border-color: var(--danger);
1874
1911
  background: color-mix(in srgb, var(--danger) 9%, var(--surface-raised));
1875
1912
  }
1913
+ .card-act.locked {
1914
+ color: var(--warning);
1915
+ border-color: color-mix(in srgb, var(--warning) 45%, var(--border));
1916
+ background: color-mix(in srgb, var(--warning) 8%, var(--surface-raised));
1917
+ }
1876
1918
  .card-act:active { transform: scale(0.9); }
1877
1919
 
1878
1920
  /* 终端访问药丸:左「终端」段(accent ghost·只读旁观) + 右「钥匙」段(accent 实心·
@@ -4216,6 +4258,11 @@ button.contrast:hover { background: var(--danger-soft); border-color: var(--dang
4216
4258
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 55%, transparent);
4217
4259
  flex: none;
4218
4260
  }
4261
+ .bd-summary-limits {
4262
+ display: grid;
4263
+ grid-template-columns: repeat(2, minmax(0, 1fr));
4264
+ gap: 10px;
4265
+ }
4219
4266
 
4220
4267
  .settings-grid {
4221
4268
  display: grid;
package/dist/dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/dashboard.ts
2
- import { createServer, get as httpGet } from 'node:http';
3
- import { createServer as createTcpServer } from 'node:net';
2
+ import { createServer, get as httpGet, request as httpRequest } from 'node:http';
3
+ import { createServer as createTcpServer, connect as netConnect } from 'node:net';
4
4
  import { readFileSync, existsSync, mkdirSync, statSync, createReadStream, } from 'node:fs';
5
5
  import { atomicWriteFileSync } from './utils/atomic-write.js';
6
6
  import { join, dirname, extname, resolve, relative, isAbsolute } from 'node:path';
@@ -178,6 +178,7 @@ function resolveDashboardSettings() {
178
178
  maintenance: global.maintenance ?? {},
179
179
  localDevInstall: isLocalDevInstall(),
180
180
  whiteboard: { enabled: global.whiteboard?.enabled === true },
181
+ remoteAccess: global.remoteAccess === true,
181
182
  };
182
183
  }
183
184
  // Single shared deps object for `applySettingsWrite` — both the browser
@@ -474,8 +475,27 @@ function configuredCliIds() {
474
475
  return new Map();
475
476
  }
476
477
  }
478
+ function configuredBotAgentFields() {
479
+ try {
480
+ return new Map(loadBotConfigs().map(b => [b.larkAppId, {
481
+ cliId: b.cliId,
482
+ wrapperCli: b.wrapperCli,
483
+ model: b.model,
484
+ }]));
485
+ }
486
+ catch {
487
+ return new Map();
488
+ }
489
+ }
477
490
  function withConfiguredCliId(bot, ids) {
478
- return bot.cliId ? bot : { ...bot, cliId: ids.get(bot.larkAppId) };
491
+ const raw = ids.get(bot.larkAppId);
492
+ const fallback = typeof raw === 'string' ? { cliId: raw } : raw;
493
+ return {
494
+ ...bot,
495
+ cliId: bot.cliId || fallback?.cliId,
496
+ wrapperCli: bot.wrapperCli || fallback?.wrapperCli,
497
+ model: bot.model || fallback?.model,
498
+ };
479
499
  }
480
500
  function liveBots() {
481
501
  const ids = configuredCliIds();
@@ -833,6 +853,15 @@ async function dashboardSkillReferences(skillName) {
833
853
  bots: [...refs.values()].sort((a, b) => a.botName.localeCompare(b.botName)),
834
854
  };
835
855
  }
856
+ /** Extract the sessionId from a terminal path `/s/<sessionId>[/...]`. Returns
857
+ * the first path segment after `/s/` (stops at the next `/`; query/hash are
858
+ * already stripped by URL.pathname). undefined when there's no segment. */
859
+ function parseTerminalSessionId(pathname) {
860
+ if (!pathname.startsWith('/s/'))
861
+ return undefined;
862
+ const seg = pathname.slice(3).split('/')[0];
863
+ return seg || undefined;
864
+ }
836
865
  const server = createServer(async (req, res) => {
837
866
  try {
838
867
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
@@ -847,6 +876,35 @@ const server = createServer(async (req, res) => {
847
876
  res.writeHead(200, { 'content-type': 'text/plain' });
848
877
  return res.end(DASHBOARD_SELF_NONCE);
849
878
  }
879
+ // Web terminal reverse-proxy: `/s/<sessionId>/*` → the owning bot daemon's
880
+ // terminal proxy. The central platform only tunnels the dashboard port, so
881
+ // terminal links served under the machine subdomain
882
+ // (`https://m-<id>.<host>/s/<sessionId>`) land here. The dashboard is the
883
+ // aggregator process (it fronts many bot daemons, each with its own terminal
884
+ // proxy on proxyBasePort+idx), so we resolve the session's owning daemon's
885
+ // proxy port from the aggregator rows and forward there, streaming the
886
+ // response straight back. Mounted before the dashboard auth gate because the
887
+ // terminal proxy / worker enforces its own write gate (read-only without
888
+ // token / platform role).
889
+ if (url.pathname === '/s' || url.pathname.startsWith('/s/')) {
890
+ const sessionId = parseTerminalSessionId(url.pathname);
891
+ const tport = sessionId ? aggregator.terminalProxyPortOf(sessionId) : undefined;
892
+ if (!tport) {
893
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
894
+ return res.end('session terminal not available');
895
+ }
896
+ const upstream = httpRequest({ host: '127.0.0.1', port: tport, method: req.method, path: req.url, headers: req.headers }, (up) => {
897
+ res.writeHead(up.statusCode ?? 502, up.headers);
898
+ up.pipe(res);
899
+ });
900
+ upstream.on('error', () => {
901
+ if (!res.headersSent)
902
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
903
+ res.end('terminal proxy error');
904
+ });
905
+ req.pipe(upstream);
906
+ return;
907
+ }
850
908
  if (await handleWebhookRoute(req, res, url, {
851
909
  proxyToDaemon,
852
910
  createLifecycleGroup: createLifecycleGroupForWebhook,
@@ -1091,7 +1149,14 @@ const server = createServer(async (req, res) => {
1091
1149
  // `lang` is the global UI locale (single source of truth shared with
1092
1150
  // `botmux lang` and the Feishu cards) — the web UI reads it as its
1093
1151
  // authoritative initial language when set.
1094
- return jsonRes(res, 200, { settings: dashboardSettings, lang: readGlobalConfig().lang ?? null, authed });
1152
+ // `bound` reflects central-platform binding; the Settings UI only shows the
1153
+ // 远程访问 toggle when bound (the central URLs are meaningless otherwise).
1154
+ return jsonRes(res, 200, {
1155
+ settings: dashboardSettings,
1156
+ lang: readGlobalConfig().lang ?? null,
1157
+ authed,
1158
+ bound: readPlatformBinding() !== null,
1159
+ });
1095
1160
  }
1096
1161
  if (req.method === 'PUT' && url.pathname === '/api/settings') {
1097
1162
  let parsed;
@@ -1480,9 +1545,9 @@ const server = createServer(async (req, res) => {
1480
1545
  return jsonRes(res, 400, { ok: false, error: 'bad_request' });
1481
1546
  return jsonRes(res, 200, { ok: true, entry });
1482
1547
  }
1483
- // 看板放置 / 重命名:带 JSON body 的会话写操作,原样转发给 owner daemon。
1548
+ // 看板放置 / 重命名 / 锁定:带 JSON body 的会话写操作,原样转发给 owner daemon。
1484
1549
  // 不在公开读白名单内 → 只读访客在 decideDashboardAuth 已被 401。
1485
- if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(board|rename)$/))) {
1550
+ if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(board|rename|lock)$/))) {
1486
1551
  const sid = decodeURIComponent(m[1]);
1487
1552
  const op = m[2];
1488
1553
  const owner = aggregator.ownerOf(sid);
@@ -1848,8 +1913,8 @@ const server = createServer(async (req, res) => {
1848
1913
  // [{larkAppId, botName, defaultOncall, ...}]
1849
1914
  // PUT /api/bots/:appId/default-oncall — proxy to that bot's daemon
1850
1915
  if (req.method === 'GET' && url.pathname === '/api/bots') {
1851
- const cliIds = configuredCliIds();
1852
- const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
1916
+ const agentFields = configuredBotAgentFields();
1917
+ const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, agentFields)).sort((a, b) => a.botIndex - b.botIndex);
1853
1918
  const out = await Promise.all(onlineBots.map(async (d) => {
1854
1919
  try {
1855
1920
  const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/bot-default-oncall`);
@@ -1857,7 +1922,13 @@ const server = createServer(async (req, res) => {
1857
1922
  return botDefaultsPayload(d, undefined, `http_${r.status}`);
1858
1923
  }
1859
1924
  const j = await r.json();
1860
- return botDefaultsPayload({ ...d, botName: d.botName ?? j.botName }, j);
1925
+ return botDefaultsPayload({
1926
+ ...d,
1927
+ botName: d.botName ?? j.botName,
1928
+ cliId: j.cliId || d.cliId,
1929
+ wrapperCli: j.wrapperCli || d.wrapperCli,
1930
+ model: j.model || d.model,
1931
+ }, j);
1861
1932
  }
1862
1933
  catch (e) {
1863
1934
  return botDefaultsPayload(d, undefined, e?.message ?? String(e));
@@ -1881,6 +1952,43 @@ const server = createServer(async (req, res) => {
1881
1952
  res.end(await upstream.text());
1882
1953
  return;
1883
1954
  }
1955
+ // PUT /api/bots/:appId/working-dir-mode — proxy to that bot's daemon. Body
1956
+ // `{ mode: 'off'|'default'|'oncall', workingDir }` — sets the 3-way
1957
+ // mutually-exclusive default-dir mode (defaultWorkingDir vs defaultOncall).
1958
+ let mBotWdMode;
1959
+ if (req.method === 'PUT' && (mBotWdMode = url.pathname.match(/^\/api\/bots\/([^/]+)\/working-dir-mode$/))) {
1960
+ const appId = decodeURIComponent(mBotWdMode[1]);
1961
+ const chunks = [];
1962
+ for await (const c of req)
1963
+ chunks.push(c);
1964
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
1965
+ const upstream = await proxyToDaemon(appId, `/api/bot-working-dir-mode`, {
1966
+ method: 'PUT',
1967
+ headers: { 'content-type': 'application/json' },
1968
+ body: raw,
1969
+ });
1970
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1971
+ res.end(await upstream.text());
1972
+ return;
1973
+ }
1974
+ // PUT /api/bots/:appId/agent — proxy to that bot's daemon. Body
1975
+ // `{ cliId, model }`; cliId is the dashboard selection key.
1976
+ let mBotAgent;
1977
+ if (req.method === 'PUT' && (mBotAgent = url.pathname.match(/^\/api\/bots\/([^/]+)\/agent$/))) {
1978
+ const appId = decodeURIComponent(mBotAgent[1]);
1979
+ const chunks = [];
1980
+ for await (const c of req)
1981
+ chunks.push(c);
1982
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
1983
+ const upstream = await proxyToDaemon(appId, `/api/bot-agent`, {
1984
+ method: 'PUT',
1985
+ headers: { 'content-type': 'application/json' },
1986
+ body: raw,
1987
+ });
1988
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1989
+ res.end(await upstream.text());
1990
+ return;
1991
+ }
1884
1992
  // PUT /api/bots/:appId/skills — proxy to that bot's daemon. Body accepts
1885
1993
  // `{ action:'attach'|'detach', name }` or `{ action:'set', policy|null }`.
1886
1994
  let mBotSkills;
@@ -1935,6 +2043,24 @@ const server = createServer(async (req, res) => {
1935
2043
  res.end(await upstream.text());
1936
2044
  return;
1937
2045
  }
2046
+ // PUT /api/bots/:appId/launch-shell — proxy to that bot's daemon. Body
2047
+ // `{ launchShell: string }` (shell name or absolute path; '' = clear → $SHELL).
2048
+ let mBotLaunchShell;
2049
+ if (req.method === 'PUT' && (mBotLaunchShell = url.pathname.match(/^\/api\/bots\/([^/]+)\/launch-shell$/))) {
2050
+ const appId = decodeURIComponent(mBotLaunchShell[1]);
2051
+ const chunks = [];
2052
+ for await (const c of req)
2053
+ chunks.push(c);
2054
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
2055
+ const upstream = await proxyToDaemon(appId, `/api/bot-launch-shell`, {
2056
+ method: 'PUT',
2057
+ headers: { 'content-type': 'application/json' },
2058
+ body: raw,
2059
+ });
2060
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
2061
+ res.end(await upstream.text());
2062
+ return;
2063
+ }
1938
2064
  // PUT /api/bots/:appId/env — proxy to that bot's daemon. Body
1939
2065
  // `{ env: string }` (raw JSON text; '' = clear).
1940
2066
  let mBotEnv;
@@ -1988,6 +2114,41 @@ const server = createServer(async (req, res) => {
1988
2114
  res.end(await upstream.text());
1989
2115
  return;
1990
2116
  }
2117
+ // PUT /api/bots/:appId/summary-range — proxy to that bot's daemon. Body
2118
+ // `{ limit, sinceHours }`; daemon updates the explicit /summary range.
2119
+ let mBotSummaryRange;
2120
+ if (req.method === 'PUT' && (mBotSummaryRange = url.pathname.match(/^\/api\/bots\/([^/]+)\/summary-range$/))) {
2121
+ const appId = decodeURIComponent(mBotSummaryRange[1]);
2122
+ const chunks = [];
2123
+ for await (const c of req)
2124
+ chunks.push(c);
2125
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
2126
+ const upstream = await proxyToDaemon(appId, `/api/bot-summary-range`, {
2127
+ method: 'PUT',
2128
+ headers: { 'content-type': 'application/json' },
2129
+ body: raw,
2130
+ });
2131
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
2132
+ res.end(await upstream.text());
2133
+ return;
2134
+ }
2135
+ // Backward-compatible alias from the short-lived keyword-trigger dashboard.
2136
+ let mBotSummaryTrigger;
2137
+ if (req.method === 'PUT' && (mBotSummaryTrigger = url.pathname.match(/^\/api\/bots\/([^/]+)\/summary-trigger$/))) {
2138
+ const appId = decodeURIComponent(mBotSummaryTrigger[1]);
2139
+ const chunks = [];
2140
+ for await (const c of req)
2141
+ chunks.push(c);
2142
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
2143
+ const upstream = await proxyToDaemon(appId, `/api/bot-summary-trigger`, {
2144
+ method: 'PUT',
2145
+ headers: { 'content-type': 'application/json' },
2146
+ body: raw,
2147
+ });
2148
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
2149
+ res.end(await upstream.text());
2150
+ return;
2151
+ }
1991
2152
  // PUT /api/bots/:appId/p2p-mode — proxy to that bot's daemon. Body
1992
2153
  // `{ p2pMode: 'chat' | 'thread' }` ('chat' = flat continuous DM session;
1993
2154
  // anything else clears back to the per-message thread default).
@@ -2307,6 +2468,59 @@ const server = createServer(async (req, res) => {
2307
2468
  jsonRes(res, 500, { error: String(err) });
2308
2469
  }
2309
2470
  });
2471
+ // Web terminal WebSocket reverse-proxy: bridge `/s/*` upgrade requests through to
2472
+ // the local terminal proxy (which in turn bridges to the session worker). Raw
2473
+ // socket-to-socket bridge: dial 127.0.0.1:<terminalProxyPort>, replay the upgrade
2474
+ // request line + headers verbatim, then pipe both directions. Mirrors how
2475
+ // terminal-proxy.ts bridges to the worker. Non-`/s/*` upgrades are dropped (the
2476
+ // dashboard SPA uses SSE, not WebSocket).
2477
+ server.on('upgrade', (req, clientSocket, head) => {
2478
+ try {
2479
+ const rawUrl = req.url ?? '/';
2480
+ if (!(rawUrl === '/s' || rawUrl.startsWith('/s/') || rawUrl.startsWith('/s?'))) {
2481
+ return clientSocket.destroy();
2482
+ }
2483
+ // Strip query/hash before extracting the sessionId path segment.
2484
+ const pathname = rawUrl.split(/[?#]/)[0];
2485
+ const sessionId = parseTerminalSessionId(pathname);
2486
+ const tport = sessionId ? aggregator.terminalProxyPortOf(sessionId) : undefined;
2487
+ if (!tport)
2488
+ return clientSocket.destroy();
2489
+ const upstream = netConnect(tport, '127.0.0.1', () => {
2490
+ // rawHeaders is a flat [k, v, k, v, ...] list — preserves casing/duplicates.
2491
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
2492
+ const rh = req.rawHeaders;
2493
+ for (let i = 0; i + 1 < rh.length; i += 2)
2494
+ lines.push(`${rh[i]}: ${rh[i + 1]}`);
2495
+ lines.push('', '');
2496
+ upstream.write(lines.join('\r\n'));
2497
+ if (head?.length)
2498
+ upstream.write(head);
2499
+ upstream.pipe(clientSocket);
2500
+ clientSocket.pipe(upstream);
2501
+ });
2502
+ const cleanup = () => {
2503
+ try {
2504
+ upstream.destroy();
2505
+ }
2506
+ catch { /* ignore */ }
2507
+ try {
2508
+ clientSocket.destroy();
2509
+ }
2510
+ catch { /* ignore */ }
2511
+ };
2512
+ upstream.on('error', cleanup);
2513
+ clientSocket.on('error', cleanup);
2514
+ upstream.on('close', () => clientSocket.destroy());
2515
+ clientSocket.on('close', () => upstream.destroy());
2516
+ }
2517
+ catch {
2518
+ try {
2519
+ clientSocket.destroy();
2520
+ }
2521
+ catch { /* ignore */ }
2522
+ }
2523
+ });
2310
2524
  // Probe upward on EADDRINUSE rather than crashing with an unhandled 'error':
2311
2525
  // a second botmux instance on this host (or a stray process) holding the
2312
2526
  // configured port would otherwise tear the dashboard process down on bind.
@@ -2396,14 +2610,31 @@ function readPlatformBotsInfo() {
2396
2610
  const entries = JSON.parse(readFileSync(fp, 'utf8'));
2397
2611
  if (!Array.isArray(entries))
2398
2612
  return [];
2613
+ // Merge per-bot team-visibility config (showInTeam) from bots.json by
2614
+ // larkAppId so the platform team page can hide bots. Default: showInTeam =
2615
+ // true (shown). bots.json may be unreadable from the dashboard process →
2616
+ // fall back to the default.
2617
+ const cfgByAppId = new Map();
2618
+ try {
2619
+ for (const cfg of loadBotConfigs()) {
2620
+ cfgByAppId.set(cfg.larkAppId, { showInTeam: cfg.showInTeam });
2621
+ }
2622
+ }
2623
+ catch {
2624
+ /* defaults below */
2625
+ }
2399
2626
  return entries
2400
- .map((e) => ({
2401
- appId: e.larkAppId || '',
2402
- openId: e.botOpenId ?? null,
2403
- name: e.botName || e.larkAppId || 'bot',
2404
- avatar: e.botAvatarUrl || undefined,
2405
- cli: e.cliId,
2406
- }))
2627
+ .map((e) => {
2628
+ const cfg = cfgByAppId.get(e.larkAppId || '');
2629
+ return {
2630
+ appId: e.larkAppId || '',
2631
+ openId: e.botOpenId ?? null,
2632
+ name: e.botName || e.larkAppId || 'bot',
2633
+ avatar: e.botAvatarUrl || undefined,
2634
+ cli: e.cliId,
2635
+ showInTeam: cfg?.showInTeam !== false, // default true
2636
+ };
2637
+ })
2407
2638
  .filter((b) => b.appId);
2408
2639
  }
2409
2640
  catch {