From ef55b97a0f7931c7b1b693667a81eefc91d35b63 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 10 Jul 2026 14:49:34 +0800 Subject: [PATCH] docs: add stage a product prototype plan --- ...-07-10-stage-a-global-product-prototype.md | 2019 +++++++++++++++++ 1 file changed, 2019 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-stage-a-global-product-prototype.md diff --git a/docs/superpowers/plans/2026-07-10-stage-a-global-product-prototype.md b/docs/superpowers/plans/2026-07-10-stage-a-global-product-prototype.md new file mode 100644 index 0000000..c9891f2 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-stage-a-global-product-prototype.md @@ -0,0 +1,2019 @@ +# Stage A Global Product Prototype Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在不新增后端业务 API 的前提下,完成全平台中文高保真前端、类型化演示数据、权限感知导航和关键交互测试,锁定 Stage B 至 Stage D 的 UI 与数据契约。 + +**Architecture:** 保留现有真实 API 页面,新增 `features/prototype` 作为纯前端契约与演示数据层。模型、智能体、技能和 MCP 页面只消费该层;共享展示模式进入 `components/app`;路由继续使用现有 Hash Router;侧边栏根据当前用户已有权限补充 Stage A 原型菜单。后续阶段只替换数据适配层,不重做页面结构。 + +**Tech Stack:** React 19、TypeScript 5.7、Vite 6、Tailwind CSS 3.4、shadcn/ui、Radix UI、Lucide、Vitest、Testing Library。 + +## Global Constraints + +- 所有用户可见前端文案必须以中文为主。 +- 所有页面必须使用现有 shadcn/ui 原语和项目自有 `App*` 组件层。 +- 不修改 `backend/`,不新增模型、智能体、技能或 MCP 后端接口。 +- 真实工单、解决方案、知识库和系统治理页面继续调用现有 API。 +- 演示页面必须显示“演示数据”提示,不得伪装成已经持久化或真实执行。 +- 演示导航必须复用当前用户权限:`agent.use`、`agent.manage`、`model.manage`、`tool.manage`。 +- 禁止业务页面直接写原始颜色值;使用语义化主题 token 和 Badge 变体。 +- 表单使用 `AppField`;Select 必须使用 `SelectContent > SelectGroup > SelectItem`。 +- Dialog 必须包含 `DialogTitle`;图标按钮必须具有中文 `aria-label`。 +- 不使用 `space-x-*` 或 `space-y-*`,统一使用 `gap-*`。 +- 每个任务先运行目标测试验证失败,再完成最小实现并验证通过。 +- 本计划取代未执行的 `docs/superpowers/plans/2026-07-06-stage-2-5-global-frontend-prototype.md`。 + +## File Structure + +### Prototype contracts + +- `frontend/src/features/prototype/types.ts`:Stage A 所有演示实体的唯一类型定义。 +- `frontend/src/features/prototype/mockData.ts`:生产式中文演示数据,不包含页面 JSX。 +- `frontend/src/features/prototype/navigation.ts`:按权限合并原型菜单的纯函数。 + +### Shared App components + +- `frontend/src/components/app/AppPrototypeNotice.tsx`:统一演示数据提示。 +- `frontend/src/components/app/AppMetricCard.tsx`:工作台指标卡。 +- `frontend/src/components/app/AppRiskBadge.tsx`:工具风险等级。 +- `frontend/src/components/app/AppTraceTimeline.tsx`:智能体步骤轨迹。 +- `frontend/src/components/app/AppReferenceCard.tsx`:知识、方案和工单引用。 +- `frontend/src/components/app/AppApprovalPanel.tsx`:统一人工确认状态。 + +### Product pages + +- `frontend/src/features/dashboard/DashboardPage.tsx`:全平台业务概览。 +- `frontend/src/features/modelhub/ModelProviderPage.tsx`:模型渠道原型。 +- `frontend/src/features/modelhub/ModelTestingPage.tsx`:模型测试原型。 +- `frontend/src/features/agents/AgentListPage.tsx`:智能体配置原型。 +- `frontend/src/features/agents/AgentRunsPage.tsx`:运行列表原型。 +- `frontend/src/features/agents/AgentRunDetailPage.tsx`:执行轨迹详情。 +- `frontend/src/features/skills/SkillListPage.tsx`:技能配置与测试原型。 +- `frontend/src/features/tools/McpToolsPage.tsx`:MCP 服务、工具与调用原型。 +- `frontend/src/features/tickets/TicketAiCreatePanel.tsx`:建单智能体建议区。 +- `frontend/src/features/tickets/TicketAiAnalysisPanel.tsx`:支持分析、引用和相似工单区。 + +### Tests + +- `frontend/src/test/setup.ts`:Testing Library 全局配置。 +- `frontend/src/features/prototype/mockData.test.ts`:演示数据完整性。 +- `frontend/src/features/prototype/navigation.test.ts`:菜单权限与去重。 +- `frontend/src/components/app/prototype-components.test.tsx`:共享组件。 +- 每个新增页面目录包含一个聚焦交互的 `*.test.tsx`。 + +--- + +### Task 1: Prototype Test Harness And Typed Contracts + +**Files:** +- Modify: `frontend/package.json` +- Modify: `frontend/package-lock.json` +- Create: `frontend/vitest.config.ts` +- Create: `frontend/src/test/setup.ts` +- Create: `frontend/src/features/prototype/types.ts` +- Create: `frontend/src/features/prototype/mockData.ts` +- Test: `frontend/src/features/prototype/mockData.test.ts` + +**Interfaces:** +- Produces: `DashboardMetric`, `ModelProvider`, `ModelTestResult`, `AgentDefinition`, `AgentRun`, `AgentStep`, `SkillDefinition`, `McpServer`, `McpTool`, `ToolCall`, `KnowledgeReference`, `SimilarTicket`。 +- Produces: `dashboardMetrics`, `modelProviders`, `modelTestResults`, `agents`, `agentRuns`, `skills`, `mcpServers`, `mcpTools`, `toolCalls`, `knowledgeReferences`, `similarTickets`。 + +- [ ] **Step 1: Install the frontend test harness** + +Run from `frontend/`: + +```powershell +npm install --save-dev vitest@^3.2.4 jsdom@^26.1.0 @testing-library/react@^16.3.0 @testing-library/jest-dom@^6.6.3 @testing-library/user-event@^14.6.1 +``` + +Add these scripts to `frontend/package.json`: + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run" + } +} +``` + +Create `frontend/vitest.config.ts`: + +```ts +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + css: true + } +}) +``` + +Create `frontend/src/test/setup.ts`: + +```ts +import '@testing-library/jest-dom/vitest' +``` + +- [ ] **Step 2: Write the failing mock-data integrity test** + +Create `frontend/src/features/prototype/mockData.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' + +import { + agentRuns, + agents, + dashboardMetrics, + knowledgeReferences, + mcpTools, + modelProviders, + skills +} from './mockData' + +describe('Stage A 演示数据', () => { + it('提供所有产品模块所需数据', () => { + expect(dashboardMetrics).toHaveLength(4) + expect(modelProviders.length).toBeGreaterThanOrEqual(2) + expect(agents).toHaveLength(3) + expect(skills.length).toBeGreaterThanOrEqual(3) + expect(agentRuns.every((run) => run.steps.length > 0)).toBe(true) + }) + + it('保持可信边界和引用完整性', () => { + expect(mcpTools.filter((tool) => tool.access_mode === 'write').every((tool) => tool.requires_human_confirm)).toBe(true) + expect(knowledgeReferences.every((reference) => reference.score >= 0 && reference.score <= 1)).toBe(true) + }) +}) +``` + +- [ ] **Step 3: Run the test and verify RED** + +Run from `frontend/`: + +```powershell +npm run test:run -- src/features/prototype/mockData.test.ts +``` + +Expected: FAIL because `./mockData` does not exist. + +- [ ] **Step 4: Create the exact prototype types** + +Create `frontend/src/features/prototype/types.ts`: + +```ts +export type DashboardMetric = { + id: string + label: string + value: string + change: string + trend: 'up' | 'down' | 'flat' + tone: 'default' | 'success' | 'warning' | 'danger' | 'ai' +} + +export type ModelProvider = { + id: number + name: string + protocol: 'OpenAI-compatible' + base_url: string + credential_masked: string + enabled: boolean + health: 'healthy' | 'degraded' | 'unchecked' + model_count: number + last_checked_at: string +} + +export type ModelTestResult = { + id: number + provider_name: string + model_name: string + prompt: string + output: string + latency_ms: number + input_tokens: number + output_tokens: number + status: 'success' | 'failed' + created_at: string +} + +export type AgentDefinition = { + id: number + code: 'ticket_intake' | 'support_analysis' | 'knowledge_draft' + name: string + purpose: string + enabled: boolean + model_name: string + knowledge_scopes: string[] + skill_count: number + tool_count: number + updated_at: string +} + +export type AgentStep = { + id: number + step_name: string + step_type: 'model' | 'retrieval' | 'skill' | 'tool' | 'approval' + status: 'success' | 'failed' | 'waiting' + input_summary: string + output_summary: string + latency_ms: number +} + +export type AgentRun = { + id: number + agent_name: string + ticket_no: string + trigger_user: string + status: 'success' | 'failed' | 'running' | 'waiting_confirm' + output_summary: string + duration_ms: number + created_at: string + steps: AgentStep[] +} + +export type SkillDefinition = { + id: number + name: string + scenario: 'field_extract' | 'missing_info' | 'solution_recommend' | 'reply_draft' | 'knowledge_draft' + version: string + enabled: boolean + prompt_template: string + input_schema: string + output_schema: string + updated_at: string +} + +export type McpServer = { + id: number + name: string + endpoint: string + auth_type: '无认证' | '令牌' + connection_state: 'connected' | 'disconnected' | 'error' + tool_count: number + last_sync_at: string +} + +export type McpTool = { + id: number + server_name: string + name: string + description: string + access_mode: 'read' | 'write' + risk_level: 'low' | 'medium' | 'high' + requires_human_confirm: boolean + enabled: boolean +} + +export type ToolCall = { + id: number + tool_name: string + agent_name: string + input_summary: string + output_summary: string + status: 'success' | 'failed' | 'blocked' + duration_ms: number + created_at: string +} + +export type KnowledgeReference = { + id: number + title: string + source_type: 'knowledge_item' | 'solution' | 'ticket' | 'document' + content: string + score: number + source_label: string +} + +export type SimilarTicket = { + id: number + ticket_no: string + title: string + status: 'resolved' | 'closed' + similarity: number + resolution: string +} +``` + +- [ ] **Step 5: Create production-style Chinese mock data** + +Create `frontend/src/features/prototype/mockData.ts` with the exported arrays below. Keep each array typed using the interfaces from `types.ts`. + +```ts +import type { + AgentDefinition, + AgentRun, + DashboardMetric, + KnowledgeReference, + McpServer, + McpTool, + ModelProvider, + ModelTestResult, + SimilarTicket, + SkillDefinition, + ToolCall +} from './types' + +export const dashboardMetrics: DashboardMetric[] = [ + { id: 'pending', label: '待处理工单', value: '18', change: '较昨日增加 3 单', trend: 'up', tone: 'warning' }, + { id: 'resolved', label: '今日已完成', value: '42', change: '完成率 78%', trend: 'up', tone: 'success' }, + { id: 'knowledge', label: '可复用方案', value: '126', change: '本周新增 8 条', trend: 'up', tone: 'default' }, + { id: 'agent', label: '智能体成功率', value: '96.4%', change: '最近 24 小时', trend: 'flat', tone: 'ai' } +] + +export const modelProviders: ModelProvider[] = [ + { id: 1, name: '企业 OpenAI 兼容渠道', protocol: 'OpenAI-compatible', base_url: 'https://llm.example.com/v1', credential_masked: 'sk-****82F1', enabled: true, health: 'healthy', model_count: 4, last_checked_at: '2026-07-10T08:32:00+08:00' }, + { id: 2, name: '内部推理服务', protocol: 'OpenAI-compatible', base_url: 'http://model-gateway.internal/v1', credential_masked: 'token-****19AC', enabled: true, health: 'degraded', model_count: 2, last_checked_at: '2026-07-10T08:28:00+08:00' } +] + +export const modelTestResults: ModelTestResult[] = [ + { id: 1, provider_name: '企业 OpenAI 兼容渠道', model_name: 'support-large', prompt: '分析采集进度长期不更新的可能原因', output: '建议先核对任务状态、最近一次采集日志和税局接口响应。', latency_ms: 1280, input_tokens: 86, output_tokens: 42, status: 'success', created_at: '2026-07-10T09:10:00+08:00' }, + { id: 2, provider_name: '内部推理服务', model_name: 'support-small', prompt: '生成客户回复草稿', output: '已确认任务仍在采集中,我们会继续关注上游接口状态。', latency_ms: 760, input_tokens: 64, output_tokens: 31, status: 'success', created_at: '2026-07-10T09:06:00+08:00' } +] + +export const agents: AgentDefinition[] = [ + { id: 1, code: 'ticket_intake', name: '建单智能体', purpose: '补全工单字段、识别分类并追问缺失信息', enabled: true, model_name: 'support-small', knowledge_scopes: ['客户支持知识库'], skill_count: 2, tool_count: 0, updated_at: '2026-07-10T08:20:00+08:00' }, + { id: 2, code: 'support_analysis', name: '支持分析智能体', purpose: '检索知识、推荐方案并生成排查步骤', enabled: true, model_name: 'support-large', knowledge_scopes: ['客户支持知识库', '解决方案库'], skill_count: 3, tool_count: 2, updated_at: '2026-07-10T08:24:00+08:00' }, + { id: 3, code: 'knowledge_draft', name: '知识沉淀智能体', purpose: '从已解决工单生成待审核知识草稿', enabled: false, model_name: 'support-large', knowledge_scopes: ['解决方案库'], skill_count: 1, tool_count: 0, updated_at: '2026-07-10T08:26:00+08:00' } +] + +export const agentRuns: AgentRun[] = [ + { id: 1001, agent_name: '支持分析智能体', ticket_no: 'T202607100006', trigger_user: '张工', status: 'waiting_confirm', output_summary: '已生成 3 条排查建议和 1 份客户回复草稿。', duration_ms: 2180, created_at: '2026-07-10T09:18:00+08:00', steps: [ + { id: 1, step_name: '检索相关知识', step_type: 'retrieval', status: 'success', input_summary: '采集进度长期不更新', output_summary: '命中 2 条知识和 1 个解决方案', latency_ms: 180 }, + { id: 2, step_name: '查询采集任务', step_type: 'tool', status: 'success', input_summary: '任务编号 TASK-8821', output_summary: '任务运行中,上游接口最近一次返回超时', latency_ms: 420 }, + { id: 3, step_name: '生成支持分析', step_type: 'model', status: 'success', input_summary: '工单、知识引用与工具结果', output_summary: '生成原因分析、排查步骤和回复草稿', latency_ms: 1580 }, + { id: 4, step_name: '等待人工采纳', step_type: 'approval', status: 'waiting', input_summary: 'AI 支持分析结果', output_summary: '等待技术人员确认', latency_ms: 0 } + ] }, + { id: 1002, agent_name: '建单智能体', ticket_no: '草稿', trigger_user: '李娜', status: 'success', output_summary: '已识别为快速咨询并补全问题对象。', duration_ms: 940, created_at: '2026-07-10T09:12:00+08:00', steps: [ + { id: 5, step_name: '抽取工单字段', step_type: 'skill', status: 'success', input_summary: '客户自然语言描述', output_summary: '识别分类、对象和环境', latency_ms: 120 }, + { id: 6, step_name: '生成字段草稿', step_type: 'model', status: 'success', input_summary: '结构化字段', output_summary: '生成可确认的建单草稿', latency_ms: 820 } + ] } +] + +export const skills: SkillDefinition[] = [ + { id: 1, name: '工单字段抽取', scenario: 'field_extract', version: 'v1.3', enabled: true, prompt_template: '从用户描述中抽取工单分类、问题对象、问题表现、环境和关联系统。', input_schema: '{ "description": "string" }', output_schema: '{ "category_code": "string", "problem_object": "string" }', updated_at: '2026-07-10T08:00:00+08:00' }, + { id: 2, name: '缺失信息追问', scenario: 'missing_info', version: 'v1.1', enabled: true, prompt_template: '根据已填写字段判断处理工单仍缺少哪些必要信息。', input_schema: '{ "ticket_draft": "object" }', output_schema: '{ "questions": "string[]" }', updated_at: '2026-07-10T08:02:00+08:00' }, + { id: 3, name: '解决方案推荐', scenario: 'solution_recommend', version: 'v1.5', enabled: true, prompt_template: '根据工单内容和检索结果推荐可复用解决方案,并说明匹配原因。', input_schema: '{ "ticket": "object", "references": "object[]" }', output_schema: '{ "recommendations": "object[]" }', updated_at: '2026-07-10T08:04:00+08:00' } +] + +export const mcpServers: McpServer[] = [ + { id: 1, name: '采集任务查询服务', endpoint: 'http://collector-mcp.internal', auth_type: '令牌', connection_state: 'connected', tool_count: 3, last_sync_at: '2026-07-10T08:40:00+08:00' }, + { id: 2, name: '日志检索服务', endpoint: 'http://logs-mcp.internal', auth_type: '令牌', connection_state: 'connected', tool_count: 2, last_sync_at: '2026-07-10T08:36:00+08:00' } +] + +export const mcpTools: McpTool[] = [ + { id: 1, server_name: '采集任务查询服务', name: '查询采集进度', description: '按任务编号查询当前阶段和最近更新时间', access_mode: 'read', risk_level: 'low', requires_human_confirm: false, enabled: true }, + { id: 2, server_name: '日志检索服务', name: '检索任务日志', description: '按任务编号读取脱敏日志摘要', access_mode: 'read', risk_level: 'medium', requires_human_confirm: false, enabled: true }, + { id: 3, server_name: '采集任务查询服务', name: '重启采集任务', description: '重新触发指定采集任务', access_mode: 'write', risk_level: 'high', requires_human_confirm: true, enabled: false } +] + +export const toolCalls: ToolCall[] = [ + { id: 1, tool_name: '查询采集进度', agent_name: '支持分析智能体', input_summary: 'TASK-8821', output_summary: '运行中,最近更新时间 09:16', status: 'success', duration_ms: 420, created_at: '2026-07-10T09:18:20+08:00' }, + { id: 2, tool_name: '重启采集任务', agent_name: '支持分析智能体', input_summary: 'TASK-8821', output_summary: '非只读工具不允许自动执行', status: 'blocked', duration_ms: 8, created_at: '2026-07-10T09:18:40+08:00' } +] + +export const knowledgeReferences: KnowledgeReference[] = [ + { id: 1, title: '采集任务长时间无进度的排查步骤', source_type: 'knowledge_item', content: '先检查任务状态、上游接口响应和最近一次有效日志,再判断是否需要人工重试。', score: 0.93, source_label: '客户支持知识库' }, + { id: 2, title: '税局接口不稳定时的客户回复模板', source_type: 'solution', content: '确认任务仍在执行后,向客户说明上游接口状态并持续关注采集进度。', score: 0.87, source_label: '解决方案库' } +] + +export const similarTickets: SimilarTicket[] = [ + { id: 5, ticket_no: 'T202607030005', title: '税局采集任务停留在初始化阶段', status: 'closed', similarity: 0.91, resolution: '确认税局接口波动,任务恢复后回复客户并关闭。' }, + { id: 2, ticket_no: 'T202607010002', title: '查询申报数据采集进度', status: 'resolved', similarity: 0.84, resolution: '查询任务日志后确认仍在采集中。' } +] +``` + +- [ ] **Step 6: Run the integrity test and typecheck** + +Run from `frontend/`: + +```powershell +npm run test:run -- src/features/prototype/mockData.test.ts +npm run typecheck +``` + +Expected: test file passes and TypeScript exits with code 0. + +- [ ] **Step 7: Commit Task 1** + +```powershell +git add frontend/package.json frontend/package-lock.json frontend/vitest.config.ts frontend/src/test/setup.ts frontend/src/features/prototype/types.ts frontend/src/features/prototype/mockData.ts frontend/src/features/prototype/mockData.test.ts +git commit -m "test: add stage a prototype contracts" +``` + +--- + +### Task 2: Shared Prototype App Components + +**Files:** +- Create: `frontend/src/components/app/AppPrototypeNotice.tsx` +- Create: `frontend/src/components/app/AppMetricCard.tsx` +- Create: `frontend/src/components/app/AppRiskBadge.tsx` +- Create: `frontend/src/components/app/AppTraceTimeline.tsx` +- Create: `frontend/src/components/app/AppReferenceCard.tsx` +- Create: `frontend/src/components/app/AppApprovalPanel.tsx` +- Test: `frontend/src/components/app/prototype-components.test.tsx` + +**Interfaces:** +- Consumes: `DashboardMetric['tone']`, `McpTool['risk_level']`, `AgentStep`。 +- Produces: `AppPrototypeNotice`, `AppMetricCard`, `AppRiskBadge`, `AppTraceTimeline`, `AppReferenceCard`, `AppApprovalPanel`。 + +- [ ] **Step 1: Write failing shared-component tests** + +Create `frontend/src/components/app/prototype-components.test.tsx`: + +```tsx +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { AppMetricCard } from './AppMetricCard' +import { AppApprovalPanel } from './AppApprovalPanel' +import { AppPrototypeNotice } from './AppPrototypeNotice' +import { AppReferenceCard } from './AppReferenceCard' +import { AppRiskBadge } from './AppRiskBadge' +import { AppTraceTimeline } from './AppTraceTimeline' + +describe('Stage A 共享组件', () => { + it('明确标识演示数据', () => { + render() + expect(screen.getByText('当前页面使用演示数据')).toBeInTheDocument() + }) + + it('展示指标、风险、引用和执行步骤', () => { + render( + <> + + + + + + + ) + expect(screen.getByText('待处理工单')).toBeInTheDocument() + expect(screen.getByText('高风险')).toBeInTheDocument() + expect(screen.getByText('匹配度 93%')).toBeInTheDocument() + expect(screen.getByText('检索知识')).toBeInTheDocument() + expect(screen.getByText('等待人工采纳')).toBeInTheDocument() + }) +}) +``` + +- [ ] **Step 2: Run tests and verify RED** + +```powershell +npm run test:run -- src/components/app/prototype-components.test.tsx +``` + +Expected: FAIL because the six components do not exist. + +- [ ] **Step 3: Implement the shared components** + +Create `frontend/src/components/app/AppPrototypeNotice.tsx`: + +```tsx +import { FlaskConical } from 'lucide-react' + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' + +export function AppPrototypeNotice() { + return ( + + + 当前页面使用演示数据 + 用于确认产品交互和数据契约,不会保存配置或发起真实调用。 + + ) +} +``` + +Create `frontend/src/components/app/AppMetricCard.tsx`: +```tsx +import { Badge } from '@/components/ui/badge' +import { Card, CardContent } from '@/components/ui/card' + +type AppMetricCardProps = { + label: string + value: string | number + change: string + tone?: 'default' | 'success' | 'warning' | 'danger' | 'ai' +} + +const variants = { + default: 'secondary', + success: 'success', + warning: 'warning', + danger: 'danger', + ai: 'ai' +} as const + +export function AppMetricCard({ label, value, change, tone = 'default' }: AppMetricCardProps) { + return ( + + +
+

{label}

+ {change} +
+

{value}

+
+
+ ) +} +``` + +Create `frontend/src/components/app/AppRiskBadge.tsx`: + +```tsx +import { Badge } from '@/components/ui/badge' +import type { McpTool } from '@/features/prototype/types' + +const riskConfig = { + low: { label: '低风险', variant: 'success' }, + medium: { label: '中风险', variant: 'warning' }, + high: { label: '高风险', variant: 'danger' } +} as const + +export function AppRiskBadge({ risk }: { risk: McpTool['risk_level'] }) { + const config = riskConfig[risk] + return {config.label} +} +``` + +Create `frontend/src/components/app/AppTraceTimeline.tsx`: + +```tsx +import { CheckCircle2, CircleAlert, Clock3 } from 'lucide-react' + +import { Badge } from '@/components/ui/badge' +import type { AgentStep } from '@/features/prototype/types' + +const statusLabels = { success: '成功', failed: '失败', waiting: '等待确认' } +const typeLabels = { model: '模型', retrieval: '知识检索', skill: '技能', tool: '工具', approval: '人工确认' } + +export function AppTraceTimeline({ items }: { items: AgentStep[] }) { + return ( +
    + {items.map((item) => { + const Icon = item.status === 'success' ? CheckCircle2 : item.status === 'failed' ? CircleAlert : Clock3 + return ( +
  1. + +
    +
    +

    {item.step_name}

    + {typeLabels[item.step_type]} + {statusLabels[item.status]} +
    +

    输入:{item.input_summary}

    +

    输出:{item.output_summary}

    +

    耗时 {item.latency_ms} ms

    +
    +
  2. + ) + })} +
+ ) +} +``` + +Create `frontend/src/components/app/AppReferenceCard.tsx`: + +```tsx +import { Badge } from '@/components/ui/badge' + +const sourceLabels: Record = { + knowledge_item: '知识条目', + solution: '解决方案', + ticket: '历史工单', + document: '文档' +} + +type AppReferenceCardProps = { + title: string + sourceType: string + content: string + score: number + sourceLabel?: string +} + +export function AppReferenceCard({ title, sourceType, content, score, sourceLabel }: AppReferenceCardProps) { + return ( +
+
+

{title}

+ {sourceLabels[sourceType] ?? sourceType} + 匹配度 {Math.round(score * 100)}% +
+

{content}

+ {sourceLabel ?

来源:{sourceLabel}

: null} +
+ ) +} +``` + +Create `frontend/src/components/app/AppApprovalPanel.tsx`: + +```tsx +import type * as React from 'react' + +import { Badge } from '@/components/ui/badge' + +type AppApprovalPanelProps = { + title: string + description: string + state: 'waiting' | 'approved' | 'rejected' + action?: React.ReactNode +} + +const stateConfig = { + waiting: { label: '等待确认', variant: 'warning' }, + approved: { label: '已确认', variant: 'success' }, + rejected: { label: '已拒绝', variant: 'danger' } +} as const + +export function AppApprovalPanel({ title, description, state, action }: AppApprovalPanelProps) { + const config = stateConfig[state] + return ( +
+

{title}

{config.label}

{description}

+ {action} +
+ ) +} +``` + +- [ ] **Step 4: Run shared-component tests and typecheck** + +```powershell +npm run test:run -- src/components/app/prototype-components.test.tsx +npm run typecheck +``` + +Expected: tests pass and TypeScript exits with code 0. + +- [ ] **Step 5: Commit Task 2** + +```powershell +git add frontend/src/components/app/AppPrototypeNotice.tsx frontend/src/components/app/AppMetricCard.tsx frontend/src/components/app/AppRiskBadge.tsx frontend/src/components/app/AppTraceTimeline.tsx frontend/src/components/app/AppReferenceCard.tsx frontend/src/components/app/AppApprovalPanel.tsx frontend/src/components/app/prototype-components.test.tsx +git commit -m "feat: add stage a shared components" +``` + +--- +### Task 3: Permission-Aware Navigation And Operations Dashboard + +**Files:** +- Create: `frontend/src/features/prototype/navigation.ts` +- Test: `frontend/src/features/prototype/navigation.test.ts` +- Create: `frontend/src/features/dashboard/DashboardPage.tsx` +- Test: `frontend/src/features/dashboard/DashboardPage.test.tsx` +- Modify: `frontend/src/components/app/AppSidebar.tsx` +- Modify: `frontend/src/app/router.tsx` + +**Interfaces:** +- Consumes: `CurrentUser.permissions`, `CurrentUser.menus`, `dashboardMetrics`, `agentRuns`, `mcpTools`。 +- Produces: `mergePrototypeNavigation(groups, permissions): AuthMenu[]`。 +- Produces routes: `/dashboard`, `/agents`, `/agents/runs`, `/skills`, `/modelhub/providers`, `/modelhub/testing`, `/tools/mcp`。 + +- [ ] **Step 1: Write the failing navigation test** + +Create `frontend/src/features/prototype/navigation.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' + +import type { AuthMenu } from '@/features/auth/types' +import { mergePrototypeNavigation } from './navigation' + +const baseMenus: AuthMenu[] = [ + { id: 1, code: 'workbench', title: '工作台', path: '', icon: 'home', permission_code: '', is_visible: true, is_enabled: true, children: [ + { id: 2, code: 'workbench.overview', title: '运营总览', path: '#/', icon: 'home', permission_code: '', is_visible: true, is_enabled: true, children: [] } + ] } +] + +describe('Stage A 原型导航', () => { + it('根据管理员权限补充完整产品入口', () => { + const groups = mergePrototypeNavigation(baseMenus, ['knowledge.view', 'agent.use', 'agent.manage', 'model.manage', 'tool.manage']) + const paths = groups.flatMap((group) => group.children.map((child) => child.path)) + expect(paths).toEqual(expect.arrayContaining(['#/dashboard', '#/knowledge', '#/agents', '#/agents/runs', '#/skills', '#/modelhub/providers', '#/modelhub/testing', '#/tools/mcp'])) + }) + + it('不向无权限用户暴露管理入口,也不重复已有路径', () => { + const once = mergePrototypeNavigation(baseMenus, ['agent.use']) + const twice = mergePrototypeNavigation(once, ['agent.use']) + const paths = twice.flatMap((group) => group.children.map((child) => child.path)) + expect(paths.filter((path) => path === '#/agents/runs')).toHaveLength(1) + expect(paths).not.toContain('#/agents') + expect(paths).not.toContain('#/skills') + expect(paths).not.toContain('#/modelhub/providers') + }) +}) +``` + +- [ ] **Step 2: Run the navigation test and verify RED** + +```powershell +npm run test:run -- src/features/prototype/navigation.test.ts +``` + +Expected: FAIL because `navigation.ts` does not exist. + +- [ ] **Step 3: Implement permission-aware menu merging** + +Create `frontend/src/features/prototype/navigation.ts`: + +```ts +import type { AuthMenu } from '@/features/auth/types' + +export function mergePrototypeNavigation(groups: AuthMenu[], permissions: string[]): AuthMenu[] { + let nextId = -1000 + function menu(code: string, title: string, path: string, icon: string, permissionCode = ''): AuthMenu { + return { id: nextId--, code, title, path, icon, permission_code: permissionCode, is_visible: true, is_enabled: true, children: [] } + } + + const result = groups.map((group) => ({ ...group, children: group.children.map((child) => ({ ...child })) })) + const allowed = new Set(permissions) + + function hasPath(path: string) { + return result.some((group) => group.path === path || group.children.some((child) => child.path === path)) + } + + function append(groupCode: string, groupTitle: string, groupIcon: string, child: AuthMenu, requiredPermission?: string) { + if (requiredPermission && !allowed.has(requiredPermission)) return + if (hasPath(child.path)) return + let group = result.find((item) => item.code === groupCode) + if (!group) { + group = menu(groupCode, groupTitle, '', groupIcon) + result.push(group) + } + group.children = [...group.children, child] + } + + append('workbench', '工作台', 'home', menu('prototype.dashboard', '业务概览', '#/dashboard', 'activity')) + append('prototype.knowledge', '知识与方案', 'book_open', menu('prototype.knowledge.bases', '知识库', '#/knowledge', 'database', 'knowledge.view'), 'knowledge.view') + append('prototype.knowledge', '知识与方案', 'book_open', menu('prototype.knowledge.search', '知识检索', '#/knowledge/search', 'search', 'knowledge.view'), 'knowledge.view') + append('prototype.agents', '智能体中心', 'brain', menu('prototype.agents.config', '智能体配置', '#/agents', 'brain', 'agent.manage'), 'agent.manage') + append('prototype.agents', '智能体中心', 'brain', menu('prototype.agents.runs', '运行记录', '#/agents/runs', 'activity', 'agent.use'), 'agent.use') + append('prototype.skills', '技能中心', 'boxes', menu('prototype.skills.config', '技能配置', '#/skills', 'boxes', 'agent.manage'), 'agent.manage') + append('prototype.model_tools', '模型与工具', 'settings', menu('prototype.models.providers', '模型渠道', '#/modelhub/providers', 'settings', 'model.manage'), 'model.manage') + append('prototype.model_tools', '模型与工具', 'settings', menu('prototype.models.testing', '模型测试', '#/modelhub/testing', 'activity', 'model.manage'), 'model.manage') + append('prototype.model_tools', '模型与工具', 'settings', menu('prototype.tools.mcp', '工具接入', '#/tools/mcp', 'plug', 'tool.manage'), 'tool.manage') + + return result.filter((group) => group.children.length > 0) +} +``` + +- [ ] **Step 4: Replace the sidebar fallback with the merge function** + +Modify `frontend/src/components/app/AppSidebar.tsx`: + +1. Add `Activity`, `Boxes`, `BrainCircuit`, and `Plug` to the Lucide import. +2. Add icon-map entries: + +```tsx +activity: Activity, +boxes: Boxes, +brain: BrainCircuit, +plug: Plug, +``` + +3. Import the merge function: + +```tsx +import { mergePrototypeNavigation } from '@/features/prototype/navigation' +``` + +4. Delete the entire `knowledgeNavGroup` constant. +5. Replace `hasKnowledgeMenu` and the old `groups` assignment with: + +```tsx +const groups = mergePrototypeNavigation(menuGroups, user?.permissions ?? []) +``` + +- [ ] **Step 5: Create the operations dashboard** + +Create `frontend/src/features/dashboard/DashboardPage.tsx`: + +```tsx +import { ArrowRight, Plus, ShieldAlert } from 'lucide-react' + +import { AppMetricCard } from '@/components/app/AppMetricCard' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { AppApprovalPanel } from '@/components/app/AppApprovalPanel' +import { AppPrototypeNotice } from '@/components/app/AppPrototypeNotice' +import { AppRiskBadge } from '@/components/app/AppRiskBadge' +import { AppSection } from '@/components/app/AppSection' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { agentRuns, dashboardMetrics, mcpTools } from '@/features/prototype/mockData' + +const runStatusLabels = { success: '成功', failed: '失败', running: '运行中', waiting_confirm: '待确认' } + +export function DashboardPage() { + return ( + + (window.location.hash = '/tickets/new')}>新建工单} + /> + +
+ {dashboardMetrics.map((metric) => )} +
+
+ (window.location.hash = '/agents/runs')}>查看全部}> +
+ {agentRuns.map((run) => ( + + ))} +
+
+ +
+ {mcpTools.map((tool) =>

{tool.name}

{tool.server_name}

)} +
非只读工具不会自动执行。
+
+
+
+
+ ) +} +``` + +- [ ] **Step 6: Wire the dashboard route** + +Add to `frontend/src/app/router.tsx`: + +```tsx +import { DashboardPage } from '@/features/dashboard/DashboardPage' +``` + +Add before the `/` branch: + +```tsx +if (route === '/dashboard') { + return +} +``` + +- [ ] **Step 7: Write and run the dashboard test** + +Create `frontend/src/features/dashboard/DashboardPage.test.tsx`: + +```tsx +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { DashboardPage } from './DashboardPage' + +describe('业务概览', () => { + it('展示演示标识、业务指标和可信边界', () => { + render() + expect(screen.getByRole('heading', { name: '业务概览' })).toBeInTheDocument() + expect(screen.getByText('当前页面使用演示数据')).toBeInTheDocument() + expect(screen.getByText('智能体运行')).toBeInTheDocument() + expect(screen.getByText('非只读工具不会自动执行。')).toBeInTheDocument() + }) +}) +``` + +Run: + +```powershell +npm run test:run -- src/features/prototype/navigation.test.ts src/features/dashboard/DashboardPage.test.tsx +npm run typecheck +npm run build +``` + +Expected: both tests pass; typecheck and build exit with code 0. + +- [ ] **Step 8: Commit Task 3** + +```powershell +git add frontend/src/features/prototype/navigation.ts frontend/src/features/prototype/navigation.test.ts frontend/src/features/dashboard frontend/src/components/app/AppSidebar.tsx frontend/src/app/router.tsx +git commit -m "feat: add stage a navigation dashboard" +``` + +--- + +### Task 4: Model Channel And Model Testing Prototypes + +**Files:** +- Create: `frontend/src/features/modelhub/ModelProviderPage.tsx` +- Create: `frontend/src/features/modelhub/ModelTestingPage.tsx` +- Test: `frontend/src/features/modelhub/modelhub-pages.test.tsx` +- Modify: `frontend/src/app/router.tsx` + +**Interfaces:** +- Consumes: `modelProviders`, `modelTestResults`。 +- Produces routes: `/modelhub/providers`, `/modelhub/testing`。 +- Stage B contract: provider list fields and model test result fields must remain stable. + +- [ ] **Step 1: Write failing model-page interaction tests** + +Create `frontend/src/features/modelhub/modelhub-pages.test.tsx`: + +```tsx +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it } from 'vitest' + +import { ModelProviderPage } from './ModelProviderPage' +import { ModelTestingPage } from './ModelTestingPage' + +describe('模型管理原型', () => { + it('可以演示模型渠道连接测试', async () => { + render() + await userEvent.click(screen.getByRole('button', { name: '测试连接' })) + expect(screen.getByText('演示连接成功,响应耗时 1,280 ms。')).toBeInTheDocument() + }) + + it('可以运行一次模型测试并展示输出指标', async () => { + render() + await userEvent.click(screen.getByRole('button', { name: '运行测试' })) + expect(screen.getByText('测试输出')).toBeInTheDocument() + expect(screen.getByText(/输入 86 tokens/)).toBeInTheDocument() + }) +}) +``` + +- [ ] **Step 2: Run tests and verify RED** + +```powershell +npm run test:run -- src/features/modelhub/modelhub-pages.test.tsx +``` + +Expected: FAIL because both page modules do not exist. + +- [ ] **Step 3: Implement the provider page** + +Create `frontend/src/features/modelhub/ModelProviderPage.tsx`: + +```tsx +import * as React from 'react' +import { Plus, Wifi } from 'lucide-react' + +import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' +import { AppField } from '@/components/app/AppField' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { AppPrototypeNotice } from '@/components/app/AppPrototypeNotice' +import { AppSection } from '@/components/app/AppSection' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Switch } from '@/components/ui/switch' +import { modelProviders } from '@/features/prototype/mockData' +import type { ModelProvider } from '@/features/prototype/types' +import { formatDateTime } from '@/lib/format' + +const healthLabels = { healthy: '正常', degraded: '性能下降', unchecked: '未检查' } + +const columns: AppDataTableColumn[] = [ + { key: 'name', header: '渠道名称', cell: (provider) => {provider.name} }, + { key: 'health', header: '健康状态', cell: (provider) => {healthLabels[provider.health]} }, + { key: 'protocol', header: '协议', cell: (provider) => provider.protocol }, + { key: 'models', header: '模型数量', cell: (provider) => provider.model_count }, + { key: 'checked', header: '最后检查', cell: (provider) => {formatDateTime(provider.last_checked_at)} } +] + +export function ModelProviderPage() { + const [selected, setSelected] = React.useState(modelProviders[0]) + const [dialogOpen, setDialogOpen] = React.useState(false) + const [connectionMessage, setConnectionMessage] = React.useState('') + + return ( + + setDialogOpen(true)}>新增渠道} /> + +
+ provider.id} emptyTitle="暂无模型渠道" onRowClick={setSelected} /> + {selected.enabled ? '已启用' : '已停用'}}> +
+ + +

启用渠道

Stage B 接入真实保存接口。

+ + {connectionMessage ?

{connectionMessage}

: null} +
+
+
+ + + 新增模型渠道当前只演示字段和交互,不会保存配置。 +
+ +
+
+
+ ) +} +``` + +- [ ] **Step 4: Implement the model-testing page** + +Create `frontend/src/features/modelhub/ModelTestingPage.tsx`: + +```tsx +import * as React from 'react' +import { Play } from 'lucide-react' + +import { AppField } from '@/components/app/AppField' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { AppPrototypeNotice } from '@/components/app/AppPrototypeNotice' +import { AppSection } from '@/components/app/AppSection' +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { modelProviders, modelTestResults } from '@/features/prototype/mockData' +import type { ModelTestResult } from '@/features/prototype/types' + +export function ModelTestingPage() { + const [providerId, setProviderId] = React.useState(String(modelProviders[0].id)) + const [modelName, setModelName] = React.useState('support-large') + const [prompt, setPrompt] = React.useState('分析采集进度长期不更新的可能原因') + const [result, setResult] = React.useState(null) + + return ( + + + +
+ +
+ + +