botmux 2.85.1 → 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 (116) hide show
  1. package/dist/core/command-handler.d.ts.map +1 -1
  2. package/dist/core/command-handler.js +209 -1
  3. package/dist/core/command-handler.js.map +1 -1
  4. package/dist/core/cost-calculator.d.ts.map +1 -1
  5. package/dist/core/cost-calculator.js +7 -106
  6. package/dist/core/cost-calculator.js.map +1 -1
  7. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  8. package/dist/core/dashboard-ipc-server.js +240 -2
  9. package/dist/core/dashboard-ipc-server.js.map +1 -1
  10. package/dist/core/passthrough-commands.d.ts.map +1 -1
  11. package/dist/core/passthrough-commands.js +1 -1
  12. package/dist/core/passthrough-commands.js.map +1 -1
  13. package/dist/core/role-resolver.d.ts +1 -0
  14. package/dist/core/role-resolver.d.ts.map +1 -1
  15. package/dist/core/role-resolver.js +14 -0
  16. package/dist/core/role-resolver.js.map +1 -1
  17. package/dist/dashboard/web/app.d.ts.map +1 -1
  18. package/dist/dashboard/web/app.js +15 -4
  19. package/dist/dashboard/web/app.js.map +1 -1
  20. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  21. package/dist/dashboard/web/bot-defaults.js +116 -0
  22. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  23. package/dist/dashboard/web/groups.d.ts +2 -0
  24. package/dist/dashboard/web/groups.d.ts.map +1 -1
  25. package/dist/dashboard/web/groups.js +419 -3
  26. package/dist/dashboard/web/groups.js.map +1 -1
  27. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  28. package/dist/dashboard/web/i18n.js +617 -3
  29. package/dist/dashboard/web/i18n.js.map +1 -1
  30. package/dist/dashboard/web/insights.d.ts +2 -0
  31. package/dist/dashboard/web/insights.d.ts.map +1 -0
  32. package/dist/dashboard/web/insights.js +1523 -0
  33. package/dist/dashboard/web/insights.js.map +1 -0
  34. package/dist/dashboard/web/role-profile-match.d.ts +31 -0
  35. package/dist/dashboard/web/role-profile-match.d.ts.map +1 -0
  36. package/dist/dashboard/web/role-profile-match.js +58 -0
  37. package/dist/dashboard/web/role-profile-match.js.map +1 -0
  38. package/dist/dashboard/web/roles.d.ts +1 -0
  39. package/dist/dashboard/web/roles.d.ts.map +1 -1
  40. package/dist/dashboard/web/roles.js +520 -27
  41. package/dist/dashboard/web/roles.js.map +1 -1
  42. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  43. package/dist/dashboard/web/sessions.js +84 -0
  44. package/dist/dashboard/web/sessions.js.map +1 -1
  45. package/dist/dashboard-web/app.js +1243 -831
  46. package/dist/dashboard-web/index.html +2 -1
  47. package/dist/dashboard-web/style.css +1085 -3
  48. package/dist/dashboard.js +215 -3
  49. package/dist/dashboard.js.map +1 -1
  50. package/dist/i18n/en.d.ts.map +1 -1
  51. package/dist/i18n/en.js +34 -1
  52. package/dist/i18n/en.js.map +1 -1
  53. package/dist/i18n/zh.d.ts.map +1 -1
  54. package/dist/i18n/zh.js +34 -1
  55. package/dist/i18n/zh.js.map +1 -1
  56. package/dist/services/group-creator.d.ts +6 -0
  57. package/dist/services/group-creator.d.ts.map +1 -1
  58. package/dist/services/group-creator.js +54 -5
  59. package/dist/services/group-creator.js.map +1 -1
  60. package/dist/services/insight/antigravity-span-reader.d.ts +3 -0
  61. package/dist/services/insight/antigravity-span-reader.d.ts.map +1 -0
  62. package/dist/services/insight/antigravity-span-reader.js +249 -0
  63. package/dist/services/insight/antigravity-span-reader.js.map +1 -0
  64. package/dist/services/insight/classify.d.ts +7 -0
  65. package/dist/services/insight/classify.d.ts.map +1 -0
  66. package/dist/services/insight/classify.js +46 -0
  67. package/dist/services/insight/classify.js.map +1 -0
  68. package/dist/services/insight/claude-span-reader.d.ts +3 -0
  69. package/dist/services/insight/claude-span-reader.d.ts.map +1 -0
  70. package/dist/services/insight/claude-span-reader.js +257 -0
  71. package/dist/services/insight/claude-span-reader.js.map +1 -0
  72. package/dist/services/insight/codex-span-reader.d.ts +3 -0
  73. package/dist/services/insight/codex-span-reader.d.ts.map +1 -0
  74. package/dist/services/insight/codex-span-reader.js +290 -0
  75. package/dist/services/insight/codex-span-reader.js.map +1 -0
  76. package/dist/services/insight/intent.d.ts +5 -0
  77. package/dist/services/insight/intent.d.ts.map +1 -0
  78. package/dist/services/insight/intent.js +145 -0
  79. package/dist/services/insight/intent.js.map +1 -0
  80. package/dist/services/insight/jsonl.d.ts +10 -0
  81. package/dist/services/insight/jsonl.d.ts.map +1 -0
  82. package/dist/services/insight/jsonl.js +36 -0
  83. package/dist/services/insight/jsonl.js.map +1 -0
  84. package/dist/services/insight/prompt.d.ts +3 -0
  85. package/dist/services/insight/prompt.d.ts.map +1 -0
  86. package/dist/services/insight/prompt.js +99 -0
  87. package/dist/services/insight/prompt.js.map +1 -0
  88. package/dist/services/insight/redact.d.ts +4 -0
  89. package/dist/services/insight/redact.d.ts.map +1 -0
  90. package/dist/services/insight/redact.js +67 -0
  91. package/dist/services/insight/redact.js.map +1 -0
  92. package/dist/services/insight/report.d.ts +29 -0
  93. package/dist/services/insight/report.d.ts.map +1 -0
  94. package/dist/services/insight/report.js +1126 -0
  95. package/dist/services/insight/report.js.map +1 -0
  96. package/dist/services/insight/safe-detail.d.ts +5 -0
  97. package/dist/services/insight/safe-detail.d.ts.map +1 -0
  98. package/dist/services/insight/safe-detail.js +59 -0
  99. package/dist/services/insight/safe-detail.js.map +1 -0
  100. package/dist/services/insight/scrub.d.ts +22 -0
  101. package/dist/services/insight/scrub.d.ts.map +1 -0
  102. package/dist/services/insight/scrub.js +70 -0
  103. package/dist/services/insight/scrub.js.map +1 -0
  104. package/dist/services/insight/types.d.ts +394 -0
  105. package/dist/services/insight/types.d.ts.map +1 -0
  106. package/dist/services/insight/types.js +2 -0
  107. package/dist/services/insight/types.js.map +1 -0
  108. package/dist/services/role-profile-store.d.ts +25 -0
  109. package/dist/services/role-profile-store.d.ts.map +1 -0
  110. package/dist/services/role-profile-store.js +171 -0
  111. package/dist/services/role-profile-store.js.map +1 -0
  112. package/dist/services/transcript-resolver.d.ts +26 -0
  113. package/dist/services/transcript-resolver.d.ts.map +1 -0
  114. package/dist/services/transcript-resolver.js +111 -0
  115. package/dist/services/transcript-resolver.js.map +1 -0
  116. package/package.json +1 -1
package/dist/dashboard.js CHANGED
@@ -37,8 +37,13 @@ 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
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';
40
42
  const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
41
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;
42
47
  const BOTS_JSON_PATH = join(homedir(), '.botmux', 'bots.json');
43
48
  const REGISTRY_DIR = join(homedir(), '.botmux', 'data', 'dashboard-daemons');
44
49
  // The dashboard probes upward if its configured port is busy (e.g. a second
@@ -219,7 +224,7 @@ function serveFileAbs(res, fp) {
219
224
  createReadStream(fp).pipe(res);
220
225
  return true;
221
226
  }
222
- function serveStatic(_req, res, pathname) {
227
+ function serveStatic(req, res, pathname) {
223
228
  const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
224
229
  const fp = resolve(WEB_DIR, rel);
225
230
  const webRoot = resolve(WEB_DIR);
@@ -231,7 +236,23 @@ function serveStatic(_req, res, pathname) {
231
236
  const st = statSync(fp);
232
237
  if (!st.isFile())
233
238
  return false;
234
- 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);
235
256
  res.end(readFileSync(fp));
236
257
  return true;
237
258
  }
@@ -302,7 +323,13 @@ async function createTeamGroup(args) {
302
323
  const upstream = await proxyToDaemon(plan.creatorLarkAppId, '/api/groups/create', {
303
324
  method: 'POST',
304
325
  headers: { 'content-type': 'application/json' },
305
- 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
+ }),
306
333
  });
307
334
  const text = await upstream.text();
308
335
  let parsed = null;
@@ -673,6 +700,29 @@ const server = createServer(async (req, res) => {
673
700
  });
674
701
  return jsonRes(res, 200, { sessions });
675
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
+ }
676
726
  if (req.method === 'GET' && url.pathname === '/api/schedules') {
677
727
  // Public-read carve-out: the row carries CONTENT (prompt = business
678
728
  // instructions) and a bound `workingDir` (repo/customer path) — strip
@@ -1032,6 +1082,30 @@ const server = createServer(async (req, res) => {
1032
1082
  res.end(await upstream.text());
1033
1083
  return;
1034
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
+ }
1035
1109
  // Writable web-terminal link (token-bearing). Not in any public allow-list,
1036
1110
  // so decideDashboardAuth has already 401'd unauthenticated callers before we
1037
1111
  // get here — the token only reaches authenticated dashboard sessions.
@@ -1202,6 +1276,135 @@ const server = createServer(async (req, res) => {
1202
1276
  return;
1203
1277
  }
1204
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
+ }
1205
1408
  let m2;
1206
1409
  if (req.method === 'POST' && (m2 = url.pathname.match(/^\/api\/groups\/([^/]+)\/add-bots$/))) {
1207
1410
  const chatId = decodeURIComponent(m2[1]);
@@ -1570,6 +1773,12 @@ const server = createServer(async (req, res) => {
1570
1773
  if (selectedIds.length === 0) {
1571
1774
  return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
1572
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
+ }
1573
1782
  const explicit = Array.isArray(parsed.userOpenIds)
1574
1783
  ? parsed.userOpenIds.filter((x) => typeof x === 'string')
1575
1784
  : [];
@@ -1600,6 +1809,7 @@ const server = createServer(async (req, res) => {
1600
1809
  bindWorkingDir: typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
1601
1810
  ? parsed.bindWorkingDir.trim()
1602
1811
  : undefined,
1812
+ roleProfileId: roleProfileId ?? undefined,
1603
1813
  };
1604
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) });
1605
1815
  const upstreamText = await upstream.text();
@@ -1609,6 +1819,8 @@ const server = createServer(async (req, res) => {
1609
1819
  }
1610
1820
  catch { /* leave null */ }
1611
1821
  if (upstreamJson && typeof upstreamJson === 'object') {
1822
+ if (roleProfileId)
1823
+ upstreamJson.roleProfileId = roleProfileId;
1612
1824
  // If Lark rejected the invite (open_id wrong scope, banned user, etc.)
1613
1825
  // null out autoInvitedOpenId so the frontend doesn't falsely claim
1614
1826
  // success — the user actually isn't a member of the new chat.