test(admin): 收紧 Muse 管理端验证门禁

补齐 P3 E2E 未处理接口断言和契约字段校验,修正管理端请求前缀,并记录验证产物清理规则。
This commit is contained in:
zizi 2026-05-25 12:31:28 +08:00
parent a33560a03b
commit c35f64ffcc
11 changed files with 878 additions and 133 deletions

BIN
.DS_Store vendored

Binary file not shown.

5
.gitignore vendored
View File

@ -7,7 +7,6 @@ ehthumbs.db
*.swp
*.swo
*~
.DS_Store
# ====================
# Editor & IDE
@ -77,7 +76,6 @@ ENV/
npm-debug.log*
yarn-debug.log*
lerna-debug.log*
.DS_Store
# ====================
# Testing & Coverage
@ -86,6 +84,8 @@ coverage/
.nyc_output/
.coverage
.coverage.*
test-results/
playwright-report/
# ====================
# Sub-repos (独立 git 仓库,不纳入本仓)
@ -160,3 +160,4 @@ vite.config.ts.*
.agents
.cursor
skills-lock.json
precommit-scan-*.md

View File

@ -84,6 +84,20 @@
- `echarts` 插件去除重复类型导出并显式导出 `ECOption`
- `core-user` store 在 `setUserInfo(null)` 时同步清空 `userRoles`,避免登出后旧角色残留。
二次完善 E2E 覆盖时发现部分 Muse 页面在真实 Vite 运行时不会自动解析本地未注册的 Ant Design Vue 组件。已在 P3 页面内显式导入实际使用的 Ant 组件,并同步补齐页面测试中的 Ant mock export
- `muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue`
- `muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue`
- `muse-admin/apps/web-antd/src/views/muse/market/index.vue`
- `muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts`
- `muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts`
审查后继续收紧 E2E 时,发现 `museAdminApi` 原先把 `/admin-api/muse` 直接传给 Vben `requestClient`。但当前 `web-antd``VITE_GLOB_API_URL` 已经是 `/admin-api`,真实浏览器请求会变成 `/admin-api/admin-api/muse/**`。已修正为:
- `museAdminApi` 内部只拼接 `/muse/**` 资源路径。
- 由 Vben `requestClient` baseURL 统一组合成运行时 `/admin-api/muse/**`
- `apps/web-antd/src/api/muse/__tests__/client.spec.ts` 同步断言资源路径,避免再次双拼 `/admin-api`
后续需要主代理确认长期 Git 边界:要么让 `muse-admin` 成为真正独立仓库,要么调整根 `.gitignore` 和纳管策略。
### 2. 不伪造后端契约
@ -131,13 +145,55 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
- OpenAPI TypeScript 生成通过。
- P3 定向 Vitest 初次实现10 个测试文件、19 个测试全部通过。
- P3 完善后定向 Vitest13 个测试文件、26 个测试全部通过。
- P3 Playwright E2E1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
- P3 Playwright E2E 初版1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
- P3 Playwright E2E 二次完善2 个 Chromium 用例通过,覆盖 MetaSchema、功能编排、AI 配置、全局知识库、市场治理、Account/New-API、任务治理、日志审计全部主要页面的真实路由访问和边界文案。
- P3 定向 oxlint35 个文件0 warning、0 error。
- `web-antd` typecheck 通过。
- 全量 `test:unit` 通过48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
- `web-antd` build 通过。
- `git diff --check` 通过。
二次完善提交前复核:
```bash
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
pnpm --dir muse-admin test:unit
pnpm --dir muse-admin --filter @vben/web-antd run build
```
结果:
- `web-antd` typecheck 退出码 0。
- P3 Playwright E2E2 个 Chromium 用例通过Vite dev server 仍输出 `ResizeObserver loop completed with undelivered notifications` 开发噪声,但 Playwright 退出码为 0。
- P3 定向 Vitest13 个测试文件、26 个测试通过。
- P3 定向 oxlint35 个文件0 warning、0 error。
- 全量 `test:unit`48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
- `web-antd` build 退出码 0并已清理生成的 `dist/``dist.zip`,不纳入提交。
审查修复后再次复核:
```bash
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
pnpm --dir muse-admin test:unit
pnpm --dir muse-admin --filter @vben/web-antd run build
```
结果:
- P3 Playwright E2E2 个 Chromium 用例通过E2E 已改为未显式 mock 的 `/admin-api/muse/**` 请求返回 500 并在用例末尾断言无遗漏,同时断言每个主要页面的真实 mock 数据。
- 审查后删除了 E2E 中会绕开统一 mock 的重复 specific routes所有 Muse 管理端请求都由同一个 `/admin-api/muse/**` route 处理Account、Purchase、Jobs、Source Event mock 字段已改为契约字段,并补充关键列断言,避免字段漂移仍然绿。
- `web-antd` typecheck 退出码 0。
- P3 定向 Vitest13 个测试文件、26 个测试通过。
- P3 定向 oxlint35 个文件0 warning、0 error。
- 全量 `test:unit`48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
- `web-antd` build 退出码 0并已清理生成的 `test-results/``dist/``dist.zip`,不纳入提交。
## 提交与 Hook 说明
本次 P3 相关提交:
@ -153,9 +209,20 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
- Muse 路由注册测试。注意测试文件不能放在 `src/router/routes/modules/**`,该目录会被 Vite 动态路由 glob 当作业务路由加载。
- Account/New-API、任务治理、审计页面的最小集成测试。
- Playwright E2E使用本地 mock 覆盖 Vben 登录态、权限菜单、租户、字典、通知和 P3 API。
- 二次扩展 Playwright E2E将 MetaSchema、功能编排、AI 配置、全局知识库、市场治理也纳入真实路由访问,并检查管理员边界文案、影响预览、原因和审计提示。
- 修复 E2E 暴露的 Ant Design Vue 组件运行时解析问题Muse 页面改为显式导入实际使用的 Ant 组件。
- 审查修复后E2E 对未处理的 Muse 管理端 API 不再返回空分页兜底,避免错误 URL 或漏 mock 仍然绿;同时修正 New-API 真实运行时双 `/admin-api` 前缀问题。
- 再次审查后,删除 Account/Jobs/Audit 的重复 route 注册,避免 specific route 抢先匹配导致 unified mock 失效;同步修正 E2E mock 字段契约漂移。
提交前发现暂存区已有其他会话 staged 的 `P1-execution-prompt.md``P3-execution-prompt.md`,已仅执行 `git restore --staged`,没有改动文件内容。
整理提交阶段补充:
- `muse-admin/apps/web-antd/dist/``dist.zip``test-results/` 属于验证产物,提交前必须清理。
- 暂存区如果混入 `docs/memorys/2026-05-25-muse-cloud后端P1收口.md`,只执行 `git restore --staged` 移出 P3 提交,不删除并行 P1 会话文件。
- 根目录 `.DS_Store` 曾被仓库跟踪;本轮改为从 Git 索引移除,并保留本地文件由 `.gitignore` 忽略,避免继续进入提交。
- 根 `.gitignore` 保留 `muse-admin/` 独立子仓库边界,同时补充 `test-results/``playwright-report/``precommit-scan-*.md`,避免验证报告和安全扫描报告误入主提交。
提交时按当前项目约定使用:
```bash
@ -168,4 +235,4 @@ touch /tmp/.opsera-pre-commit-scan-passed
1. 主代理确认 `muse-admin/` 的长期 Git 纳管方式,避免后续继续依赖 `git add -f`
2. 后端补齐 Account 角色/权限写接口、任务独立重验接口后,管理端可接入真实命令入口。
3. P3 已补齐最小 E2E后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看。
3. P3 已补齐覆盖全部主要页面的最小 E2E后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看等具体命令流

View File

@ -9,6 +9,23 @@ const commonResult = (data: unknown) => ({
const ACCESS_STORE_KEY = 'muse-vben-antd-5.7.0-dev-core-access';
const pageResult = (list: unknown[]) => ({
list,
total: list.length,
});
const unhandledMuseAdminApiPathsByPage = new WeakMap<Page, Set<string>>();
const expectNoUnhandledMuseAdminApis = (page: Page) => {
const unhandledPaths = [
...(unhandledMuseAdminApiPathsByPage.get(page) ?? new Set<string>()),
];
expect(
unhandledPaths,
`Unhandled Muse admin API mocks: ${unhandledPaths.join(', ')}`,
).toEqual([]);
};
const routeCommonResult = async (
page: Page,
patterns: string[],
@ -24,6 +41,450 @@ const routeCommonResult = async (
}
};
const routeMuseAdminApis = async (page: Page) => {
const unhandledMuseAdminApiPaths = new Set<string>();
unhandledMuseAdminApiPathsByPage.set(page, unhandledMuseAdminApiPaths);
await page.route('**/admin-api/muse/**', (route) => {
const { pathname } = new URL(route.request().url());
const path = pathname.replace('/admin-api/muse', '');
if (path === '/governance/meta-schemas') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activeVersion: 1,
affectedObjectCount: 3,
displayName: '故事草稿结构',
draftVersion: 2,
fieldType: 'object',
migrationRisk: 'low',
schemaKey: 'story.draft',
scope: 'work',
status: 'active',
updatedAt: '2026-05-25T00:00:00Z',
},
]),
),
});
}
if (path === '/governance/function-chains') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activeVersion: 1,
chainKey: 'story-pipeline',
description: '系统故事生成链路',
displayName: '故事链路',
openSlotCount: 2,
protectionNodeCount: 3,
status: 'active',
updatedAt: '2026-05-25T00:00:00Z',
},
]),
),
});
}
if (path === '/governance/protection-nodes') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
auditSummary: '系统保护节点',
displayName: '事实校验',
irreplaceable: true,
irreplaceableReason: '保持事实边界',
nodeKey: 'fact-guard',
nodeType: 'guard',
},
]),
),
});
}
if (path === '/ai/prompts') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activeVersion: 1,
latestVersion: 2,
name: '候选生成 Prompt',
promptKey: 'candidate.generate',
updatedAt: '2026-05-25T00:00:00Z',
variableCount: 4,
},
]),
),
});
}
if (path === '/ai/agents') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activeVersion: 1,
agentId: 'system-story-agent',
description: '系统写作智能体',
name: '系统故事智能体',
promptKey: 'candidate.generate',
scope: 'system',
status: 'active',
},
{
activeVersion: 3,
agentId: 'user-private-agent',
name: '用户私有智能体摘要',
promptKey: 'user.prompt',
scope: 'user',
status: 'active',
},
]),
),
});
}
if (path === '/ai/tool-grants') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
agentId: 'system-story-agent',
agentName: '系统故事智能体',
approvedBy: 'Security facade',
budget: { amount: 1000, period: 'daily' },
grantId: 'grant-e2e',
outboundPolicy: {
allowedDestinations: ['search.internal'],
dataPolicy: 'redacted',
networkMode: 'allowlist',
},
securityApprovalId: 'sec-approval-e2e',
status: 'approved',
toolType: 'web_search',
},
]),
),
});
}
if (path === '/ai/quality-policies') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activeVersion: 1,
description: '候选交付质量门控',
dimensionCount: 3,
latestVersion: 2,
name: '候选交付策略',
policyKey: 'candidate.delivery',
updatedAt: '2026-05-25T00:00:00Z',
},
]),
),
});
}
if (path === '/knowledge/global-kbs') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
activePolicyVersion: 1,
bindingCount: 2,
currentVersion: 4,
documentCount: 8,
kbId: 'global-kb-e2e',
name: '系统世界观库',
status: 'active',
},
]),
),
});
}
if (path.includes('/knowledge/global-kbs/') && path.endsWith('/documents')) {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
documentId: 'doc-e2e',
name: '系统设定资料',
processingStatus: 'searchable',
},
]),
),
});
}
if (
path.includes('/knowledge/global-kbs/') &&
path.endsWith('/processing-tasks')
) {
return route.fulfill({
contentType: 'application/json',
json: commonResult(pageResult([{ status: 'completed', taskId: 'kb-task-e2e' }])),
});
}
if (path === '/knowledge/source-bindings') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
bindingId: 'kb-binding-e2e',
kbId: 'global-kb-e2e',
sourceStatus: 'active',
sourceSummary: '系统资产来源',
},
]),
),
});
}
if (path === '/market/publish-requests') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
assetName: '待审资产',
publisherName: '发布者',
requestId: 'review-e2e',
riskTags: ['版权'],
status: 'pending',
},
]),
),
});
}
if (path === '/market/assets') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
assetId: 'asset-e2e',
name: '市场资产',
listingStatus: 'listed',
reviewStatus: 'approved',
},
]),
),
});
}
if (path === '/market/appeals') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
appealId: 'appeal-e2e',
assetId: 'asset-e2e',
appealType: 'delist',
appellantName: '申诉人',
assetName: '市场资产',
processorId: 'admin-e2e',
reason: '申诉材料',
status: 'pending',
},
]),
),
});
}
if (path === '/account/users') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
status: 'active',
entitlementSource: 'plan',
nickname: 'E2E 用户',
newApiBindingStatus: 'bound',
quotaStatus: 'normal',
riskFlags: ['manual-review'],
userId: 'user-e2e',
},
]),
),
});
}
if (path === '/account/usage-records') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
attributionStatus: 'attributed',
nickname: 'E2E 用户',
period: '2026-05',
recordId: 'usage-e2e',
totalTokens: 42,
userId: 'user-e2e',
},
]),
),
});
}
if (path === '/account/purchase-records') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
amount: 0,
assetName: '市场资产',
assetType: 'agent',
createdAt: '2026-05-25T00:00:00Z',
purchaseType: 'admin_grant',
recordId: 'purchase-e2e',
status: 'completed',
userId: 'user-e2e',
},
]),
),
});
}
if (path === '/account/new-api-bindings') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
bindingStatus: 'bound',
externalUserId: 'newapi-user-e2e',
syncStatus: 'synced',
userId: 'user-e2e',
},
]),
),
});
}
if (path.includes('/account/users/') && path.endsWith('/balance-snapshots')) {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([{ balance: 100, snapshotId: 'balance-e2e', userId: 'user-e2e' }]),
),
});
}
if (path === '/jobs') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
createdAt: '2026-05-25T00:00:00Z',
createdBy: 'system',
jobId: 1001,
owner: 'market',
retryCount: 1,
status: 'failed',
type: 'source-projection',
},
]),
),
});
}
if (path === '/source-events') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
eventId: 2001,
actionPolicy: 'needs_recheck',
affectedScope: 'asset-e2e',
createdAt: '2026-05-25T00:00:00Z',
sourceId: 1,
sourceStatus: 'revoked',
sourceType: 'market_asset',
},
]),
),
});
}
if (path === '/audit/api-logs') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
actor: 'admin',
createdAt: '2026-05-25T00:00:00Z',
durationMs: 12,
logId: 1,
method: 'GET',
module: 'audit',
pathSummary: '/admin-api/muse/audit/api-logs',
requestId: 'req-e2e',
status: 'success',
statusCode: 200,
},
]),
),
});
}
if (path === '/audit/business-events') {
return route.fulfill({
contentType: 'application/json',
json: commonResult(
pageResult([
{
createdAt: '2026-05-25T00:00:00Z',
eventId: 1,
eventType: 'market.delist',
operator: 'admin',
resourceId: 'asset-e2e',
resourceType: 'market_asset',
summary: '市场资产下架',
},
]),
),
});
}
unhandledMuseAdminApiPaths.add(path);
return route.fulfill({
contentType: 'application/json',
json: {
code: 500,
data: null,
msg: `Unhandled Muse admin E2E mock: ${path}`,
},
status: 500,
});
});
};
test.beforeEach(async ({ page }) => {
await page.addInitScript((accessStoreKey) => {
localStorage.setItem(
@ -50,34 +511,94 @@ test.beforeEach(async ({ page }) => {
menus: [
{
children: [
{
component: 'muse/governance/meta-schema/index',
componentName: 'MuseMetaSchemaList',
id: 101,
name: '元结构定义',
parentId: 100,
path: 'governance/meta-schemas',
sort: 10,
visible: true,
},
{
component: 'muse/governance/function-orch/index',
componentName: 'MuseFunctionOrchestration',
id: 102,
name: '功能编排',
parentId: 100,
path: 'governance/function-orchestration',
sort: 20,
visible: true,
},
{
component: 'muse/ai/index',
componentName: 'MuseAiConfig',
id: 103,
name: 'AI 配置',
parentId: 100,
path: 'ai',
sort: 30,
visible: true,
},
{
component: 'muse/knowledge/index',
componentName: 'MuseGlobalKnowledge',
id: 104,
name: '全局知识库',
parentId: 100,
path: 'knowledge',
sort: 40,
visible: true,
},
{
component: 'muse/market/index',
componentName: 'MuseMarketGovernance',
id: 105,
name: '市场治理',
parentId: 100,
path: 'market',
sort: 50,
visible: true,
},
{
component: 'muse/account/index',
componentName: 'MuseAccountGovernance',
id: 101,
id: 106,
name: '用户与权限',
parentId: 100,
path: 'account',
sort: 10,
sort: 60,
visible: true,
},
{
component: 'muse/newapi/index',
componentName: 'MuseNewApiUsage',
id: 107,
name: 'New-API 与用量',
parentId: 100,
path: 'new-api',
sort: 70,
visible: true,
},
{
component: 'muse/jobs/index',
componentName: 'MuseTaskOps',
id: 102,
id: 108,
name: '任务治理',
parentId: 100,
path: 'jobs',
sort: 20,
sort: 80,
visible: true,
},
{
component: 'muse/audit/index',
componentName: 'MuseAuditLog',
id: 103,
id: 109,
name: '日志与审计',
parentId: 100,
path: 'audit',
sort: 30,
sort: 90,
visible: true,
},
],
@ -133,110 +654,7 @@ test.beforeEach(async ({ page }) => {
0,
);
await page.route('**/admin-api/muse/account/users**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [
{
accountStatus: 'active',
entitlementSource: 'plan',
nickname: 'E2E 用户',
quotaStatus: 'normal',
userId: 'user-e2e',
},
],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/account/usage-records**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [{ recordId: 'usage-e2e', totalTokens: 42, userId: 'user-e2e' }],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/account/purchase-records**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [{ orderId: 'order-e2e', status: 'paid', userId: 'user-e2e' }],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/jobs**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [
{
jobId: 'job-e2e',
jobType: 'source-projection',
owner: 'market',
status: 'failed',
},
],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/source-events**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [
{
eventId: 'event-e2e',
objectSummary: 'asset-e2e',
sourceStatus: 'revoked',
},
],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/audit/api-logs**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [
{
actor: 'admin',
durationMs: 12,
logId: 1,
method: 'GET',
pathSummary: '/admin-api/muse/audit/api-logs',
requestId: 'req-e2e',
status: 'success',
statusCode: 200,
},
],
total: 1,
}),
}),
);
await page.route('**/admin-api/muse/audit/business-events**', (route) =>
route.fulfill({
contentType: 'application/json',
json: commonResult({
list: [
{
eventId: 1,
eventType: 'market.delist',
objectSummary: 'asset-e2e',
operator: 'admin',
},
],
total: 1,
}),
}),
);
await routeMuseAdminApis(page);
});
test('管理员可进入账号、任务和审计治理页面并看到边界文案', async ({
@ -246,13 +664,73 @@ test('管理员可进入账号、任务和审计治理页面并看到边界文
await expect(page.getByText('用户、角色与权限治理')).toBeVisible();
await expect(page.getByText('不提供用户侧完整个人中心或私有正文入口')).toBeVisible();
await expect(page.getByText('E2E 用户')).toBeVisible();
await expect(page.getByText('manual-review')).toBeVisible();
await page.getByRole('tab', { name: '用量与购买摘要' }).click();
await expect(page.getByText('usage-e2e')).toBeVisible();
await expect(page.getByText('purchase-e2e')).toBeVisible();
await expect(page.getByText('2026-05-25T00:00:00Z')).toBeVisible();
await page.goto('/muse/jobs');
await expect(page.getByText('管理员不能代用户重新生成写作候选')).toBeVisible();
await expect(page.getByText('job-e2e')).toBeVisible();
await expect(page.getByText('1001')).toBeVisible();
await expect(page.getByText('source-projection')).toBeVisible();
await page.getByRole('tab', { name: '来源事件' }).click();
await expect(page.getByText('2001')).toBeVisible();
await expect(page.getByText('market_asset')).toBeVisible();
await expect(page.getByText('needs_recheck')).toBeVisible();
await page.goto('/muse/audit');
await expect(page.getByRole('tab', { name: '接口调用日志' })).toBeVisible();
await expect(page.getByRole('tab', { name: '业务审计日志' })).toBeVisible();
await expect(page.getByText('req-e2e')).toBeVisible();
await page.getByRole('tab', { name: '业务审计日志' }).click();
await expect(page.getByText('市场资产下架')).toBeVisible();
expectNoUnhandledMuseAdminApis(page);
});
test('管理员可进入核心治理配置页面并看到系统边界', async ({ page }) => {
await page.goto('/muse/governance/meta-schemas');
await expect(page.getByText('元结构定义').first()).toBeVisible();
await expect(page.getByText('story.draft')).toBeVisible();
await page.goto('/muse/governance/function-orchestration');
await expect(
page.getByText('开放槽位只表示可替换的非保护节点').first(),
).toBeVisible();
await expect(page.getByText('故事链路')).toBeVisible();
await page.getByRole('button', { name: '保护节点' }).first().click();
await expect(page.getByText('事实校验')).toBeVisible();
await page.goto('/muse/ai');
await expect(page.getByText('AI 配置域治理边界')).toBeVisible();
await expect(page.getByText('候选生成 Prompt')).toBeVisible();
await page.getByRole('tab', { name: '系统智能体' }).click();
await expect(page.getByText('用户私有 Agent 边界')).toBeVisible();
await expect(page.getByText('系统故事智能体')).toBeVisible();
await page.goto('/muse/knowledge');
await expect(page.getByRole('heading', { name: '全局知识管理' })).toBeVisible();
await expect(page.getByText('不读取用户私有正文')).toBeVisible();
await expect(page.getByText('系统世界观库').first()).toBeVisible();
await page.goto('/muse/market');
await expect(page.getByRole('heading', { name: '市场治理' })).toBeVisible();
await expect(page.getByText('待审资产').first()).toBeVisible();
await page.getByRole('tab', { name: '资产治理' }).click();
await expect(page.getByText('不改用户私有副本').first()).toBeVisible();
await expect(page.getByText('市场资产').first()).toBeVisible();
await page.getByRole('tab', { name: '申诉处理' }).click();
await expect(page.getByText('申诉人').first()).toBeVisible();
await page.goto('/muse/new-api');
await expect(
page.getByText('New-API 页面只展示治理和对账摘要'),
).toBeVisible();
await expect(page.getByText('不展示凭据')).toBeVisible();
await expect(page.getByRole('cell', { name: 'user-e2e' })).toBeVisible();
await page.getByRole('tab', { name: '调用归属' }).click();
await expect(page.getByText('usage-e2e')).toBeVisible();
expectNoUnhandledMuseAdminApis(page);
});

View File

@ -21,11 +21,11 @@ describe('muse admin api client', () => {
vi.clearAllMocks();
});
it('为所有管理端 GET 请求追加 Muse 前缀和版本头', async () => {
it('为所有管理端 GET 请求追加 Muse 资源前缀和版本头', async () => {
await museAdminApi.get('/audit/api-logs', { pageNo: 1, pageSize: 20 });
expect(requestClientMock.get).toHaveBeenCalledWith(
'/admin-api/muse/audit/api-logs',
'/muse/audit/api-logs',
{
headers: { 'X-API-Version': '1' },
params: { pageNo: 1, pageSize: 20 },
@ -41,12 +41,12 @@ describe('muse admin api client', () => {
await museAdminApi.delete('/knowledge/global-kbs/kb-1/documents/doc-1');
expect(requestClientMock.post).toHaveBeenCalledWith(
'/admin-api/muse/jobs/1/retry',
'/muse/jobs/1/retry',
body,
{ headers: { 'X-API-Version': '1' } },
);
expect(requestClientMock.request).toHaveBeenCalledWith(
'/admin-api/muse/knowledge/global-kbs/kb-1',
'/muse/knowledge/global-kbs/kb-1',
{
data: body,
headers: { 'X-API-Version': '1' },
@ -54,7 +54,7 @@ describe('muse admin api client', () => {
},
);
expect(requestClientMock.delete).toHaveBeenCalledWith(
'/admin-api/muse/knowledge/global-kbs/kb-1/documents/doc-1',
'/muse/knowledge/global-kbs/kb-1/documents/doc-1',
{
headers: { 'X-API-Version': '1' },
params: undefined,

View File

@ -4,9 +4,11 @@ import { requestClient } from '#/api/request';
* Muse API
*
* `/admin-api/muse/**`
* Vben requestClient baseURL `/admin-api` `/muse/**`
* `/admin-api/muse/**` `/admin-api/admin-api/muse/**`
* Vben requestClient `code/data/msg` data
*/
const MUSE_ADMIN_BASE_URL = '/admin-api/muse';
const MUSE_ADMIN_RESOURCE_PREFIX = '/muse';
/** Muse API v1 强制版本头,所有管理端请求都必须显式携带。 */
const MUSE_API_HEADERS = {
@ -21,7 +23,7 @@ type MuseRequestBody = unknown;
/** 拼接 Muse 管理端路径,确保调用方不能越过 `/admin-api/muse` 边界。 */
function toMuseUrl(url: string) {
return `${MUSE_ADMIN_BASE_URL}${url}`;
return `${MUSE_ADMIN_RESOURCE_PREFIX}${url}`;
}
export const museAdminApi = {

View File

@ -17,7 +17,27 @@ import { computed, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message, Modal } from 'ant-design-vue';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Descriptions as ADescriptions,
DescriptionsItem as ADescriptionsItem,
Drawer as ADrawer,
Empty as AEmpty,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
InputNumber as AInputNumber,
List as AList,
ListItem as AListItem,
Space as ASpace,
Table as ATable,
Tag as ATag,
Textarea as ATextarea,
message,
Modal,
} from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {

View File

@ -1,8 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import { createApp, nextTick } from 'vue';
import { createApp, defineComponent, h, nextTick } from 'vue';
import KnowledgeIndex from '../index.vue';
function makeStub(name: string) {
return defineComponent({
name,
props: ['dataSource', 'description', 'message', 'title', 'value'],
setup(props, { slots }) {
return () =>
h('section', { 'data-stub': name }, [
props.title,
props.message,
props.description,
props.value,
Array.isArray(props.dataSource)
? JSON.stringify(props.dataSource)
: '',
slots.default?.(),
slots.extra?.(),
slots.bodyCell?.({
column: { dataIndex: 'status', key: 'status' },
record: {},
}),
]);
},
});
}
vi.mock('@vben/common-ui', () => ({
Page: {
name: 'Page',
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
}));
vi.mock('ant-design-vue', () => ({
Alert: makeStub('Alert'),
Button: makeStub('Button'),
Card: makeStub('Card'),
Col: makeStub('Col'),
Descriptions: makeStub('Descriptions'),
DescriptionsItem: makeStub('DescriptionsItem'),
Form: makeStub('Form'),
FormItem: makeStub('FormItem'),
Input: makeStub('Input'),
Modal: makeStub('Modal'),
Row: makeStub('Row'),
Select: makeStub('Select'),
SelectOption: makeStub('SelectOption'),
Space: makeStub('Space'),
Statistic: makeStub('Statistic'),
Table: makeStub('Table'),
TabPane: makeStub('TabPane'),
Tabs: makeStub('Tabs'),
Tag: makeStub('Tag'),
Textarea: makeStub('Textarea'),
TypographyParagraph: makeStub('TypographyParagraph'),
message: {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
},
}));

View File

@ -17,7 +17,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Col as ACol,
Descriptions as ADescriptions,
DescriptionsItem as ADescriptionsItem,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Modal as AModal,
Row as ARow,
Select as ASelect,
SelectOption as ASelectOption,
Space as ASpace,
Statistic as AStatistic,
Table as ATable,
TabPane as ATabPane,
Tabs as ATabs,
Tag as ATag,
Textarea as ATextarea,
TypographyParagraph as ATypographyParagraph,
message,
} from 'ant-design-vue';
import {
disableGlobalKnowledgeBaseApi,

View File

@ -1,8 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import { createApp, nextTick } from 'vue';
import { createApp, defineComponent, h, nextTick } from 'vue';
import MarketIndex from '../index.vue';
function makeStub(name: string) {
return defineComponent({
name,
props: ['dataSource', 'description', 'message', 'title', 'value'],
setup(props, { slots }) {
return () =>
h('section', { 'data-stub': name }, [
props.title,
props.message,
props.description,
props.value,
Array.isArray(props.dataSource)
? JSON.stringify(props.dataSource)
: '',
slots.default?.(),
slots.extra?.(),
slots.bodyCell?.({
column: { dataIndex: 'status', key: 'status' },
record: {},
}),
]);
},
});
}
vi.mock('@vben/common-ui', () => ({
Page: {
name: 'Page',
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
}));
vi.mock('ant-design-vue', () => ({
Alert: makeStub('Alert'),
Button: makeStub('Button'),
Card: makeStub('Card'),
Col: makeStub('Col'),
Descriptions: makeStub('Descriptions'),
DescriptionsItem: makeStub('DescriptionsItem'),
Form: makeStub('Form'),
FormItem: makeStub('FormItem'),
Input: makeStub('Input'),
Modal: makeStub('Modal'),
Row: makeStub('Row'),
Select: makeStub('Select'),
SelectOption: makeStub('SelectOption'),
Space: makeStub('Space'),
Statistic: makeStub('Statistic'),
Table: makeStub('Table'),
TabPane: makeStub('TabPane'),
Tabs: makeStub('Tabs'),
Tag: makeStub('Tag'),
Textarea: makeStub('Textarea'),
TypographyParagraph: makeStub('TypographyParagraph'),
message: {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
},
}));

View File

@ -21,7 +21,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Col as ACol,
Descriptions as ADescriptions,
DescriptionsItem as ADescriptionsItem,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Modal as AModal,
Row as ARow,
Select as ASelect,
SelectOption as ASelectOption,
Space as ASpace,
Statistic as AStatistic,
Table as ATable,
TabPane as ATabPane,
Tabs as ATabs,
Tag as ATag,
Textarea as ATextarea,
TypographyParagraph as ATypographyParagraph,
message,
} from 'ant-design-vue';
import {
approveMarketPublishRequestApi,
@ -470,13 +493,26 @@ onMounted(loadMarketDashboard);
</template>
<template v-else-if="column.dataIndex === 'actions'">
<a-space>
<a-button size="small" @click="openReviewAction('approve', record)">
<a-button
size="small"
@click="
openReviewAction(
'approve',
record as AdminPublishRequestSummaryVO,
)
"
>
通过
</a-button>
<a-button
danger
size="small"
@click="openReviewAction('reject', record)"
@click="
openReviewAction(
'reject',
record as AdminPublishRequestSummaryVO,
)
"
>
驳回
</a-button>
@ -518,16 +554,37 @@ onMounted(loadMarketDashboard);
</template>
<template v-else-if="column.dataIndex === 'actions'">
<a-space>
<a-button size="small" @click="openAssetAction('preview', record)">
<a-button
size="small"
@click="
openAssetAction(
'preview',
record as AdminMarketAssetSummaryVO,
)
"
>
影响预览
</a-button>
<a-button size="small" @click="openAssetAction('delist', record)">
<a-button
size="small"
@click="
openAssetAction(
'delist',
record as AdminMarketAssetSummaryVO,
)
"
>
下架
</a-button>
<a-button
danger
size="small"
@click="openAssetAction('recall', record)"
@click="
openAssetAction(
'recall',
record as AdminMarketAssetSummaryVO,
)
"
>
召回
</a-button>
@ -558,7 +615,10 @@ onMounted(loadMarketDashboard);
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'actions'">
<a-button size="small" @click="openAppealAction(record)">
<a-button
size="small"
@click="openAppealAction(record as AdminAppealSummaryVO)"
>
处理
</a-button>
</template>