diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 4dd5912c..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index cf088483..ea2c1135 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/docs/memorys/2026-05-25-muse-admin管理端搭建.md b/docs/memorys/2026-05-25-muse-admin管理端搭建.md index fafc664d..2349eb23 100644 --- a/docs/memorys/2026-05-25-muse-admin管理端搭建.md +++ b/docs/memorys/2026-05-25-muse-admin管理端搭建.md @@ -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 完善后定向 Vitest:13 个测试文件、26 个测试全部通过。 -- P3 Playwright E2E:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。 +- P3 Playwright E2E 初版:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。 +- P3 Playwright E2E 二次完善:2 个 Chromium 用例通过,覆盖 MetaSchema、功能编排、AI 配置、全局知识库、市场治理、Account/New-API、任务治理、日志审计全部主要页面的真实路由访问和边界文案。 - P3 定向 oxlint:35 个文件,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 E2E:2 个 Chromium 用例通过;Vite dev server 仍输出 `ResizeObserver loop completed with undelivered notifications` 开发噪声,但 Playwright 退出码为 0。 +- P3 定向 Vitest:13 个测试文件、26 个测试通过。 +- P3 定向 oxlint:35 个文件,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 E2E:2 个 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 定向 Vitest:13 个测试文件、26 个测试通过。 +- P3 定向 oxlint:35 个文件,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 发布、市场审核/下架、任务重试和审计详情查看等具体命令流。 diff --git a/muse-admin/apps/web-antd/e2e/muse-admin-governance.spec.ts b/muse-admin/apps/web-antd/e2e/muse-admin-governance.spec.ts index d1f855a8..3cf1dc08 100644 --- a/muse-admin/apps/web-antd/e2e/muse-admin-governance.spec.ts +++ b/muse-admin/apps/web-antd/e2e/muse-admin-governance.spec.ts @@ -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>(); + +const expectNoUnhandledMuseAdminApis = (page: Page) => { + const unhandledPaths = [ + ...(unhandledMuseAdminApiPathsByPage.get(page) ?? new Set()), + ]; + 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(); + 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); }); diff --git a/muse-admin/apps/web-antd/src/api/muse/__tests__/client.spec.ts b/muse-admin/apps/web-antd/src/api/muse/__tests__/client.spec.ts index 954162b7..a81a29ad 100644 --- a/muse-admin/apps/web-antd/src/api/muse/__tests__/client.spec.ts +++ b/muse-admin/apps/web-antd/src/api/muse/__tests__/client.spec.ts @@ -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, diff --git a/muse-admin/apps/web-antd/src/api/muse/client.ts b/muse-admin/apps/web-antd/src/api/muse/client.ts index 27b4b113..7b2b0ed7 100644 --- a/muse-admin/apps/web-antd/src/api/muse/client.ts +++ b/muse-admin/apps/web-antd/src/api/muse/client.ts @@ -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 = { diff --git a/muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue b/muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue index 78950ee9..c9269fb5 100644 --- a/muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue +++ b/muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue @@ -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 { diff --git a/muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts b/muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts index a9863a5c..c243cf2b 100644 --- a/muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts +++ b/muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts @@ -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(), }, })); diff --git a/muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue b/muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue index dfebd05f..b411c388 100644 --- a/muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue +++ b/muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue @@ -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, diff --git a/muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts b/muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts index c0c8fb40..cbc62d30 100644 --- a/muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts +++ b/muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts @@ -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(), }, })); diff --git a/muse-admin/apps/web-antd/src/views/muse/market/index.vue b/muse-admin/apps/web-antd/src/views/muse/market/index.vue index 9cc215cc..a7491fda 100644 --- a/muse-admin/apps/web-antd/src/views/muse/market/index.vue +++ b/muse-admin/apps/web-antd/src/views/muse/market/index.vue @@ -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);