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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -13
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +209 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +7 -106
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +240 -2
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +1 -1
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/role-resolver.d.ts +1 -0
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +14 -0
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +4 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +24 -8
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +170 -49
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +43 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -0
- package/dist/dashboard/bot-payload.js +44 -0
- package/dist/dashboard/bot-payload.js.map +1 -0
- package/dist/dashboard/registry.d.ts +2 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +15 -4
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +122 -3
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +60 -4
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/groups.d.ts +2 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -1
- package/dist/dashboard/web/groups.js +419 -3
- package/dist/dashboard/web/groups.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +631 -3
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/insights.d.ts +2 -0
- package/dist/dashboard/web/insights.d.ts.map +1 -0
- package/dist/dashboard/web/insights.js +1523 -0
- package/dist/dashboard/web/insights.js.map +1 -0
- package/dist/dashboard/web/overview.d.ts +22 -0
- package/dist/dashboard/web/overview.d.ts.map +1 -1
- package/dist/dashboard/web/overview.js +6 -1
- package/dist/dashboard/web/overview.js.map +1 -1
- package/dist/dashboard/web/role-profile-match.d.ts +31 -0
- package/dist/dashboard/web/role-profile-match.d.ts.map +1 -0
- package/dist/dashboard/web/role-profile-match.js +58 -0
- package/dist/dashboard/web/role-profile-match.js.map +1 -0
- package/dist/dashboard/web/roles.d.ts +1 -0
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +520 -27
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +84 -0
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard-web/app.js +1246 -823
- package/dist/dashboard-web/index.html +2 -1
- package/dist/dashboard-web/style.css +1085 -3
- package/dist/dashboard.js +273 -39
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +34 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +34 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +23 -1
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +16 -9
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/services/group-creator.d.ts +6 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +54 -5
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/insight/antigravity-span-reader.d.ts +3 -0
- package/dist/services/insight/antigravity-span-reader.d.ts.map +1 -0
- package/dist/services/insight/antigravity-span-reader.js +249 -0
- package/dist/services/insight/antigravity-span-reader.js.map +1 -0
- package/dist/services/insight/classify.d.ts +7 -0
- package/dist/services/insight/classify.d.ts.map +1 -0
- package/dist/services/insight/classify.js +46 -0
- package/dist/services/insight/classify.js.map +1 -0
- package/dist/services/insight/claude-span-reader.d.ts +3 -0
- package/dist/services/insight/claude-span-reader.d.ts.map +1 -0
- package/dist/services/insight/claude-span-reader.js +257 -0
- package/dist/services/insight/claude-span-reader.js.map +1 -0
- package/dist/services/insight/codex-span-reader.d.ts +3 -0
- package/dist/services/insight/codex-span-reader.d.ts.map +1 -0
- package/dist/services/insight/codex-span-reader.js +290 -0
- package/dist/services/insight/codex-span-reader.js.map +1 -0
- package/dist/services/insight/intent.d.ts +5 -0
- package/dist/services/insight/intent.d.ts.map +1 -0
- package/dist/services/insight/intent.js +145 -0
- package/dist/services/insight/intent.js.map +1 -0
- package/dist/services/insight/jsonl.d.ts +10 -0
- package/dist/services/insight/jsonl.d.ts.map +1 -0
- package/dist/services/insight/jsonl.js +36 -0
- package/dist/services/insight/jsonl.js.map +1 -0
- package/dist/services/insight/prompt.d.ts +3 -0
- package/dist/services/insight/prompt.d.ts.map +1 -0
- package/dist/services/insight/prompt.js +99 -0
- package/dist/services/insight/prompt.js.map +1 -0
- package/dist/services/insight/redact.d.ts +4 -0
- package/dist/services/insight/redact.d.ts.map +1 -0
- package/dist/services/insight/redact.js +67 -0
- package/dist/services/insight/redact.js.map +1 -0
- package/dist/services/insight/report.d.ts +29 -0
- package/dist/services/insight/report.d.ts.map +1 -0
- package/dist/services/insight/report.js +1126 -0
- package/dist/services/insight/report.js.map +1 -0
- package/dist/services/insight/safe-detail.d.ts +5 -0
- package/dist/services/insight/safe-detail.d.ts.map +1 -0
- package/dist/services/insight/safe-detail.js +59 -0
- package/dist/services/insight/safe-detail.js.map +1 -0
- package/dist/services/insight/scrub.d.ts +22 -0
- package/dist/services/insight/scrub.d.ts.map +1 -0
- package/dist/services/insight/scrub.js +70 -0
- package/dist/services/insight/scrub.js.map +1 -0
- package/dist/services/insight/types.d.ts +394 -0
- package/dist/services/insight/types.d.ts.map +1 -0
- package/dist/services/insight/types.js +2 -0
- package/dist/services/insight/types.js.map +1 -0
- package/dist/services/role-profile-store.d.ts +25 -0
- package/dist/services/role-profile-store.d.ts.map +1 -0
- package/dist/services/role-profile-store.js +171 -0
- package/dist/services/role-profile-store.js.map +1 -0
- package/dist/services/transcript-resolver.d.ts +26 -0
- package/dist/services/transcript-resolver.d.ts.map +1 -0
- package/dist/services/transcript-resolver.js +111 -0
- package/dist/services/transcript-resolver.js.map +1 -0
- package/dist/setup/cli-selection.d.ts +20 -1
- package/dist/setup/cli-selection.d.ts.map +1 -1
- package/dist/setup/cli-selection.js +45 -5
- package/dist/setup/cli-selection.js.map +1 -1
- package/dist/worker.js +10 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,26 @@
|
|
|
3
3
|
// by chatId; the dashboard displays this as a matrix where each cell shows
|
|
4
4
|
// whether a bot is a member of a given chat.
|
|
5
5
|
import { chatAvatarHtml, escapeHtml, loadingHtml, t } from './ui.js';
|
|
6
|
+
import { hasExplicitChatRole, summarizeGroupProfileMatches, } from './role-profile-match.js';
|
|
6
7
|
let cache = { chats: [], bots: [] };
|
|
8
|
+
const PROFILE_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
9
|
+
const roleKey = (larkAppId, chatId) => `${larkAppId}\u0000${chatId}`;
|
|
10
|
+
let roleProfiles = [];
|
|
11
|
+
let roleProfileEntriesById = new Map();
|
|
12
|
+
let groupRoleContentByBot = new Map();
|
|
13
|
+
let roleProfileContextLoaded = false;
|
|
14
|
+
function isValidProfileId(profileId) {
|
|
15
|
+
return PROFILE_ID_RE.test(profileId) && profileId !== '.' && profileId !== '..';
|
|
16
|
+
}
|
|
17
|
+
export function suggestRoleProfileIdFromChat(value) {
|
|
18
|
+
const cleaned = String(value ?? '')
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '')
|
|
23
|
+
.slice(0, 64);
|
|
24
|
+
return isValidProfileId(cleaned) ? cleaned : 'profile';
|
|
25
|
+
}
|
|
7
26
|
function pageHtml() {
|
|
8
27
|
return `<section class="page">
|
|
9
28
|
<div class="page-heading">
|
|
@@ -39,6 +58,52 @@ async function fetchGroups() {
|
|
|
39
58
|
const r = await fetch('/api/groups');
|
|
40
59
|
return r.json();
|
|
41
60
|
}
|
|
61
|
+
async function loadGroupRoleProfileContext() {
|
|
62
|
+
const profileResp = await fetch('/api/role-profiles');
|
|
63
|
+
const profileBody = await profileResp.json().catch(() => ({}));
|
|
64
|
+
const nextProfiles = Array.isArray(profileBody.profiles) ? profileBody.profiles : [];
|
|
65
|
+
const detailPairs = await Promise.all(nextProfiles.map(async (profile) => {
|
|
66
|
+
try {
|
|
67
|
+
const r = await fetch(`/api/role-profiles/${encodeURIComponent(profile.profileId)}`);
|
|
68
|
+
const body = await r.json().catch(() => ({}));
|
|
69
|
+
return [profile.profileId, Array.isArray(body.entries) ? body.entries : []];
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [profile.profileId, []];
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
const nextGroupRoles = new Map();
|
|
76
|
+
const seenRoleKeys = new Set();
|
|
77
|
+
await Promise.all((cache.chats ?? []).flatMap((chat) => (chat.memberBots ?? [])
|
|
78
|
+
.filter((bot) => bot?.inChat && bot?.larkAppId)
|
|
79
|
+
.map(async (bot) => {
|
|
80
|
+
const key = roleKey(bot.larkAppId, chat.chatId);
|
|
81
|
+
if (seenRoleKeys.has(key))
|
|
82
|
+
return;
|
|
83
|
+
seenRoleKeys.add(key);
|
|
84
|
+
try {
|
|
85
|
+
const r = await fetch(`/api/roles/${encodeURIComponent(bot.larkAppId)}/${encodeURIComponent(chat.chatId)}`);
|
|
86
|
+
const body = await r.json().catch(() => ({}));
|
|
87
|
+
const hasEffectiveRole = body?.hasEffectiveRole ?? body?.hasRole;
|
|
88
|
+
const effectiveContent = 'effectiveContent' in body ? body.effectiveContent : body.content;
|
|
89
|
+
nextGroupRoles.set(key, {
|
|
90
|
+
content: hasEffectiveRole ? String(effectiveContent ?? '') : null,
|
|
91
|
+
source: body?.effectiveSource ?? (body?.hasRole ? 'chat' : 'none'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
nextGroupRoles.set(key, null);
|
|
96
|
+
}
|
|
97
|
+
})));
|
|
98
|
+
roleProfiles = nextProfiles;
|
|
99
|
+
roleProfileEntriesById = new Map(detailPairs);
|
|
100
|
+
groupRoleContentByBot = nextGroupRoles;
|
|
101
|
+
}
|
|
102
|
+
async function fetchRoleProfileSummaries() {
|
|
103
|
+
const r = await fetch('/api/role-profiles');
|
|
104
|
+
const body = await r.json().catch(() => ({}));
|
|
105
|
+
return Array.isArray(body.profiles) ? body.profiles : [];
|
|
106
|
+
}
|
|
42
107
|
/** True iff every expected bot id appears in the row's memberBots with
|
|
43
108
|
* inChat:true. Used by refreshUntilSeen to defer committing a canonical
|
|
44
109
|
* snapshot until all invited bots have caught up Lark-side. Exported so
|
|
@@ -68,6 +133,25 @@ export function renderBotCheckboxes(bots, excludeIds) {
|
|
|
68
133
|
</label>
|
|
69
134
|
`).join('');
|
|
70
135
|
}
|
|
136
|
+
export function renderRoleProfileBootstrapSummary(profileId, messageId, error) {
|
|
137
|
+
const cleanProfileId = String(profileId ?? '').trim();
|
|
138
|
+
if (!cleanProfileId)
|
|
139
|
+
return '';
|
|
140
|
+
if (error) {
|
|
141
|
+
return `<p class="hint-warn">${escapeHtml(t('groups.roleProfileBootstrapFailed', {
|
|
142
|
+
name: cleanProfileId,
|
|
143
|
+
reason: String(error),
|
|
144
|
+
}))}</p>`;
|
|
145
|
+
}
|
|
146
|
+
const cleanMessageId = typeof messageId === 'string' && messageId.trim() ? messageId.trim() : '';
|
|
147
|
+
if (cleanMessageId) {
|
|
148
|
+
return `<p class="hint-ok">${escapeHtml(t('groups.roleProfileBootstrapSent', {
|
|
149
|
+
name: cleanProfileId,
|
|
150
|
+
messageId: cleanMessageId,
|
|
151
|
+
}))}</p>`;
|
|
152
|
+
}
|
|
153
|
+
return `<p class="hint-ok">${escapeHtml(t('groups.roleProfileBootstrapDone', { name: cleanProfileId }))}</p>`;
|
|
154
|
+
}
|
|
71
155
|
export async function renderGroupsPage(root) {
|
|
72
156
|
root.innerHTML = pageHtml();
|
|
73
157
|
const head = root.querySelector('#g-head');
|
|
@@ -80,13 +164,14 @@ export async function renderGroupsPage(root) {
|
|
|
80
164
|
try {
|
|
81
165
|
await loadGroups();
|
|
82
166
|
rerender();
|
|
167
|
+
void refreshRoleProfileContext();
|
|
83
168
|
}
|
|
84
169
|
finally {
|
|
85
170
|
refreshBtn.disabled = false;
|
|
86
171
|
}
|
|
87
172
|
};
|
|
88
173
|
const createBtn = root.querySelector('#g-create');
|
|
89
|
-
createBtn.onclick = () => openCreateModal();
|
|
174
|
+
createBtn.onclick = () => { void openCreateModal(); };
|
|
90
175
|
// /api/groups 要扇出到所有 daemon、逐群查成员,慢——先亮 loading,回来再换表格。
|
|
91
176
|
const loadingEl = root.querySelector('#g-loading');
|
|
92
177
|
const tableWrap = root.querySelector('#g-table-wrap');
|
|
@@ -97,12 +182,33 @@ export async function renderGroupsPage(root) {
|
|
|
97
182
|
loadingEl.remove();
|
|
98
183
|
tableWrap.hidden = false;
|
|
99
184
|
}
|
|
100
|
-
function
|
|
185
|
+
async function refreshRoleProfileContext() {
|
|
186
|
+
try {
|
|
187
|
+
await loadGroupRoleProfileContext();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
roleProfiles = [];
|
|
191
|
+
roleProfileEntriesById = new Map();
|
|
192
|
+
groupRoleContentByBot = new Map();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
roleProfileContextLoaded = true;
|
|
196
|
+
rerender();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function openCreateModal() {
|
|
101
200
|
const allBots = cache.bots;
|
|
102
201
|
if (allBots.length === 0) {
|
|
103
202
|
alert(t('groups.noBotsOnline'));
|
|
104
203
|
return;
|
|
105
204
|
}
|
|
205
|
+
let roleProfiles = [];
|
|
206
|
+
try {
|
|
207
|
+
const r = await fetch('/api/role-profiles');
|
|
208
|
+
const data = await r.json();
|
|
209
|
+
roleProfiles = data.profiles ?? [];
|
|
210
|
+
}
|
|
211
|
+
catch { /* profile selector is optional */ }
|
|
106
212
|
drawer.innerHTML = `
|
|
107
213
|
<article>
|
|
108
214
|
<header><h3>${t('groups.createTitle')}</h3></header>
|
|
@@ -117,6 +223,14 @@ export async function renderGroupsPage(root) {
|
|
|
117
223
|
<input type="text" name="bindWorkingDir" placeholder="e.g. ~/projects/botmux">
|
|
118
224
|
<small>${t('groups.bindDirHelp')}</small>
|
|
119
225
|
</label>
|
|
226
|
+
<label class="form-row">
|
|
227
|
+
<span>${t('groups.roleProfile')}</span>
|
|
228
|
+
<select name="roleProfileId">
|
|
229
|
+
<option value="">${t('groups.roleProfileNone')}</option>
|
|
230
|
+
${roleProfiles.map(p => `<option value="${escapeHtml(p.profileId)}">${escapeHtml(p.profileId)}</option>`).join('')}
|
|
231
|
+
</select>
|
|
232
|
+
<small>${t('groups.roleProfileHelp')}</small>
|
|
233
|
+
</label>
|
|
120
234
|
<fieldset>
|
|
121
235
|
<legend>${t('groups.botPicker')}</legend>
|
|
122
236
|
${renderBotCheckboxes(allBots)}
|
|
@@ -134,6 +248,7 @@ export async function renderGroupsPage(root) {
|
|
|
134
248
|
const fd = new FormData(ev.target);
|
|
135
249
|
const name = (fd.get('name') ?? '').trim();
|
|
136
250
|
const bindWorkingDir = (fd.get('bindWorkingDir') ?? '').trim();
|
|
251
|
+
const roleProfileId = (fd.get('roleProfileId') ?? '').trim();
|
|
137
252
|
const ids = fd.getAll('bot');
|
|
138
253
|
if (ids.length === 0) {
|
|
139
254
|
alert('Pick at least one bot.');
|
|
@@ -148,7 +263,12 @@ export async function renderGroupsPage(root) {
|
|
|
148
263
|
const r = await fetch('/api/groups/create', {
|
|
149
264
|
method: 'POST',
|
|
150
265
|
headers: { 'content-type': 'application/json' },
|
|
151
|
-
body: JSON.stringify({
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
name: name || undefined,
|
|
268
|
+
larkAppIds: ids,
|
|
269
|
+
bindWorkingDir: bindWorkingDir || undefined,
|
|
270
|
+
roleProfileId: roleProfileId || undefined,
|
|
271
|
+
}),
|
|
152
272
|
});
|
|
153
273
|
const respBody = await r.json();
|
|
154
274
|
if (respBody.ok && respBody.chatId) {
|
|
@@ -174,6 +294,7 @@ export async function renderGroupsPage(root) {
|
|
|
174
294
|
expectedBotIds.add(respBody.creator);
|
|
175
295
|
injectOptimisticChat(respBody.chatId, name || respBody.chatId, validIds, respBody.creator);
|
|
176
296
|
rerender();
|
|
297
|
+
void refreshRoleProfileContext();
|
|
177
298
|
void refreshUntilSeen(respBody.chatId, expectedBotIds).catch(() => { });
|
|
178
299
|
}
|
|
179
300
|
else {
|
|
@@ -227,6 +348,7 @@ export async function renderGroupsPage(root) {
|
|
|
227
348
|
if (row && allExpectedInChat(row, expectedBotIds)) {
|
|
228
349
|
cache = next;
|
|
229
350
|
rerender();
|
|
351
|
+
void refreshRoleProfileContext();
|
|
230
352
|
return;
|
|
231
353
|
}
|
|
232
354
|
}
|
|
@@ -280,6 +402,7 @@ export async function renderGroupsPage(root) {
|
|
|
280
402
|
invalidBots.length ? `<li>无效 bot id: <code>${invalidBots.map(escapeHtml).join(', ')}</code></li>` : '',
|
|
281
403
|
invalidUsers.length ? `<li>无效用户 open_id: <code>${invalidUsers.map(escapeHtml).join(', ')}</code></li>` : '',
|
|
282
404
|
].filter(Boolean).join('');
|
|
405
|
+
const profileNote = renderRoleProfileBootstrapSummary(typeof resp.roleProfileId === 'string' ? resp.roleProfileId : '', resp.roleProfileBootstrapMessageId, resp.roleProfileBootstrapError);
|
|
283
406
|
drawer.innerHTML = `
|
|
284
407
|
<article>
|
|
285
408
|
<header><h3>${t('groups.successTitle')}</h3></header>
|
|
@@ -287,6 +410,7 @@ export async function renderGroupsPage(root) {
|
|
|
287
410
|
<p><b>创建者:</b> <code>${escapeHtml(resp.creator ?? '?')}</code></p>
|
|
288
411
|
${inviteNote}
|
|
289
412
|
${bindNote}
|
|
413
|
+
${profileNote}
|
|
290
414
|
${invalidNote ? `<ul>${invalidNote}</ul>` : ''}
|
|
291
415
|
<div class="actions">
|
|
292
416
|
<a class="btn-link primary" href="${appLink}" target="_blank" rel="noopener">${t('groups.openGroup')}</a>
|
|
@@ -302,6 +426,32 @@ export async function renderGroupsPage(root) {
|
|
|
302
426
|
});
|
|
303
427
|
drawer.querySelector('#g-create-close').onclick = () => drawer.close();
|
|
304
428
|
}
|
|
429
|
+
function renderGroupProfileStatus(chat) {
|
|
430
|
+
if (!roleProfiles.length || !roleProfileContextLoaded)
|
|
431
|
+
return '';
|
|
432
|
+
const rolesByBot = new Map();
|
|
433
|
+
for (const bot of chat.memberBots ?? []) {
|
|
434
|
+
if (!bot?.inChat)
|
|
435
|
+
continue;
|
|
436
|
+
rolesByBot.set(bot.larkAppId, groupRoleContentByBot.get(roleKey(bot.larkAppId, chat.chatId)) ?? null);
|
|
437
|
+
}
|
|
438
|
+
if (!hasExplicitChatRole(rolesByBot))
|
|
439
|
+
return '';
|
|
440
|
+
const matches = summarizeGroupProfileMatches(chat.memberBots ?? [], roleProfiles, roleProfileEntriesById, rolesByBot);
|
|
441
|
+
const best = matches[0];
|
|
442
|
+
if (!best) {
|
|
443
|
+
return `<div class="g-profile-status muted">${t('groups.profileStatusUnmatched')}</div>`;
|
|
444
|
+
}
|
|
445
|
+
const key = best.kind === 'full' ? 'groups.profileStatusFullChat' : 'groups.profileStatusPartial';
|
|
446
|
+
return `<div class="g-profile-status ${best.kind}">
|
|
447
|
+
${escapeHtml(t(key, {
|
|
448
|
+
name: best.profileId,
|
|
449
|
+
matched: best.matched,
|
|
450
|
+
total: best.total,
|
|
451
|
+
chat: best.chatMatched,
|
|
452
|
+
}))}
|
|
453
|
+
</div>`;
|
|
454
|
+
}
|
|
305
455
|
function renderHead() {
|
|
306
456
|
head.innerHTML = `<tr>
|
|
307
457
|
<th>${t('groups.chat')}</th>
|
|
@@ -331,6 +481,7 @@ export async function renderGroupsPage(root) {
|
|
|
331
481
|
<div class="g-chat-meta">
|
|
332
482
|
<strong>${escapeHtml(c.name ?? c.chatId)}</strong><br>
|
|
333
483
|
<small><code>${escapeHtml(c.chatId)}</code></small>
|
|
484
|
+
${renderGroupProfileStatus(c)}
|
|
334
485
|
</div>
|
|
335
486
|
</div>
|
|
336
487
|
</td>
|
|
@@ -342,11 +493,13 @@ export async function renderGroupsPage(root) {
|
|
|
342
493
|
}).join('')}
|
|
343
494
|
<td>
|
|
344
495
|
<button class="add-bots" type="button">${t('groups.addBots')}</button>
|
|
496
|
+
<button class="save-profile" type="button">${t('groups.saveAsProfile')}</button>
|
|
345
497
|
<button class="manage-chat" type="button">${t('groups.manage')}</button>
|
|
346
498
|
</td>
|
|
347
499
|
</tr>`).join('');
|
|
348
500
|
}
|
|
349
501
|
rerender();
|
|
502
|
+
void refreshRoleProfileContext();
|
|
350
503
|
body.addEventListener('click', async (e) => {
|
|
351
504
|
const btn = e.target.closest('button.add-bots');
|
|
352
505
|
if (!btn)
|
|
@@ -416,6 +569,266 @@ export async function renderGroupsPage(root) {
|
|
|
416
569
|
}
|
|
417
570
|
};
|
|
418
571
|
});
|
|
572
|
+
body.addEventListener('click', async (e) => {
|
|
573
|
+
const btn = e.target.closest('button.save-profile');
|
|
574
|
+
if (!btn)
|
|
575
|
+
return;
|
|
576
|
+
const tr = btn.closest('tr[data-chat]');
|
|
577
|
+
const chatId = tr.dataset.chat;
|
|
578
|
+
const chat = cache.chats.find(c => c.chatId === chatId);
|
|
579
|
+
if (!chat)
|
|
580
|
+
return;
|
|
581
|
+
await saveGroupRolesAsProfile(chat, btn);
|
|
582
|
+
});
|
|
583
|
+
async function saveGroupRolesAsProfile(chat, btn) {
|
|
584
|
+
const suggestedByName = suggestRoleProfileIdFromChat(chat.name ?? '');
|
|
585
|
+
const suggested = suggestedByName === 'profile'
|
|
586
|
+
? suggestRoleProfileIdFromChat(chat.chatId)
|
|
587
|
+
: suggestedByName;
|
|
588
|
+
btn.disabled = true;
|
|
589
|
+
const originalText = btn.textContent;
|
|
590
|
+
btn.textContent = t('groups.saveProfileSaving');
|
|
591
|
+
try {
|
|
592
|
+
const [entries, profiles] = await Promise.all([
|
|
593
|
+
collectGroupProfileEntries(chat),
|
|
594
|
+
fetchRoleProfileSummaries(),
|
|
595
|
+
]);
|
|
596
|
+
openSaveProfileDialog(chat, suggested, entries, profiles);
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
btn.disabled = false;
|
|
600
|
+
btn.textContent = originalText;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function collectGroupProfileEntries(chat) {
|
|
604
|
+
const inChat = (chat.memberBots ?? []).filter((bot) => bot?.inChat && bot?.larkAppId);
|
|
605
|
+
const roleRows = await Promise.all(inChat.map(async (bot) => {
|
|
606
|
+
try {
|
|
607
|
+
const r = await fetch(`/api/roles/${encodeURIComponent(bot.larkAppId)}/${encodeURIComponent(chat.chatId)}`);
|
|
608
|
+
const body = await r.json().catch(() => ({}));
|
|
609
|
+
const hasEffectiveRole = body?.hasEffectiveRole ?? body?.hasRole;
|
|
610
|
+
const effectiveContent = 'effectiveContent' in body ? body.effectiveContent : body.content;
|
|
611
|
+
const content = hasEffectiveRole ? String(effectiveContent ?? '').trim() : '';
|
|
612
|
+
const source = body?.effectiveSource === 'chat' || body?.effectiveSource === 'team'
|
|
613
|
+
? body.effectiveSource
|
|
614
|
+
: null;
|
|
615
|
+
return {
|
|
616
|
+
larkAppId: bot.larkAppId,
|
|
617
|
+
botName: bot.botName,
|
|
618
|
+
content,
|
|
619
|
+
status: content ? (source ?? 'chat') : 'empty',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
return {
|
|
624
|
+
larkAppId: bot.larkAppId,
|
|
625
|
+
botName: bot.botName,
|
|
626
|
+
content: '',
|
|
627
|
+
status: 'error',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}));
|
|
631
|
+
return roleRows;
|
|
632
|
+
}
|
|
633
|
+
function openSaveProfileDialog(chat, suggestedProfileId, entries, profiles) {
|
|
634
|
+
const sortedProfiles = [...profiles].sort((a, b) => a.profileId.localeCompare(b.profileId));
|
|
635
|
+
const profileButtons = sortedProfiles
|
|
636
|
+
.map((profile, index) => `
|
|
637
|
+
<button type="button"
|
|
638
|
+
class="g-save-profile-pick ${index === 0 ? 'selected' : ''}"
|
|
639
|
+
data-profile-id="${escapeHtml(profile.profileId)}"
|
|
640
|
+
aria-pressed="${index === 0 ? 'true' : 'false'}">
|
|
641
|
+
<span>${escapeHtml(profile.profileId)}</span>
|
|
642
|
+
<small>${escapeHtml(t('groups.saveProfileExistingMeta', { count: profile.entryCount ?? 0 }))}</small>
|
|
643
|
+
</button>`)
|
|
644
|
+
.join('');
|
|
645
|
+
const hasExistingProfiles = sortedProfiles.length > 0;
|
|
646
|
+
const emptyCount = entries.filter(entry => entry.status === 'empty').length;
|
|
647
|
+
const failedCount = entries.filter(entry => entry.status === 'error').length;
|
|
648
|
+
const canSubmitSnapshot = entries.length > 0 && failedCount === 0;
|
|
649
|
+
const entryRows = entries.map(entry => `
|
|
650
|
+
<div class="g-save-profile-entry ${entry.status === 'error' ? 'error' : ''}">
|
|
651
|
+
<div>
|
|
652
|
+
<strong>${escapeHtml(entry.botName ?? entry.larkAppId)}</strong>
|
|
653
|
+
<code>${escapeHtml(entry.larkAppId)}</code>
|
|
654
|
+
</div>
|
|
655
|
+
<span class="g-save-profile-entry-status ${entry.status === 'error' ? 'error' : 'ok'}">
|
|
656
|
+
${t(entry.status === 'error' ? 'groups.saveProfileStatus.error' : 'groups.saveProfileStatus.entry')}
|
|
657
|
+
</span>
|
|
658
|
+
</div>`).join('');
|
|
659
|
+
drawer.innerHTML = `
|
|
660
|
+
<article class="g-save-profile-dialog">
|
|
661
|
+
<header>
|
|
662
|
+
<h3>${t('groups.saveProfileTitle')}</h3>
|
|
663
|
+
<p>${escapeHtml(t('groups.saveProfileIntro', {
|
|
664
|
+
name: chat.name ?? chat.chatId,
|
|
665
|
+
count: entries.length,
|
|
666
|
+
}))}</p>
|
|
667
|
+
</header>
|
|
668
|
+
<form id="g-save-profile-form">
|
|
669
|
+
<section class="g-save-profile-panel">
|
|
670
|
+
<div class="g-save-profile-section-head">
|
|
671
|
+
<span>${t('groups.saveProfileScope')}</span>
|
|
672
|
+
<small>${t('groups.saveProfileScopeHelp')}</small>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="g-save-profile-stats">
|
|
675
|
+
<span>${t('groups.saveProfileBotCount')} <strong>${entries.length}</strong></span>
|
|
676
|
+
${failedCount ? `<span class="warn">${t('groups.saveProfileLoadFailed')} <strong>${failedCount}</strong></span>` : ''}
|
|
677
|
+
</div>
|
|
678
|
+
<div class="g-save-profile-entry-list">
|
|
679
|
+
${entryRows || `<div class="g-save-profile-empty">${t('groups.saveProfileNoRoles')}</div>`}
|
|
680
|
+
</div>
|
|
681
|
+
</section>
|
|
682
|
+
|
|
683
|
+
<div class="form-row">
|
|
684
|
+
<span>${t('groups.saveProfileMode')}</span>
|
|
685
|
+
<div class="g-save-profile-switch" role="tablist" aria-label="${t('groups.saveProfileMode')}">
|
|
686
|
+
<button type="button" class="active" data-save-profile-mode="new">${t('groups.saveProfileNew')}</button>
|
|
687
|
+
<button type="button" data-save-profile-mode="overwrite" ${hasExistingProfiles ? '' : 'disabled'}>${t('groups.saveProfileOverwrite')}</button>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
<label class="form-row" data-profile-mode-row="new">
|
|
691
|
+
<span>${t('groups.saveProfileIdLabel')}</span>
|
|
692
|
+
<input type="text" name="profileId" value="${escapeHtml(suggestedProfileId)}" maxlength="64" autocomplete="off">
|
|
693
|
+
<small>${t('groups.saveProfileInvalid')}</small>
|
|
694
|
+
</label>
|
|
695
|
+
<div class="form-row" data-profile-mode-row="overwrite" hidden>
|
|
696
|
+
<span>${t('groups.saveProfileExistingLabel')}</span>
|
|
697
|
+
${hasExistingProfiles
|
|
698
|
+
? `<div class="g-save-profile-picker">${profileButtons}</div>`
|
|
699
|
+
: `<div class="g-save-profile-summary warn">${t('groups.saveProfileExistingEmpty')}</div>`}
|
|
700
|
+
<small>${t('groups.saveProfileOverwriteHelp')}</small>
|
|
701
|
+
</div>
|
|
702
|
+
<div class="g-save-profile-target">
|
|
703
|
+
<span>${t('groups.saveProfileTarget')}</span>
|
|
704
|
+
<code data-save-profile-target>${escapeHtml(suggestedProfileId)}</code>
|
|
705
|
+
<small data-save-profile-target-mode>${t('groups.saveProfileTargetNew')}</small>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="g-save-profile-summary ${canSubmitSnapshot ? '' : 'warn'}">
|
|
708
|
+
${failedCount
|
|
709
|
+
? escapeHtml(t('groups.saveProfileFailedLoadSummary', { count: failedCount }))
|
|
710
|
+
: entries.length
|
|
711
|
+
? escapeHtml(emptyCount
|
|
712
|
+
? t('groups.saveProfileEntrySummaryWithEmpty', { count: entries.length, emptyCount })
|
|
713
|
+
: t('groups.saveProfileEntrySummary', { count: entries.length }))
|
|
714
|
+
: escapeHtml(t('groups.saveProfileNoRoles'))}
|
|
715
|
+
</div>
|
|
716
|
+
<div class="g-save-profile-status" data-save-profile-status></div>
|
|
717
|
+
<div class="actions">
|
|
718
|
+
<button type="submit" class="primary" ${canSubmitSnapshot ? '' : 'disabled'}>${t('groups.saveProfileSubmit')}</button>
|
|
719
|
+
<button type="button" id="g-save-profile-cancel">${t('groups.cancel')}</button>
|
|
720
|
+
</div>
|
|
721
|
+
</form>
|
|
722
|
+
</article>`;
|
|
723
|
+
drawer.showModal();
|
|
724
|
+
const formEl = drawer.querySelector('#g-save-profile-form');
|
|
725
|
+
const modeRows = [...drawer.querySelectorAll('[data-profile-mode-row]')];
|
|
726
|
+
const statusEl = drawer.querySelector('[data-save-profile-status]');
|
|
727
|
+
const submitBtn = formEl.querySelector('button[type=submit]');
|
|
728
|
+
const modeButtons = [...formEl.querySelectorAll('[data-save-profile-mode]')];
|
|
729
|
+
const profilePickButtons = [...formEl.querySelectorAll('[data-profile-id]')];
|
|
730
|
+
const profileIdInput = formEl.querySelector('input[name=profileId]');
|
|
731
|
+
const targetEl = formEl.querySelector('[data-save-profile-target]');
|
|
732
|
+
const targetModeEl = formEl.querySelector('[data-save-profile-target-mode]');
|
|
733
|
+
let selectedMode = 'new';
|
|
734
|
+
let selectedExistingProfileId = sortedProfiles[0]?.profileId ?? '';
|
|
735
|
+
function currentProfileId() {
|
|
736
|
+
return selectedMode === 'overwrite'
|
|
737
|
+
? selectedExistingProfileId
|
|
738
|
+
: profileIdInput.value.trim();
|
|
739
|
+
}
|
|
740
|
+
function updateMode() {
|
|
741
|
+
for (const row of modeRows) {
|
|
742
|
+
row.hidden = row.dataset.profileModeRow !== selectedMode;
|
|
743
|
+
}
|
|
744
|
+
for (const button of modeButtons) {
|
|
745
|
+
const isActive = button.dataset.saveProfileMode === selectedMode;
|
|
746
|
+
button.classList.toggle('active', isActive);
|
|
747
|
+
button.setAttribute('aria-pressed', String(isActive));
|
|
748
|
+
}
|
|
749
|
+
for (const button of profilePickButtons) {
|
|
750
|
+
const isSelected = button.dataset.profileId === selectedExistingProfileId;
|
|
751
|
+
button.classList.toggle('selected', isSelected);
|
|
752
|
+
button.setAttribute('aria-pressed', String(isSelected));
|
|
753
|
+
}
|
|
754
|
+
submitBtn.textContent = selectedMode === 'overwrite'
|
|
755
|
+
? t('groups.saveProfileOverwriteSubmit')
|
|
756
|
+
: t('groups.saveProfileSubmit');
|
|
757
|
+
submitBtn.disabled = !canSubmitSnapshot || (selectedMode === 'overwrite' && !selectedExistingProfileId);
|
|
758
|
+
targetEl.textContent = currentProfileId() || '-';
|
|
759
|
+
targetModeEl.textContent = selectedMode === 'overwrite'
|
|
760
|
+
? t('groups.saveProfileTargetOverwrite')
|
|
761
|
+
: t('groups.saveProfileTargetNew');
|
|
762
|
+
statusEl.textContent = '';
|
|
763
|
+
statusEl.className = 'g-save-profile-status';
|
|
764
|
+
}
|
|
765
|
+
modeButtons.forEach(button => {
|
|
766
|
+
button.addEventListener('click', () => {
|
|
767
|
+
const mode = button.dataset.saveProfileMode === 'overwrite' ? 'overwrite' : 'new';
|
|
768
|
+
if (mode === 'overwrite' && !hasExistingProfiles)
|
|
769
|
+
return;
|
|
770
|
+
selectedMode = mode;
|
|
771
|
+
updateMode();
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
profilePickButtons.forEach(button => {
|
|
775
|
+
button.addEventListener('click', () => {
|
|
776
|
+
selectedExistingProfileId = button.dataset.profileId ?? '';
|
|
777
|
+
selectedMode = 'overwrite';
|
|
778
|
+
updateMode();
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
profileIdInput.addEventListener('input', updateMode);
|
|
782
|
+
updateMode();
|
|
783
|
+
drawer.querySelector('#g-save-profile-cancel').onclick = () => drawer.close();
|
|
784
|
+
formEl.onsubmit = async (ev) => {
|
|
785
|
+
ev.preventDefault();
|
|
786
|
+
if (!canSubmitSnapshot)
|
|
787
|
+
return;
|
|
788
|
+
const profileId = currentProfileId();
|
|
789
|
+
if (!isValidProfileId(profileId)) {
|
|
790
|
+
statusEl.textContent = t('groups.saveProfileInvalid');
|
|
791
|
+
statusEl.className = 'g-save-profile-status error';
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
submitBtn.disabled = true;
|
|
795
|
+
submitBtn.textContent = t('groups.saveProfileSaving');
|
|
796
|
+
statusEl.textContent = t('groups.saveProfileSaving');
|
|
797
|
+
statusEl.className = 'g-save-profile-status';
|
|
798
|
+
try {
|
|
799
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
800
|
+
const r = await fetch(`/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(entry.larkAppId)}`, {
|
|
801
|
+
method: 'PUT',
|
|
802
|
+
headers: { 'content-type': 'application/json' },
|
|
803
|
+
body: JSON.stringify({ content: entry.content, allowEmpty: true }),
|
|
804
|
+
});
|
|
805
|
+
return r.ok;
|
|
806
|
+
}));
|
|
807
|
+
const saved = results.filter(Boolean).length;
|
|
808
|
+
if (saved !== entries.length) {
|
|
809
|
+
statusEl.textContent = t('groups.saveProfileFailed', { saved, total: entries.length });
|
|
810
|
+
statusEl.className = 'g-save-profile-status error';
|
|
811
|
+
submitBtn.disabled = false;
|
|
812
|
+
submitBtn.textContent = selectedMode === 'overwrite'
|
|
813
|
+
? t('groups.saveProfileOverwriteSubmit')
|
|
814
|
+
: t('groups.saveProfileSubmit');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
statusEl.textContent = t('groups.saveProfileDone', { name: profileId, count: saved });
|
|
818
|
+
statusEl.className = 'g-save-profile-status ok';
|
|
819
|
+
await refreshRoleProfileContext();
|
|
820
|
+
setTimeout(() => drawer.close(), 700);
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
statusEl.textContent = String(err);
|
|
824
|
+
statusEl.className = 'g-save-profile-status error';
|
|
825
|
+
submitBtn.disabled = false;
|
|
826
|
+
submitBtn.textContent = selectedMode === 'overwrite'
|
|
827
|
+
? t('groups.saveProfileOverwriteSubmit')
|
|
828
|
+
: t('groups.saveProfileSubmit');
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
419
832
|
body.addEventListener('click', async (e) => {
|
|
420
833
|
const btn = e.target.closest('button.manage-chat');
|
|
421
834
|
if (!btn)
|
|
@@ -528,6 +941,7 @@ export async function renderGroupsPage(root) {
|
|
|
528
941
|
try {
|
|
529
942
|
await loadGroups();
|
|
530
943
|
rerender();
|
|
944
|
+
void refreshRoleProfileContext();
|
|
531
945
|
}
|
|
532
946
|
catch { /* tolerate */ }
|
|
533
947
|
}
|
|
@@ -575,6 +989,7 @@ export async function renderGroupsPage(root) {
|
|
|
575
989
|
alert(lines || `Unexpected: ${JSON.stringify(respBody)}`);
|
|
576
990
|
await loadGroups();
|
|
577
991
|
rerender();
|
|
992
|
+
void refreshRoleProfileContext();
|
|
578
993
|
}
|
|
579
994
|
catch (e) {
|
|
580
995
|
alert('Network error: ' + e);
|
|
@@ -611,6 +1026,7 @@ export async function renderGroupsPage(root) {
|
|
|
611
1026
|
alert(`已解散(由 ${m.botName ?? m.larkAppId} 执行)${closedNote}`);
|
|
612
1027
|
await loadGroups();
|
|
613
1028
|
rerender();
|
|
1029
|
+
void refreshRoleProfileContext();
|
|
614
1030
|
drawer.close();
|
|
615
1031
|
return;
|
|
616
1032
|
}
|