From 494f3b5fd64757fd170c45b4ec2f208d2e1a3ad7 Mon Sep 17 00:00:00 2001 From: lili Date: Sun, 21 Jun 2026 09:42:29 -0700 Subject: [PATCH] =?UTF-8?q?feat(studio):=20=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E5=81=9C=E7=94=A8/=E6=81=A2=E5=A4=8D=20UI=20+=20e2e=20?= =?UTF-8?q?=E7=9C=9F=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F——补自建知识库停用/恢复端面(后端 disable/restore 端点已有,缺 UI): - useKnowledge: useKnowledgeDisable/useKnowledgeRestore mutation hook - KnowledgePage: 自建 KB 卡片按 status 切换「停用」(confirm)/「恢复」按钮 - mock: 补 restore handler(disable 已有) - 核实真后端 SummaryRespVO.processingStatus = kb.getStatus(),启用状态(disabled/searchable) 经其透出可观测,排除"RespVO 缺启用状态字段"误判,确认无需改后端 - vitest useKnowledge 5/5(disable→disabled→restore→active)+ 全套 78/78,tsc/eslint 0 - e2e knowledge-disable-restore 真后端 1 passed;global-setup section 9 复位 KB1 status='draft' Co-Authored-By: Claude Opus 4.8 (1M context) --- muse-studio/e2e/global-setup.ts | 11 ++++- .../e2e/knowledge-disable-restore.spec.ts | 48 +++++++++++++++++++ .../src/api/mocks/handlers/knowledge.ts | 19 ++++++++ .../knowledge/hooks/useKnowledge.test.tsx | 36 ++++++++++++++ .../features/knowledge/hooks/useKnowledge.ts | 39 +++++++++++++++ muse-studio/src/pages/KnowledgePage.tsx | 39 ++++++++++++++- 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 muse-studio/e2e/knowledge-disable-restore.spec.ts diff --git a/muse-studio/e2e/global-setup.ts b/muse-studio/e2e/global-setup.ts index 75cc1e22..20f6c171 100644 --- a/muse-studio/e2e/global-setup.ts +++ b/muse-studio/e2e/global-setup.ts @@ -230,7 +230,16 @@ async function globalSetup(): Promise { AND id <> (SELECT id FROM muse_content_block WHERE tenant_id=1 AND work_id=1 AND chapter_id=1 ORDER BY order_no, id LIMIT 1)` ); - console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block)'); + // 9) knowledge 停用/恢复:复位自建 KB(id=1)status='draft'——供 F disable/restore e2e 幂等重跑 + // (e2e 会 disable→'disabled'、restore→'searchable',含中途残留态一并归位到初始 draft)。 + const kbReset = await client.query( + "UPDATE muse_knowledge_base SET status='draft' WHERE tenant_id=1 AND id=1" + ); + if (kbReset.rowCount === 0) { + console.warn('[e2e globalSetup] ⚠️ 未找到 KB id=1(knowledge 停用/恢复用例需基础 KB 种子)'); + } + + console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status)'); } finally { await client.end(); } diff --git a/muse-studio/e2e/knowledge-disable-restore.spec.ts b/muse-studio/e2e/knowledge-disable-restore.spec.ts new file mode 100644 index 00000000..ba758556 --- /dev/null +++ b/muse-studio/e2e/knowledge-disable-restore.spec.ts @@ -0,0 +1,48 @@ +import { expect, test, type Page } from '@playwright/test'; + +/** + * 知识库停用/恢复端到端(真实后端,MSW off)。 + * + * 覆盖 F 链路:进 /knowledge 自建 tab → 自建 KB →「恢复」(restore→searchable→「已启用」)→ + *「停用」(disable→disabled→「已停用」)。 + * 关键:真后端 SummaryRespVO.processingStatus = kb.getStatus()(MuseKnowledgeBaseService#toAppSummary), + * 故启用状态(disabled/searchable)经 processingStatus 透出,studio normalizeKnowledgeStatus 据此判定 active/disabled。 + * 幂等:global-setup 每轮复位 KB id=1 status='draft'(disable→'disabled'、restore→'searchable' 均归位)。 + */ +const TOKEN = 'test1'; +const KB_NAME = '活体绑定测试KB'; + +async function seedToken(page: Page): Promise { + await page.addInitScript((t) => { + window.localStorage.setItem('accessToken', t); + window.localStorage.setItem('tenantId', '1'); + }, TOKEN); +} + +test('用户停用/恢复自建知识库(真实后端)', async ({ page }) => { + await seedToken(page); + // 「停用」走 window.confirm,自动接受。 + page.on('dialog', (d) => d.accept()); + + await page.goto('/knowledge'); + // 等自建 KB 卡片(真后端 "活体绑定测试KB")。初始 draft → 非 active → 显示「恢复」。 + await expect(page.getByText(KB_NAME)).toBeVisible({ timeout: 15_000 }); + + // 1) 恢复:点「恢复」→ restore(kb.status→searchable→normalize active)→「已启用」+「停用」按钮。 + const restoreResp = page.waitForResponse( + (r) => /\/knowledge-bases\/\d+\/restore/.test(r.url()) && r.request().method() === 'POST' + ); + await page.getByRole('button', { name: `恢复知识库 ${KB_NAME}` }).click(); + expect((await restoreResp).status()).toBe(200); + await expect(page.getByText('已启用')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('button', { name: `停用知识库 ${KB_NAME}` })).toBeVisible({ timeout: 10_000 }); + + // 2) 停用:点「停用」(confirm 自动接受)→ disable(kb.status→disabled)→「已停用」+「恢复」按钮。 + const disableResp = page.waitForResponse( + (r) => /\/knowledge-bases\/\d+\/disable/.test(r.url()) && r.request().method() === 'POST' + ); + await page.getByRole('button', { name: `停用知识库 ${KB_NAME}` }).click(); + expect((await disableResp).status()).toBe(200); + await expect(page.getByText('已停用')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('button', { name: `恢复知识库 ${KB_NAME}` })).toBeVisible({ timeout: 10_000 }); +}); diff --git a/muse-studio/src/api/mocks/handlers/knowledge.ts b/muse-studio/src/api/mocks/handlers/knowledge.ts index cf583cf3..bc416afa 100644 --- a/muse-studio/src/api/mocks/handlers/knowledge.ts +++ b/muse-studio/src/api/mocks/handlers/knowledge.ts @@ -365,6 +365,25 @@ export const knowledgeHandlers = [ }); }), + // POST /app-api/muse/knowledge-bases/:kbId/restore — 恢复已停用知识库(status→active) + http.post('/app-api/muse/knowledge-bases/:kbId/restore', ({ params }) => { + const { kbId } = params; + const kb = mockKnowledgeBases.find((item) => item.kbId === kbId); + + if (!kb) { + return HttpResponse.json({ code: 40403, msg: '知识库未找到', data: null }, { status: 404 }); + } + + kb.status = 'active'; + kb.updatedAt = new Date().toISOString(); + + return HttpResponse.json({ + code: 0, + msg: 'success', + data: kb, + }); + }), + // GET /app-api/muse/knowledge-bases/:kbId/documents — 获取知识库下的资料列表 http.get('/app-api/muse/knowledge-bases/:kbId/documents', ({ params }) => { const { kbId } = params; diff --git a/muse-studio/src/features/knowledge/hooks/useKnowledge.test.tsx b/muse-studio/src/features/knowledge/hooks/useKnowledge.test.tsx index 33673a11..13942cdd 100644 --- a/muse-studio/src/features/knowledge/hooks/useKnowledge.test.tsx +++ b/muse-studio/src/features/knowledge/hooks/useKnowledge.test.tsx @@ -6,6 +6,8 @@ import { useKnowledgeCreate, useKnowledgeDelete, useInstalledKnowledgeUninstall, + useKnowledgeDisable, + useKnowledgeRestore, useDocumentList, useDocumentUpload, type DocumentVO, @@ -172,4 +174,38 @@ describe('useKnowledge API Hooks 单元测试', () => { unmountActive(); }); + + it('useKnowledgeDisable & useKnowledgeRestore 应切换知识库启用状态', async () => { + // 拿一个 active 的自建 KB。 + const { result: listHook, unmount: unmountList } = renderHook(() => useKnowledgeList('user'), { wrapper }); + await waitFor(() => expect(listHook.current.isSuccess).toBe(true)); + const target = (listHook.current.data?.list || []).filter(isKnowledgeBaseSummary).find((k) => k.status === 'active'); + const kbId = target?.kbId ?? ''; + expect(kbId).toBeTruthy(); + unmountList(); + + // 停用 → status=disabled。 + const { result: disableHook, unmount: unmountDisable } = renderHook(() => useKnowledgeDisable(), { wrapper }); + disableHook.current.mutate(kbId); + await waitFor(() => expect(disableHook.current.isSuccess).toBe(true)); + unmountDisable(); + + const { result: afterDisable, unmount: u1 } = renderHook(() => useKnowledgeList('user'), { wrapper }); + await waitFor(() => expect(afterDisable.current.isSuccess).toBe(true)); + const disabledKb = (afterDisable.current.data?.list || []).filter(isKnowledgeBaseSummary).find((k) => k.kbId === kbId); + expect(disabledKb?.status).toBe('disabled'); + u1(); + + // 恢复 → status=active。 + const { result: restoreHook, unmount: unmountRestore } = renderHook(() => useKnowledgeRestore(), { wrapper }); + restoreHook.current.mutate(kbId); + await waitFor(() => expect(restoreHook.current.isSuccess).toBe(true)); + unmountRestore(); + + const { result: afterRestore, unmount: u2 } = renderHook(() => useKnowledgeList('user'), { wrapper }); + await waitFor(() => expect(afterRestore.current.isSuccess).toBe(true)); + const restoredKb = (afterRestore.current.data?.list || []).filter(isKnowledgeBaseSummary).find((k) => k.kbId === kbId); + expect(restoredKb?.status).toBe('active'); + u2(); + }); }); diff --git a/muse-studio/src/features/knowledge/hooks/useKnowledge.ts b/muse-studio/src/features/knowledge/hooks/useKnowledge.ts index f620462d..0a2d66ea 100644 --- a/muse-studio/src/features/knowledge/hooks/useKnowledge.ts +++ b/muse-studio/src/features/knowledge/hooks/useKnowledge.ts @@ -328,6 +328,45 @@ export function useInstalledKnowledgeUninstall() { }); } +/** + * 停用用户自有知识库的 Mutation Hook。 + * 对应 POST /knowledge-bases/{kbId}/disable;停用后 status→disabled,但资产保留(可恢复,区别于物理删除)。 + * + * @returns React Query 停用变动状态 + */ +export function useKnowledgeDisable() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (kbId: string) => api.post(`/knowledge-bases/${kbId}/disable`, { + commandId: createCommandId(), + impactConfirmed: true, + }), + onSuccess: (_, kbId) => { + queryClient.invalidateQueries({ queryKey: ['knowledgeBases'] }); + queryClient.invalidateQueries({ queryKey: ['knowledgeDetail', kbId] }); + }, + }); +} + +/** + * 恢复已停用知识库的 Mutation Hook。 + * 对应 POST /knowledge-bases/{kbId}/restore;恢复后 status→active。 + * + * @returns React Query 恢复变动状态 + */ +export function useKnowledgeRestore() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (kbId: string) => api.post(`/knowledge-bases/${kbId}/restore`, { + commandId: createCommandId(), + }), + onSuccess: (_, kbId) => { + queryClient.invalidateQueries({ queryKey: ['knowledgeBases'] }); + queryClient.invalidateQueries({ queryKey: ['knowledgeDetail', kbId] }); + }, + }); +} + /** * 获取指定知识库下资料文件列表 of Query Hook * 支持根据活性状态进行 refetchInterval 动态轮询 diff --git a/muse-studio/src/pages/KnowledgePage.tsx b/muse-studio/src/pages/KnowledgePage.tsx index b860e1af..f3dcd2b1 100644 --- a/muse-studio/src/pages/KnowledgePage.tsx +++ b/muse-studio/src/pages/KnowledgePage.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom'; import KnowledgeDraftPanel from '@/features/knowledge/components/KnowledgeDraftPanel'; import KnowledgeGraphPanel from '@/features/knowledge/components/KnowledgeGraphPanel'; import KnowledgeBindingsPanel from '@/features/knowledge/components/KnowledgeBindingsPanel'; -import { useKnowledgeList, useKnowledgeDelete, useInstalledKnowledgeUninstall } from '@/features/knowledge/hooks/useKnowledge'; +import { useKnowledgeList, useKnowledgeDelete, useInstalledKnowledgeUninstall, useKnowledgeDisable, useKnowledgeRestore } from '@/features/knowledge/hooks/useKnowledge'; import type { KnowledgeBaseVO } from '@/features/knowledge/hooks/useKnowledge'; import CreateKBModal from '@/features/knowledge/components/CreateKBModal'; import KnowledgePublishModal from '@/features/knowledge/components/KnowledgePublishModal'; @@ -27,6 +27,8 @@ export default function KnowledgePage() { const { data, isLoading, isError } = useKnowledgeList(activeTab); const { mutate: deleteKB } = useKnowledgeDelete(); const { mutate: uninstallKB } = useInstalledKnowledgeUninstall(); + const { mutate: disableKB } = useKnowledgeDisable(); + const { mutate: restoreKB } = useKnowledgeRestore(); // 2. 严格的运行期类型收窄与防空校验 const rawList = data?.list || []; @@ -55,6 +57,19 @@ export default function KnowledgePage() { } }; + // 停用:软关闭知识库(资产保留可恢复,区别于物理删除);恢复:重新启用。均阻止冒泡防误触跳转资料面板。 + const handleDisableKB = (e: React.MouseEvent, kbId: string, name: string) => { + e.stopPropagation(); + if (window.confirm(`确认停用知识库《${name}》吗?\n停用后该库在生成中不可用,但资产保留,可随时恢复。`)) { + disableKB(kbId); + } + }; + + const handleRestoreKB = (e: React.MouseEvent, kbId: string) => { + e.stopPropagation(); + restoreKB(kbId); + }; + // 3. 拦截状态:如果选中了具体知识库,渲染资料文件管理器 (Detail/Materials Panel) if (selectedKbId) { const activeKb = knowledgeBases.find((kb) => kb.kbId === selectedKbId); @@ -219,6 +234,28 @@ export default function KnowledgePage() { > 发布 + {/* 停用/恢复:按 status 切换(active 显示停用,否则显示恢复);软状态管理,区别于物理删除。 */} + {kb.status === 'active' ? ( + + ) : ( + + )}