botmux 2.85.0 → 2.86.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 (153) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +22 -13
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/command-handler.d.ts.map +1 -1
  5. package/dist/core/command-handler.js +209 -1
  6. package/dist/core/command-handler.js.map +1 -1
  7. package/dist/core/cost-calculator.d.ts.map +1 -1
  8. package/dist/core/cost-calculator.js +7 -106
  9. package/dist/core/cost-calculator.js.map +1 -1
  10. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  11. package/dist/core/dashboard-ipc-server.js +240 -2
  12. package/dist/core/dashboard-ipc-server.js.map +1 -1
  13. package/dist/core/passthrough-commands.d.ts.map +1 -1
  14. package/dist/core/passthrough-commands.js +1 -1
  15. package/dist/core/passthrough-commands.js.map +1 -1
  16. package/dist/core/role-resolver.d.ts +1 -0
  17. package/dist/core/role-resolver.d.ts.map +1 -1
  18. package/dist/core/role-resolver.js +14 -0
  19. package/dist/core/role-resolver.js.map +1 -1
  20. package/dist/daemon.d.ts.map +1 -1
  21. package/dist/daemon.js +4 -1
  22. package/dist/daemon.js.map +1 -1
  23. package/dist/dashboard/bot-onboarding.d.ts +24 -8
  24. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  25. package/dist/dashboard/bot-onboarding.js +170 -49
  26. package/dist/dashboard/bot-onboarding.js.map +1 -1
  27. package/dist/dashboard/bot-payload.d.ts +43 -0
  28. package/dist/dashboard/bot-payload.d.ts.map +1 -0
  29. package/dist/dashboard/bot-payload.js +44 -0
  30. package/dist/dashboard/bot-payload.js.map +1 -0
  31. package/dist/dashboard/registry.d.ts +2 -0
  32. package/dist/dashboard/registry.d.ts.map +1 -1
  33. package/dist/dashboard/registry.js.map +1 -1
  34. package/dist/dashboard/web/app.d.ts.map +1 -1
  35. package/dist/dashboard/web/app.js +15 -4
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  38. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  39. package/dist/dashboard/web/bot-defaults.js +122 -3
  40. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  41. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  42. package/dist/dashboard/web/bot-onboarding.js +60 -4
  43. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  44. package/dist/dashboard/web/groups.d.ts +2 -0
  45. package/dist/dashboard/web/groups.d.ts.map +1 -1
  46. package/dist/dashboard/web/groups.js +419 -3
  47. package/dist/dashboard/web/groups.js.map +1 -1
  48. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  49. package/dist/dashboard/web/i18n.js +631 -3
  50. package/dist/dashboard/web/i18n.js.map +1 -1
  51. package/dist/dashboard/web/insights.d.ts +2 -0
  52. package/dist/dashboard/web/insights.d.ts.map +1 -0
  53. package/dist/dashboard/web/insights.js +1523 -0
  54. package/dist/dashboard/web/insights.js.map +1 -0
  55. package/dist/dashboard/web/overview.d.ts +22 -0
  56. package/dist/dashboard/web/overview.d.ts.map +1 -1
  57. package/dist/dashboard/web/overview.js +6 -1
  58. package/dist/dashboard/web/overview.js.map +1 -1
  59. package/dist/dashboard/web/role-profile-match.d.ts +31 -0
  60. package/dist/dashboard/web/role-profile-match.d.ts.map +1 -0
  61. package/dist/dashboard/web/role-profile-match.js +58 -0
  62. package/dist/dashboard/web/role-profile-match.js.map +1 -0
  63. package/dist/dashboard/web/roles.d.ts +1 -0
  64. package/dist/dashboard/web/roles.d.ts.map +1 -1
  65. package/dist/dashboard/web/roles.js +520 -27
  66. package/dist/dashboard/web/roles.js.map +1 -1
  67. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  68. package/dist/dashboard/web/sessions.js +84 -0
  69. package/dist/dashboard/web/sessions.js.map +1 -1
  70. package/dist/dashboard-web/app.js +1246 -823
  71. package/dist/dashboard-web/index.html +2 -1
  72. package/dist/dashboard-web/style.css +1085 -3
  73. package/dist/dashboard.js +273 -39
  74. package/dist/dashboard.js.map +1 -1
  75. package/dist/i18n/en.d.ts.map +1 -1
  76. package/dist/i18n/en.js +34 -1
  77. package/dist/i18n/en.js.map +1 -1
  78. package/dist/i18n/zh.d.ts.map +1 -1
  79. package/dist/i18n/zh.js +34 -1
  80. package/dist/i18n/zh.js.map +1 -1
  81. package/dist/im/lark/client.d.ts.map +1 -1
  82. package/dist/im/lark/client.js +23 -1
  83. package/dist/im/lark/client.js.map +1 -1
  84. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  85. package/dist/im/lark/event-dispatcher.js +16 -9
  86. package/dist/im/lark/event-dispatcher.js.map +1 -1
  87. package/dist/services/group-creator.d.ts +6 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +54 -5
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/insight/antigravity-span-reader.d.ts +3 -0
  92. package/dist/services/insight/antigravity-span-reader.d.ts.map +1 -0
  93. package/dist/services/insight/antigravity-span-reader.js +249 -0
  94. package/dist/services/insight/antigravity-span-reader.js.map +1 -0
  95. package/dist/services/insight/classify.d.ts +7 -0
  96. package/dist/services/insight/classify.d.ts.map +1 -0
  97. package/dist/services/insight/classify.js +46 -0
  98. package/dist/services/insight/classify.js.map +1 -0
  99. package/dist/services/insight/claude-span-reader.d.ts +3 -0
  100. package/dist/services/insight/claude-span-reader.d.ts.map +1 -0
  101. package/dist/services/insight/claude-span-reader.js +257 -0
  102. package/dist/services/insight/claude-span-reader.js.map +1 -0
  103. package/dist/services/insight/codex-span-reader.d.ts +3 -0
  104. package/dist/services/insight/codex-span-reader.d.ts.map +1 -0
  105. package/dist/services/insight/codex-span-reader.js +290 -0
  106. package/dist/services/insight/codex-span-reader.js.map +1 -0
  107. package/dist/services/insight/intent.d.ts +5 -0
  108. package/dist/services/insight/intent.d.ts.map +1 -0
  109. package/dist/services/insight/intent.js +145 -0
  110. package/dist/services/insight/intent.js.map +1 -0
  111. package/dist/services/insight/jsonl.d.ts +10 -0
  112. package/dist/services/insight/jsonl.d.ts.map +1 -0
  113. package/dist/services/insight/jsonl.js +36 -0
  114. package/dist/services/insight/jsonl.js.map +1 -0
  115. package/dist/services/insight/prompt.d.ts +3 -0
  116. package/dist/services/insight/prompt.d.ts.map +1 -0
  117. package/dist/services/insight/prompt.js +99 -0
  118. package/dist/services/insight/prompt.js.map +1 -0
  119. package/dist/services/insight/redact.d.ts +4 -0
  120. package/dist/services/insight/redact.d.ts.map +1 -0
  121. package/dist/services/insight/redact.js +67 -0
  122. package/dist/services/insight/redact.js.map +1 -0
  123. package/dist/services/insight/report.d.ts +29 -0
  124. package/dist/services/insight/report.d.ts.map +1 -0
  125. package/dist/services/insight/report.js +1126 -0
  126. package/dist/services/insight/report.js.map +1 -0
  127. package/dist/services/insight/safe-detail.d.ts +5 -0
  128. package/dist/services/insight/safe-detail.d.ts.map +1 -0
  129. package/dist/services/insight/safe-detail.js +59 -0
  130. package/dist/services/insight/safe-detail.js.map +1 -0
  131. package/dist/services/insight/scrub.d.ts +22 -0
  132. package/dist/services/insight/scrub.d.ts.map +1 -0
  133. package/dist/services/insight/scrub.js +70 -0
  134. package/dist/services/insight/scrub.js.map +1 -0
  135. package/dist/services/insight/types.d.ts +394 -0
  136. package/dist/services/insight/types.d.ts.map +1 -0
  137. package/dist/services/insight/types.js +2 -0
  138. package/dist/services/insight/types.js.map +1 -0
  139. package/dist/services/role-profile-store.d.ts +25 -0
  140. package/dist/services/role-profile-store.d.ts.map +1 -0
  141. package/dist/services/role-profile-store.js +171 -0
  142. package/dist/services/role-profile-store.js.map +1 -0
  143. package/dist/services/transcript-resolver.d.ts +26 -0
  144. package/dist/services/transcript-resolver.d.ts.map +1 -0
  145. package/dist/services/transcript-resolver.js +111 -0
  146. package/dist/services/transcript-resolver.js.map +1 -0
  147. package/dist/setup/cli-selection.d.ts +20 -1
  148. package/dist/setup/cli-selection.d.ts.map +1 -1
  149. package/dist/setup/cli-selection.js +45 -5
  150. package/dist/setup/cli-selection.js.map +1 -1
  151. package/dist/worker.js +10 -1
  152. package/dist/worker.js.map +1 -1
  153. package/package.json +1 -1
package/dist/dashboard.js CHANGED
@@ -36,8 +36,14 @@ import { redactGitUrlCredentials } from './core/skills/sources.js';
36
36
  import { loadBotConfigs } from './bot-registry.js';
37
37
  import { analyzeSkillReferences } from './core/skills/references.js';
38
38
  import { installDashboardSkill, parseDashboardSkillInstallRequest } from './dashboard/skill-install-request.js';
39
+ import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
40
+ import { isValidRoleProfileId } from './services/role-profile-store.js';
41
+ import { mergeSafeInsightOverviews } from './services/insight/report.js';
39
42
  const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
40
43
  const TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
44
+ /** Per-daemon budget for the cross-daemon insight overview fan-out — bounds
45
+ * aggregate latency when one daemon's insight parse is slow or hung. */
46
+ const INSIGHT_FANOUT_TIMEOUT_MS = 10_000;
41
47
  const BOTS_JSON_PATH = join(homedir(), '.botmux', 'bots.json');
42
48
  const REGISTRY_DIR = join(homedir(), '.botmux', 'data', 'dashboard-daemons');
43
49
  // The dashboard probes upward if its configured port is busy (e.g. a second
@@ -218,7 +224,7 @@ function serveFileAbs(res, fp) {
218
224
  createReadStream(fp).pipe(res);
219
225
  return true;
220
226
  }
221
- function serveStatic(_req, res, pathname) {
227
+ function serveStatic(req, res, pathname) {
222
228
  const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
223
229
  const fp = resolve(WEB_DIR, rel);
224
230
  const webRoot = resolve(WEB_DIR);
@@ -230,7 +236,23 @@ function serveStatic(_req, res, pathname) {
230
236
  const st = statSync(fp);
231
237
  if (!st.isFile())
232
238
  return false;
233
- res.writeHead(200, { 'content-type': MIME[extname(fp)] ?? 'application/octet-stream' });
239
+ // Bundle filenames are fixed (app.js/style.css), so without revalidation
240
+ // browsers heuristic-cache them and serve a stale build after a deploy
241
+ // (new JS + old CSS → broken layout). `no-cache` + an mtime/size ETag makes
242
+ // the browser revalidate every load: 304 when unchanged (cheap), fresh 200
243
+ // when the build changed. No manual hard-refresh needed after deploy.
244
+ const etag = `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
245
+ const headers = {
246
+ 'content-type': MIME[extname(fp)] ?? 'application/octet-stream',
247
+ 'cache-control': 'no-cache',
248
+ etag,
249
+ };
250
+ if (req.headers['if-none-match'] === etag) {
251
+ res.writeHead(304, headers);
252
+ res.end();
253
+ return true;
254
+ }
255
+ res.writeHead(200, headers);
234
256
  res.end(readFileSync(fp));
235
257
  return true;
236
258
  }
@@ -260,10 +282,25 @@ async function proxyToDaemon(larkAppId, daemonPath, init) {
260
282
  * Surfaces invalidBotIds/invalidUserIds so the UI never implies a non-added
261
283
  * bot/user joined. */
262
284
  /** Live daemon-registry bots — authoritative source for THIS deployment's
263
- * bots (cliId added from bots-info.json downstream). Fixes an empty/stale
264
- * bots-info.json hiding running bots from the team roster / federation. */
285
+ * bots. cliId comes from the daemon descriptor, with bots.json as a
286
+ * compatibility fallback for descriptors written by older daemons. */
287
+ function configuredCliIds() {
288
+ try {
289
+ return new Map(loadBotConfigs().map(b => [b.larkAppId, b.cliId]));
290
+ }
291
+ catch {
292
+ return new Map();
293
+ }
294
+ }
295
+ function withConfiguredCliId(bot, ids) {
296
+ return bot.cliId ? bot : { ...bot, cliId: ids.get(bot.larkAppId) };
297
+ }
265
298
  function liveBots() {
266
- return registry.list().map(d => ({ larkAppId: d.larkAppId, botName: d.botName }));
299
+ const ids = configuredCliIds();
300
+ return registry.list().map(d => {
301
+ const b = withConfiguredCliId(d, ids);
302
+ return { larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId };
303
+ });
267
304
  }
268
305
  async function createTeamGroup(args) {
269
306
  const selectedIds = Array.from(new Set(args.larkAppIds.filter(Boolean)));
@@ -286,7 +323,13 @@ async function createTeamGroup(args) {
286
323
  const upstream = await proxyToDaemon(plan.creatorLarkAppId, '/api/groups/create', {
287
324
  method: 'POST',
288
325
  headers: { 'content-type': 'application/json' },
289
- body: JSON.stringify({ name: args.name, larkAppIds: selectedIds, userOpenIds, ownerUnionIds: args.ownerUnionIds ?? [] }),
326
+ body: JSON.stringify({
327
+ name: args.name,
328
+ larkAppIds: selectedIds,
329
+ userOpenIds,
330
+ ownerUnionIds: args.ownerUnionIds ?? [],
331
+ ...(args.roleProfileId ? { roleProfileId: args.roleProfileId } : {}),
332
+ }),
290
333
  });
291
334
  const text = await upstream.text();
292
335
  let parsed = null;
@@ -657,6 +700,29 @@ const server = createServer(async (req, res) => {
657
700
  });
658
701
  return jsonRes(res, 200, { sessions });
659
702
  }
703
+ if (req.method === 'GET' && url.pathname === '/api/insights/summary') {
704
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
705
+ // Per-daemon timeout + isolate failures: an upstream insight parse can be
706
+ // heavy, so a slow/hung daemon must not stall the aggregated summary. A
707
+ // timed-out / errored chunk drops to null and is filtered out below.
708
+ const chunks = await Promise.all(registry.list().map(async (d) => {
709
+ try {
710
+ const upstream = await proxyToDaemon(d.larkAppId, `/api/insights/summary?limit=${limit}`, {
711
+ method: 'GET',
712
+ signal: AbortSignal.timeout(INSIGHT_FANOUT_TIMEOUT_MS),
713
+ });
714
+ if (!upstream.ok)
715
+ return null;
716
+ const body = await upstream.json().catch(() => null);
717
+ return body?.overview ?? null;
718
+ }
719
+ catch {
720
+ return null;
721
+ }
722
+ }));
723
+ const overview = mergeSafeInsightOverviews(chunks.filter((x) => !!x), { limit });
724
+ return jsonRes(res, 200, { ok: true, overview });
725
+ }
660
726
  if (req.method === 'GET' && url.pathname === '/api/schedules') {
661
727
  // Public-read carve-out: the row carries CONTENT (prompt = business
662
728
  // instructions) and a bound `workingDir` (repo/customer path) — strip
@@ -908,6 +974,35 @@ const server = createServer(async (req, res) => {
908
974
  const job = botOnboarding.start({ cliId, wrapperCli, workingDir, model });
909
975
  return jsonRes(res, 202, { job: botOnboarding.get(job.id) });
910
976
  }
977
+ let mOwner;
978
+ if (req.method === 'POST' && (mOwner = url.pathname.match(/^\/api\/bot-onboarding\/([^/]+)\/owner$/))) {
979
+ // needs_owner 状态下用户手动提交 owner:扫码人身份验证不了时的兜底入口。
980
+ // submitOwner 内部做格式 + 可用性校验, 通过才落盘并转 completed。
981
+ const onboardingId = decodeURIComponent(mOwner[1]);
982
+ let parsedOwner;
983
+ try {
984
+ const chunks = [];
985
+ for await (const c of req)
986
+ chunks.push(c);
987
+ const raw = Buffer.concat(chunks).toString('utf8');
988
+ parsedOwner = raw ? JSON.parse(raw) : {};
989
+ }
990
+ catch {
991
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
992
+ }
993
+ // 接受 owner 字符串 (逗号/空白分隔) 或 allowedUsers 数组。
994
+ const entries = Array.isArray(parsedOwner.allowedUsers)
995
+ ? parsedOwner.allowedUsers.filter((v) => typeof v === 'string')
996
+ : typeof parsedOwner.owner === 'string'
997
+ ? parsedOwner.owner.split(/[,\s]+/).map(s => s.trim()).filter(Boolean)
998
+ : [];
999
+ const r = await botOnboarding.submitOwner(onboardingId, entries);
1000
+ if (!r.ok) {
1001
+ const status = r.error === 'unknown_onboarding_job' ? 404 : 400;
1002
+ return jsonRes(res, status, r);
1003
+ }
1004
+ return jsonRes(res, 200, { job: botOnboarding.get(onboardingId) });
1005
+ }
911
1006
  let mOnboard;
912
1007
  if (req.method === 'GET' && (mOnboard = url.pathname.match(/^\/api\/bot-onboarding\/([^/]+)$/))) {
913
1008
  const job = botOnboarding.get(decodeURIComponent(mOnboard[1]));
@@ -987,6 +1082,30 @@ const server = createServer(async (req, res) => {
987
1082
  res.end(await upstream.text());
988
1083
  return;
989
1084
  }
1085
+ // 会话 insight(只读 trace 分析:动作 span / 失败聚合 / 规则建议)。
1086
+ // owner-only:不在公开读白名单 → decideDashboardAuth 已对只读访客 401,
1087
+ // 公开/联邦访客看不到 tab 也拿不到 span。代理到 owner daemon 的同名 IPC。
1088
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight$/))) {
1089
+ const sid = decodeURIComponent(m[1]);
1090
+ const owner = aggregator.ownerOf(sid);
1091
+ if (!owner)
1092
+ return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
1093
+ const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight${url.search ?? ''}`, { method: 'GET' });
1094
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1095
+ res.end(await upstream.text());
1096
+ return;
1097
+ }
1098
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight\/turn\/([^/]+)$/))) {
1099
+ const sid = decodeURIComponent(m[1]);
1100
+ const turnIndex = decodeURIComponent(m[2]);
1101
+ const owner = aggregator.ownerOf(sid);
1102
+ if (!owner)
1103
+ return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
1104
+ const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight/turn/${turnIndex}${url.search ?? ''}`, { method: 'GET' });
1105
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1106
+ res.end(await upstream.text());
1107
+ return;
1108
+ }
990
1109
  // Writable web-terminal link (token-bearing). Not in any public allow-list,
991
1110
  // so decideDashboardAuth has already 401'd unauthenticated callers before we
992
1111
  // get here — the token only reaches authenticated dashboard sessions.
@@ -1054,7 +1173,8 @@ const server = createServer(async (req, res) => {
1054
1173
  const out = new Map();
1055
1174
  // Sort by botIndex so the matrix columns + the create-group bot picker
1056
1175
  // both match the order in bots.json (fs.readdir order is unstable).
1057
- const onlineBots = [...registry.list()].sort((a, b) => a.botIndex - b.botIndex);
1176
+ const cliIds = configuredCliIds();
1177
+ const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
1058
1178
  await Promise.all(onlineBots.map(async (d) => {
1059
1179
  try {
1060
1180
  const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups`);
@@ -1074,6 +1194,7 @@ const server = createServer(async (req, res) => {
1074
1194
  cur.memberBots.push({
1075
1195
  larkAppId: d.larkAppId,
1076
1196
  botName: d.botName,
1197
+ cliId: d.cliId,
1077
1198
  inChat: true,
1078
1199
  oncallChat: oncallChat ?? null,
1079
1200
  hasRole: hasRole ?? false,
@@ -1093,7 +1214,7 @@ const server = createServer(async (req, res) => {
1093
1214
  const present = new Set(c.memberBots.map((mb) => mb.larkAppId));
1094
1215
  for (const b of onlineBots) {
1095
1216
  if (!present.has(b.larkAppId)) {
1096
- c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, inChat: false, oncallChat: null, hasRole: false });
1217
+ c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId, inChat: false, oncallChat: null, hasRole: false });
1097
1218
  }
1098
1219
  }
1099
1220
  }
@@ -1117,7 +1238,7 @@ const server = createServer(async (req, res) => {
1117
1238
  // prompt strip + keeps /api/bots oncall removal honest).
1118
1239
  return jsonRes(res, 200, {
1119
1240
  chats: authed ? sorted : redactGroupsForPublic(sorted),
1120
- bots: onlineBots.map(b => ({ larkAppId: b.larkAppId, botName: b.botName, botAvatarUrl: b.botAvatarUrl })),
1241
+ bots: onlineBots.map(botSummaryPayload),
1121
1242
  });
1122
1243
  }
1123
1244
  // ─── Roles (proxy to daemon) ────────────────────────────────────────────
@@ -1155,6 +1276,135 @@ const server = createServer(async (req, res) => {
1155
1276
  return;
1156
1277
  }
1157
1278
  }
1279
+ // ─── Profiles (aggregate/proxy to daemon) ─────────────────────────────
1280
+ if (req.method === 'GET' && url.pathname === '/api/role-profiles') {
1281
+ const merged = new Map();
1282
+ await Promise.all(registry.list().map(async (d) => {
1283
+ try {
1284
+ const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles`);
1285
+ if (!r.ok)
1286
+ return;
1287
+ const j = await r.json();
1288
+ for (const p of j.profiles ?? []) {
1289
+ if (typeof p.profileId !== 'string')
1290
+ continue;
1291
+ const cur = merged.get(p.profileId) ?? { profileId: p.profileId, entryCount: 0, updatedAt: null, botEntries: [] };
1292
+ cur.entryCount = Math.max(cur.entryCount, typeof p.entryCount === 'number' ? p.entryCount : 0);
1293
+ if (typeof p.updatedAt === 'number')
1294
+ cur.updatedAt = cur.updatedAt === null ? p.updatedAt : Math.max(cur.updatedAt, p.updatedAt);
1295
+ const larkAppId = j.larkAppId ?? d.larkAppId;
1296
+ if (!cur.botEntries.some(entry => entry.larkAppId === larkAppId)) {
1297
+ cur.botEntries.push({ larkAppId, hasEntry: p.hasCurrentBotEntry === true });
1298
+ }
1299
+ merged.set(p.profileId, cur);
1300
+ }
1301
+ }
1302
+ catch { /* skip offline/bad daemon */ }
1303
+ }));
1304
+ return jsonRes(res, 200, {
1305
+ profiles: [...merged.values()]
1306
+ .map(p => ({
1307
+ ...p,
1308
+ entryCount: Math.max(p.entryCount, p.botEntries.filter(entry => entry.hasEntry).length),
1309
+ }))
1310
+ .sort((a, b) => a.profileId.localeCompare(b.profileId)),
1311
+ });
1312
+ }
1313
+ let mRoleProfileApply;
1314
+ if (req.method === 'POST' && (mRoleProfileApply = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/apply$/))) {
1315
+ const profileId = decodeURIComponent(mRoleProfileApply[1]);
1316
+ if (!isValidRoleProfileId(profileId))
1317
+ return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
1318
+ let raw = '{}';
1319
+ let parsed;
1320
+ try {
1321
+ const chunks = [];
1322
+ for await (const c of req)
1323
+ chunks.push(c);
1324
+ raw = Buffer.concat(chunks).toString('utf8') || '{}';
1325
+ parsed = JSON.parse(raw);
1326
+ }
1327
+ catch {
1328
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
1329
+ }
1330
+ const larkAppId = typeof parsed.larkAppId === 'string' ? parsed.larkAppId : '';
1331
+ if (!larkAppId)
1332
+ return jsonRes(res, 400, { ok: false, error: 'larkAppId_required' });
1333
+ const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/apply`, {
1334
+ method: 'POST',
1335
+ headers: { 'content-type': 'application/json' },
1336
+ body: raw,
1337
+ });
1338
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1339
+ res.end(await upstream.text());
1340
+ return;
1341
+ }
1342
+ let mRoleProfileEntry;
1343
+ if ((mRoleProfileEntry = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/([^/]+)$/))) {
1344
+ const profileId = decodeURIComponent(mRoleProfileEntry[1]);
1345
+ const larkAppId = decodeURIComponent(mRoleProfileEntry[2]);
1346
+ if (!isValidRoleProfileId(profileId))
1347
+ return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
1348
+ if (req.method === 'GET') {
1349
+ const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'GET' });
1350
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1351
+ res.end(await upstream.text());
1352
+ return;
1353
+ }
1354
+ if (req.method === 'PUT') {
1355
+ const chunks = [];
1356
+ for await (const c of req)
1357
+ chunks.push(c);
1358
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
1359
+ const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, {
1360
+ method: 'PUT',
1361
+ headers: { 'content-type': 'application/json' },
1362
+ body: raw,
1363
+ });
1364
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1365
+ res.end(await upstream.text());
1366
+ return;
1367
+ }
1368
+ if (req.method === 'DELETE') {
1369
+ const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'DELETE' });
1370
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1371
+ res.end(await upstream.text());
1372
+ return;
1373
+ }
1374
+ }
1375
+ let mRoleProfile;
1376
+ if (req.method === 'GET' && (mRoleProfile = url.pathname.match(/^\/api\/role-profiles\/([^/]+)$/))) {
1377
+ const profileId = decodeURIComponent(mRoleProfile[1]);
1378
+ if (!isValidRoleProfileId(profileId))
1379
+ return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
1380
+ const byBot = new Map();
1381
+ await Promise.all(registry.list().map(async (d) => {
1382
+ try {
1383
+ const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles/${encodeURIComponent(profileId)}`);
1384
+ if (!r.ok)
1385
+ return;
1386
+ const j = await r.json();
1387
+ for (const entry of j.entries ?? []) {
1388
+ if (typeof entry.larkAppId !== 'string' || typeof entry.content !== 'string')
1389
+ continue;
1390
+ const updatedAt = typeof entry.updatedAt === 'number' ? entry.updatedAt : null;
1391
+ const current = byBot.get(entry.larkAppId);
1392
+ if (current && (current.updatedAt ?? 0) > (updatedAt ?? 0))
1393
+ continue;
1394
+ byBot.set(entry.larkAppId, {
1395
+ profileId,
1396
+ larkAppId: entry.larkAppId,
1397
+ content: entry.content,
1398
+ byteLength: typeof entry.byteLength === 'number' ? entry.byteLength : Buffer.byteLength(entry.content, 'utf-8'),
1399
+ updatedAt,
1400
+ });
1401
+ }
1402
+ }
1403
+ catch { /* skip */ }
1404
+ }));
1405
+ const entries = [...byBot.values()].sort((a, b) => a.larkAppId.localeCompare(b.larkAppId));
1406
+ return jsonRes(res, 200, { profileId, entries });
1407
+ }
1158
1408
  let m2;
1159
1409
  if (req.method === 'POST' && (m2 = url.pathname.match(/^\/api\/groups\/([^/]+)\/add-bots$/))) {
1160
1410
  const chatId = decodeURIComponent(m2[1]);
@@ -1321,44 +1571,19 @@ const server = createServer(async (req, res) => {
1321
1571
  // [{larkAppId, botName, defaultOncall, ...}]
1322
1572
  // PUT /api/bots/:appId/default-oncall — proxy to that bot's daemon
1323
1573
  if (req.method === 'GET' && url.pathname === '/api/bots') {
1324
- const onlineBots = [...registry.list()].sort((a, b) => a.botIndex - b.botIndex);
1574
+ const cliIds = configuredCliIds();
1575
+ const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
1325
1576
  const out = await Promise.all(onlineBots.map(async (d) => {
1326
1577
  try {
1327
1578
  const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/bot-default-oncall`);
1328
1579
  if (!r.ok) {
1329
- return { larkAppId: d.larkAppId, botName: d.botName, online: true, error: `http_${r.status}` };
1580
+ return botDefaultsPayload(d, undefined, `http_${r.status}`);
1330
1581
  }
1331
1582
  const j = await r.json();
1332
- return {
1333
- larkAppId: d.larkAppId,
1334
- botName: d.botName ?? j.botName,
1335
- online: true,
1336
- defaultOncall: j.defaultOncall,
1337
- autoboundChatCount: j.autoboundChatCount ?? 0,
1338
- brandLabel: j.brandLabel ?? null,
1339
- sandbox: j.sandbox === true,
1340
- disableStreamingCard: j.disableStreamingCard === true,
1341
- writableTerminalLinkInCard: j.writableTerminalLinkInCard === true,
1342
- privateCard: j.privateCard === true,
1343
- autoStartOnGroupJoin: j.autoStartOnGroupJoin === true,
1344
- autoStartOnGroupJoinPrompt: typeof j.autoStartOnGroupJoinPrompt === 'string' ? j.autoStartOnGroupJoinPrompt : '',
1345
- autoStartOnNewTopic: j.autoStartOnNewTopic === true,
1346
- regularGroupReplyMode: (j.regularGroupReplyMode === 'new-topic' || j.regularGroupReplyMode === 'shared')
1347
- ? j.regularGroupReplyMode
1348
- : 'chat',
1349
- regularGroupMentionMode: (j.regularGroupMentionMode === 'topic' || j.regularGroupMentionMode === 'never')
1350
- ? j.regularGroupMentionMode
1351
- : 'always',
1352
- restrictGrantCommands: j.restrictGrantCommands === true,
1353
- messageQuotaDefaultLimit: typeof j.messageQuotaDefaultLimit === 'number' ? j.messageQuotaDefaultLimit : null,
1354
- p2pMode: j.p2pMode === 'chat' ? 'chat' : 'thread',
1355
- maxLiveWorkers: typeof j.maxLiveWorkers === 'number' ? j.maxLiveWorkers : null,
1356
- startupCommands: typeof j.startupCommands === 'string' ? j.startupCommands : '',
1357
- skills: j.skills && typeof j.skills === 'object' ? j.skills : null,
1358
- };
1583
+ return botDefaultsPayload({ ...d, botName: d.botName ?? j.botName }, j);
1359
1584
  }
1360
1585
  catch (e) {
1361
- return { larkAppId: d.larkAppId, botName: d.botName, online: true, error: e?.message ?? String(e) };
1586
+ return botDefaultsPayload(d, undefined, e?.message ?? String(e));
1362
1587
  }
1363
1588
  }));
1364
1589
  return jsonRes(res, 200, { bots: out });
@@ -1548,6 +1773,12 @@ const server = createServer(async (req, res) => {
1548
1773
  if (selectedIds.length === 0) {
1549
1774
  return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
1550
1775
  }
1776
+ const roleProfileId = typeof parsed.roleProfileId === 'string' && parsed.roleProfileId.trim()
1777
+ ? parsed.roleProfileId.trim()
1778
+ : null;
1779
+ if (roleProfileId && !isValidRoleProfileId(roleProfileId)) {
1780
+ return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
1781
+ }
1551
1782
  const explicit = Array.isArray(parsed.userOpenIds)
1552
1783
  ? parsed.userOpenIds.filter((x) => typeof x === 'string')
1553
1784
  : [];
@@ -1578,6 +1809,7 @@ const server = createServer(async (req, res) => {
1578
1809
  bindWorkingDir: typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
1579
1810
  ? parsed.bindWorkingDir.trim()
1580
1811
  : undefined,
1812
+ roleProfileId: roleProfileId ?? undefined,
1581
1813
  };
1582
1814
  const upstream = await fetch(`http://127.0.0.1:${creator.ipcPort}/api/groups/create`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(forwardBody) });
1583
1815
  const upstreamText = await upstream.text();
@@ -1587,6 +1819,8 @@ const server = createServer(async (req, res) => {
1587
1819
  }
1588
1820
  catch { /* leave null */ }
1589
1821
  if (upstreamJson && typeof upstreamJson === 'object') {
1822
+ if (roleProfileId)
1823
+ upstreamJson.roleProfileId = roleProfileId;
1590
1824
  // If Lark rejected the invite (open_id wrong scope, banned user, etc.)
1591
1825
  // null out autoInvitedOpenId so the frontend doesn't falsely claim
1592
1826
  // success — the user actually isn't a member of the new chat.