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
@@ -1,23 +1,48 @@
1
- // Roles page: hierarchical group bot role editor.
2
- // Displays groups as collapsible sections with bots nested inside.
3
- // Each bot has its own per-group role definition selectable for editing.
1
+ // Roles page: group role editor + reusable role profile management.
4
2
  import { botAvatarHtml, escapeHtml, loadNameMaps, loadingHtml, t } from './ui.js';
3
+ import { hasExplicitChatRole, summarizeGroupProfileMatches, } from './role-profile-match.js';
5
4
  const MAX_ROLE_BYTES = 4096;
5
+ const PROFILE_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
6
6
  let cache = [];
7
+ let allBots = [];
8
+ let profiles = [];
9
+ let profileEntries = [];
10
+ let groupProfileEntriesById = new Map();
11
+ let groupEffectiveRolesByBot = new Map();
12
+ let groupProfileContextLoaded = false;
13
+ let activeTab = 'groups';
7
14
  let selectedGroupId = null;
8
15
  let selectedBotId = null;
9
16
  let editingContent = '';
10
17
  let expandedGroups = new Set();
11
- function pageHtml() {
18
+ let selectedProfileId = null;
19
+ let selectedProfileBotId = null;
20
+ let profileEditingContent = '';
21
+ let selectedApplyGroupId = null;
22
+ function isValidProfileId(profileId) {
23
+ return PROFILE_ID_RE.test(profileId) && profileId !== '.' && profileId !== '..';
24
+ }
25
+ function hashChatId() {
26
+ const [, query = ''] = location.hash.split('?');
27
+ const chatId = new URLSearchParams(query).get('chatId')?.trim();
28
+ return chatId || null;
29
+ }
30
+ function pageHtml(tab) {
31
+ const isProfiles = tab === 'profiles';
12
32
  return `<section class="page roles-page">
13
- <div class="page-heading">
33
+ <div class="page-heading roles-heading">
14
34
  <div>
15
35
  <p class="eyebrow">${t('nav.roles')}</p>
16
36
  <h1>${t('roles.title')}</h1>
17
37
  <p>${t('roles.subtitle')}</p>
18
38
  </div>
19
39
  </div>
20
- <div class="roles-layout">
40
+ <nav class="wf-subnav roles-subnav">
41
+ <a href="#/roles" ${isProfiles ? '' : 'class="active"'}>${t('roles.tabGroups')}</a>
42
+ <a href="#/roles/profile" ${isProfiles ? 'class="active"' : ''}>${t('roles.tabProfiles')}</a>
43
+ </nav>
44
+
45
+ <div id="roles-by-group-view" class="roles-layout" ${isProfiles ? 'hidden' : ''}>
21
46
  <div class="roles-tree-panel">
22
47
  <div class="roles-tree-header">
23
48
  <input type="search" id="roles-search" placeholder="${t('roles.search')}" />
@@ -48,11 +73,34 @@ function pageHtml() {
48
73
  </div>
49
74
  </div>
50
75
  </div>
76
+
77
+ <div id="roles-profiles-view" class="roles-layout roles-profiles-layout" ${isProfiles ? '' : 'hidden'}>
78
+ <div class="roles-tree-panel">
79
+ <div class="roles-tree-header roles-profile-create">
80
+ <input type="text" id="roles-profile-id" placeholder="${t('roles.profileIdPlaceholder')}" maxlength="64" />
81
+ <button type="button" id="roles-profile-select">${t('roles.openProfile')}</button>
82
+ </div>
83
+ <div class="roles-tree-header">
84
+ <input type="search" id="roles-profile-search" placeholder="${t('roles.profileSearch')}" />
85
+ <button type="button" id="roles-profile-refresh">${t('roles.refresh')}</button>
86
+ </div>
87
+ <div id="roles-profile-list" class="roles-tree"></div>
88
+ </div>
89
+ <div class="roles-editor-panel">
90
+ <div id="roles-profile-empty" class="roles-editor-empty">${t('roles.profileSelectHint')}</div>
91
+ <div id="roles-profile-detail" class="roles-editor-form roles-profile-detail" style="display:none"></div>
92
+ </div>
93
+ </div>
51
94
  </section>`;
52
95
  }
53
96
  async function loadGroups() {
54
97
  const r = await fetch('/api/groups');
55
98
  const data = await r.json();
99
+ allBots = (data.bots ?? []).map((b) => ({
100
+ larkAppId: b.larkAppId,
101
+ botName: b.botName ?? b.larkAppId,
102
+ botAvatarUrl: b.botAvatarUrl,
103
+ }));
56
104
  cache = (data.chats ?? []).map((c) => ({
57
105
  chatId: c.chatId,
58
106
  name: c.name ?? c.chatId,
@@ -65,10 +113,72 @@ async function loadGroups() {
65
113
  })),
66
114
  }));
67
115
  }
116
+ async function loadProfiles() {
117
+ const r = await fetch('/api/role-profiles');
118
+ const data = await r.json();
119
+ profiles = data.profiles ?? [];
120
+ }
121
+ async function loadProfileEntries(profileId) {
122
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}`);
123
+ const data = await r.json();
124
+ profileEntries = data.entries ?? [];
125
+ }
68
126
  async function loadRole(larkAppId, chatId) {
69
127
  const r = await fetch(`/api/roles/${encodeURIComponent(larkAppId)}/${encodeURIComponent(chatId)}`);
70
128
  return r.json();
71
129
  }
130
+ function roleKey(larkAppId, chatId) {
131
+ return `${larkAppId}\u0000${chatId}`;
132
+ }
133
+ async function loadGroupProfileContext() {
134
+ const detailPairs = await Promise.all(profiles.map(async (profile) => {
135
+ try {
136
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profile.profileId)}`);
137
+ const body = await r.json().catch(() => ({}));
138
+ return [profile.profileId, Array.isArray(body.entries) ? body.entries : []];
139
+ }
140
+ catch {
141
+ return [profile.profileId, []];
142
+ }
143
+ }));
144
+ const nextEffectiveRoles = new Map();
145
+ const seen = new Set();
146
+ await Promise.all(cache.flatMap(group => group.memberBots
147
+ .filter(bot => bot.inChat)
148
+ .map(async (bot) => {
149
+ const key = roleKey(bot.larkAppId, group.chatId);
150
+ if (seen.has(key))
151
+ return;
152
+ seen.add(key);
153
+ try {
154
+ const role = await loadRole(bot.larkAppId, group.chatId);
155
+ const hasEffectiveRole = role.hasEffectiveRole ?? role.hasRole;
156
+ const effectiveContent = 'effectiveContent' in role ? role.effectiveContent : role.content;
157
+ nextEffectiveRoles.set(key, {
158
+ content: hasEffectiveRole ? String(effectiveContent ?? '') : null,
159
+ source: role.effectiveSource ?? (role.hasRole ? 'chat' : 'none'),
160
+ });
161
+ }
162
+ catch {
163
+ nextEffectiveRoles.set(key, null);
164
+ }
165
+ })));
166
+ groupProfileEntriesById = new Map(detailPairs);
167
+ groupEffectiveRolesByBot = nextEffectiveRoles;
168
+ }
169
+ async function refreshGroupProfileContext() {
170
+ try {
171
+ await loadGroupProfileContext();
172
+ }
173
+ catch {
174
+ groupProfileEntriesById = new Map();
175
+ groupEffectiveRolesByBot = new Map();
176
+ }
177
+ finally {
178
+ groupProfileContextLoaded = true;
179
+ renderTree(document.getElementById('roles-search')?.value ?? '');
180
+ }
181
+ }
72
182
  async function saveRole(larkAppId, chatId, content) {
73
183
  const r = await fetch(`/api/roles/${encodeURIComponent(larkAppId)}/${encodeURIComponent(chatId)}`, {
74
184
  method: 'PUT',
@@ -81,12 +191,66 @@ async function deleteRole(larkAppId, chatId) {
81
191
  const r = await fetch(`/api/roles/${encodeURIComponent(larkAppId)}/${encodeURIComponent(chatId)}`, { method: 'DELETE' });
82
192
  return r.ok;
83
193
  }
194
+ async function loadProfileEntry(profileId, larkAppId) {
195
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`);
196
+ return r.json();
197
+ }
198
+ async function saveProfileEntry(profileId, larkAppId, content) {
199
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, {
200
+ method: 'PUT',
201
+ headers: { 'content-type': 'application/json' },
202
+ body: JSON.stringify({ content, allowEmpty: true }),
203
+ });
204
+ return r.ok;
205
+ }
206
+ async function deleteProfileEntry(profileId, larkAppId) {
207
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'DELETE' });
208
+ return r.ok;
209
+ }
210
+ function byteLength(s) {
211
+ return new TextEncoder().encode(s).length;
212
+ }
84
213
  function botRoleCount(group) {
85
214
  return group.memberBots.filter(b => b.inChat && b.hasRole).length;
86
215
  }
87
216
  function botInChatCount(group) {
88
217
  return group.memberBots.filter(b => b.inChat).length;
89
218
  }
219
+ function profileHasEntry(profile, larkAppId) {
220
+ return (profile.botEntries ?? []).some(entry => entry.larkAppId === larkAppId && entry.hasEntry);
221
+ }
222
+ function entryForBot(larkAppId) {
223
+ return profileEntries.find(entry => entry.larkAppId === larkAppId);
224
+ }
225
+ function switchTab(tab) {
226
+ activeTab = tab;
227
+ document.getElementById('roles-by-group-view')?.toggleAttribute('hidden', tab !== 'groups');
228
+ document.getElementById('roles-profiles-view')?.toggleAttribute('hidden', tab !== 'profiles');
229
+ }
230
+ function renderRolesGroupProfileStatus(group) {
231
+ if (!profiles.length || !groupProfileContextLoaded)
232
+ return '';
233
+ const rolesByBot = new Map();
234
+ for (const bot of group.memberBots) {
235
+ if (!bot.inChat)
236
+ continue;
237
+ rolesByBot.set(bot.larkAppId, groupEffectiveRolesByBot.get(roleKey(bot.larkAppId, group.chatId)) ?? null);
238
+ }
239
+ if (!hasExplicitChatRole(rolesByBot))
240
+ return '';
241
+ const best = summarizeGroupProfileMatches(group.memberBots, profiles, groupProfileEntriesById, rolesByBot)[0];
242
+ if (!best)
243
+ return `<div class="roles-profile-match muted">${t('groups.profileStatusUnmatched')}</div>`;
244
+ const key = best.kind === 'full' ? 'groups.profileStatusFullChat' : 'groups.profileStatusPartial';
245
+ return `<div class="roles-profile-match ${best.kind}">
246
+ ${escapeHtml(t(key, {
247
+ name: best.profileId,
248
+ matched: best.matched,
249
+ total: best.total,
250
+ chat: best.chatMatched,
251
+ }))}
252
+ </div>`;
253
+ }
90
254
  function renderTree(filter = '') {
91
255
  const tree = document.getElementById('roles-tree');
92
256
  if (!tree)
@@ -139,28 +303,25 @@ function renderTree(filter = '') {
139
303
  <div class="roles-group-meta">
140
304
  ${roleCount}/${totalInChat} ${t('roles.botsWithRoles')}
141
305
  </div>
306
+ ${renderRolesGroupProfileStatus(g)}
142
307
  </div>
143
308
  <span class="roles-group-chevron"></span>
144
309
  </div>
145
310
  <div class="roles-bot-list">${botRows}</div>
146
311
  </div>`;
147
312
  }).join('');
148
- // Group row click → toggle expand
149
313
  tree.querySelectorAll('.roles-group-row').forEach(row => {
150
314
  row.addEventListener('click', () => {
151
315
  const gid = row.dataset.groupId;
152
316
  if (!gid)
153
317
  return;
154
- if (expandedGroups.has(gid)) {
318
+ if (expandedGroups.has(gid))
155
319
  expandedGroups.delete(gid);
156
- }
157
- else {
320
+ else
158
321
  expandedGroups.add(gid);
159
- }
160
322
  renderTree(document.getElementById('roles-search')?.value ?? '');
161
323
  });
162
324
  });
163
- // Bot row click → select for editing
164
325
  tree.querySelectorAll('.roles-bot-row').forEach(row => {
165
326
  row.addEventListener('click', (e) => {
166
327
  e.stopPropagation();
@@ -209,7 +370,7 @@ function updateByteCount() {
209
370
  const el = document.getElementById('roles-editor-bytecount');
210
371
  if (!el)
211
372
  return;
212
- const len = new TextEncoder().encode(editingContent).length;
373
+ const len = byteLength(editingContent);
213
374
  el.textContent = `${len} / ${MAX_ROLE_BYTES} bytes`;
214
375
  el.className = `roles-bytecount ${len > 3800 ? 'warn' : ''} ${len > MAX_ROLE_BYTES ? 'over' : ''}`;
215
376
  updateSaveButton(len);
@@ -218,7 +379,7 @@ function updateSaveButton(byteLen) {
218
379
  const btn = document.getElementById('roles-save');
219
380
  if (!btn)
220
381
  return;
221
- const len = byteLen ?? new TextEncoder().encode(editingContent).length;
382
+ const len = byteLen ?? byteLength(editingContent);
222
383
  btn.disabled = len > MAX_ROLE_BYTES || editingContent.trim().length === 0;
223
384
  }
224
385
  function updatePreview() {
@@ -249,31 +410,326 @@ function resetEditor() {
249
410
  if (delBtn)
250
411
  delBtn.style.display = 'none';
251
412
  }
252
- export async function renderRolesPage(root) {
253
- root.innerHTML = pageHtml();
413
+ function renderProfileList(filter = '') {
414
+ const list = document.getElementById('roles-profile-list');
415
+ if (!list)
416
+ return;
417
+ const q = filter.toLowerCase();
418
+ const filtered = profiles.filter(p => !q || p.profileId.toLowerCase().includes(q));
419
+ if (filtered.length === 0) {
420
+ list.innerHTML = `<div class="roles-empty">${t('roles.profileEmpty')}</div>`;
421
+ return;
422
+ }
423
+ list.innerHTML = filtered.map(p => {
424
+ const selected = selectedProfileId === p.profileId;
425
+ const hasAnyLocal = (p.botEntries ?? []).some(entry => entry.hasEntry);
426
+ return `
427
+ <div class="roles-profile-row ${selected ? 'selected' : ''}" data-profile-id="${escapeHtml(p.profileId)}">
428
+ <div class="roles-profile-row-main">
429
+ <div class="roles-profile-name">${escapeHtml(p.profileId)}</div>
430
+ <div class="roles-group-meta">${p.entryCount} ${t('roles.profileEntries')}</div>
431
+ </div>
432
+ <span class="roles-badge ${hasAnyLocal ? 'has-role' : 'no-role'}">${hasAnyLocal ? t('roles.configured') : t('roles.profileMissing')}</span>
433
+ </div>`;
434
+ }).join('');
435
+ list.querySelectorAll('.roles-profile-row').forEach(row => {
436
+ row.addEventListener('click', () => {
437
+ const profileId = row.dataset.profileId;
438
+ if (profileId)
439
+ void selectProfile(profileId);
440
+ });
441
+ });
442
+ }
443
+ async function selectProfile(profileId) {
444
+ if (!isValidProfileId(profileId.trim()))
445
+ return;
446
+ selectedProfileId = profileId.trim();
447
+ selectedProfileBotId = null;
448
+ profileEditingContent = '';
449
+ selectedApplyGroupId = selectedApplyGroupId ?? cache[0]?.chatId ?? null;
450
+ await loadProfileEntries(selectedProfileId);
451
+ renderProfileList(document.getElementById('roles-profile-search')?.value ?? '');
452
+ renderProfileDetail();
453
+ }
454
+ function renderProfileDetail() {
455
+ const empty = document.getElementById('roles-profile-empty');
456
+ const detail = document.getElementById('roles-profile-detail');
457
+ if (!empty || !detail)
458
+ return;
459
+ if (!selectedProfileId) {
460
+ empty.style.display = '';
461
+ detail.style.display = 'none';
462
+ detail.innerHTML = '';
463
+ return;
464
+ }
465
+ empty.style.display = 'none';
466
+ detail.style.display = '';
467
+ const selectedBot = allBots.find(b => b.larkAppId === selectedProfileBotId);
468
+ const entry = selectedProfileBotId ? entryForBot(selectedProfileBotId) : undefined;
469
+ detail.innerHTML = `
470
+ <div class="roles-profile-title">
471
+ <div>
472
+ <div class="roles-editor-breadcrumb">
473
+ <span>${escapeHtml(selectedProfileId)}</span>
474
+ ${selectedBot ? `<span class="roles-breadcrumb-sep">›</span><span>${escapeHtml(selectedBot.botName ?? selectedBot.larkAppId)}</span>` : ''}
475
+ </div>
476
+ <div class="roles-editor-meta-line">${t('roles.profileRuntimeHint')}</div>
477
+ </div>
478
+ </div>
479
+ <div class="roles-profile-grid">
480
+ <div class="roles-profile-bots">
481
+ <div class="roles-profile-section-title">${t('roles.profileBots')}</div>
482
+ <div class="roles-profile-bot-list">
483
+ ${allBots.map(bot => {
484
+ const hasEntry = !!entryForBot(bot.larkAppId);
485
+ const selected = selectedProfileBotId === bot.larkAppId;
486
+ return `
487
+ <div class="roles-bot-row roles-profile-bot-row ${selected ? 'selected' : ''}" data-profile-bot-id="${escapeHtml(bot.larkAppId)}">
488
+ ${botAvatarHtml({ name: bot.botName, larkAppId: bot.larkAppId, size: 'sm' })}
489
+ <div class="roles-bot-info">
490
+ <div class="roles-bot-name">${escapeHtml(bot.botName ?? bot.larkAppId)}</div>
491
+ <div class="roles-bot-id">${escapeHtml(bot.larkAppId)}</div>
492
+ </div>
493
+ <span class="roles-badge ${hasEntry ? 'has-role' : 'no-role'}">${hasEntry ? t('roles.configured') : t('roles.unconfigured')}</span>
494
+ </div>`;
495
+ }).join('')}
496
+ </div>
497
+ </div>
498
+ <div class="roles-profile-editor">
499
+ ${selectedProfileBotId ? `
500
+ <textarea id="roles-profile-textarea" placeholder="${t('roles.profileEditorPlaceholder')}" rows="12">${escapeHtml(profileEditingContent || entry?.content || '')}</textarea>
501
+ <div class="roles-editor-footer">
502
+ <span id="roles-profile-bytecount" class="roles-bytecount"></span>
503
+ <div class="roles-editor-actions">
504
+ <button type="button" id="roles-profile-delete" class="danger" ${entry ? '' : 'style="display:none"'}>${t('roles.delete')}</button>
505
+ <button type="button" id="roles-profile-save" class="primary">${t('roles.saveEntry')}</button>
506
+ </div>
507
+ </div>
508
+ <div id="roles-profile-preview" class="roles-preview"></div>
509
+ ` : `<div class="roles-editor-empty roles-profile-inline-empty">${t('roles.profileBotSelectHint')}</div>`}
510
+ </div>
511
+ </div>
512
+ <div class="roles-profile-apply">
513
+ <div class="roles-profile-section-title">${t('roles.applyToGroup')}</div>
514
+ <div class="roles-profile-apply-controls">
515
+ <select id="roles-profile-apply-group">
516
+ ${cache.map(g => `<option value="${escapeHtml(g.chatId)}" ${selectedApplyGroupId === g.chatId ? 'selected' : ''}>${escapeHtml(g.name ?? g.chatId)}</option>`).join('')}
517
+ </select>
518
+ <label class="roles-profile-force"><input type="checkbox" id="roles-profile-apply-force"> ${t('roles.applyForce')}</label>
519
+ </div>
520
+ <div id="roles-profile-apply-bots"></div>
521
+ <div class="roles-editor-actions">
522
+ <button type="button" id="roles-profile-preview-apply">${t('roles.previewApply')}</button>
523
+ <button type="button" id="roles-profile-apply" class="primary">${t('roles.applyProfile')}</button>
524
+ </div>
525
+ <div id="roles-profile-apply-status" class="roles-profile-status"></div>
526
+ </div>
527
+ `;
528
+ detail.querySelectorAll('.roles-profile-bot-row').forEach(row => {
529
+ row.addEventListener('click', () => {
530
+ const botId = row.dataset.profileBotId;
531
+ if (botId)
532
+ void selectProfileBot(botId);
533
+ });
534
+ });
535
+ renderProfileApplyBots();
536
+ bindProfileEditor();
537
+ }
538
+ async function selectProfileBot(botId) {
539
+ if (!selectedProfileId)
540
+ return;
541
+ selectedProfileBotId = botId;
542
+ const entry = await loadProfileEntry(selectedProfileId, botId);
543
+ profileEditingContent = entry.content ?? '';
544
+ await loadProfileEntries(selectedProfileId);
545
+ renderProfileDetail();
546
+ }
547
+ function bindProfileEditor() {
548
+ const textarea = document.getElementById('roles-profile-textarea');
549
+ if (textarea) {
550
+ profileEditingContent = textarea.value;
551
+ updateProfileByteCount();
552
+ updateProfilePreview();
553
+ textarea.addEventListener('input', (e) => {
554
+ profileEditingContent = e.target.value;
555
+ updateProfileByteCount();
556
+ updateProfilePreview();
557
+ });
558
+ }
559
+ document.getElementById('roles-profile-save')?.addEventListener('click', async function () {
560
+ if (!selectedProfileId || !selectedProfileBotId)
561
+ return;
562
+ this.disabled = true;
563
+ this.textContent = '...';
564
+ try {
565
+ const ok = await saveProfileEntry(selectedProfileId, selectedProfileBotId, profileEditingContent);
566
+ await loadProfiles();
567
+ await loadProfileEntries(selectedProfileId);
568
+ renderProfileList(document.getElementById('roles-profile-search')?.value ?? '');
569
+ void refreshGroupProfileContext();
570
+ renderProfileDetail();
571
+ flashProfileStatus(ok ? t('roles.saved') : t('roles.saveFailed'), !ok);
572
+ }
573
+ finally {
574
+ this.disabled = false;
575
+ this.textContent = t('roles.saveEntry');
576
+ }
577
+ });
578
+ document.getElementById('roles-profile-delete')?.addEventListener('click', async function () {
579
+ if (!selectedProfileId || !selectedProfileBotId)
580
+ return;
581
+ if (!confirm(t('roles.confirmDeleteProfileEntry')))
582
+ return;
583
+ this.disabled = true;
584
+ try {
585
+ await deleteProfileEntry(selectedProfileId, selectedProfileBotId);
586
+ profileEditingContent = '';
587
+ await loadProfiles();
588
+ await loadProfileEntries(selectedProfileId);
589
+ renderProfileList(document.getElementById('roles-profile-search')?.value ?? '');
590
+ void refreshGroupProfileContext();
591
+ renderProfileDetail();
592
+ }
593
+ finally {
594
+ this.disabled = false;
595
+ }
596
+ });
597
+ document.getElementById('roles-profile-apply-group')?.addEventListener('change', (e) => {
598
+ selectedApplyGroupId = e.target.value;
599
+ renderProfileApplyBots();
600
+ });
601
+ document.getElementById('roles-profile-preview-apply')?.addEventListener('click', () => runProfileApply(true));
602
+ document.getElementById('roles-profile-apply')?.addEventListener('click', () => runProfileApply(false));
603
+ }
604
+ function updateProfileByteCount() {
605
+ const el = document.getElementById('roles-profile-bytecount');
606
+ const btn = document.getElementById('roles-profile-save');
607
+ if (!el)
608
+ return;
609
+ const len = byteLength(profileEditingContent);
610
+ el.textContent = `${len} / ${MAX_ROLE_BYTES} bytes`;
611
+ el.className = `roles-bytecount ${len > 3800 ? 'warn' : ''} ${len > MAX_ROLE_BYTES ? 'over' : ''}`;
612
+ if (btn)
613
+ btn.disabled = len > MAX_ROLE_BYTES || profileEditingContent.trim().length === 0;
614
+ }
615
+ function updateProfilePreview() {
616
+ const preview = document.getElementById('roles-profile-preview');
617
+ if (!preview)
618
+ return;
619
+ preview.innerHTML = profileEditingContent.trim()
620
+ ? `<strong>${t('roles.preview')}</strong><pre>${escapeHtml(profileEditingContent)}</pre>`
621
+ : `<small>${t('roles.previewEmpty')}</small>`;
622
+ }
623
+ function renderProfileApplyBots() {
624
+ const wrap = document.getElementById('roles-profile-apply-bots');
625
+ if (!wrap)
626
+ return;
627
+ const groupId = selectedApplyGroupId ?? cache[0]?.chatId ?? '';
628
+ const group = cache.find(g => g.chatId === groupId);
629
+ const bots = group?.memberBots.filter(b => b.inChat) ?? [];
630
+ if (!group || bots.length === 0) {
631
+ wrap.innerHTML = `<div class="roles-empty">${t('roles.noChats')}</div>`;
632
+ return;
633
+ }
634
+ wrap.innerHTML = bots.map(bot => {
635
+ const hasEntry = !!entryForBot(bot.larkAppId);
636
+ return `
637
+ <label class="checkbox-row roles-profile-apply-bot">
638
+ <input type="checkbox" name="profile-apply-bot" value="${escapeHtml(bot.larkAppId)}" ${hasEntry ? 'checked' : ''}>
639
+ <span>${escapeHtml(bot.botName ?? bot.larkAppId)}</span>
640
+ <small>${hasEntry ? t('roles.configured') : t('roles.profileMissing')}</small>
641
+ </label>`;
642
+ }).join('');
643
+ }
644
+ async function runProfileApply(preview) {
645
+ const profileId = selectedProfileId;
646
+ if (!profileId)
647
+ return;
648
+ const groupId = selectedApplyGroupId ?? cache[0]?.chatId;
649
+ if (!groupId)
650
+ return;
651
+ const force = document.getElementById('roles-profile-apply-force')?.checked === true;
652
+ const selected = [...document.querySelectorAll('input[name=profile-apply-bot]:checked')].map(i => i.value);
653
+ const status = document.getElementById('roles-profile-apply-status');
654
+ if (selected.length === 0) {
655
+ if (status)
656
+ status.textContent = t('roles.applyPickBots');
657
+ return;
658
+ }
659
+ if (status)
660
+ status.textContent = '...';
661
+ const results = await Promise.all(selected.map(async (larkAppId) => {
662
+ const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}/apply`, {
663
+ method: 'POST',
664
+ headers: { 'content-type': 'application/json' },
665
+ body: JSON.stringify({ chatId: groupId, larkAppId, force, preview }),
666
+ });
667
+ const body = await r.json().catch(() => ({}));
668
+ return { larkAppId, ok: r.ok && body.ok !== false, status: r.status, error: body.error, wouldRefuse: body.wouldRefuse };
669
+ }));
670
+ if (status) {
671
+ status.innerHTML = results.map(r => {
672
+ const bot = allBots.find(b => b.larkAppId === r.larkAppId);
673
+ const label = escapeHtml(bot?.botName ?? r.larkAppId);
674
+ const outcome = r.ok
675
+ ? (preview ? (r.wouldRefuse ? t('roles.applyWouldRefuse') : t('roles.applyPreviewOk')) : t('roles.applyOk'))
676
+ : `${t('roles.applyFailed')}: ${escapeHtml(r.error ?? `HTTP ${r.status}`)}`;
677
+ return `<div>${label}: ${outcome}</div>`;
678
+ }).join('');
679
+ }
680
+ if (!preview) {
681
+ await loadGroups();
682
+ renderTree(document.getElementById('roles-search')?.value ?? '');
683
+ void refreshGroupProfileContext();
684
+ }
685
+ }
686
+ function flashProfileStatus(text, isError = false) {
687
+ const footer = document.querySelector('#roles-profile-detail .roles-editor-footer');
688
+ if (!footer)
689
+ return;
690
+ const statusEl = document.createElement('span');
691
+ statusEl.className = `roles-saved-flash ${isError ? 'roles-save-error' : ''}`;
692
+ statusEl.textContent = ` ${text}`;
693
+ footer.appendChild(statusEl);
694
+ setTimeout(() => statusEl.remove(), isError ? 3000 : 2000);
695
+ }
696
+ async function renderRolesSurface(root, tab) {
697
+ activeTab = tab;
698
+ root.innerHTML = pageHtml(tab);
254
699
  expandedGroups.clear();
255
700
  resetEditor();
256
- // /api/groups 慢——树区域先亮 loading,避免左栏长时间空白像挂了。
701
+ switchTab(activeTab);
257
702
  const treeEl = document.getElementById('roles-tree');
703
+ const profileListEl = document.getElementById('roles-profile-list');
258
704
  if (treeEl)
259
705
  treeEl.innerHTML = loadingHtml();
706
+ if (profileListEl)
707
+ profileListEl.innerHTML = loadingHtml();
260
708
  await loadGroups();
261
- await loadNameMaps(); // 预热共享头像表,让角色树首屏就能出真实头像
262
- // Auto-expand groups that have at least one bot with a role
709
+ await loadProfiles();
710
+ await loadNameMaps();
711
+ if (tab === 'profiles') {
712
+ const requestedChatId = hashChatId();
713
+ if (requestedChatId && cache.some(g => g.chatId === requestedChatId)) {
714
+ selectedApplyGroupId = requestedChatId;
715
+ }
716
+ }
263
717
  for (const g of cache) {
264
718
  if (botRoleCount(g) > 0)
265
719
  expandedGroups.add(g.chatId);
266
720
  }
267
721
  renderTree();
268
- // Search
722
+ renderProfileList();
723
+ if (selectedProfileId)
724
+ await selectProfile(selectedProfileId);
725
+ void refreshGroupProfileContext();
269
726
  document.getElementById('roles-search')?.addEventListener('input', (e) => {
270
727
  renderTree(e.target.value);
271
728
  });
272
- // Refresh
273
729
  document.getElementById('roles-refresh')?.addEventListener('click', async () => {
274
730
  await loadGroups();
275
731
  renderTree(document.getElementById('roles-search')?.value ?? '');
276
- // Re-fetch current selection if any
732
+ void refreshGroupProfileContext();
277
733
  if (selectedGroupId && selectedBotId) {
278
734
  const role = await loadRole(selectedBotId, selectedGroupId);
279
735
  const textarea = document.getElementById('roles-editor-textarea');
@@ -287,7 +743,6 @@ export async function renderRolesPage(root) {
287
743
  delBtn.style.display = role.hasRole ? '' : 'none';
288
744
  }
289
745
  });
290
- // Save
291
746
  document.getElementById('roles-save')?.addEventListener('click', async function () {
292
747
  if (!selectedGroupId || !selectedBotId)
293
748
  return;
@@ -298,10 +753,10 @@ export async function renderRolesPage(root) {
298
753
  if (ok) {
299
754
  await loadGroups();
300
755
  renderTree(document.getElementById('roles-search')?.value ?? '');
756
+ void refreshGroupProfileContext();
301
757
  const delBtn = document.getElementById('roles-delete');
302
758
  if (delBtn)
303
759
  delBtn.style.display = '';
304
- // Brief saved indicator
305
760
  const statusEl = document.createElement('span');
306
761
  statusEl.className = 'roles-saved-flash';
307
762
  statusEl.textContent = ` ${t('roles.saved')}`;
@@ -310,7 +765,6 @@ export async function renderRolesPage(root) {
310
765
  setTimeout(() => statusEl.remove(), 2000);
311
766
  }
312
767
  else {
313
- // Show error feedback
314
768
  const statusEl = document.createElement('span');
315
769
  statusEl.className = 'roles-saved-flash roles-save-error';
316
770
  statusEl.textContent = editingContent.trim().length === 0
@@ -326,7 +780,6 @@ export async function renderRolesPage(root) {
326
780
  this.textContent = t('roles.save');
327
781
  }
328
782
  });
329
- // Delete
330
783
  document.getElementById('roles-delete')?.addEventListener('click', async function () {
331
784
  if (!selectedGroupId || !selectedBotId)
332
785
  return;
@@ -340,6 +793,7 @@ export async function renderRolesPage(root) {
340
793
  await loadGroups();
341
794
  resetEditor();
342
795
  renderTree(document.getElementById('roles-search')?.value ?? '');
796
+ void refreshGroupProfileContext();
343
797
  }
344
798
  }
345
799
  finally {
@@ -347,11 +801,50 @@ export async function renderRolesPage(root) {
347
801
  this.textContent = t('roles.delete');
348
802
  }
349
803
  });
350
- // Live edit
351
804
  document.getElementById('roles-editor-textarea')?.addEventListener('input', (e) => {
352
805
  editingContent = e.target.value;
353
806
  updateByteCount();
354
807
  updatePreview();
355
808
  });
809
+ document.getElementById('roles-profile-search')?.addEventListener('input', (e) => {
810
+ renderProfileList(e.target.value);
811
+ });
812
+ document.getElementById('roles-profile-refresh')?.addEventListener('click', async () => {
813
+ await loadGroups();
814
+ await loadProfiles();
815
+ void refreshGroupProfileContext();
816
+ if (selectedProfileId)
817
+ await loadProfileEntries(selectedProfileId);
818
+ renderProfileList(document.getElementById('roles-profile-search')?.value ?? '');
819
+ renderProfileDetail();
820
+ });
821
+ document.getElementById('roles-profile-select')?.addEventListener('click', async () => {
822
+ const input = document.getElementById('roles-profile-id');
823
+ const profileId = input?.value.trim();
824
+ if (!profileId)
825
+ return;
826
+ if (!isValidProfileId(profileId)) {
827
+ input?.setCustomValidity(t('roles.profileIdInvalid'));
828
+ input?.reportValidity();
829
+ return;
830
+ }
831
+ input?.setCustomValidity('');
832
+ await selectProfile(profileId);
833
+ if (!location.hash.startsWith('#/roles/profile')) {
834
+ location.hash = '#/roles/profile';
835
+ }
836
+ else {
837
+ switchTab('profiles');
838
+ }
839
+ });
840
+ document.getElementById('roles-profile-id')?.addEventListener('input', (e) => {
841
+ e.target.setCustomValidity('');
842
+ });
843
+ }
844
+ export async function renderRolesPage(root) {
845
+ await renderRolesSurface(root, 'groups');
846
+ }
847
+ export async function renderRoleProfilesPage(root) {
848
+ await renderRolesSurface(root, 'profiles');
356
849
  }
357
850
  //# sourceMappingURL=roles.js.map