From eb142e9c5e217401ec7743259965bb29cd75f7d8 Mon Sep 17 00:00:00 2001 From: catwithtudou <949812478@qq.com> Date: Sat, 27 Jun 2026 16:16:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(groups):=20=E4=BC=98=E5=8C=96=20dashboard?= =?UTF-8?q?=20=E7=BE=A4=E7=BB=84=E6=93=8D=E4=BD=9C=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboard/web/groups.ts | 92 ++++++++++++++++++++++----- test/dashboard-groups-helpers.test.ts | 29 +++++++++ 2 files changed, 104 insertions(+), 17 deletions(-) diff --git a/src/dashboard/web/groups.ts b/src/dashboard/web/groups.ts index 52035b1fa..d504bc1ae 100644 --- a/src/dashboard/web/groups.ts +++ b/src/dashboard/web/groups.ts @@ -26,6 +26,11 @@ type SaveProfileEntry = { content: string; status: SaveProfileEntryStatus; }; +type GroupAddBotResult = { + id?: unknown; + ok?: unknown; + error?: unknown; +}; function isValidProfileId(profileId: string): boolean { return PROFILE_ID_RE.test(profileId) && profileId !== '.' && profileId !== '..'; @@ -187,6 +192,26 @@ export function renderRoleProfileBootstrapSummary( return `

${escapeHtml(t('groups.roleProfileBootstrapDone', { name: cleanProfileId }))}

`; } +export function renderAddBotsResultSummary(result: GroupAddBotResult[]): string { + const rows = Array.isArray(result) ? result : []; + if (!rows.length) { + return `

没有返回添加结果。

`; + } + const okCount = rows.filter(x => !!x?.ok).length; + const failed = rows.length - okCount; + const cls = failed ? 'hint-warn' : 'hint-ok'; + const items = rows.map(x => { + const id = String(x?.id ?? '?'); + return x?.ok + ? `
  • ${escapeHtml(id)}: OK
  • ` + : `
  • ${escapeHtml(id)}: failed (${escapeHtml(String(x?.error ?? 'unknown'))})
  • `; + }).join(''); + return `
    + 添加结果:成功 ${okCount}/${rows.length}${failed ? `,失败 ${failed}` : ''} + +
    `; +} + export async function renderGroupsPage(root: HTMLElement) { root.innerHTML = pageHtml(); const head = root.querySelector('#g-head')!; @@ -230,6 +255,20 @@ export async function renderGroupsPage(root: HTMLElement) { } } + function setDialogStatus(el: HTMLElement, html: string): void { + el.innerHTML = html; + } + + function renderDialogError(title: string, reason: unknown): string { + return `

    ${escapeHtml(title)}
    ${escapeHtml(String(reason ?? 'unknown'))}

    `; + } + + function restoreSubmit(btn: HTMLButtonElement | null | undefined, label: string): void { + if (!btn) return; + btn.disabled = false; + btn.textContent = label; + } + async function openCreateModal() { const allBots = cache.bots; if (allBots.length === 0) { @@ -268,6 +307,7 @@ export async function renderGroupsPage(root: HTMLElement) { ${t('groups.botPicker')} ${renderBotCheckboxes(allBots)} +
    @@ -285,9 +325,14 @@ export async function renderGroupsPage(root: HTMLElement) { const bindWorkingDir = ((fd.get('bindWorkingDir') as string) ?? '').trim(); const roleProfileId = ((fd.get('roleProfileId') as string) ?? '').trim(); const ids = fd.getAll('bot') as string[]; - if (ids.length === 0) { alert('Pick at least one bot.'); return; } + const statusEl = drawer.querySelector('[data-create-status]')!; + if (ids.length === 0) { + setDialogStatus(statusEl, renderDialogError('请选择 bot', '至少选择一个 bot 后再创建群聊。')); + return; + } const submitBtn = (ev.target as HTMLFormElement).querySelector('button[type=submit]'); if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Creating...'; } + setDialogStatus(statusEl, ''); try { const r = await fetch('/api/groups/create', { method: 'POST', @@ -325,12 +370,12 @@ export async function renderGroupsPage(root: HTMLElement) { void refreshRoleProfileContext(); void refreshUntilSeen(respBody.chatId, expectedBotIds).catch(() => { /* tolerate */ }); } else { - alert(`Failed: ${respBody.error ?? r.status}`); - drawer.close(); + setDialogStatus(statusEl, renderDialogError('创建失败', respBody.error ?? `HTTP ${r.status}`)); + restoreSubmit(submitBtn, t('groups.createSubmit')); } } catch (e) { - alert('Network error: ' + e); - drawer.close(); + setDialogStatus(statusEl, renderDialogError('网络错误', e)); + restoreSubmit(submitBtn, t('groups.createSubmit')); } }; @@ -561,6 +606,7 @@ export async function renderGroupsPage(root: HTMLElement) {

    ${t('groups.createHelp')}

    ${renderBotCheckboxes(cache.bots, inChatSet)} +
    @@ -575,7 +621,14 @@ export async function renderGroupsPage(root: HTMLElement) { ev.preventDefault(); const fd = new FormData(ev.target as HTMLFormElement); const ids = fd.getAll('bot') as string[]; - if (ids.length === 0) { alert('Pick at least one bot.'); return; } + const statusEl = drawer.querySelector('[data-add-status]')!; + if (ids.length === 0) { + setDialogStatus(statusEl, renderDialogError('请选择 bot', '至少选择一个 bot 后再添加。')); + return; + } + const submitBtn = (ev.target as HTMLFormElement).querySelector('button[type=submit]'); + if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Adding...'; } + setDialogStatus(statusEl, ''); try { const r = await fetch(`/api/groups/${encodeURIComponent(chatId)}/add-bots`, { method: 'POST', @@ -584,22 +637,27 @@ export async function renderGroupsPage(root: HTMLElement) { }); const respBody = await r.json(); if (respBody.error === 'no_proxy_bot') { - alert('No bot is currently in this chat — add one manually in Feishu first, then retry.'); + setDialogStatus(statusEl, renderDialogError( + '无法添加 bot', + '当前群里没有可代理操作的 bot。请先在飞书里手动拉入一个 bot,然后重试。', + )); } else if (respBody.result) { - const lines = respBody.result.map((x: any) => - `${x.id}: ${x.ok ? 'OK' : `failed (${x.error ?? 'unknown'})`}` - ).join('\n'); - alert(lines); - // Refresh after change - await loadGroups(); - rerender(); + const resultHtml = renderAddBotsResultSummary(respBody.result); + setDialogStatus(statusEl, resultHtml); + try { + await loadGroups(); + rerender(); + void refreshRoleProfileContext(); + } catch (e) { + setDialogStatus(statusEl, `${resultHtml}${renderDialogError('刷新失败', `添加结果已返回,但刷新群组列表失败:${e}`)}`); + } } else { - alert(`Unexpected response: ${JSON.stringify(respBody)}`); + setDialogStatus(statusEl, renderDialogError('响应异常', JSON.stringify(respBody))); } } catch (e) { - alert('Network error: ' + e); + setDialogStatus(statusEl, renderDialogError('网络错误', e)); } finally { - drawer.close(); + restoreSubmit(submitBtn, t('groups.addBots')); } }; }); diff --git a/test/dashboard-groups-helpers.test.ts b/test/dashboard-groups-helpers.test.ts index a70556189..1d51ac440 100644 --- a/test/dashboard-groups-helpers.test.ts +++ b/test/dashboard-groups-helpers.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { allExpectedInChat, renderBotCheckboxes, + renderAddBotsResultSummary, renderRoleProfileBootstrapSummary, suggestRoleProfileIdFromChat, } from '../src/dashboard/web/groups.js'; @@ -88,6 +89,34 @@ describe('renderRoleProfileBootstrapSummary — create-group profile feedback', }); }); +describe('renderAddBotsResultSummary — add-bots inline feedback', () => { + it('summarizes a clean add-bots result as success', () => { + const html = renderAddBotsResultSummary([ + { id: 'cli_a', ok: true }, + { id: 'cli_b', ok: true }, + ]); + + expect(html).toContain('hint-ok'); + expect(html).toContain('成功 2/2'); + expect(html).toContain('cli_a: OK'); + expect(html).toContain('cli_b: OK'); + }); + + it('summarizes partial failures and escapes ids/errors', () => { + const html = renderAddBotsResultSummary([ + { id: 'cli_ok', ok: true }, + { id: '', ok: false, error: '' }, + ]); + + expect(html).toContain('hint-warn'); + expect(html).toContain('成功 1/2,失败 1'); + expect(html).toContain('<bad>'); + expect(html).toContain('<script>alert(1)</script>'); + expect(html).not.toContain(''); + expect(html).not.toContain('