feat(studio): 知识库停用/恢复 UI + e2e 真跑
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) <noreply@anthropic.com>
This commit is contained in:
parent
cc57c595d8
commit
494f3b5fd6
@ -230,7 +230,16 @@ async function globalSetup(): Promise<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
48
muse-studio/e2e/knowledge-disable-restore.spec.ts
Normal file
48
muse-studio/e2e/knowledge-disable-restore.spec.ts
Normal file
@ -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<void> {
|
||||
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 });
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<unknown>(`/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<unknown>(`/knowledge-bases/${kbId}/restore`, {
|
||||
commandId: createCommandId(),
|
||||
}),
|
||||
onSuccess: (_, kbId) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledgeBases'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledgeDetail', kbId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定知识库下资料文件列表 of Query Hook
|
||||
* 支持根据活性状态进行 refetchInterval 动态轮询
|
||||
|
||||
@ -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() {
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
{/* 停用/恢复:按 status 切换(active 显示停用,否则显示恢复);软状态管理,区别于物理删除。 */}
|
||||
{kb.status === 'active' ? (
|
||||
<button
|
||||
onClick={(e) => handleDisableKB(e, kb.kbId, kb.name)}
|
||||
type="button"
|
||||
title="停用知识库"
|
||||
aria-label={`停用知识库 ${kb.name}`}
|
||||
className="rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-amber-600 transition-all hover:bg-amber-50 hover:text-amber-800"
|
||||
>
|
||||
停用
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => handleRestoreKB(e, kb.kbId)}
|
||||
type="button"
|
||||
title="恢复知识库"
|
||||
aria-label={`恢复知识库 ${kb.name}`}
|
||||
className="rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 transition-all hover:bg-emerald-50 hover:text-emerald-800"
|
||||
>
|
||||
恢复
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleDeleteKB(e, kb.kbId, kb.name)}
|
||||
type="button"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user