diff --git a/.gitignore b/.gitignore index 1613254..1674678 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,10 @@ storage/ minio-data/ postgres-data/ redis-data/ + +.agents/ +.codegraph/ +skills-lock.json +frontend/skills-lock.json + +infra/data/ diff --git a/README.md b/README.md index 0ff5457..e56acbf 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Active implementation. The project has a runnable Django + React baseline with e Key docs: -- [Product design spec](docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md) +- [Project blueprint](docs/product/blueprint.md) +- [Capability status](docs/product/capability-status.md) - [Design discussion log](docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md) -- [Implementation roadmap](docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md) - [Local setup](docs/development/local-setup.md) - [Frontend design system](docs/frontend/design-system.md) diff --git a/docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md b/docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md index 19c3212..d378365 100644 --- a/docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md +++ b/docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md @@ -14,9 +14,7 @@ | 文档 | 用途 | |---|---| -| `docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md` | 第一版正式产品与架构设计 | -| `docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md` | 分阶段实施路线图 | -| `docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md` | 阶段 0 详细实施计划 | +| `docs/product/blueprint.md` | 基于本记录与当前代码现状重新规划的唯一主蓝图 | ## 2. 初始背景:从现有系统 AI 工单识别开始 @@ -503,12 +501,6 @@ Agent ## 20. 正式设计文档产出 -正式设计文档保存到: - -```text -docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md -``` - 文档内容包括: - 产品定位 @@ -527,18 +519,6 @@ docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md ## 21. 实施计划产出 -总路线图保存到: - -```text -docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md -``` - -阶段 0 详细计划保存到: - -```text -docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md -``` - 阶段 0 计划目标: 建立可运行项目底座,包括: @@ -579,14 +559,11 @@ docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md 建议新会话按以下顺序读取: 1. `README.md` -2. `docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md` -3. `docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md` -4. `docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md` -5. `docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md` +2. `docs/product/blueprint.md` +3. `docs/communication/2026-06-30-ai-ticket-platform-discussion-log.md` -如果要开始编码,建议从阶段 0 计划执行,并遵守计划文档顶部说明: +如果要开始编码,建议从阶段 0 开始,并遵守以下说明: -- 使用 `superpowers:subagent-driven-development` 或 `superpowers:executing-plans` - 按 task 逐项执行 - 每个可验证任务单独提交 - 不跳过测试和构建验证 diff --git a/docs/product/blueprint.md b/docs/product/blueprint.md new file mode 100644 index 0000000..90ae3ef --- /dev/null +++ b/docs/product/blueprint.md @@ -0,0 +1,542 @@ +# AI 工单平台项目蓝图 + +更新日期:2026-07-14 +状态:当前唯一主蓝图 +需求基线:[2026-06-30 AI 工单平台设计沟通记录](../communication/2026-06-30-ai-ticket-platform-discussion-log.md) + +## 1. 蓝图结论 + +本项目继续定位为面向单企业私有化部署的 AI-native 技术支持工单产品,不退回旧系统外挂,也不扩张为通用 Agent 平台。 + +本次重规划保留原有产品愿景,但调整交付逻辑: + +```text +旧交付逻辑:先铺完整平台页面 -> 再逐步补真实能力 +新交付逻辑:先跑通一个真实业务闭环 -> 抽取共性平台能力 -> 扩展下一个闭环 +``` + +最终平台仍覆盖工单、知识库、模型、Agent、Skill、MCP、权限和审计;区别在于它们不再作为彼此平行的模块同时展开,而是围绕以下价值链依次落地: + +```text +把问题说清楚 -> 帮支持人员查清楚 -> 把处理结果复用起来 -> 安全连接外部系统 +``` + +第一个可交付 AI 价值不是“Agent 配置台”,而是“自然语言建单草稿经人工确认后创建真实工单”。第一个可交付知识价值不是“向量库管理页”,而是“支持人员能看到有来源、可采纳的检索结果”。 + +## 2. 北极星与成功定义 + +### 2.1 产品北极星 + +让技术支持人员在一个可信工作区内,更快取得完整问题上下文、找到可靠处理依据、完成客户回复,并把有效经验沉淀为可复用知识。 + +### 2.2 首个企业试运行版本的成功定义 + +- 普通建单人可以用自然语言、图片或附件描述问题,确认 AI 草稿后创建工单。 +- 技术支持可以在工单详情内完成接单、沟通、检索、分析、方案采纳、解决和关闭。 +- 每条 AI 建议都能区分生成内容、引用依据、人工修改和最终采纳结果。 +- 已解决工单可以生成知识或解决方案草稿,经知识管理员审核后发布。 +- 系统管理员可以管理用户、角色、模型和内置 Agent;只读 MCP 工具在明确授权后可被调用。 +- 审计人员可以追溯业务变更、模型调用、知识引用、工具调用和人工确认。 +- AI、向量检索或 MCP 不可用时,人工工单主流程仍可继续。 + +### 2.3 衡量指标 + +试运行阶段至少采集以下指标,先建立基线,再设目标值: + +| 维度 | 指标 | +|---|---| +| 建单质量 | 首次信息完整率、补充信息次数、建单草稿采纳率 | +| 支持效率 | 首次响应时长、进入有效排查的时长、平均解决时长 | +| 知识复用 | 检索命中率、引用点击率、方案采纳率、重复方案率 | +| AI 质量 | 运行成功率、人工采纳率、修改后采纳率、拒绝率、无引用回答率 | +| 可信治理 | 越权拦截率、未审计调用数、敏感信息泄露事件数、人工流程降级成功率 | +| 系统质量 | 核心接口可用率、异步任务失败率、恢复时间、备份恢复演练结果 | + +## 3. 产品边界 + +### 3.1 本版本必须做 + +- 单企业用户、用户组、角色、菜单/操作权限和操作审计。 +- 工单创建、查询、处理、评论、附件、状态流转、导出和解决方案复用。 +- 文档知识、结构化知识、解决方案、来源追溯和审核发布。 +- OpenAI-compatible 模型渠道,以及对话、视觉、Embedding 等能力标识。 +- 建单、支持分析、知识沉淀三类内置 Agent。 +- 配置型 Skill、版本、输入输出 Schema 和受控资源范围。 +- Agent 运行、步骤、产物、引用、用量、错误和人工确认记录。 +- 只读 MCP 服务注册、工具同步、Agent 授权、风险分级和调用审计。 +- Docker Compose 私有化部署、监控、备份恢复和升级说明。 + +### 3.2 明确不做 + +- 多租户 SaaS 和租户隔离。 +- 复杂 BPMN、可视化流程编排和自动派单闭环。 +- 多 Agent 自主协作。 +- 任意 Python 代码型 Skill 或用户上传代码执行。 +- 非只读 MCP 工具的无人值守执行。 +- AI 自动解决、自动关闭或绕过业务服务直接写核心表。 +- 为兼容旧系统而复制旧字段、旧权限或旧组织模型。 +- 在首个试运行前建设独立微服务集群。 + +## 4. 目标用户与关键任务 + +| 角色 | 首要任务 | AI 提供的帮助 | 最终责任 | +|---|---|---|---| +| 普通建单人 | 准确提交问题、跟踪进度 | 字段草稿、缺失信息追问、分类和相似问题建议 | 确认提交内容 | +| 技术支持 | 理解问题、排查、回复、解决 | 上下文摘要、知识检索、原因假设、排查步骤、回复草稿 | 判断、执行和状态变更 | +| 知识管理员 | 审核、发布、维护知识 | 从工单生成草稿、重复检测、失效提醒 | 内容正确性和发布 | +| 系统管理员 | 管理权限与 AI 能力 | 模型测试、配置校验、运行诊断 | 配置生效和风险边界 | +| 只读审计员 | 追踪业务和 AI 行为 | 聚合运行链路和异常事件 | 审计结论 | + +## 5. 核心业务闭环 + +```mermaid +flowchart LR + A["用户描述问题"] --> B["建单 Agent 生成草稿"] + B --> C{"用户确认"} + C -->|修改或确认| D["业务服务创建工单"] + D --> E["技术支持处理"] + E --> F["检索知识、方案和历史工单"] + F --> G["支持分析 Agent 生成建议"] + G --> H{"技术人员采纳"} + H --> I["回复、解决、关闭"] + I --> J["知识沉淀 Agent 生成草稿"] + J --> K{"知识管理员审核"} + K -->|发布| L["知识与解决方案资产"] + L --> F +``` + +任何 AI 结果都先写入 AI 产物或草稿,不直接成为正式业务事实。正式写入必须经过“用户确认 -> 对应业务服务 -> 业务事件与审计”这条路径。 + +### 5.1 工单类型与状态 + +第一版保留同一套工单主模型,采用两种受控流转策略: + +| 类型 | 适用场景 | 建议主路径 | +|---|---|---| +| 快速咨询 | 进度查询、配置确认、简单答复 | 待处理 -> 已解决/已关闭 | +| 技术问题 | 需要排查、验证或协作 | 待处理 -> 处理中 -> 待确认 -> 已解决 -> 已关闭 | + +状态流转由领域服务校验,不在前端拼接规则,也不在 Agent Prompt 中决定。 + +## 6. 当前真实基线 + +截至 2026-07-14,仓库并非空白项目。重规划必须在已有实现上演进。 + +| 能力 | 当前状态 | 蓝图判断 | +|---|---|---| +| Django/Django Ninja 模块化单体 | 已实现并可运行 | 保留 | +| React/Vite/Tailwind/shadcn 前端底座 | 已实现并可构建 | 保留,继续做产品组件层 | +| 用户、用户组、角色、菜单权限、审计 | 有真实模型和 API 基础 | 补权限矩阵和关键操作覆盖 | +| 工单 | 已有创建、列表、详情、编辑、分类流转、评论、附件元数据、导出 | 补真实文件上传、协作和业务一致性 | +| 解决方案 | 已有维护、关联、采纳和工单处理结果 | 保留,统一权威数据来源 | +| 知识库 | 已有知识库、条目、切片和检索 API | 当前是 JSON 向量和确定性哈希检索,不能视为真实向量能力 | +| 模型、Agent、Skill、MCP | 前端存在类型和演示数据 | 尚无真实后端闭环 | +| Docker Compose | 有 PostgreSQL、Redis、MinIO、前后端和 worker 基础 | 补生产化配置、健康检查和恢复流程 | +| 自动化验证 | 后端 43 项测试、前端 6 项测试当前通过;前端可类型检查和构建 | 覆盖面仍不足以证明端到端可用 | + +当前需要正视的结构债务: + +- `Ticket.ai_solution` 把 AI 内容放回业务主表,与“AI 产物独立”原则冲突,应停止新增依赖并在后续迁移。 +- `Ticket.resolution_summary` 与 `TicketResolution` 存在权威来源重叠,需要明确实例处理结果以 `TicketResolution` 为准。 +- `KnowledgeChunk.embedding` 使用 JSON,检索在应用内遍历并限制前 1000 条,只适合功能桩,不适合真实知识检索。 +- 附件 API 当前记录存储路径和元数据,但不等于已经完成 MinIO 上传、解析、病毒/类型校验和访问控制。 +- 原型和概念页面可以作为设计资产,但不计入真实产品完成度;默认业务导航不得让用户误认演示操作已经持久化。 + +## 7. 目标架构 + +### 7.1 架构原则 + +- 继续使用模块化单体,先稳定领域边界,再根据容量和部署需求拆 worker。 +- 业务模块拥有业务事实,AI 模块拥有运行过程和生成产物。 +- 所有模型供应商差异收敛在模型网关,业务代码不直接调用供应商 SDK。 +- 检索、模型和工具调用都生成可追溯快照,避免源内容变化后无法还原当时依据。 +- 同步 API 负责短事务;文档解析、Embedding 和 Agent 长运行进入 Celery。 +- 权限校验以后端为最终边界,前端隐藏入口只用于体验。 + +### 7.2 逻辑架构 + +```mermaid +flowchart TB + UI["React 企业支持工作台"] --> API["Django Ninja API"] + + subgraph Business["业务资产域"] + T["tickets 工单"] + S["solutions 解决方案"] + K["knowledge 知识库"] + end + + subgraph Intelligence["智能能力域"] + M["modelhub 模型网关"] + A["agents 运行时与产物"] + SK["skills 配置型能力"] + TO["tools MCP 与工具调用"] + end + + subgraph Governance["企业治理域"] + AC["accounts RBAC"] + AU["audit 全链路审计"] + SY["system 系统配置"] + end + + API --> T + API --> S + API --> K + API --> A + A --> M + A --> SK + A --> TO + A --> K + A --> S + A -. "只生成草稿/建议" .-> T + AC --> API + AU --> T + AU --> A + AU --> TO + + A --> Q["Celery / Redis"] + K --> Q + T --> DB["PostgreSQL / pgvector"] + S --> DB + K --> DB + A --> DB + T --> OBJ["MinIO"] + K --> OBJ +``` + +### 7.3 模块边界 + +| Django app | 拥有的数据与职责 | 不应承担 | +|---|---|---| +| `accounts` | 用户、组、角色、权限、菜单授权 | 业务状态判断 | +| `tickets` | 工单、评论、附件、事件、协作人、处理结果 | 直接调用模型 | +| `solutions` | 可复用方案、版本、工单关联与采纳统计 | 保存某次 Agent 的完整输出 | +| `knowledge` | 知识库、文档、版本、条目、切片、向量、检索 | Agent 流程编排 | +| `modelhub` | 渠道、模型、能力、凭证、测试、调用用量 | 工单业务规则 | +| `agents` | Agent 定义版本、运行、步骤、产物、引用、人工决定 | 越过业务服务写正式数据 | +| `skills` | Prompt 模板、Schema、版本、资源范围 | 任意代码执行 | +| `tools` | MCP 服务、工具快照、授权、风险、调用记录 | 自动批准高风险操作 | +| `audit` | 统一审计事件和查询 | 代替领域事件 | +| `system` | 全局开关、字典和运行配置 | 存放模块私有配置 | + +## 8. 关键数据设计 + +### 8.1 标识策略 + +- 延续当前 `BigAutoField` 作为数据库内部主键,避免无业务收益的全表 UUID 改造。 +- 工单继续使用不可重复的 `ticket_no` 作为业务标识。 +- Agent run、外部回调和可分享资源增加 UUID `public_id`,不暴露可枚举内部主键。 +- 旧系统编号只存在迁移 staging 和映射表,不进入工单主表。 + +### 8.2 工单与知识 + +- `Ticket` 保持瘦主表,只保留身份、当前状态、分类、优先级、提出人、负责人和关键时间。 +- 评论、附件、协作人、事件和状态历史继续使用子表。 +- `TicketResolution` 是某次工单处理结果的权威记录。 +- `Solution` 是可复用知识资产;通过 `TicketSolutionLink` 表达推荐、引用、采纳或派生。 +- 知识文档必须有版本和处理状态;切片绑定文档版本,发布新版本不能静默改写历史 Agent 引用。 +- 向量字段改为 pgvector,记录 Embedding 模型、维度、内容哈希和生成时间。 + +### 8.3 AI 运行最小实体集 + +| 实体 | 作用 | +|---|---| +| `ModelProvider` | 渠道地址、协议、加密凭证和启用状态 | +| `ModelDeployment` | 模型名称、能力类型、上下文限制、默认用途和参数 | +| `AgentDefinition` / `AgentVersion` | 稳定身份与不可变运行配置快照 | +| `SkillDefinition` / `SkillVersion` | 配置型能力与版本化 Prompt/Schema | +| `AgentRun` | 触发人、业务上下文、状态、版本、耗时、错误和用量 | +| `AgentStep` | 模型、检索、Skill、工具或人工确认步骤 | +| `AgentArtifact` | 字段草稿、分析、排查清单、回复或知识草稿 | +| `AgentReference` | 引用的知识、方案、工单、文档切片及当时内容快照 | +| `ArtifactDecision` | 采纳、修改后采纳、拒绝及操作者 | +| `ToolCall` | 工具输入/输出摘要、状态、风险、耗时和授权依据 | + +Agent 运行必须绑定不可变的 Agent/Skill 版本和实际模型配置,不能只保存当前配置的外键,否则无法复盘历史结果。 + +## 9. Agent、Skill、MCP 运行边界 + +### 9.1 Agent + +第一版只提供三个系统内置 Agent,不提供可视化自由编排器: + +1. 建单 Agent:抽取字段、判断缺失信息、推荐分类和相似内容。 +2. 支持分析 Agent:生成问题摘要、原因假设、排查步骤、方案推荐和回复草稿。 +3. 知识沉淀 Agent:从已解决工单生成解决方案或知识草稿。 + +管理员可以选择模型、Skill、知识库和工具范围,但不能改变人工确认边界。 + +### 9.2 Skill + +Skill 是有版本的配置型专业能力,不是代码插件。每个 Skill 至少包含适用场景、Prompt 模板、输入 Schema、输出 Schema、允许的知识范围、允许的工具范围和测试样例。发布后的版本不可原地修改。 + +### 9.3 MCP + +- 第一版只允许只读工具进入自动调用白名单。 +- 每个工具同步后生成本地快照,并由管理员标注读写属性、风险等级和数据敏感级别。 +- Agent 只能调用同时满足“服务启用、工具启用、Agent 已绑定、用户有权限”的工具。 +- 无法确认读写属性的工具按高风险处理并阻止自动执行。 +- 写操作只保留接口和确认模型的设计,不进入首个试运行的真实执行范围。 + +## 10. 交付路线 + +本路线不按模块横向铺开,而按可演示、可测试、可度量的纵向业务切片推进。每一阶段通过退出门槛后再进入下一阶段;平台完整性是终点,不是第一批代码的组织方式。 + +### Phase 0:基线收敛 + +目标:把当前真实能力、原型资产和结构债务分开,形成可持续演进的干净基线。 + +交付: + +- 本蓝图成为唯一主蓝图,旧阶段文档只作 Git 历史参考。 +- 盘点并处理当前工作树中的原型、设计实验和依赖变更。 +- 建立“真实 / 演示 / 未实现”能力清单,演示页面有统一边界。 +- 确认 `TicketResolution`、AI 产物和知识版本的权威数据规则。 +- 建立后端、前端和端到端测试命令;为核心接口生成稳定契约。 +- 补充架构决策记录:主键、Vite、模块化单体、人工确认、检索实现。 + +退出门槛: + +- 默认产品路径不把 Mock 操作表现为真实成功。 +- 当前真实业务回归通过,迁移可在全新数据库执行。 +- 数据债务有明确迁移顺序,后续阶段不再向 `Ticket.ai_solution` 写新逻辑。 + +### Phase 1:可信建单闭环 + +目标:交付第一个真实 AI 业务闭环,并由它长出最小模型网关和 Agent 运行时。 + +交付: + +- 最小模型渠道、模型配置、加密凭证和连通性测试。 +- `AgentRun`、步骤、产物、引用和人工决定的最小数据模型。 +- 建单 Agent:自然语言输入 -> 字段草稿 -> 缺失信息 -> 用户确认 -> 创建工单。 +- AI 失败、超时或输出 Schema 不合法时回退到普通建单。 +- 工单事件和审计记录用户采纳了哪些 AI 字段。 + +退出门槛: + +- 至少一个真实 OpenAI-compatible 渠道可完成建单分析。 +- 未经确认不能创建或修改工单。 +- 同一确认请求具备幂等性,不会重复建单。 +- 正常、拒绝、人工修改、模型失败和超时路径均有自动化测试。 + +### Demo Milestone:AI 平台配置演示版 + +该里程碑位于 Phase 0 与 Phase 1 之间,是面向研发管理员的真实运行验证,不等同于业务试运行版本。它用于提前验证模型渠道、配置型 Skill、版本化 Agent 和异步执行轨迹能否形成稳定技术底座,完成后仍按 Phase 1 接入可信建单闭环。 + +交付: + +- 一个真实 OpenAI-compatible 文本模型渠道和模型测试能力。 +- Skill 草稿、发布版本、JSON Schema、受限 Prompt 模板和回滚。 +- Agent 草稿、发布版本、有序 Skill 步骤、沙箱测试和回滚。 +- Celery 异步运行、步骤轨迹、结构修复、一次瞬时重试、尽力取消和 90 天留存。 +- 模型、Skill、Agent 和运行记录的研发管理员工作台。 +- 预置技术支持分析 Agent,完成问题结构化、排查建议和客户回复草稿三步演示。 + +退出门槛: + +- 研发管理员可从模型配置开始,完成 Agent 发布、真实运行和轨迹查看。 +- 发布版本不可变,历史运行能够还原实际 Agent、Skill、模型和参数快照。 +- 凭证加密、权限、审计、失败终止、重试和取消路径通过自动化测试。 +- 演示页面与真实页面边界明确,不把 Mock 数据声明为真实运行结果。 + +### Phase 2:支持分析工作区 + +目标:让技术支持在真实工单详情中获得有依据、可采纳的分析,而不是使用独立聊天页。 + +交付: + +- PostgreSQL/pgvector 与真实 Embedding 模型接入。 +- 知识、解决方案和历史工单的混合检索及引用展示。 +- 支持分析 Agent:摘要、原因假设、排查步骤、方案推荐和客户回复草稿。 +- 运行轨迹、引用、耗时、Token/费用和错误详情。 +- 采纳、修改后采纳、拒绝以及关联解决方案的完整记录。 + +退出门槛: + +- 支持人员能从一张真实工单完成“分析 -> 查依据 -> 采纳 -> 回复/解决”。 +- 每条可采纳结论至少能展示来源,无法提供来源时明确标为模型推断。 +- 检索或模型失败不阻断人工处理。 +- 建立离线样例集,能比较调整前后的检索和建议效果。 + +### Phase 3:知识飞轮 + +目标:把一次性处理结果转化为经过审核、可持续维护的业务资产。 + +交付: + +- MinIO 真实文件上传、访问控制、解析状态和失败重试。 +- 文档版本、解析、切片、Embedding、重建索引和下线流程。 +- 知识沉淀 Agent 从已解决工单生成草稿。 +- 知识/方案审核、发布、驳回、版本和来源链路。 +- 重复方案检测、过期内容标记和被引用影响提示。 + +退出门槛: + +- 已解决工单可生成草稿,但未经审核不可进入正式检索范围。 +- 任一检索结果可回到原文档版本、方案或工单。 +- 文档更新、重建索引和回滚不会破坏历史 Agent 引用。 + +### Phase 4:平台配置与 MCP 可信调用 + +目标:在三个内置业务闭环已经稳定后,开放可配置能力和外部只读工具。 + +交付: + +- 模型能力注册、用途绑定、参数模板、测试和用量统计。 +- Agent/Skill 版本、发布、回滚和测试样例管理。 +- MCP 服务注册、认证配置、连接检查、工具同步和本地快照。 +- Agent 工具授权矩阵、只读调用、敏感字段脱敏和全量审计。 +- 高风险、未授权、定义变化和服务不可用时的阻止策略。 + +退出门槛: + +- 不改业务代码即可为内置 Agent 切换已授权模型或 Skill 版本。 +- 至少一个真实只读 MCP 工具能在授权范围内被支持分析 Agent 调用。 +- 未授权、非只读和高风险调用均被后端拦截并留下审计记录。 + +### Phase 5:企业试运行与历史迁移 + +目标:达到单企业私有化试运行标准,并导入对检索有价值的历史工单。 + +交付: + +- 全新环境一键初始化、迁移、管理员和预置角色创建。 +- 健康检查、结构化日志、任务监控、告警、备份恢复和升级回滚。 +- 密钥轮换、日志脱敏、文件安全、权限矩阵和安全检查。 +- 历史工单 staging、清洗、映射、批次校验和失败重跑。 +- 关键用户旅程的端到端验收、使用说明和已知限制。 + +退出门槛: + +- 新环境可按文档独立部署并通过验收。 +- 完成一次数据库与对象存储备份恢复演练。 +- 历史迁移可重复执行且不污染正式工单模型。 +- 核心业务、三类 Agent 和只读 MCP 均有可追溯验收记录。 + +## 11. 阶段依赖与并行边界 + +```mermaid +flowchart LR + P0["Phase 0 基线收敛"] --> P1["Phase 1 可信建单"] + P1 --> P2["Phase 2 支持分析"] + P2 --> P3["Phase 3 知识飞轮"] + P3 --> P4["Phase 4 平台配置与 MCP"] + P4 --> P5["Phase 5 企业试运行"] +``` + +在不改变阶段退出门槛的前提下,可并行的工作只有: + +- 设计系统和通用交互组件可以跟随每个纵向切片同步演进。 +- 运维基线、安全检查和历史数据画像可以提前准备,但正式迁移在数据模型稳定后进行。 +- 知识样例集可在 Phase 1 开始整理,真实 pgvector 和发布链路分别在 Phase 2、Phase 3 落地。 + +模型配置台、Skill 管理台和 Agent 管理台只在 Demo Milestone 的真实运行链路内建设;MCP 管理台不提前展开。完成演示后,平台配置能力必须进入 Phase 1 的真实建单场景,而不是继续横向扩张。 + +## 12. 产品信息架构 + +首个试运行版本采用任务导向导航: + +| 一级导航 | 页面 | +|---|---| +| 工作台 | 我的待办、待回复、即将超时、近期 Agent 异常 | +| 工单 | 工单列表、创建工单、工单详情 | +| 知识 | 统一检索、知识库、知识条目、文档、待审核草稿 | +| 解决方案 | 方案列表、详情、版本、引用工单 | +| AI 能力 | Agent、运行记录、Skill、模型渠道、模型测试 | +| 工具 | MCP 服务、工具授权、调用记录 | +| 系统管理 | 用户、用户组、角色、菜单、审计、系统设置 | + +AI 不单独占据主要工作流:建单辅助出现在创建工单页,支持分析出现在工单详情,知识沉淀出现在解决/关闭后的操作区。管理页面只负责配置和诊断。 + +## 13. API、异步任务与错误约定 + +- 所有写接口使用明确动作语义,例如 `confirm-draft`、`adopt-artifact`,不通过通用 PATCH 隐式触发 AI 或状态变化。 +- 状态流转、方案采纳、知识发布和 AI 产物采纳均使用数据库事务和幂等键。 +- Agent 长运行返回 run id;前端轮询状态作为第一版基线,确有实时体验需求再引入 SSE。 +- 异步任务至少记录 `queued/running/succeeded/failed/cancelled/timed_out`。 +- 供应商错误统一映射为平台错误码,原始敏感响应仅进入受控诊断日志。 +- OpenAPI 是前后端契约来源;正式页面不得长期手写一套与后端脱离的重复类型。 + +## 14. 安全与可信边界 + +- 模型和 MCP 凭证加密保存,前端仅显示脱敏值,审计日志不得记录明文。 +- 传给外部模型或工具的数据在运行前经过字段级策略和脱敏处理。 +- Prompt、知识内容和工具返回都视为不可信输入,不能直接转化为业务操作。 +- Agent 运行使用触发用户的权限上下文,不因系统账号执行而扩大访问范围。 +- 引用内容、模型输出和人工编辑在界面与数据中保持来源区分。 +- AI 输出不得成为状态流转的唯一校验条件。 +- 附件需校验大小、类型和访问权限;恶意文件扫描作为试运行安全门槛。 + +## 15. 质量门禁 + +每个 Phase 必须同时满足: + +- 后端单元/接口测试通过,Ruff 和迁移检查通过。 +- 前端测试、类型检查、Lint 和生产构建通过。 +- 核心用户旅程有跨端测试,不以页面能打开代替闭环完成。 +- 新增权限点有预置角色矩阵和越权测试。 +- 新增 AI 能力有成功、失败、超时、非法结构、拒绝和降级测试。 +- 数据迁移可在全新数据库执行,回滚或向前修复策略明确。 +- README、蓝图、接口说明和运行手册与代码状态一致。 + +“已完成”的定义必须标注层级: + +| 标记 | 含义 | +|---|---| +| 设计 | 只有设计或交互稿 | +| 演示 | 使用 Mock/本地桩,可展示但不持久化 | +| 集成 | 已接真实 API/模型,但尚未通过完整验收 | +| 可试运行 | 通过权限、审计、失败降级和端到端验收 | + +## 16. 立即执行的下一迭代 + +下一迭代先完成 Phase 0,再实施研发管理员使用的 AI 平台配置演示里程碑。该演示只包含模型、Agent 和 Skill,不启动 MCP,也不计为业务闭环完成。 + +建议任务顺序: + +1. 将当前工作树中的产品概念实验、依赖升级和文档删除整理成可审查的变更集。 +2. 建立 `docs/product/capability-status.md`,逐项标注真实、演示、缺失和债务。 +3. 写出 `TicketResolution`、`AgentArtifact`、知识版本和引用快照的数据决策记录。 +4. 禁止新增代码读取或写入 `Ticket.ai_solution`,设计兼容迁移。 +5. 为工单创建、状态流转、方案采纳和知识检索补最小跨端回归。 +6. 固化全新数据库迁移、后端测试、前端测试/类型检查/Lint/构建命令。 +7. 完成 AI 平台配置演示里程碑后,更新能力状态并编写 Phase 1 可信建单闭环的实施计划。 + +AI 平台配置演示只验证真实运行底座,不建设通用 Agent 平台。其后下一份实施文档必须是“可信建单闭环设计”。 + +## 17. 已决策事项 + +| 事项 | 决策 | +|---|---| +| 产品路线 | 新产品替代旧系统,不做旧系统 AI 外挂 | +| 部署 | 单企业私有化,不做多租户 | +| 架构 | Django 模块化单体 + 异步 worker,不提前微服务化 | +| 前端 | 保留 React + Vite;当前无 SSR/SEO 需求,不改 Next.js | +| 主键 | 内部 bigint;工单使用业务编号;外部关联场景增加 UUID public id | +| 工作流 | 领域服务中的有限状态机,不引入 BPMN | +| Agent | 三类内置 Agent,默认只生成建议和草稿 | +| Skill | 只做配置型、版本化 Skill | +| MCP | 只读优先,未明确只读即阻止自动执行 | +| 向量检索 | PostgreSQL + pgvector,当前 JSON 哈希向量仅视为临时桩 | +| 历史数据 | 只迁历史工单,经 staging 清洗,不污染正式模型 | + +## 18. 仍需在对应阶段确认 + +以下问题不阻塞 Phase 0,但必须在指定阶段开始前形成决策记录: + +| 问题 | 最晚时机 | +|---|---| +| 首个真实模型渠道、数据出境约束和脱敏规则 | Phase 1 前 | +| 图片/附件进入视觉模型的文件范围和保留策略 | Phase 1 前 | +| Embedding 模型、维度、中文检索评测集和重建成本 | Phase 2 前 | +| 知识审核角色、发布 SLA 和过期策略 | Phase 3 前 | +| 首个只读 MCP 服务、认证方式和工具风险责任人 | Phase 4 前 | +| 历史工单规模、质量、字段映射和脱敏要求 | Phase 5 前 | + +## 19. 蓝图维护规则 + +- 本文描述跨阶段目标、边界、依赖和完成定义,不写文件级编码步骤。 +- 每次只为当前 Phase 新建设计与实施计划;未开始阶段不提前拆成大量任务。 +- 产品范围或关键架构变化先更新本文,再修改实施计划。 +- 已完成阶段的事实写入能力状态文档,讨论过程保留在沟通记录或 Git 历史中。 +- 如果代码、README 和本文冲突,以已验证代码事实为现状,以本文为后续方向,并立即登记差距。 diff --git a/docs/product/capability-status.md b/docs/product/capability-status.md new file mode 100644 index 0000000..44ef539 --- /dev/null +++ b/docs/product/capability-status.md @@ -0,0 +1,41 @@ +# AI 工单平台能力状态 + +更新日期:2026-07-14 + +状态定义: + +| 状态 | 含义 | +|---|---| +| 真实 | 已接真实数据库或服务,具备自动化测试 | +| 演示 | 使用 Mock、确定性桩或独立概念页面 | +| 缺失 | 尚无可用实现 | +| 债务 | 已有实现与目标架构存在明确差距 | + +## 当前能力 + +| 领域 | 能力 | 状态 | 说明 | +|---|---|---|---| +| 企业治理 | 登录、用户、用户组、角色、菜单权限 | 真实 | Django 权限与菜单 API 已实现 | +| 企业治理 | 操作审计 | 真实 | 已覆盖部分治理和工单操作,AI 审计待补 | +| 工单 | 创建、查询、编辑、分类状态流转、评论、导出 | 真实 | 后端与前端均使用真实 API | +| 工单 | 附件 | 债务 | 当前仅记录附件元数据和存储路径,尚无真实对象上传 | +| 工单 | AI 建议 | 债务 | `Ticket.ai_solution` 位于业务主表,后续停止新增依赖 | +| 解决方案 | 维护、关联、采纳并关闭工单 | 真实 | 已有 API、事件和使用次数统计 | +| 知识 | 知识库、条目、文档文本、切片和检索 | 真实 | 基础 CRUD 与检索 API 可运行 | +| 知识 | 向量检索 | 演示 | 使用 JSON 哈希向量和应用内排序,不是 pgvector/真实 Embedding | +| AI 平台 | 模型渠道和真实模型测试 | 缺失 | 本次演示里程碑实施 | +| AI 平台 | Skill 草稿、发布、Schema 和回滚 | 缺失 | 本次演示里程碑实施 | +| AI 平台 | Agent 版本、有序执行和运行轨迹 | 缺失 | 本次演示里程碑实施 | +| AI 平台 | MCP | 缺失 | 不在本次演示范围 | +| 前端 | 工单工作台概念方案 | 演示 | 保留在 `/concept` 设计实验室,不计为真实功能 | +| 部署 | PostgreSQL、Redis、MinIO 本地基础设施 | 真实 | `infra/docker-compose.local.yml` | +| 部署 | Celery worker | 债务 | 配置存在但尚无真实任务,本次演示里程碑补齐 | + +## 当前质量基线 + +- 后端测试:43 项通过。 +- 前端测试:6 项通过。 +- 前端 TypeScript 类型检查和生产构建通过。 +- 前端生产包存在体积告警,后续管理页面接入时需要按路由拆分评估。 + +能力状态只依据已验证代码更新。页面存在、类型定义存在或 Mock 可交互,不视为真实能力。 diff --git a/docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md b/docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md deleted file mode 100644 index d0c85a5..0000000 --- a/docs/superpowers/plans/2026-06-30-ai-ticket-platform-implementation-roadmap.md +++ /dev/null @@ -1,100 +0,0 @@ -# AI Ticket Platform Implementation Roadmap - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:writing-plans to expand each stage into a detailed implementation plan before writing code. Use superpowers:subagent-driven-development or superpowers:executing-plans when executing a detailed stage plan. - -**Goal:** Break the AI-native technical support ticket platform into independently deliverable implementation stages. - -**Architecture:** Build a modular monolith first. Keep Django apps, React feature modules, workers, model gateway, Agent runtime, and MCP integration separated by clear interfaces so heavily loaded areas can be split later. - -**Tech Stack:** Django, Django Ninja, PostgreSQL, pgvector, Redis, Celery, MinIO, React, Vite, Tailwind CSS, shadcn/ui, Radix UI, TanStack Table, OpenAI-compatible model adapter. - ---- - -## Source Spec - -- `docs/superpowers/specs/2026-06-30-ai-ticket-platform-design.md` - -## Planning Rule - -The product contains multiple independent subsystems. Do not implement the entire spec from one plan. Use this roadmap to create one detailed plan per stage. Each detailed plan must produce working, testable software. - -## Stage Breakdown - -| Stage | Name | Purpose | Depends On | Detailed Plan | -|---|---|---|---|---| -| 0 | Foundation and design system | Create the repo skeleton, local runtime, auth/RBAC baseline, audit baseline, frontend theme and component standards. | None | `docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md` | -| 1 | Ticket workflow and reusable solutions | Implement ticket lifecycle, comments, attachments, events, export, solution library, and solution links. | Stage 0 | Create after Stage 0 passes. | -| 1.5 | shadcn enterprise UI foundation | Formalize shadcn/ui initialization, add a project-owned App component layer, redesign current ticket and solution pages, and enforce Chinese frontend text. | Stage 1 | `docs/superpowers/specs/2026-07-01-stage-1-5-shadcn-ui-foundation-design.md` | -| 1.6 | Enterprise governance and login | Add login/logout/current-user APIs, user/user-group/role/menu management, API permission enforcement, permission-driven frontend menus, and deployment bootstrap. | Stage 1.5 | `docs/superpowers/specs/2026-07-01-stage-1-6-enterprise-governance-design.md` | -| 2 | Knowledge base and vector retrieval | Implement knowledge bases, documents, parsing jobs, chunks, embeddings, and retrieval. | Stage 0, partial Stage 1 | Create after Stage 1 model contracts stabilize. | -| 3 | Model gateway and Agent/Skill runtime | Implement provider configs, model configs, model testing, Skill configs, built-in Agents, runs, steps, AI artifacts. | Stage 0, Stage 1, Stage 2 retrieval interfaces | Create after Stage 2 retrieval contracts exist. | -| 4 | MCP tools and trust boundary | Implement MCP server registration, tool sync, risk levels, Agent tool binding, tool calls, confirmation, audit. | Stage 3 | Create after Agent runtime exists. | -| 5 | Historical ticket migration and polish | Import historical tickets, standardize fields, vectorize, generate solution drafts, add dashboards and UI polish. | Stage 1, Stage 2, Stage 3 | Create after core flows work. | - -## Stage 0 Deliverables - -| Area | Deliverable | -|---|---| -| Backend | Django project, settings split, Django Ninja API root, health endpoint, accounts app, audit app, system app. | -| Frontend | Vite React project, Tailwind, shadcn/Radix base, theme tokens, layout shell, component preview page. | -| Runtime | Docker Compose with backend, frontend, PostgreSQL, Redis, MinIO, Celery worker, Celery beat. | -| Governance | Login baseline, users/groups/roles baseline, menu definitions, permission seed command, audit model. | -| Quality | pytest, ruff, mypy baseline, frontend lint/typecheck/build, pre-commit optional. | -| Documentation | `.env.example`, local startup guide, design system notes. | - -## Stage 1 Deliverables - -| Area | Deliverable | -|---|---| -| Ticket core | `ticket`, `ticket_category`, `ticket_comment`, `ticket_attachment`, `ticket_event`, `ticket_collaborator`. | -| Ticket APIs | Create/list/detail/update/status transition/comment/upload/export. | -| Ticket UI | List page, create page, detail page, status timeline, comments, attachments. | -| Solutions | `solution`, `ticket_resolution`, `ticket_solution_link`, solution list/detail/linking. | -| Audit | Ticket operations written to audit/event tables. | - -## Stage 2 Deliverables - -| Area | Deliverable | -|---|---| -| Knowledge | `knowledge_base`, `knowledge_document`, `knowledge_item`, `knowledge_chunk`. | -| Jobs | Document parsing, chunking, embedding enqueue/retry/status. | -| Retrieval | Keyword search plus vector search interface. | -| UI | Knowledge base, document status, knowledge item editor, search page. | - -## Stage 3 Deliverables - -| Area | Deliverable | -|---|---| -| Model gateway | `model_provider`, `model_config`, encrypted secrets, OpenAI-compatible client adapter. | -| Model testing | Single model test and multi-model comparison UI. | -| Skills | `skill_definition`, prompt template rendering, input/output schema validation. | -| Agents | `agent_definition`, `agent_run`, `agent_step`, `ticket_ai_artifact`. | -| Built-ins | Ticket creation Agent, support analysis Agent, knowledge distillation Agent. | - -## Stage 4 Deliverables - -| Area | Deliverable | -|---|---| -| MCP config | `tool_server`, `tool_definition`, `tool_call`. | -| Sync | Tool discovery/sync from configured MCP server. | -| Risk control | low/medium/high risk, human confirmation for high risk. | -| Agent integration | Authorized tool calls appear in `agent_step` and `tool_call`. | - -## Stage 5 Deliverables - -| Area | Deliverable | -|---|---| -| Migration | Import script and staging model for historical tickets. | -| AI cleanup | Summaries, categories, error codes, solution draft generation. | -| Retrieval | Historical tickets available for similar-ticket search. | -| Dashboards | Ticket trends, high-frequency problems, solution usage, Agent usage. | - -## Cross-Stage Invariants - -- Business writes generated by AI require user confirmation by default. -- Secrets are encrypted at rest. -- Raw model/tool payloads stored in logs must support masking. -- No business page may define one-off colors, status badges, or table styling. -- Every backend stage includes API tests. -- Every frontend stage includes typecheck and build verification. -- Commit after each coherent task. diff --git a/docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md b/docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md deleted file mode 100644 index 1c2fb36..0000000 --- a/docs/superpowers/plans/2026-06-30-stage-0-foundation-design-system.md +++ /dev/null @@ -1,1585 +0,0 @@ -# Stage 0 Foundation And Design System 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:** Build the initial runnable project foundation: backend, frontend, local runtime, RBAC/audit baseline, and frontend design system standards. - -**Architecture:** Use a modular monorepo with `backend/`, `frontend/`, `infra/`, and `docs/`. The backend is a Django modular monolith with Django Ninja APIs and dedicated apps for accounts, audit, and system configuration. The frontend is Vite React with Tailwind/shadcn-style primitives wrapped by product-level components. - -**Tech Stack:** Python 3.12, Django, Django Ninja, PostgreSQL, Redis, Celery, MinIO, pytest, ruff, mypy, React, Vite, TypeScript, Tailwind CSS, Radix UI, lucide-react, TanStack Table, React Hook Form, Zod. - ---- - -## Files To Create - -```text -backend/ - pyproject.toml - manage.py - config/ - __init__.py - settings/ - __init__.py - base.py - local.py - test.py - urls.py - asgi.py - wsgi.py - celery.py - apps/ - __init__.py - accounts/ - __init__.py - apps.py - models.py - schemas.py - api.py - permissions.py - management/ - __init__.py - commands/ - __init__.py - seed_permissions.py - tests/ - __init__.py - test_permissions_seed.py - audit/ - __init__.py - apps.py - models.py - api.py - tests/ - __init__.py - test_audit_api.py - system/ - __init__.py - apps.py - api.py - tests/ - __init__.py - test_health_api.py - tests/ - __init__.py - conftest.py -frontend/ - package.json - tsconfig.json - tsconfig.node.json - vite.config.ts - index.html - tailwind.config.ts - postcss.config.js - src/ - main.tsx - app/ - App.tsx - router.tsx - layout/ - AppShell.tsx - Sidebar.tsx - Topbar.tsx - components/ - ui/ - button.tsx - badge.tsx - card.tsx - app/ - AppPageHeader.tsx - AppStatusBadge.tsx - AppPriorityBadge.tsx - features/ - design-system/ - DesignSystemPreview.tsx - lib/ - cn.ts - styles/ - globals.css -infra/ - docker-compose.yml - backend.Dockerfile - frontend.Dockerfile -.env.example -docs/development/local-setup.md -docs/frontend/design-system.md -``` - -## Task 1: Create Backend Project Skeleton - -**Files:** -- Create: `backend/pyproject.toml` -- Create: `backend/manage.py` -- Create: `backend/config/settings/base.py` -- Create: `backend/config/settings/local.py` -- Create: `backend/config/settings/test.py` -- Create: `backend/config/urls.py` -- Create: `backend/config/asgi.py` -- Create: `backend/config/wsgi.py` -- Create: `backend/config/celery.py` -- Create: `backend/apps/system/api.py` -- Test: `backend/apps/system/tests/test_health_api.py` - -- [ ] **Step 1: Create the backend dependency manifest** - -Create `backend/pyproject.toml`: - -```toml -[project] -name = "ai-ticket-platform-backend" -version = "0.1.0" -description = "Backend for AI-native technical support ticket platform" -requires-python = ">=3.12" -dependencies = [ - "Django>=5.0,<5.3", - "django-ninja>=1.3,<2.0", - "psycopg[binary]>=3.2,<4.0", - "celery>=5.4,<6.0", - "redis>=5.0,<6.0", - "django-cors-headers>=4.4,<5.0", - "pydantic>=2.8,<3.0", - "python-dotenv>=1.0,<2.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=8.2,<9.0", - "pytest-django>=4.8,<5.0", - "ruff>=0.6,<1.0", - "mypy>=1.11,<2.0", - "django-stubs[compatible-mypy]>=5.0,<6.0", -] - -[tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "config.settings.test" -python_files = ["test_*.py", "*_test.py"] -testpaths = ["apps", "tests"] - -[tool.ruff] -line-length = 100 -target-version = "py312" - -[tool.ruff.lint] -select = ["E", "F", "I", "UP", "B"] - -[tool.mypy] -python_version = "3.12" -plugins = ["mypy_django_plugin.main"] -ignore_missing_imports = true -strict_optional = true - -[tool.django-stubs] -django_settings_module = "config.settings.test" -``` - -- [ ] **Step 2: Create Django entry points** - -Create `backend/manage.py`: - -```python -#!/usr/bin/env python -import os -import sys - - -def main() -> None: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() -``` - -Create `backend/config/asgi.py`: - -```python -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - -application = get_asgi_application() -``` - -Create `backend/config/wsgi.py`: - -```python -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - -application = get_wsgi_application() -``` - -Create `backend/config/celery.py`: - -```python -import os - -from celery import Celery - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - -app = Celery("ai_ticket_platform") -app.config_from_object("django.conf:settings", namespace="CELERY") -app.autodiscover_tasks() -``` - -- [ ] **Step 3: Create settings** - -Create `backend/config/settings/base.py`: - -```python -from pathlib import Path -import os - -BASE_DIR = Path(__file__).resolve().parents[3] - -SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-me") -DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() == "true" -ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "corsheaders", - "apps.accounts", - "apps.audit", - "apps.system", -] - -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "config.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - } -] - -WSGI_APPLICATION = "config.wsgi.application" -ASGI_APPLICATION = "config.asgi.application" - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.getenv("POSTGRES_DB", "ai_ticket_platform"), - "USER": os.getenv("POSTGRES_USER", "ai_ticket"), - "PASSWORD": os.getenv("POSTGRES_PASSWORD", "ai_ticket"), - "HOST": os.getenv("POSTGRES_HOST", "localhost"), - "PORT": int(os.getenv("POSTGRES_PORT", "5432")), - } -} - -LANGUAGE_CODE = "zh-hans" -TIME_ZONE = "Asia/Shanghai" -USE_I18N = True -USE_TZ = True - -STATIC_URL = "static/" -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -CORS_ALLOWED_ORIGINS = os.getenv( - "CORS_ALLOWED_ORIGINS", - "http://localhost:5173", -).split(",") - -CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") -CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/1") -``` - -Create `backend/config/settings/local.py`: - -```python -from .base import * # noqa: F403 - -DEBUG = True -``` - -Create `backend/config/settings/test.py`: - -```python -from .base import * # noqa: F403 - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": ":memory:", - } -} - -PASSWORD_HASHERS = [ - "django.contrib.auth.hashers.MD5PasswordHasher", -] -``` - -- [ ] **Step 4: Create API root and health endpoint** - -Create `backend/apps/system/api.py`: - -```python -from ninja import Router - -router = Router(tags=["system"]) - - -@router.get("/health") -def health(request): - return {"status": "ok"} -``` - -Create `backend/config/urls.py`: - -```python -from django.contrib import admin -from django.urls import path -from ninja import NinjaAPI - -from apps.system.api import router as system_router - -api = NinjaAPI(title="AI Ticket Platform API", version="0.1.0") -api.add_router("/system", system_router) - -urlpatterns = [ - path("admin/", admin.site.urls), - path("api/", api.urls), -] -``` - -- [ ] **Step 5: Write the failing health API test** - -Create `backend/apps/system/tests/test_health_api.py`: - -```python -def test_health_endpoint_returns_ok(client): - response = client.get("/api/system/health") - - assert response.status_code == 200 - assert response.json() == {"status": "ok"} -``` - -- [ ] **Step 6: Run the backend test** - -Run: - -```bash -cd backend -python -m pip install -e ".[dev]" -pytest apps/system/tests/test_health_api.py -v -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add backend -git commit -m "feat: add backend foundation" -``` - -## Task 2: Add Accounts Permission Seed Baseline - -**Files:** -- Create: `backend/apps/accounts/apps.py` -- Create: `backend/apps/accounts/models.py` -- Create: `backend/apps/accounts/permissions.py` -- Create: `backend/apps/accounts/management/commands/seed_permissions.py` -- Test: `backend/apps/accounts/tests/test_permissions_seed.py` - -- [ ] **Step 1: Create accounts app config and empty models** - -Create `backend/apps/accounts/apps.py`: - -```python -from django.apps import AppConfig - - -class AccountsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.accounts" -``` - -Create `backend/apps/accounts/models.py`: - -```python -# Use Django's built-in User, Group, and Permission models for MVP RBAC. -``` - -- [ ] **Step 2: Define permission catalog** - -Create `backend/apps/accounts/permissions.py`: - -```python -from dataclasses import dataclass - - -@dataclass(frozen=True) -class PermissionSpec: - codename: str - name: str - - -PERMISSIONS: tuple[PermissionSpec, ...] = ( - PermissionSpec("ticket.view", "View tickets"), - PermissionSpec("ticket.create", "Create tickets"), - PermissionSpec("ticket.edit", "Edit tickets"), - PermissionSpec("ticket.export", "Export tickets"), - PermissionSpec("ticket.assign", "Assign tickets"), - PermissionSpec("ticket.close", "Close tickets"), - PermissionSpec("knowledge.view", "View knowledge"), - PermissionSpec("knowledge.manage", "Manage knowledge"), - PermissionSpec("solution.view", "View solutions"), - PermissionSpec("solution.manage", "Manage solutions"), - PermissionSpec("agent.use", "Use Agents"), - PermissionSpec("agent.manage", "Manage Agents"), - PermissionSpec("model.manage", "Manage model configs"), - PermissionSpec("tool.manage", "Manage MCP tools"), - PermissionSpec("audit.view", "View audit logs"), - PermissionSpec("system.manage", "Manage system settings"), -) - -DEFAULT_GROUPS: dict[str, tuple[str, ...]] = { - "System Admin": tuple(permission.codename for permission in PERMISSIONS), - "Knowledge Admin": ( - "ticket.view", - "knowledge.view", - "knowledge.manage", - "solution.view", - "solution.manage", - "agent.use", - ), - "Support Engineer": ( - "ticket.view", - "ticket.create", - "ticket.edit", - "ticket.assign", - "ticket.close", - "knowledge.view", - "solution.view", - "agent.use", - ), - "Requester": ( - "ticket.view", - "ticket.create", - "knowledge.view", - "solution.view", - ), - "Auditor": ( - "ticket.view", - "knowledge.view", - "solution.view", - "audit.view", - ), -} -``` - -- [ ] **Step 3: Create permission seed command** - -Create `backend/apps/accounts/management/commands/seed_permissions.py`: - -```python -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.core.management.base import BaseCommand - -from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS - - -class Command(BaseCommand): - help = "Seed MVP permissions and default user groups." - - def handle(self, *args, **options): - content_type, _ = ContentType.objects.get_or_create( - app_label="system", - model="platformpermission", - ) - - permissions_by_codename: dict[str, Permission] = {} - for spec in PERMISSIONS: - permission, _ = Permission.objects.get_or_create( - content_type=content_type, - codename=spec.codename, - defaults={"name": spec.name}, - ) - if permission.name != spec.name: - permission.name = spec.name - permission.save(update_fields=["name"]) - permissions_by_codename[spec.codename] = permission - - for group_name, codenames in DEFAULT_GROUPS.items(): - group, _ = Group.objects.get_or_create(name=group_name) - group.permissions.set(permissions_by_codename[codename] for codename in codenames) - - self.stdout.write(self.style.SUCCESS("Seeded permissions and groups.")) -``` - -- [ ] **Step 4: Write the seed command test** - -Create `backend/apps/accounts/tests/test_permissions_seed.py`: - -```python -from django.contrib.auth.models import Group, Permission -from django.core.management import call_command - -from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS - - -def test_seed_permissions_creates_permissions_and_groups(db): - call_command("seed_permissions") - - assert Permission.objects.filter( - codename__in=[permission.codename for permission in PERMISSIONS] - ).count() == len(PERMISSIONS) - - for group_name, codenames in DEFAULT_GROUPS.items(): - group = Group.objects.get(name=group_name) - actual = set(group.permissions.values_list("codename", flat=True)) - assert actual == set(codenames) -``` - -- [ ] **Step 5: Run the accounts test** - -Run: - -```bash -cd backend -pytest apps/accounts/tests/test_permissions_seed.py -v -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add backend/apps/accounts -git commit -m "feat: seed baseline RBAC permissions" -``` - -## Task 3: Add Audit Model And API - -**Files:** -- Create: `backend/apps/audit/apps.py` -- Create: `backend/apps/audit/models.py` -- Create: `backend/apps/audit/api.py` -- Modify: `backend/config/urls.py` -- Test: `backend/apps/audit/tests/test_audit_api.py` - -- [ ] **Step 1: Create audit app config** - -Create `backend/apps/audit/apps.py`: - -```python -from django.apps import AppConfig - - -class AuditConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.audit" -``` - -- [ ] **Step 2: Create operation audit model** - -Create `backend/apps/audit/models.py`: - -```python -from django.conf import settings -from django.db import models - - -class OperationAuditLog(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="operation_audit_logs", - ) - module = models.CharField(max_length=80) - action = models.CharField(max_length=80) - target_type = models.CharField(max_length=80) - target_id = models.CharField(max_length=120, blank=True) - request_snapshot = models.JSONField(default=dict, blank=True) - result = models.CharField(max_length=20) - ip_address = models.GenericIPAddressField(null=True, blank=True) - user_agent = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ["-created_at"] - indexes = [ - models.Index(fields=["module", "action"]), - models.Index(fields=["target_type", "target_id"]), - models.Index(fields=["created_at"]), - ] - - def __str__(self) -> str: - return f"{self.module}.{self.action}:{self.result}" -``` - -- [ ] **Step 3: Create read-only audit API** - -Create `backend/apps/audit/api.py`: - -```python -from datetime import datetime -from typing import Any - -from ninja import Router, Schema - -from apps.audit.models import OperationAuditLog - -router = Router(tags=["audit"]) - - -class OperationAuditLogOut(Schema): - id: int - module: str - action: str - target_type: str - target_id: str - request_snapshot: dict[str, Any] - result: str - ip_address: str | None - user_agent: str - created_at: datetime - - -@router.get("/operation-logs", response=list[OperationAuditLogOut]) -def list_operation_logs(request): - return OperationAuditLog.objects.all()[:100] -``` - -Modify `backend/config/urls.py` to include the audit router: - -```python -from django.contrib import admin -from django.urls import path -from ninja import NinjaAPI - -from apps.audit.api import router as audit_router -from apps.system.api import router as system_router - -api = NinjaAPI(title="AI Ticket Platform API", version="0.1.0") -api.add_router("/system", system_router) -api.add_router("/audit", audit_router) - -urlpatterns = [ - path("admin/", admin.site.urls), - path("api/", api.urls), -] -``` - -- [ ] **Step 4: Write the audit API test** - -Create `backend/apps/audit/tests/test_audit_api.py`: - -```python -from apps.audit.models import OperationAuditLog - - -def test_list_operation_logs_returns_recent_logs(client, db): - OperationAuditLog.objects.create( - module="system", - action="health_check", - target_type="system", - target_id="health", - result="success", - request_snapshot={"path": "/api/system/health"}, - ) - - response = client.get("/api/audit/operation-logs") - - assert response.status_code == 200 - payload = response.json() - assert len(payload) == 1 - assert payload[0]["module"] == "system" - assert payload[0]["action"] == "health_check" -``` - -- [ ] **Step 5: Run migrations and tests** - -Run: - -```bash -cd backend -python manage.py makemigrations audit -pytest apps/audit/tests/test_audit_api.py -v -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add backend/apps/audit backend/config/urls.py -git commit -m "feat: add operation audit baseline" -``` - -## Task 4: Create Frontend Foundation And Design Tokens - -**Files:** -- Create: `frontend/package.json` -- Create: `frontend/tsconfig.json` -- Create: `frontend/tsconfig.node.json` -- Create: `frontend/vite.config.ts` -- Create: `frontend/index.html` -- Create: `frontend/tailwind.config.ts` -- Create: `frontend/postcss.config.js` -- Create: `frontend/src/main.tsx` -- Create: `frontend/src/app/App.tsx` -- Create: `frontend/src/lib/cn.ts` -- Create: `frontend/src/styles/globals.css` - -- [ ] **Step 1: Create frontend package manifest** - -Create `frontend/package.json`: - -```json -{ - "name": "ai-ticket-platform-frontend", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "typecheck": "tsc -b", - "lint": "eslint src --ext ts,tsx" - }, - "dependencies": { - "@radix-ui/react-slot": "^1.1.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "lucide-react": "^0.468.0", - "tailwind-merge": "^2.5.0", - "tailwindcss-animate": "^1.0.7", - "vite": "^6.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.3.0", - "autoprefixer": "^10.4.20", - "eslint": "^9.16.0", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.16", - "typescript": "^5.7.0" - } -} -``` - -- [ ] **Step 2: Create Vite and TypeScript config** - -Create `frontend/tsconfig.json`: - -```json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2020"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} -``` - -Create `frontend/tsconfig.node.json`: - -```json -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} -``` - -Create `frontend/vite.config.ts`: - -```typescript -import react from '@vitejs/plugin-react' -import path from 'node:path' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src') - } - }, - server: { - port: 5173 - } -}) -``` - -- [ ] **Step 3: Create Tailwind theme tokens** - -Create `frontend/tailwind.config.ts`: - -```typescript -import type { Config } from 'tailwindcss' - -const config: Config = { - darkMode: ['class'], - content: ['./index.html', './src/**/*.{ts,tsx}'], - theme: { - extend: { - colors: { - border: 'hsl(var(--border))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - surface: 'hsl(var(--surface))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - success: 'hsl(var(--success))', - warning: 'hsl(var(--warning))', - danger: 'hsl(var(--danger))', - info: 'hsl(var(--info))', - ai: 'hsl(var(--ai-accent))' - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - } - } - }, - plugins: [require('tailwindcss-animate')] -} - -export default config -``` - -Create `frontend/postcss.config.js`: - -```javascript -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } -} -``` - -Create `frontend/src/styles/globals.css`: - -```css -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 210 20% 98%; - --foreground: 222 25% 12%; - --surface: 0 0% 100%; - --muted: 210 18% 94%; - --muted-foreground: 215 12% 42%; - --border: 214 18% 86%; - --primary: 213 52% 32%; - --primary-foreground: 0 0% 100%; - --success: 151 55% 36%; - --warning: 38 86% 48%; - --danger: 0 70% 50%; - --info: 204 76% 44%; - --ai-accent: 186 52% 36%; - --radius: 0.5rem; - } - - body { - margin: 0; - background: hsl(var(--background)); - color: hsl(var(--foreground)); - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - } -} -``` - -- [ ] **Step 4: Create minimal app entry** - -Create `frontend/index.html`: - -```html - - - - - - AI Ticket Platform - - -
- - - -``` - -Create `frontend/src/lib/cn.ts`: - -```typescript -import { clsx, type ClassValue } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} -``` - -Create `frontend/src/app/App.tsx`: - -```tsx -export function App() { - return ( -
-
-

AI Ticket Platform

-

Foundation ready

-
-
- ) -} -``` - -Create `frontend/src/main.tsx`: - -```tsx -import React from 'react' -import ReactDOM from 'react-dom/client' - -import { App } from './app/App' -import './styles/globals.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - -) -``` - -- [ ] **Step 5: Verify frontend build** - -Run: - -```bash -cd frontend -npm install -npm run typecheck -npm run build -``` - -Expected: both commands pass. - -- [ ] **Step 6: Commit** - -```bash -git add frontend -git commit -m "feat: add frontend foundation and theme tokens" -``` - -## Task 5: Add Product Components And Preview Page - -**Files:** -- Create: `frontend/src/components/ui/button.tsx` -- Create: `frontend/src/components/ui/badge.tsx` -- Create: `frontend/src/components/ui/card.tsx` -- Create: `frontend/src/components/app/AppPageHeader.tsx` -- Create: `frontend/src/components/app/AppStatusBadge.tsx` -- Create: `frontend/src/components/app/AppPriorityBadge.tsx` -- Create: `frontend/src/features/design-system/DesignSystemPreview.tsx` -- Modify: `frontend/src/app/App.tsx` - -- [ ] **Step 1: Create base button** - -Create `frontend/src/components/ui/button.tsx`: - -```tsx -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' -import * as React from 'react' - -import { cn } from '@/lib/cn' - -const buttonVariants = cva( - 'inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:opacity-90', - secondary: 'border border-border bg-surface text-foreground hover:bg-muted', - ghost: 'text-foreground hover:bg-muted', - danger: 'bg-danger text-white hover:opacity-90' - } - }, - defaultVariants: { - variant: 'default' - } - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -export const Button = React.forwardRef( - ({ className, variant, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button' - return - } -) - -Button.displayName = 'Button' -``` - -- [ ] **Step 2: Create badge and card primitives** - -Create `frontend/src/components/ui/badge.tsx`: - -```tsx -import { cva, type VariantProps } from 'class-variance-authority' - -import { cn } from '@/lib/cn' - -const badgeVariants = cva( - 'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium', - { - variants: { - variant: { - default: 'border-border bg-muted text-foreground', - success: 'border-transparent bg-success/10 text-success', - warning: 'border-transparent bg-warning/15 text-warning', - danger: 'border-transparent bg-danger/10 text-danger', - info: 'border-transparent bg-info/10 text-info', - ai: 'border-transparent bg-ai/10 text-ai' - } - }, - defaultVariants: { - variant: 'default' - } - } -) - -type BadgeProps = React.HTMLAttributes & VariantProps - -export function Badge({ className, variant, ...props }: BadgeProps) { - return -} -``` - -Create `frontend/src/components/ui/card.tsx`: - -```tsx -import * as React from 'react' - -import { cn } from '@/lib/cn' - -export function Card({ className, ...props }: React.HTMLAttributes) { - return
-} - -export function CardHeader({ className, ...props }: React.HTMLAttributes) { - return
-} - -export function CardContent({ className, ...props }: React.HTMLAttributes) { - return
-} -``` - -- [ ] **Step 3: Create product app components** - -Create `frontend/src/components/app/AppPageHeader.tsx`: - -```tsx -type AppPageHeaderProps = { - title: string - description?: string - actions?: React.ReactNode -} - -export function AppPageHeader({ title, description, actions }: AppPageHeaderProps) { - return ( -
-
-

{title}

- {description ?

{description}

: null} -
- {actions ?
{actions}
: null} -
- ) -} -``` - -Create `frontend/src/components/app/AppStatusBadge.tsx`: - -```tsx -import { Badge } from '@/components/ui/badge' - -const STATUS_LABELS: Record = { - pending_info: '待补充', - pending: '待处理', - in_progress: '处理中', - pending_confirm: '待确认', - resolved: '已解决', - closed: '已关闭' -} - -export function AppStatusBadge({ status }: { status: string }) { - const variant = status === 'resolved' || status === 'closed' ? 'success' : 'info' - return {STATUS_LABELS[status] ?? status} -} -``` - -Create `frontend/src/components/app/AppPriorityBadge.tsx`: - -```tsx -import { Badge } from '@/components/ui/badge' - -const PRIORITY_LABELS: Record = { - low: '低', - medium: '中', - high: '高', - urgent: '紧急' -} - -export function AppPriorityBadge({ priority }: { priority: string }) { - const variant = priority === 'urgent' ? 'danger' : priority === 'high' ? 'warning' : 'default' - return {PRIORITY_LABELS[priority] ?? priority} -} -``` - -- [ ] **Step 4: Create design system preview** - -Create `frontend/src/features/design-system/DesignSystemPreview.tsx`: - -```tsx -import { AppPageHeader } from '@/components/app/AppPageHeader' -import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' -import { AppStatusBadge } from '@/components/app/AppStatusBadge' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader } from '@/components/ui/card' - -export function DesignSystemPreview() { - return ( -
-
- 创建工单} - /> - - - -

Buttons

-
- - - - - - -
- - - -

Badges

-
- - Default - Success - Warning - Danger - Info - AI - - - -
-
-
- ) -} -``` - -Modify `frontend/src/app/App.tsx`: - -```tsx -import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview' - -export function App() { - return -} -``` - -- [ ] **Step 5: Verify frontend** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add frontend/src -git commit -m "feat: add design system preview components" -``` - -## Task 6: Add Local Docker Runtime - -**Files:** -- Create: `infra/docker-compose.yml` -- Create: `infra/backend.Dockerfile` -- Create: `infra/frontend.Dockerfile` -- Create: `.env.example` -- Create: `docs/development/local-setup.md` - -- [ ] **Step 1: Create environment example** - -Create `.env.example`: - -```dotenv -DJANGO_SECRET_KEY=dev-only-change-me -DJANGO_DEBUG=true -DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 -CORS_ALLOWED_ORIGINS=http://localhost:5173 - -POSTGRES_DB=ai_ticket_platform -POSTGRES_USER=ai_ticket -POSTGRES_PASSWORD=ai_ticket -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 - -CELERY_BROKER_URL=redis://redis:6379/0 -CELERY_RESULT_BACKEND=redis://redis:6379/1 - -MINIO_ROOT_USER=minio -MINIO_ROOT_PASSWORD=minio123 -``` - -- [ ] **Step 2: Create backend Dockerfile** - -Create `infra/backend.Dockerfile`: - -```dockerfile -FROM python:3.12-slim - -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -COPY backend/pyproject.toml /app/pyproject.toml -RUN pip install --no-cache-dir -e ".[dev]" - -COPY backend /app - -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] -``` - -- [ ] **Step 3: Create frontend Dockerfile** - -Create `infra/frontend.Dockerfile`: - -```dockerfile -FROM node:22-slim - -WORKDIR /app - -COPY frontend/package.json /app/package.json -RUN npm install - -COPY frontend /app - -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] -``` - -- [ ] **Step 4: Create Docker Compose** - -Create `infra/docker-compose.yml`: - -```yaml -services: - postgres: - image: pgvector/pgvector:pg16 - environment: - POSTGRES_DB: ${POSTGRES_DB:-ai_ticket_platform} - POSTGRES_USER: ${POSTGRES_USER:-ai_ticket} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ai_ticket} - ports: - - "5432:5432" - volumes: - - postgres-data:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - ports: - - "6379:6379" - - minio: - image: minio/minio:latest - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio123} - ports: - - "9000:9000" - - "9001:9001" - volumes: - - minio-data:/data - - backend: - build: - context: .. - dockerfile: infra/backend.Dockerfile - env_file: - - ../.env - ports: - - "8000:8000" - depends_on: - - postgres - - redis - volumes: - - ../backend:/app - - worker: - build: - context: .. - dockerfile: infra/backend.Dockerfile - env_file: - - ../.env - command: celery -A config.celery worker -l info - depends_on: - - backend - - redis - volumes: - - ../backend:/app - - frontend: - build: - context: .. - dockerfile: infra/frontend.Dockerfile - ports: - - "5173:5173" - volumes: - - ../frontend:/app - - frontend-node-modules:/app/node_modules - -volumes: - postgres-data: - minio-data: - frontend-node-modules: -``` - -- [ ] **Step 5: Create local setup docs** - -Create `docs/development/local-setup.md`: - -```markdown -# Local Setup - -## Prerequisites - -- Python 3.12 -- Node.js 22 -- Docker Desktop - -## First Run - -```bash -copy .env.example .env -cd infra -docker compose --env-file ../.env up --build -``` - -Backend: - -- API: http://localhost:8000/api/docs -- Health: http://localhost:8000/api/system/health - -Frontend: - -- App: http://localhost:5173 - -## Backend Tests - -```bash -cd backend -python -m pip install -e ".[dev]" -pytest -v -``` - -## Frontend Checks - -```bash -cd frontend -npm install -npm run typecheck -npm run build -``` -``` - -- [ ] **Step 6: Verify compose config** - -Run: - -```bash -cd infra -docker compose --env-file ../.env.example config -``` - -Expected: command prints a valid compose config without errors. - -- [ ] **Step 7: Commit** - -```bash -git add .env.example infra docs/development -git commit -m "feat: add local docker runtime" -``` - -## Task 7: Final Stage 0 Verification - -**Files:** -- Modify only if verification exposes an issue. - -- [ ] **Step 1: Run backend tests** - -Run: - -```bash -cd backend -pytest -v -``` - -Expected: all tests pass. - -- [ ] **Step 2: Run backend lint** - -Run: - -```bash -cd backend -ruff check . -``` - -Expected: no lint errors. - -- [ ] **Step 3: Run frontend checks** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both commands pass. - -- [ ] **Step 4: Run compose config verification** - -Run: - -```bash -cd infra -docker compose --env-file ../.env.example config -``` - -Expected: valid config is printed. - -- [ ] **Step 5: Commit verification fixes if any** - -If any files were changed to fix verification failures: - -```bash -git add -git commit -m "fix: stabilize stage 0 foundation" -``` - -If no files changed, do not create an empty commit. - -## Self-Review Notes - -Spec coverage: - -- Covers project foundation, backend modular monolith start, frontend design system, RBAC seed, audit baseline, and local runtime. -- Does not implement tickets, knowledge, model gateway, Agents, Skills, or MCP. Those are intentionally deferred to later stage plans. - -Placeholder scan: - -- No unfinished placeholder markers are used. -- Later stages are referenced through the roadmap, not as incomplete Stage 0 work. - -Type consistency: - -- Backend app paths match `INSTALLED_APPS`. -- Frontend alias `@/*` matches Vite and TypeScript config. -- Docker service names match environment defaults. diff --git a/docs/superpowers/plans/2026-06-30-stage-1-ticket-workflow-solutions.md b/docs/superpowers/plans/2026-06-30-stage-1-ticket-workflow-solutions.md deleted file mode 100644 index baffff3..0000000 --- a/docs/superpowers/plans/2026-06-30-stage-1-ticket-workflow-solutions.md +++ /dev/null @@ -1,1222 +0,0 @@ -# Stage 1 Ticket Workflow And Reusable Solutions 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:** Implement the first business loop: users can create, search, update, comment on, resolve, export, and link tickets to reusable solutions. - -**Architecture:** Add Django apps for `tickets` and `solutions`, keeping status transition rules in service modules and write history in `TicketEvent` plus operation audit logs. Expose Django Ninja APIs with schema objects per business action. Extend the Vite app with a small client-side router, typed API helpers, and ticket/solution pages built from shared design-system primitives. - -**Tech Stack:** Django 5, Django Ninja, pytest, SQLite test database, React 19, Vite, TypeScript, Tailwind CSS, lucide-react. - ---- - -## Files To Create Or Modify - -```text -backend/ - config/settings/base.py - config/urls.py - apps/tickets/ - __init__.py - apps.py - models.py - schemas.py - services.py - api.py - migrations/__init__.py - tests/ - __init__.py - test_ticket_api.py - test_ticket_status.py - test_ticket_export.py - apps/solutions/ - __init__.py - apps.py - models.py - schemas.py - api.py - migrations/__init__.py - tests/ - __init__.py - test_solution_api.py -frontend/ - src/app/App.tsx - src/app/router.tsx - src/app/layout/AppShell.tsx - src/app/layout/Sidebar.tsx - src/lib/api.ts - src/lib/format.ts - src/components/ui/input.tsx - src/components/ui/textarea.tsx - src/components/ui/select.tsx - src/components/app/AppEmptyState.tsx - src/components/app/AppField.tsx - src/features/tickets/ - api.ts - types.ts - TicketListPage.tsx - TicketCreatePage.tsx - TicketDetailPage.tsx - TicketStatusTimeline.tsx - TicketComments.tsx - src/features/solutions/ - api.ts - types.ts - SolutionListPage.tsx - SolutionLinkPanel.tsx -docs/development/local-setup.md -``` - -## Task 1: Add Ticket Domain Models And Status Service - -**Files:** -- Modify: `backend/config/settings/base.py` -- Create: `backend/apps/tickets/apps.py` -- Create: `backend/apps/tickets/models.py` -- Create: `backend/apps/tickets/services.py` -- Create: `backend/apps/tickets/migrations/__init__.py` -- Test: `backend/apps/tickets/tests/test_ticket_status.py` - -- [ ] **Step 1: Add the failing status transition tests** - -Create `backend/apps/tickets/tests/test_ticket_status.py`: - -```python -import pytest - -from apps.tickets.services import TicketStatusError, validate_status_transition - - -def test_pending_ticket_can_move_to_in_progress(): - validate_status_transition("pending", "in_progress") - - -def test_resolved_ticket_cannot_move_back_to_in_progress(): - with pytest.raises(TicketStatusError, match="resolved -> in_progress"): - validate_status_transition("resolved", "in_progress") -``` - -- [ ] **Step 2: Run the status tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_status.py -v -``` - -Expected: FAIL because `apps.tickets` does not exist. - -- [ ] **Step 3: Register the tickets app** - -Modify `backend/config/settings/base.py`, adding `"apps.tickets"` after `"apps.system"` in `INSTALLED_APPS`. - -- [ ] **Step 4: Create ticket app config** - -Create `backend/apps/tickets/apps.py`: - -```python -from django.apps import AppConfig - - -class TicketsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.tickets" -``` - -- [ ] **Step 5: Create ticket models** - -Create `backend/apps/tickets/models.py`: - -```python -from django.conf import settings -from django.db import models - - -class TicketStatus(models.TextChoices): - PENDING_INFO = "pending_info", "Pending info" - PENDING = "pending", "Pending" - IN_PROGRESS = "in_progress", "In progress" - PENDING_CONFIRM = "pending_confirm", "Pending confirm" - RESOLVED = "resolved", "Resolved" - CLOSED = "closed", "Closed" - - -class TicketPriority(models.TextChoices): - LOW = "low", "Low" - MEDIUM = "medium", "Medium" - HIGH = "high", "High" - URGENT = "urgent", "Urgent" - - -class TicketCategory(models.Model): - parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL) - name = models.CharField(max_length=120) - code = models.CharField(max_length=80, unique=True) - description = models.TextField(blank=True) - enabled = models.BooleanField(default=True) - sort_order = models.PositiveIntegerField(default=0) - - class Meta: - ordering = ["sort_order", "name"] - - def __str__(self) -> str: - return self.name - - -class Ticket(models.Model): - ticket_no = models.CharField(max_length=32, unique=True) - title = models.CharField(max_length=200) - description = models.TextField() - category = models.ForeignKey(TicketCategory, null=True, blank=True, on_delete=models.SET_NULL) - status = models.CharField( - max_length=32, - choices=TicketStatus.choices, - default=TicketStatus.PENDING, - ) - priority = models.CharField( - max_length=20, - choices=TicketPriority.choices, - default=TicketPriority.MEDIUM, - ) - impact_scope = models.CharField(max_length=80, blank=True) - environment = models.CharField(max_length=80, blank=True) - related_system = models.CharField(max_length=120, blank=True) - requester = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="requested_tickets", - ) - assignee = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="assigned_tickets", - ) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_tickets", - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - resolved_at = models.DateTimeField(null=True, blank=True) - closed_at = models.DateTimeField(null=True, blank=True) - - class Meta: - ordering = ["-created_at"] - indexes = [ - models.Index(fields=["ticket_no"]), - models.Index(fields=["status", "priority"]), - models.Index(fields=["created_at"]), - ] - - def __str__(self) -> str: - return f"{self.ticket_no} {self.title}" - - -class TicketComment(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="comments") - content = models.TextField() - comment_type = models.CharField(max_length=40, default="user") - visibility = models.CharField(max_length=40, default="internal") - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ["created_at"] - - -class TicketAttachment(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="attachments") - file_name = models.CharField(max_length=255) - file_type = models.CharField(max_length=80, blank=True) - mime_type = models.CharField(max_length=120, blank=True) - file_size = models.PositiveBigIntegerField(default=0) - storage_path = models.CharField(max_length=500) - ocr_text = models.TextField(blank=True) - parsed_text = models.TextField(blank=True) - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(auto_now_add=True) - - -class TicketEvent(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="events") - event_type = models.CharField(max_length=80) - from_value = models.JSONField(default=dict, blank=True) - to_value = models.JSONField(default=dict, blank=True) - operator = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - remark = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ["created_at"] -``` - -- [ ] **Step 6: Create status transition service** - -Create `backend/apps/tickets/services.py`: - -```python -from django.utils import timezone - -from apps.audit.models import OperationAuditLog -from apps.tickets.models import Ticket, TicketEvent - - -class TicketStatusError(ValueError): - pass - - -ALLOWED_TRANSITIONS: dict[str, set[str]] = { - "pending_info": {"pending", "closed"}, - "pending": {"in_progress", "closed"}, - "in_progress": {"pending_confirm", "resolved", "closed"}, - "pending_confirm": {"resolved", "in_progress", "closed"}, - "resolved": {"closed"}, - "closed": set(), -} - - -def validate_status_transition(from_status: str, to_status: str) -> None: - if to_status not in ALLOWED_TRANSITIONS.get(from_status, set()): - raise TicketStatusError(f"Invalid ticket status transition: {from_status} -> {to_status}") - - -def transition_ticket(ticket: Ticket, to_status: str, remark: str = "", operator=None) -> Ticket: - from_status = ticket.status - validate_status_transition(from_status, to_status) - ticket.status = to_status - if to_status == "resolved": - ticket.resolved_at = timezone.now() - if to_status == "closed": - ticket.closed_at = timezone.now() - ticket.save(update_fields=["status", "resolved_at", "closed_at", "updated_at"]) - TicketEvent.objects.create( - ticket=ticket, - event_type="status_change", - from_value={"status": from_status}, - to_value={"status": to_status}, - operator=operator, - remark=remark, - ) - OperationAuditLog.objects.create( - user=operator, - module="tickets", - action="status_transition", - target_type="ticket", - target_id=str(ticket.id), - result="success", - request_snapshot={"from": from_status, "to": to_status, "remark": remark}, - ) - return ticket -``` - -- [ ] **Step 7: Generate migrations and run status tests** - -Run: - -```bash -cd backend -$env:DJANGO_SETTINGS_MODULE="config.settings.test" -python manage.py makemigrations tickets -python -m pytest apps/tickets/tests/test_ticket_status.py -v -``` - -Expected: PASS. - -- [ ] **Step 8: Commit** - -```bash -git add backend/config/settings/base.py backend/apps/tickets -git commit -m "feat: add ticket domain models" -``` - -## Task 2: Add Ticket CRUD And Status APIs - -**Files:** -- Create: `backend/apps/tickets/schemas.py` -- Create: `backend/apps/tickets/api.py` -- Modify: `backend/config/urls.py` -- Test: `backend/apps/tickets/tests/test_ticket_api.py` - -- [ ] **Step 1: Add failing API tests** - -Create `backend/apps/tickets/tests/test_ticket_api.py`: - -```python -from apps.tickets.models import Ticket, TicketEvent - - -def test_create_ticket_returns_ticket_detail(client, db): - response = client.post( - "/api/tickets", - data={ - "title": "Payment callback fails", - "description": "Callback returns HTTP 500 in production", - "priority": "high", - "impact_scope": "payment users", - "environment": "production", - "related_system": "payment", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["ticket_no"].startswith("TK") - assert payload["title"] == "Payment callback fails" - assert payload["status"] == "pending" - assert TicketEvent.objects.filter(ticket_id=payload["id"], event_type="created").exists() - - -def test_list_tickets_filters_by_status(client, db): - Ticket.objects.create(ticket_no="TK202606300001", title="A", description="A", status="pending") - Ticket.objects.create(ticket_no="TK202606300002", title="B", description="B", status="resolved") - - response = client.get("/api/tickets?status=pending") - - assert response.status_code == 200 - payload = response.json() - assert len(payload) == 1 - assert payload[0]["ticket_no"] == "TK202606300001" - - -def test_transition_ticket_status_creates_event(client, db): - ticket = Ticket.objects.create(ticket_no="TK202606300003", title="A", description="A", status="pending") - - response = client.post( - f"/api/tickets/{ticket.id}/status", - data={"status": "in_progress", "remark": "Start handling"}, - content_type="application/json", - ) - - assert response.status_code == 200 - ticket.refresh_from_db() - assert ticket.status == "in_progress" - assert ticket.events.filter(event_type="status_change").exists() -``` - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_api.py -v -``` - -Expected: FAIL because ticket API is not routed. - -- [ ] **Step 3: Create ticket schemas** - -Create `backend/apps/tickets/schemas.py`: - -```python -from datetime import datetime - -from ninja import Schema - - -class TicketIn(Schema): - title: str - description: str - priority: str = "medium" - impact_scope: str = "" - environment: str = "" - related_system: str = "" - assignee_id: int | None = None - - -class TicketUpdateIn(Schema): - title: str | None = None - description: str | None = None - priority: str | None = None - impact_scope: str | None = None - environment: str | None = None - related_system: str | None = None - assignee_id: int | None = None - - -class TicketStatusIn(Schema): - status: str - remark: str = "" - - -class TicketOut(Schema): - id: int - ticket_no: str - title: str - description: str - status: str - priority: str - impact_scope: str - environment: str - related_system: str - assignee_id: int | None - created_at: datetime - updated_at: datetime - - -class TicketEventOut(Schema): - id: int - event_type: str - from_value: dict - to_value: dict - remark: str - created_at: datetime -``` - -- [ ] **Step 4: Create ticket API** - -Create `backend/apps/tickets/api.py`: - -```python -from django.contrib.auth import get_user_model -from django.db.models import Q -from django.utils import timezone -from ninja import Router - -from apps.tickets.models import Ticket, TicketEvent -from apps.tickets.schemas import TicketIn, TicketOut, TicketStatusIn, TicketUpdateIn -from apps.tickets.services import transition_ticket - -router = Router(tags=["tickets"]) - - -def next_ticket_no() -> str: - prefix = timezone.localdate().strftime("TK%Y%m%d") - count = Ticket.objects.filter(ticket_no__startswith=prefix).count() + 1 - return f"{prefix}{count:04d}" - - -@router.post("", response=TicketOut) -def create_ticket(request, payload: TicketIn): - assignee = None - if payload.assignee_id: - assignee = get_user_model().objects.filter(id=payload.assignee_id).first() - ticket = Ticket.objects.create( - ticket_no=next_ticket_no(), - title=payload.title, - description=payload.description, - priority=payload.priority, - impact_scope=payload.impact_scope, - environment=payload.environment, - related_system=payload.related_system, - assignee=assignee, - created_by=request.user if request.user.is_authenticated else None, - ) - TicketEvent.objects.create(ticket=ticket, event_type="created", to_value={"status": ticket.status}) - return ticket - - -@router.get("", response=list[TicketOut]) -def list_tickets(request, status: str | None = None, q: str | None = None): - queryset = Ticket.objects.all() - if status: - queryset = queryset.filter(status=status) - if q: - queryset = queryset.filter(Q(ticket_no__icontains=q) | Q(title__icontains=q)) - return queryset[:100] - - -@router.get("/{ticket_id}", response=TicketOut) -def get_ticket(request, ticket_id: int): - return Ticket.objects.get(id=ticket_id) - - -@router.patch("/{ticket_id}", response=TicketOut) -def update_ticket(request, ticket_id: int, payload: TicketUpdateIn): - ticket = Ticket.objects.get(id=ticket_id) - before = { - "title": ticket.title, - "priority": ticket.priority, - "impact_scope": ticket.impact_scope, - "environment": ticket.environment, - "related_system": ticket.related_system, - } - for field, value in payload.dict(exclude_unset=True).items(): - if field == "assignee_id": - user = get_user_model().objects.filter(id=value).first() if value else None - ticket.assignee = user - else: - setattr(ticket, field, value) - ticket.save() - TicketEvent.objects.create( - ticket=ticket, - event_type="updated", - from_value=before, - to_value=payload.dict(exclude_unset=True), - operator=request.user if request.user.is_authenticated else None, - ) - return ticket - - -@router.post("/{ticket_id}/status", response=TicketOut) -def update_ticket_status(request, ticket_id: int, payload: TicketStatusIn): - ticket = Ticket.objects.get(id=ticket_id) - return transition_ticket( - ticket, - payload.status, - payload.remark, - request.user if request.user.is_authenticated else None, - ) -``` - -- [ ] **Step 5: Route ticket API** - -Modify `backend/config/urls.py`: - -```python -from apps.tickets.api import router as tickets_router - -api.add_router("/tickets", tickets_router) -``` - -- [ ] **Step 6: Run API tests** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_api.py -v -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add backend/apps/tickets backend/config/urls.py -git commit -m "feat: add ticket workflow APIs" -``` - -## Task 3: Add Comments, Attachments, Export, And Audit - -**Files:** -- Modify: `backend/apps/tickets/schemas.py` -- Modify: `backend/apps/tickets/api.py` -- Test: `backend/apps/tickets/tests/test_ticket_export.py` - -- [ ] **Step 1: Add failing tests** - -Create `backend/apps/tickets/tests/test_ticket_export.py`: - -```python -from apps.audit.models import OperationAuditLog -from apps.tickets.models import Ticket - - -def test_add_comment_to_ticket(client, db): - ticket = Ticket.objects.create(ticket_no="TK202606300010", title="A", description="A") - - response = client.post( - f"/api/tickets/{ticket.id}/comments", - data={"content": "Checked logs", "comment_type": "internal_note", "visibility": "internal"}, - content_type="application/json", - ) - - assert response.status_code == 200 - assert response.json()["content"] == "Checked logs" - assert ticket.comments.count() == 1 - - -def test_add_attachment_to_ticket(client, db): - ticket = Ticket.objects.create(ticket_no="TK202606300011", title="A", description="A") - - response = client.post( - f"/api/tickets/{ticket.id}/attachments", - data={ - "file_name": "error.log", - "mime_type": "text/plain", - "file_size": 123, - "storage_path": "tickets/TK202606300011/error.log", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - assert response.json()["file_name"] == "error.log" - - -def test_export_tickets_returns_csv_and_audit_log(client, db): - Ticket.objects.create(ticket_no="TK202606300012", title="Export me", description="A") - - response = client.get("/api/tickets/export") - - assert response.status_code == 200 - assert response["content-type"].startswith("text/csv") - assert "TK202606300012" in response.content.decode() - assert OperationAuditLog.objects.filter(module="tickets", action="export").exists() -``` - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_export.py -v -``` - -Expected: FAIL because routes do not exist. - -- [ ] **Step 3: Extend schemas** - -Append to `backend/apps/tickets/schemas.py`: - -```python -class TicketCommentIn(Schema): - content: str - comment_type: str = "user" - visibility: str = "internal" - - -class TicketCommentOut(Schema): - id: int - content: str - comment_type: str - visibility: str - created_at: datetime - - -class TicketAttachmentIn(Schema): - file_name: str - file_type: str = "" - mime_type: str = "" - file_size: int = 0 - storage_path: str - - -class TicketAttachmentOut(Schema): - id: int - file_name: str - file_type: str - mime_type: str - file_size: int - storage_path: str - created_at: datetime -``` - -- [ ] **Step 4: Add comment, attachment, and export endpoints** - -Extend `backend/apps/tickets/api.py` with: - -```python -import csv -from django.http import HttpResponse - -from apps.audit.models import OperationAuditLog -from apps.tickets.models import TicketAttachment, TicketComment -from apps.tickets.schemas import ( - TicketAttachmentIn, - TicketAttachmentOut, - TicketCommentIn, - TicketCommentOut, -) - - -@router.post("/{ticket_id}/comments", response=TicketCommentOut) -def add_ticket_comment(request, ticket_id: int, payload: TicketCommentIn): - ticket = Ticket.objects.get(id=ticket_id) - comment = TicketComment.objects.create( - ticket=ticket, - content=payload.content, - comment_type=payload.comment_type, - visibility=payload.visibility, - created_by=request.user if request.user.is_authenticated else None, - ) - TicketEvent.objects.create( - ticket=ticket, - event_type="commented", - to_value={"comment_id": comment.id, "comment_type": comment.comment_type}, - operator=request.user if request.user.is_authenticated else None, - ) - return comment - - -@router.post("/{ticket_id}/attachments", response=TicketAttachmentOut) -def add_ticket_attachment(request, ticket_id: int, payload: TicketAttachmentIn): - ticket = Ticket.objects.get(id=ticket_id) - attachment = TicketAttachment.objects.create(ticket=ticket, **payload.dict()) - TicketEvent.objects.create( - ticket=ticket, - event_type="attachment_added", - to_value={"attachment_id": attachment.id, "file_name": attachment.file_name}, - ) - return attachment - - -@router.get("/export") -def export_tickets(request): - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="tickets.csv"' - writer = csv.writer(response) - writer.writerow(["ticket_no", "title", "status", "priority", "created_at"]) - for ticket in Ticket.objects.all().order_by("ticket_no"): - writer.writerow([ticket.ticket_no, ticket.title, ticket.status, ticket.priority, ticket.created_at.isoformat()]) - OperationAuditLog.objects.create( - user=request.user if request.user.is_authenticated else None, - module="tickets", - action="export", - target_type="ticket", - result="success", - request_snapshot={}, - ) - return response -``` - -- [ ] **Step 5: Run tests** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_export.py -v -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add backend/apps/tickets -git commit -m "feat: add ticket comments attachments and export" -``` - -## Task 4: Add Reusable Solutions And Ticket Links - -**Files:** -- Modify: `backend/config/settings/base.py` -- Modify: `backend/config/urls.py` -- Create: `backend/apps/solutions/apps.py` -- Create: `backend/apps/solutions/models.py` -- Create: `backend/apps/solutions/schemas.py` -- Create: `backend/apps/solutions/api.py` -- Create: `backend/apps/solutions/migrations/__init__.py` -- Test: `backend/apps/solutions/tests/test_solution_api.py` - -- [ ] **Step 1: Add failing solution API tests** - -Create `backend/apps/solutions/tests/test_solution_api.py`: - -```python -from apps.tickets.models import Ticket - - -def test_create_and_list_solution(client, db): - response = client.post( - "/api/solutions", - data={ - "title": "Restart callback worker", - "problem_signature": "HTTP 500 from callback", - "root_cause": "Worker connection pool stale", - "solution": "Restart callback worker and verify queue depth.", - "status": "published", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - solution_id = response.json()["id"] - - response = client.get("/api/solutions") - assert response.status_code == 200 - assert response.json()[0]["id"] == solution_id - - -def test_link_solution_to_ticket_increments_usage(client, db): - ticket = Ticket.objects.create(ticket_no="TK202606300020", title="A", description="A") - solution = client.post( - "/api/solutions", - data={"title": "S", "problem_signature": "P", "root_cause": "R", "solution": "Do S"}, - content_type="application/json", - ).json() - - response = client.post( - f"/api/solutions/{solution['id']}/link-ticket", - data={"ticket_id": ticket.id, "link_type": "adopted"}, - content_type="application/json", - ) - - assert response.status_code == 200 - assert response.json()["link_type"] == "adopted" -``` - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/solutions/tests/test_solution_api.py -v -``` - -Expected: FAIL because `apps.solutions` does not exist. - -- [ ] **Step 3: Register solutions app** - -Modify `backend/config/settings/base.py`, adding `"apps.solutions"` after `"apps.tickets"`. - -- [ ] **Step 4: Create solution models** - -Create `backend/apps/solutions/models.py`: - -```python -from django.conf import settings -from django.db import models - -from apps.tickets.models import Ticket, TicketCategory - - -class Solution(models.Model): - title = models.CharField(max_length=200) - problem_signature = models.TextField() - related_system = models.CharField(max_length=120, blank=True) - environment_scope = models.CharField(max_length=120, blank=True) - category = models.ForeignKey(TicketCategory, null=True, blank=True, on_delete=models.SET_NULL) - root_cause = models.TextField() - solution = models.TextField() - risk_note = models.TextField(blank=True) - status = models.CharField(max_length=40, default="draft") - usage_count = models.PositiveIntegerField(default=0) - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-updated_at"] - - def __str__(self) -> str: - return self.title - - -class TicketResolution(models.Model): - ticket = models.OneToOneField(Ticket, on_delete=models.CASCADE, related_name="resolution") - resolution_type = models.CharField(max_length=80, default="resolved") - root_cause = models.TextField(blank=True) - solution_text = models.TextField() - linked_solution = models.ForeignKey(Solution, null=True, blank=True, on_delete=models.SET_NULL) - confirmed_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - -class TicketSolutionLink(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="solution_links") - solution = models.ForeignKey(Solution, on_delete=models.CASCADE, related_name="ticket_links") - link_type = models.CharField(max_length=40, default="referenced") - agent_run_id = models.BigIntegerField(null=True, blank=True) - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - unique_together = [("ticket", "solution", "link_type")] -``` - -- [ ] **Step 5: Create solution schemas and API** - -Create `backend/apps/solutions/schemas.py` and `backend/apps/solutions/api.py` with create/list/detail/link endpoints using the fields in the tests. Link endpoint must increment `Solution.usage_count` when `link_type == "adopted"`. - -- [ ] **Step 6: Route solution API** - -Modify `backend/config/urls.py`: - -```python -from apps.solutions.api import router as solutions_router - -api.add_router("/solutions", solutions_router) -``` - -- [ ] **Step 7: Generate migrations and run tests** - -Run: - -```bash -cd backend -$env:DJANGO_SETTINGS_MODULE="config.settings.test" -python manage.py makemigrations solutions -python -m pytest apps/solutions/tests/test_solution_api.py -v -``` - -Expected: PASS. - -- [ ] **Step 8: Commit** - -```bash -git add backend/apps/solutions backend/config/settings/base.py backend/config/urls.py -git commit -m "feat: add reusable solution library" -``` - -## Task 5: Add Frontend API Client And Routing - -**Files:** -- Modify: `frontend/src/app/App.tsx` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Modify: `frontend/src/app/layout/Sidebar.tsx` -- Create: `frontend/src/lib/api.ts` -- Create: `frontend/src/lib/format.ts` - -- [ ] **Step 1: Create API helper** - -Create `frontend/src/lib/api.ts`: - -```typescript -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000/api' - -export async function apiRequest(path: string, init?: RequestInit): Promise { - const response = await fetch(`${API_BASE_URL}${path}`, { - headers: { - 'content-type': 'application/json', - ...init?.headers - }, - ...init - }) - - if (!response.ok) { - throw new Error(`API request failed: ${response.status}`) - } - - return response.json() as Promise -} -``` - -- [ ] **Step 2: Create date formatting helper** - -Create `frontend/src/lib/format.ts`: - -```typescript -export function formatDateTime(value: string) { - return new Intl.DateTimeFormat('zh-CN', { - dateStyle: 'medium', - timeStyle: 'short' - }).format(new Date(value)) -} -``` - -- [ ] **Step 3: Replace router with hash-based routes** - -Modify `frontend/src/app/router.tsx` so it uses `window.location.hash` and supports `/tickets`, `/tickets/new`, `/tickets/:id`, and `/solutions`. Use React state and `hashchange` listener only; do not add React Router in this stage. - -- [ ] **Step 4: Update shell navigation** - -Modify `frontend/src/app/layout/Sidebar.tsx` to show navigation links for Tickets, Create Ticket, Solutions, and Design System. Use existing layout and no one-off colors. - -- [ ] **Step 5: Run frontend checks** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add frontend/src/app frontend/src/lib -git commit -m "feat: add frontend app routing" -``` - -## Task 6: Add Ticket Frontend Pages - -**Files:** -- Create: `frontend/src/components/ui/input.tsx` -- Create: `frontend/src/components/ui/textarea.tsx` -- Create: `frontend/src/components/ui/select.tsx` -- Create: `frontend/src/components/app/AppEmptyState.tsx` -- Create: `frontend/src/components/app/AppField.tsx` -- Create: `frontend/src/features/tickets/api.ts` -- Create: `frontend/src/features/tickets/types.ts` -- Create: `frontend/src/features/tickets/TicketListPage.tsx` -- Create: `frontend/src/features/tickets/TicketCreatePage.tsx` -- Create: `frontend/src/features/tickets/TicketDetailPage.tsx` -- Create: `frontend/src/features/tickets/TicketStatusTimeline.tsx` -- Create: `frontend/src/features/tickets/TicketComments.tsx` - -- [ ] **Step 1: Create ticket types** - -Create `frontend/src/features/tickets/types.ts` matching backend `TicketOut`, comment, attachment, and event schemas. - -- [ ] **Step 2: Create ticket API wrappers** - -Create `frontend/src/features/tickets/api.ts` with `listTickets`, `createTicket`, `getTicket`, `transitionTicket`, and `addTicketComment` using `apiRequest`. - -- [ ] **Step 3: Create form primitives** - -Create simple `Input`, `Textarea`, and `Select` components that use design tokens, fixed heights, and no raw color values. - -- [ ] **Step 4: Create ticket list page** - -Create `TicketListPage` with search input, status filter, create action, and a dense table-like list showing ticket number, title, status badge, priority badge, system, and created time. - -- [ ] **Step 5: Create ticket create page** - -Create `TicketCreatePage` with controlled form fields for title, description, priority, impact scope, environment, and related system. On submit call `createTicket` and navigate to `#/tickets/`. - -- [ ] **Step 6: Create ticket detail page** - -Create `TicketDetailPage` showing ticket fields, status transition buttons, event timeline, and comments. Keep AI-specific panels out of Stage 1. - -- [ ] **Step 7: Run frontend checks** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: PASS. - -- [ ] **Step 8: Commit** - -```bash -git add frontend/src/components frontend/src/features/tickets frontend/src/app -git commit -m "feat: add ticket workflow pages" -``` - -## Task 7: Add Solutions Frontend Pages - -**Files:** -- Create: `frontend/src/features/solutions/types.ts` -- Create: `frontend/src/features/solutions/api.ts` -- Create: `frontend/src/features/solutions/SolutionListPage.tsx` -- Create: `frontend/src/features/solutions/SolutionLinkPanel.tsx` -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` - -- [ ] **Step 1: Create solution types and API wrappers** - -Create TypeScript types matching solution backend schemas and wrappers for list/create/link. - -- [ ] **Step 2: Create solution list page** - -Create `SolutionListPage` with published/draft badges, usage count, title, problem signature, root cause, and solution text preview. - -- [ ] **Step 3: Create solution link panel** - -Create `SolutionLinkPanel` that lists solutions and lets the user link one to the current ticket as `referenced` or `adopted`. - -- [ ] **Step 4: Add panel to ticket detail** - -Modify `TicketDetailPage` to render `SolutionLinkPanel` below comments. - -- [ ] **Step 5: Run frontend checks** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add frontend/src/features/solutions frontend/src/features/tickets/TicketDetailPage.tsx -git commit -m "feat: add solution library pages" -``` - -## Task 8: Final Stage 1 Verification And Documentation - -**Files:** -- Modify: `docs/development/local-setup.md` -- Modify only if verification exposes an issue. - -- [ ] **Step 1: Document Stage 1 APIs** - -Update `docs/development/local-setup.md` with: - -```markdown -## Stage 1 API Smoke Tests - -- Tickets: `GET http://localhost:8000/api/tickets` -- Ticket export: `GET http://localhost:8000/api/tickets/export` -- Solutions: `GET http://localhost:8000/api/solutions` -``` - -- [ ] **Step 2: Run backend tests** - -Run: - -```bash -cd backend -python -m pytest -v -``` - -Expected: all backend tests pass. - -- [ ] **Step 3: Run backend lint** - -Run: - -```bash -cd backend -python -m ruff check . -``` - -Expected: no lint errors. - -- [ ] **Step 4: Run frontend checks** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both commands pass. - -- [ ] **Step 5: Run compose config verification** - -Run: - -```bash -cd infra -docker compose --env-file ../.env.example config -``` - -Expected: valid config is printed without errors. - -- [ ] **Step 6: Commit verification fixes** - -If documentation or fixes changed files: - -```bash -git add docs/development/local-setup.md -git commit -m "docs: document stage 1 local smoke tests" -``` - -## Self-Review Notes - -Spec coverage: - -- Covers ticket lifecycle, comments, attachments metadata, events, export, reusable solutions, ticket-resolution link data, and solution linking. -- Defers real object upload to MinIO, AI recommendations, dashboards, and historical migration to later stages. -- Frontend uses existing design tokens and product badges; no one-off page color system is introduced. - -Placeholder scan: - -- No `TBD`, `TODO`, or open-ended implementation placeholders are required for a worker to start. -- Task 4 and frontend tasks intentionally specify file responsibilities and exact required behavior while leaving straightforward component layout implementation to the worker. - -Type consistency: - -- Backend status values match `AppStatusBadge`. -- Backend priority values match `AppPriorityBadge`. -- `TicketOut`, `TicketCommentOut`, and solution response names are the source for frontend TypeScript types. diff --git a/docs/superpowers/plans/2026-07-01-stage-1-5-shadcn-ui-foundation.md b/docs/superpowers/plans/2026-07-01-stage-1-5-shadcn-ui-foundation.md deleted file mode 100644 index b07c834..0000000 --- a/docs/superpowers/plans/2026-07-01-stage-1-5-shadcn-ui-foundation.md +++ /dev/null @@ -1,601 +0,0 @@ -# Stage 1.5 shadcn UI Foundation 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:** Formalize shadcn/ui in the Vite frontend, build a reusable Chinese enterprise admin App component layer, and redesign the existing ticket and solution pages around a production workbench experience. - -**Architecture:** Keep shadcn-generated primitives under `frontend/src/components/ui` and project-owned product components under `frontend/src/components/app`. Business pages should compose App components for layout, table, filters, detail workbench, status, priority, and empty states while preserving existing backend API contracts. - -**Tech Stack:** React 19, Vite 6, TypeScript, Tailwind CSS v3, shadcn/ui, Radix UI, lucide-react, npm. - ---- - -## Source Spec - -- `docs/superpowers/specs/2026-07-01-stage-1-5-shadcn-ui-foundation-design.md` - -## Scope - -Stage 1.5 is frontend-only unless shadcn initialization unexpectedly requires a metadata adjustment. Do not add knowledge base, Agent, Skill, MCP, model gateway, or new backend behavior. - -## File Structure - -Expected created or modified files: - -- Create: `frontend/components.json` -- Modify: `frontend/package.json` -- Modify: `frontend/package-lock.json` -- Modify: `frontend/tailwind.config.ts` -- Modify: `frontend/src/styles/globals.css` -- Create or replace shadcn primitives in `frontend/src/components/ui/*` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Replace or remove: `frontend/src/app/layout/Sidebar.tsx` -- Replace or remove: `frontend/src/app/layout/Topbar.tsx` -- Create: `frontend/src/components/app/AppSidebar.tsx` -- Create: `frontend/src/components/app/AppTopbar.tsx` -- Modify: `frontend/src/components/app/AppPageHeader.tsx` -- Create: `frontend/src/components/app/AppDataTable.tsx` -- Create: `frontend/src/components/app/AppFilterBar.tsx` -- Create: `frontend/src/components/app/AppDetailLayout.tsx` -- Create: `frontend/src/components/app/AppSection.tsx` -- Modify: `frontend/src/components/app/AppEmptyState.tsx` -- Modify: `frontend/src/components/app/AppStatusBadge.tsx` -- Modify: `frontend/src/components/app/AppPriorityBadge.tsx` -- Modify: `frontend/src/features/design-system/DesignSystemPreview.tsx` -- Create: `frontend/src/features/workbench/WorkbenchPage.tsx` -- Modify: `frontend/src/features/tickets/TicketListPage.tsx` -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` -- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` -- Modify: `frontend/src/features/tickets/TicketComments.tsx` -- Modify: `frontend/src/features/solutions/SolutionListPage.tsx` -- Modify: `frontend/src/features/solutions/SolutionLinkPanel.tsx` - -## Task 1: Initialize shadcn/ui - -**Files:** -- Create: `frontend/components.json` -- Modify: `frontend/package.json` -- Modify: `frontend/package-lock.json` -- Modify: `frontend/tailwind.config.ts` -- Modify: `frontend/src/styles/globals.css` -- Create or replace: `frontend/src/components/ui/*` - -- [ ] **Step 1: Capture current shadcn state** - -Run: - -```bash -cd frontend -npx shadcn@latest info -``` - -Expected before initialization: - -```text -config: null -components: [] -``` - -- [ ] **Step 2: Initialize shadcn for the existing Vite app** - -Run: - -```bash -cd frontend -npx shadcn@latest init -``` - -Select options that preserve the existing project shape: - -```text -Framework: Vite -TypeScript: yes -Tailwind CSS file: src/styles/globals.css -Tailwind config: tailwind.config.ts -Import alias: @ -Use CSS variables: yes -Base color: neutral or zinc -``` - -Do not accept changes that move the app to Next.js or switch the package manager away from npm. - -- [ ] **Step 3: Add the required shadcn primitives** - -Run: - -```bash -cd frontend -npx shadcn@latest add button badge card input textarea select label separator sidebar breadcrumb dropdown-menu table tabs sheet dialog tooltip skeleton sonner -``` - -If the CLI reports that one component name is unavailable, retry without only that component and document the exact skipped name in this plan's execution notes. - -- [ ] **Step 4: Verify shadcn state** - -Run: - -```bash -cd frontend -npx shadcn@latest info -``` - -Expected: - -```text -config: present -components: non-empty -``` - -- [ ] **Step 5: Run frontend typecheck** - -Run: - -```bash -cd frontend -npm run typecheck -``` - -Expected: exit code `0`. If shadcn overwrote local component APIs, fix imports and variants in Task 2 instead of editing business pages in this task. - -- [ ] **Step 6: Commit** - -Run: - -```bash -git add frontend/components.json frontend/package.json frontend/package-lock.json frontend/tailwind.config.ts frontend/src/styles/globals.css frontend/src/components/ui -git commit -m "feat: initialize shadcn ui" -``` - -## Task 2: Build the App Component Layer - -**Files:** -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Create: `frontend/src/components/app/AppSidebar.tsx` -- Create: `frontend/src/components/app/AppTopbar.tsx` -- Modify: `frontend/src/components/app/AppPageHeader.tsx` -- Create: `frontend/src/components/app/AppDataTable.tsx` -- Create: `frontend/src/components/app/AppFilterBar.tsx` -- Create: `frontend/src/components/app/AppDetailLayout.tsx` -- Create: `frontend/src/components/app/AppSection.tsx` -- Modify: `frontend/src/components/app/AppEmptyState.tsx` -- Modify: `frontend/src/components/app/AppStatusBadge.tsx` -- Modify: `frontend/src/components/app/AppPriorityBadge.tsx` - -- [ ] **Step 1: Replace the layout imports** - -`frontend/src/app/layout/AppShell.tsx` should import from the App component layer: - -```tsx -import type * as React from 'react' - -import { AppSidebar } from '@/components/app/AppSidebar' -import { AppTopbar } from '@/components/app/AppTopbar' - -type AppShellProps = { - children: React.ReactNode -} - -export function AppShell({ children }: AppShellProps) { - return ( -
- -
- - {children} -
-
- ) -} -``` - -- [ ] **Step 2: Create Chinese sidebar navigation** - -`frontend/src/components/app/AppSidebar.tsx` should define grouped navigation: - -```tsx -import { Bot, BriefcaseBusiness, Database, Home, Library, Settings, ShieldCheck, Ticket } from 'lucide-react' - -const navGroups = [ - { - label: '工作台', - items: [{ href: '#/', label: '运营总览', icon: Home }] - }, - { - label: '业务处理', - items: [ - { href: '#/tickets', label: '工单中心', icon: Ticket }, - { href: '#/tickets/new', label: '创建工单', icon: BriefcaseBusiness }, - { href: '#/solutions', label: '解决方案库', icon: Library } - ] - }, - { - label: '平台能力', - items: [ - { href: '#/knowledge', label: '知识库', icon: Database }, - { href: '#/agents', label: 'Agent 配置', icon: Bot }, - { href: '#/system', label: '系统治理', icon: ShieldCheck }, - { href: '#/design-system', label: '设计系统', icon: Settings } - ] - } -] -``` - -Render the groups in a fixed left sidebar, using lucide icons and Chinese labels only. - -- [ ] **Step 3: Create topbar and breadcrumb placeholder** - -`frontend/src/components/app/AppTopbar.tsx` should show: - -```text -AI 工单平台 / 当前工作区 -``` - -and right-side placeholders: - -```text -本地环境 -系统管理员 -``` - -Use `Badge` for `本地环境`, `Button` with `variant="ghost"` for `系统管理员`, `Separator` between breadcrumb and right-side controls, and lucide icons for visible icon buttons. - -- [ ] **Step 4: Create reusable data table wrapper** - -`frontend/src/components/app/AppDataTable.tsx` should accept: - -```tsx -type AppDataTableColumn = { - key: string - header: React.ReactNode - cell: (row: Row) => React.ReactNode - className?: string -} - -type AppDataTableProps = { - columns: AppDataTableColumn[] - rows: Row[] - getRowKey: (row: Row) => React.Key - emptyTitle: string - emptyDescription?: string - toolbar?: React.ReactNode - footer?: React.ReactNode - onRowClick?: (row: Row) => void -} -``` - -Use shadcn `Table` primitives internally. Keep row height stable and use Chinese empty-state copy from props. - -- [ ] **Step 5: Create filter and detail helpers** - -Create: - -```text -AppFilterBar: section wrapper for search input, select filters, and reset/actions. -AppDetailLayout: responsive detail layout with main and side columns. -AppSection: titled section wrapper with optional description and action. -``` - -These components must not know about ticket-specific fields. - -- [ ] **Step 6: Update badge and empty-state components** - -Ensure status labels are Chinese: - -```text -pending_info=待补充 -pending=待处理 -in_progress=处理中 -pending_confirm=待确认 -resolved=已解决 -closed=已关闭 -``` - -Ensure priority labels are Chinese: - -```text -low=低 -medium=中 -high=高 -urgent=紧急 -``` - -- [ ] **Step 7: Run frontend typecheck** - -Run: - -```bash -cd frontend -npm run typecheck -``` - -Expected: exit code `0`. - -- [ ] **Step 8: Commit** - -Run: - -```bash -git add frontend/src/app/layout frontend/src/components/app -git commit -m "feat: add enterprise app ui layer" -``` - -## Task 3: Add Chinese Workbench and Routing - -**Files:** -- Modify: `frontend/src/app/router.tsx` -- Create: `frontend/src/features/workbench/WorkbenchPage.tsx` -- Modify: `frontend/src/features/design-system/DesignSystemPreview.tsx` - -- [ ] **Step 1: Add the default workbench route** - -In `frontend/src/app/router.tsx`, change the default route from `/tickets` to `/` and route `/` to `WorkbenchPage`. - -Required behavior: - -```text -#/ -> WorkbenchPage -#/tickets -> TicketListPage -#/tickets/new -> TicketCreatePage -#/tickets/:id -> TicketDetailPage -#/solutions -> SolutionListPage -#/design-system -> DesignSystemPreview -``` - -- [ ] **Step 2: Create workbench page** - -`WorkbenchPage` should load tickets with `listTickets({})` and show Chinese operational sections: - -```text -今日工作台 -待处理工单 -处理中 -高优先级 -已解决 -最近工单 -快捷入口 -``` - -Use the existing ticket API only. Counts can be computed from the loaded list. - -- [ ] **Step 3: Update design system preview** - -The preview page should demonstrate: - -```text -按钮 -状态标签 -优先级标签 -筛选条 -数据表格 -详情区块 -空状态 -``` - -All visible text must be Chinese. - -- [ ] **Step 4: Run frontend typecheck** - -Run: - -```bash -cd frontend -npm run typecheck -``` - -Expected: exit code `0`. - -- [ ] **Step 5: Commit** - -Run: - -```bash -git add frontend/src/app/router.tsx frontend/src/features/workbench frontend/src/features/design-system/DesignSystemPreview.tsx -git commit -m "feat: add chinese workbench page" -``` - -## Task 4: Redesign Ticket Pages - -**Files:** -- Modify: `frontend/src/features/tickets/TicketListPage.tsx` -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` -- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` -- Modify: `frontend/src/features/tickets/TicketComments.tsx` - -- [ ] **Step 1: Redesign ticket list as a table workbench** - -Use `AppPageHeader`, `AppFilterBar`, `AppDataTable`, `AppStatusBadge`, and `AppPriorityBadge`. - -Required Chinese table columns: - -```text -编号 -标题 -状态 -优先级 -关联系统 -影响范围 -创建时间 -操作 -``` - -Required actions: - -```text -新建工单 -查看详情 -重置筛选 -``` - -- [ ] **Step 2: Redesign ticket creation page** - -Use Chinese sections: - -```text -创建工单 -问题描述 -处理属性 -影响范围 -AI 建单辅助 -``` - -Use Chinese labels: - -```text -工单标题 -问题描述 -优先级 -关联系统 -影响范围 -运行环境 -提交工单 -``` - -The AI panel must clearly say: - -```text -后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。 -``` - -- [ ] **Step 3: Redesign ticket detail as an operations workspace** - -Use `AppDetailLayout` with: - -```text -Main column: 工单概览, 问题描述, 处理动态, 评论协作 -Side column: 关键字段, 关联解决方案, AI 支持分析 -``` - -Status transition buttons must show Chinese labels instead of raw enum values. - -- [ ] **Step 4: Localize ticket timeline and comments** - -Replace touched English labels with Chinese: - -```text -状态流转 -评论协作 -暂无评论 -发表回复 -``` - -- [ ] **Step 5: Run frontend typecheck** - -Run: - -```bash -cd frontend -npm run typecheck -``` - -Expected: exit code `0`. - -- [ ] **Step 6: Commit** - -Run: - -```bash -git add frontend/src/features/tickets -git commit -m "feat: redesign ticket workbench ui" -``` - -## Task 5: Redesign Solution Pages and Final Verification - -**Files:** -- Modify: `frontend/src/features/solutions/SolutionListPage.tsx` -- Modify: `frontend/src/features/solutions/SolutionLinkPanel.tsx` -- Modify: `docs/frontend/design-system.md` - -- [ ] **Step 1: Redesign solution list** - -Use Chinese copy and table/list structure: - -```text -解决方案库 -沉淀可复用的问题特征、排查步骤和处理方案。 -搜索解决方案 -状态 -引用次数 -查看关联 -``` - -Use badges for published/draft status: - -```text -published=已发布 -draft=草稿 -``` - -- [ ] **Step 2: Localize solution link panel** - -Use Chinese labels: - -```text -关联解决方案 -推荐方案 -已采纳 -暂无关联方案 -``` - -- [ ] **Step 3: Update design system docs** - -Update `docs/frontend/design-system.md` so it documents: - -```text -shadcn/ui primitives -App component layer -Chinese-first frontend copy rule -Business pages must use AppDataTable/AppFilterBar/AppDetailLayout for repeated patterns -``` - -- [ ] **Step 4: Run final frontend verification** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both commands exit `0`. - -- [ ] **Step 5: Run shadcn info** - -Run: - -```bash -cd frontend -npx shadcn@latest info -``` - -Expected: - -```text -components.json exists -components list is non-empty -``` - -- [ ] **Step 6: Commit** - -Run: - -```bash -git add frontend/src/features/solutions docs/frontend/design-system.md -git commit -m "feat: polish solution ui and design docs" -``` - -## Final Review Checklist - -- [ ] `frontend/components.json` exists. -- [ ] App sidebar/topbar text is Chinese. -- [ ] Workbench default route exists at `#/`. -- [ ] Ticket list is a table-like enterprise view, not a simple card/list dump. -- [ ] Ticket create page uses Chinese labels and includes a future AI helper panel. -- [ ] Ticket detail uses a main/side workbench layout. -- [ ] Solution list and solution link panel are Chinese. -- [ ] Touched frontend text is Chinese. -- [ ] `cd frontend && npm run typecheck` passes. -- [ ] `cd frontend && npm run build` passes. diff --git a/docs/superpowers/plans/2026-07-01-stage-1-6-enterprise-governance.md b/docs/superpowers/plans/2026-07-01-stage-1-6-enterprise-governance.md deleted file mode 100644 index 22f704b..0000000 --- a/docs/superpowers/plans/2026-07-01-stage-1-6-enterprise-governance.md +++ /dev/null @@ -1,628 +0,0 @@ -# Stage 1.6 Enterprise Governance 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:** Add a usable enterprise governance baseline with login, current user, users, user groups, roles, menus, API permission enforcement, permission-driven frontend navigation, and deployment bootstrap. - -**Architecture:** Backend keeps Django's built-in `User`, `Group`, and `Permission` as the core auth model, adds `UserGroup` and `MenuItem` for product governance, exposes session-authenticated Django Ninja APIs, and enforces permissions in ticket/solution/audit/governance routers. Frontend wraps the Vite SPA in an auth provider, renders a shadcn-based Chinese login page for anonymous users, consumes backend menu data in `AppSidebar`, and adds shadcn-based system governance pages. - -**Tech Stack:** Python 3.12, Django, Django Ninja, pytest, ruff, React, Vite, TypeScript, Tailwind CSS v3, shadcn/ui, Radix UI, npm. - ---- - -## Source Spec - -- `docs/superpowers/specs/2026-07-01-stage-1-6-enterprise-governance-design.md` - -## File Structure - -Backend: - -- Modify: `backend/config/settings/base.py` -- Modify: `backend/config/urls.py` -- Modify: `backend/apps/accounts/models.py` -- Modify: `backend/apps/accounts/permissions.py` -- Modify: `backend/apps/accounts/schemas.py` -- Modify: `backend/apps/accounts/api.py` -- Create: `backend/apps/accounts/auth.py` -- Create: `backend/apps/accounts/menus.py` -- Create: `backend/apps/accounts/bootstrap.py` -- Create: `backend/apps/accounts/management/commands/bootstrap_governance.py` -- Create: `backend/apps/accounts/migrations/0001_initial.py` -- Modify: `backend/apps/tickets/api.py` -- Modify: `backend/apps/solutions/api.py` -- Modify: `backend/apps/audit/api.py` -- Create: `backend/apps/accounts/tests/test_auth_api.py` -- Create: `backend/apps/accounts/tests/test_governance_api.py` -- Create: `backend/apps/accounts/tests/test_permission_enforcement.py` -- Create: `backend/apps/accounts/tests/test_bootstrap_governance.py` -- Modify: existing ticket/solution/audit tests to authenticate where protected endpoints are exercised. - -Frontend: - -- Modify: `frontend/src/lib/api.ts` -- Create: `frontend/src/features/auth/types.ts` -- Create: `frontend/src/features/auth/api.ts` -- Create: `frontend/src/features/auth/AuthProvider.tsx` -- Create: `frontend/src/features/auth/LoginPage.tsx` -- Modify: `frontend/src/app/App.tsx` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` -- Modify: `frontend/src/components/app/AppTopbar.tsx` -- Create: `frontend/src/features/system/api.ts` -- Create: `frontend/src/features/system/types.ts` -- Create: `frontend/src/features/system/SystemUsersPage.tsx` -- Create: `frontend/src/features/system/SystemUserGroupsPage.tsx` -- Create: `frontend/src/features/system/SystemRolesPage.tsx` -- Create: `frontend/src/features/system/SystemMenusPage.tsx` -- Create: `frontend/src/features/audit/AuditLogPage.tsx` -- Add shadcn components if missing: `checkbox`, `switch`, `alert`, `pagination`. - -Deployment/docs: - -- Modify: `.env.example` -- Create: `infra/backend-entrypoint.sh` -- Modify: `infra/backend.Dockerfile` -- Modify: `infra/docker-compose.yml` -- Modify: `docs/development/local-setup.md` - -## Task 1: Backend Governance Models And Bootstrap - -**Files:** -- Modify: `backend/apps/accounts/models.py` -- Modify: `backend/apps/accounts/permissions.py` -- Create: `backend/apps/accounts/menus.py` -- Create: `backend/apps/accounts/bootstrap.py` -- Create: `backend/apps/accounts/management/commands/bootstrap_governance.py` -- Create: `backend/apps/accounts/tests/test_bootstrap_governance.py` - -- [ ] **Step 1: Write failing bootstrap test** - -Create `backend/apps/accounts/tests/test_bootstrap_governance.py` with tests proving: - -```python -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, Permission -from django.core.management import call_command - -from apps.accounts.models import MenuItem, UserGroup - - -def test_bootstrap_governance_seeds_admin_roles_groups_permissions_and_menus(db, settings): - settings.DJANGO_SUPERUSER_USERNAME = "admin" - settings.DJANGO_SUPERUSER_PASSWORD = "admin12345" - settings.DJANGO_SUPERUSER_EMAIL = "admin@example.com" - - call_command("bootstrap_governance") - call_command("bootstrap_governance") - - user = get_user_model().objects.get(username="admin") - assert user.is_superuser is True - assert user.check_password("admin12345") - assert Group.objects.filter(name="系统管理员").exists() - assert Permission.objects.filter(codename="system.user.manage").exists() - assert UserGroup.objects.filter(code="default").exists() - assert MenuItem.objects.filter(code="system.users", title="用户管理").exists() - assert MenuItem.objects.filter(code="system.roles", permission_code="system.role.view").exists() -``` - -- [ ] **Step 2: Run the test to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_bootstrap_governance.py -v -``` - -Expected: fails because `MenuItem`, `UserGroup`, and `bootstrap_governance` do not exist. - -- [ ] **Step 3: Implement models, catalog, menus, bootstrap command** - -Implement: - -- `UserGroup` with `name`, `code`, `description`, `users`, `is_active`, timestamps. -- `MenuItem` with `code`, `title`, `path`, `icon`, `parent`, `sort_order`, `permission_code`, `is_visible`, `is_enabled`, timestamps. -- Chinese permission names and governance permission codes in `permissions.py`. -- Default role names in Chinese. -- Default menu catalog in `menus.py`. -- `bootstrap_governance()` service function and `bootstrap_governance` management command. - -- [ ] **Step 4: Make migrations and verify GREEN** - -Run: - -```bash -cd backend -python manage.py makemigrations accounts -python -m pytest apps/accounts/tests/test_bootstrap_governance.py -v -``` - -Expected: test passes. - -- [ ] **Step 5: Commit** - -```bash -git add backend/apps/accounts -git commit -m "feat: add governance bootstrap models" -``` - -## Task 2: Backend Auth And Current User APIs - -**Files:** -- Modify: `backend/config/settings/base.py` -- Modify: `backend/config/urls.py` -- Create: `backend/apps/accounts/auth.py` -- Modify: `backend/apps/accounts/schemas.py` -- Modify: `backend/apps/accounts/api.py` -- Create: `backend/apps/accounts/tests/test_auth_api.py` - -- [ ] **Step 1: Write failing auth API tests** - -Create `backend/apps/accounts/tests/test_auth_api.py` with tests for: - -```python -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.core.management import call_command - - -def test_login_me_and_logout_flow(client, db): - call_command("bootstrap_governance") - - login = client.post( - "/api/accounts/auth/login", - data={"username": "admin", "password": "admin12345"}, - content_type="application/json", - ) - assert login.status_code == 200 - assert login.json()["username"] == "admin" - - me = client.get("/api/accounts/auth/me") - assert me.status_code == 200 - payload = me.json() - assert payload["username"] == "admin" - assert "system.user.manage" in payload["permissions"] - assert any(menu["code"] == "system" for menu in payload["menus"]) - - logout = client.post("/api/accounts/auth/logout") - assert logout.status_code == 200 - assert client.get("/api/accounts/auth/me").status_code == 401 - - -def test_login_rejects_invalid_password(client, db): - get_user_model().objects.create_user(username="u", password="right") - - response = client.post( - "/api/accounts/auth/login", - data={"username": "u", "password": "wrong"}, - content_type="application/json", - ) - - assert response.status_code == 401 -``` - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_auth_api.py -v -``` - -Expected: fails because auth endpoints do not exist. - -- [ ] **Step 3: Implement session auth endpoints** - -Implement: - -- `GET /api/accounts/auth/csrf` -- `POST /api/accounts/auth/login` -- `POST /api/accounts/auth/logout` -- `GET /api/accounts/auth/me` -- `CORS_ALLOW_CREDENTIALS = True` -- `CSRF_TRUSTED_ORIGINS` env parsing - -- [ ] **Step 4: Verify GREEN** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_auth_api.py -v -``` - -Expected: tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add backend/config backend/apps/accounts -git commit -m "feat: add session auth api" -``` - -## Task 3: Backend Governance CRUD APIs - -**Files:** -- Modify: `backend/apps/accounts/schemas.py` -- Modify: `backend/apps/accounts/api.py` -- Create: `backend/apps/accounts/tests/test_governance_api.py` - -- [ ] **Step 1: Write failing governance API tests** - -Tests must cover: - -- Admin lists users. -- Admin creates a user. -- Admin creates a user group and assigns members. -- Admin creates a role and assigns permissions. -- Admin lists menus and updates one menu permission. -- Anonymous calls to governance endpoints return 401. - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_governance_api.py -v -``` - -Expected: fails because CRUD endpoints do not exist. - -- [ ] **Step 3: Implement schemas and APIs** - -Implement `/api/accounts` endpoints: - -- `/users` -- `/users/{id}` -- `/users/{id}/reset-password` -- `/users/{id}/roles` -- `/users/{id}/user-groups` -- `/user-groups` -- `/user-groups/{id}` -- `/user-groups/{id}/members` -- `/roles` -- `/roles/{id}` -- `/roles/{id}/permissions` -- `/permissions` -- `/menus` -- `/menus/{id}` -- `/menus/reorder` - -- [ ] **Step 4: Verify GREEN** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_governance_api.py -v -``` - -Expected: tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add backend/apps/accounts -git commit -m "feat: add governance management api" -``` - -## Task 4: Backend Permission Enforcement - -**Files:** -- Create: `backend/apps/accounts/auth.py` if not already created. -- Modify: `backend/apps/tickets/api.py` -- Modify: `backend/apps/solutions/api.py` -- Modify: `backend/apps/audit/api.py` -- Modify: existing ticket/solution/audit tests. -- Create: `backend/apps/accounts/tests/test_permission_enforcement.py` - -- [ ] **Step 1: Write failing permission enforcement tests** - -Tests must prove: - -- Anonymous ticket list is denied. -- User with `ticket.view` can list tickets. -- User without `ticket.create` cannot create tickets. -- User with `solution.view` can list solutions. -- User without `system.audit.view` cannot list audit logs. - -- [ ] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests/test_permission_enforcement.py -v -``` - -Expected: fails because current APIs are public. - -- [ ] **Step 3: Implement helpers and protect APIs** - -Implement helpers: - -```python -require_login(request) -require_permission(request, "ticket.view") -``` - -Update routers: - -- `/api/system/health` remains public. -- tickets require ticket permissions. -- solutions require solution permissions. -- audit requires `system.audit.view`. -- governance endpoints use governance permissions. - -- [ ] **Step 4: Update existing tests to authenticate** - -Existing ticket/solution/audit API tests should create a superuser or assign required permissions before calling protected endpoints. - -- [ ] **Step 5: Verify backend API tests** - -Run: - -```bash -cd backend -python -m pytest apps/accounts/tests apps/tickets/tests apps/solutions/tests apps/audit/tests -v -``` - -Expected: tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add backend/apps/accounts backend/apps/tickets backend/apps/solutions backend/apps/audit -git commit -m "feat: enforce api permissions" -``` - -## Task 5: Frontend Auth Shell And Permission Menus - -**Files:** -- Modify: `frontend/src/lib/api.ts` -- Create: `frontend/src/features/auth/types.ts` -- Create: `frontend/src/features/auth/api.ts` -- Create: `frontend/src/features/auth/AuthProvider.tsx` -- Create: `frontend/src/features/auth/LoginPage.tsx` -- Modify: `frontend/src/app/App.tsx` -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` -- Modify: `frontend/src/components/app/AppTopbar.tsx` - -- [ ] **Step 1: Add missing shadcn components** - -Run: - -```bash -cd frontend -npx shadcn@latest add alert checkbox switch pagination --yes -``` - -Then review generated files for Tailwind v3 compatibility. - -- [ ] **Step 2: Implement auth API client and provider** - -Implement: - -- CSRF fetch. -- login/logout/me calls. -- `AuthProvider`. -- `useAuth`. -- `apiRequest` sends `credentials: "include"` and CSRF token on unsafe methods. - -- [ ] **Step 3: Implement Chinese login page** - -`LoginPage` uses shadcn `Card`, `Input`, `Label`, `Button`, and `Alert`. - -Visible text must include: - -```text -AI 工单平台 -企业技术支持工作台 -账号 -密码 -登录 -``` - -- [ ] **Step 4: Gate app by auth state** - -Anonymous users see `LoginPage`. Authenticated users see `AppShell` and `Router`. - -- [ ] **Step 5: Make sidebar backend-driven** - -`AppSidebar` receives menu data from auth context. Keep local icon mapping with lucide icons. - -- [ ] **Step 6: Run frontend verification** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both pass. - -- [ ] **Step 7: Commit** - -```bash -git add frontend/src frontend/components.json frontend/package.json frontend/package-lock.json -git commit -m "feat: add frontend auth shell" -``` - -## Task 6: Frontend System Governance Pages - -**Files:** -- Create: `frontend/src/features/system/types.ts` -- Create: `frontend/src/features/system/api.ts` -- Create: `frontend/src/features/system/SystemUsersPage.tsx` -- Create: `frontend/src/features/system/SystemUserGroupsPage.tsx` -- Create: `frontend/src/features/system/SystemRolesPage.tsx` -- Create: `frontend/src/features/system/SystemMenusPage.tsx` -- Create: `frontend/src/features/audit/AuditLogPage.tsx` -- Modify: `frontend/src/app/router.tsx` - -- [ ] **Step 1: Add system API client** - -Create typed API functions for users, user groups, roles, permissions, menus, and audit logs. - -- [ ] **Step 2: Add users page** - -`#/system/users` shows table columns: - -```text -用户名 -显示名称 -邮箱 -状态 -角色 -用户组 -操作 -``` - -Use Dialog or Sheet for create/edit. - -- [ ] **Step 3: Add user groups page** - -`#/system/user-groups` shows Chinese table and create/edit dialog. - -- [ ] **Step 4: Add roles page** - -`#/system/roles` shows role list and permission matrix grouped by module. - -- [ ] **Step 5: Add menus page** - -`#/system/menus` shows menu rows, required permission, visibility/enabled status, and edit dialog. - -- [ ] **Step 6: Add audit page** - -`#/audit` shows existing operation logs in a Chinese table. - -- [ ] **Step 7: Run frontend verification** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both pass. - -- [ ] **Step 8: Commit** - -```bash -git add frontend/src/features/system frontend/src/features/audit frontend/src/app/router.tsx -git commit -m "feat: add system governance pages" -``` - -## Task 7: Deployment Bootstrap And Final Verification - -**Files:** -- Modify: `.env.example` -- Create: `infra/backend-entrypoint.sh` -- Modify: `infra/backend.Dockerfile` -- Modify: `infra/docker-compose.yml` -- Modify: `docs/development/local-setup.md` - -- [ ] **Step 1: Add environment defaults** - -Add admin bootstrap env vars to `.env.example`: - -```dotenv -DJANGO_SUPERUSER_USERNAME=admin -DJANGO_SUPERUSER_PASSWORD=admin12345 -DJANGO_SUPERUSER_EMAIL=admin@example.com -CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 -``` - -- [ ] **Step 2: Add backend entrypoint** - -Create `infra/backend-entrypoint.sh`: - -```sh -#!/bin/sh -set -e - -python manage.py migrate -python manage.py bootstrap_governance -exec "$@" -``` - -- [ ] **Step 3: Wire backend Dockerfile and compose** - -Backend service uses the entrypoint before running `runserver`. Worker can still start Celery after migrations are handled by backend. - -- [ ] **Step 4: Update setup docs** - -Document: - -- fresh Docker startup -- default login account -- production warning to override password - -- [ ] **Step 5: Run full verification** - -Run: - -```bash -cd backend -python -m pytest -v -python -m ruff check . - -cd ../frontend -npm run typecheck -npm run build -npx shadcn@latest info -``` - -Expected: all commands pass. - -- [ ] **Step 6: Verify compose config** - -Run: - -```bash -cd infra -docker compose --env-file ../.env.example config -``` - -Expected: config prints without errors. On this Windows host, use Docker Desktop's full docker executable path if `docker` is unavailable in shell PATH. - -- [ ] **Step 7: Browser smoke test** - -Start backend and frontend locally if feasible, then verify: - -- anonymous user sees login page -- admin can log in -- sidebar shows system governance menus -- `#/system/users`, `#/system/roles`, `#/system/menus`, `#/audit` render Chinese UI -- logout returns to login - -- [ ] **Step 8: Commit** - -```bash -git add .env.example infra docs/development -git commit -m "feat: add governance deployment bootstrap" -``` - -## Final Review Checklist - -- [ ] `bootstrap_governance` is idempotent. -- [ ] Login/logout/me APIs work. -- [ ] Frontend anonymous state shows login only. -- [ ] Sidebar menus are backend-driven. -- [ ] User/user-group/role/menu pages use shadcn/App components. -- [ ] Ticket/solution/audit/governance APIs enforce permissions. -- [ ] Docker startup can migrate and seed governance. -- [ ] Backend tests pass. -- [ ] Backend ruff passes. -- [ ] Frontend typecheck passes. -- [ ] Frontend build passes. -- [ ] shadcn info still reports Vite/Tailwind v3 and installed components. diff --git a/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md b/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md deleted file mode 100644 index b2b8430..0000000 --- a/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md +++ /dev/null @@ -1,631 +0,0 @@ -# Stage 1.7 Ticket Experience Visual Redesign 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:** Redesign ticket list, detail, and create pages into a polished Chinese enterprise support experience using shadcn/ui, with minimal backend support for pagination, filters, display users, and transition remarks. - -**Architecture:** Backend keeps the existing Django Ninja ticket router and adds a paginated response shape plus richer filters and display user serialization. Frontend keeps the Vite SPA and current app shell, then decomposes ticket pages into small shadcn-composed components for metrics, filters, metadata, and transition dialogs. - -**Tech Stack:** Python 3.12, Django, Django Ninja, pytest, ruff, React, Vite, TypeScript, Tailwind CSS v3, shadcn/ui, Radix UI, npm. - ---- - -## Source Spec - -- `docs/superpowers/specs/2026-07-01-stage-1-7-ticket-experience-visual-redesign-design.md` - -## File Structure - -Backend: - -- Modify: `backend/apps/tickets/schemas.py` -- Modify: `backend/apps/tickets/api.py` -- Modify: `backend/apps/tickets/services.py` -- Modify: `backend/apps/tickets/tests/test_ticket_api.py` - -Frontend: - -- Modify: `frontend/src/features/tickets/types.ts` -- Modify: `frontend/src/features/tickets/api.ts` -- Modify: `frontend/src/features/tickets/TicketListPage.tsx` -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` -- Modify: `frontend/src/features/tickets/TicketComments.tsx` -- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` -- Create: `frontend/src/features/tickets/TicketMetricCards.tsx` -- Create: `frontend/src/features/tickets/TicketFilterBar.tsx` -- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx` -- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx` - -## Implementation Progress - -As of 2026-07-03, Stage 1.7 has moved beyond the first visual redesign into a stronger ticket workflow baseline: - -- Ticket categories are standardized as `业务问题`, `技术问题`, `数据问题`, and `操作问题`. -- Category-driven status policies shorten simple business/operation tickets while preserving the full technical workflow. -- Ticket creation captures business-facing context fields including problem performance, problem object, submitter, submitter organization, and environment. -- Solution adoption can close a ticket, write the resolution summary, update usage count, and record timeline events. -- Recommended linked solutions remain non-closing references, but resolve/close dialogs prefill the resolution from linked solutions. -- Ticket detail side metadata was reduced to avoid repeating the overview card. - -## Task 1: Backend Ticket List Support - -**Files:** -- Modify: `backend/apps/tickets/schemas.py` -- Modify: `backend/apps/tickets/api.py` -- Modify: `backend/apps/tickets/services.py` -- Modify: `backend/apps/tickets/tests/test_ticket_api.py` - -- [x] **Step 1: Write failing pagination/filter/display tests** - -Add these tests to `backend/apps/tickets/tests/test_ticket_api.py`: - -```python -from django.contrib.auth import get_user_model - - -def test_list_tickets_returns_paginated_response_with_display_users(admin_client, db): - user_model = get_user_model() - requester = user_model.objects.create_user(username="requester", first_name="请求人") - assignee = user_model.objects.create_user(username="assignee", first_name="处理人") - Ticket.objects.create( - ticket_no="TK202607010100", - title="Payment failed", - description="A", - requester=requester, - assignee=assignee, - related_system="CRM", - ) - - response = admin_client.get("/api/tickets?page=1&page_size=20") - - assert response.status_code == 200 - payload = response.json() - assert payload["page"] == 1 - assert payload["page_size"] == 20 - assert payload["total"] == 1 - assert payload["total_pages"] == 1 - assert payload["items"][0]["requester"]["display_name"] == "请求人" - assert payload["items"][0]["assignee"]["display_name"] == "处理人" - - -def test_list_tickets_filters_by_priority_related_system_and_view(admin_client, db): - user_model = get_user_model() - current_user = user_model.objects.get(username="admin") - Ticket.objects.create( - ticket_no="TK202607010101", - title="Mine urgent CRM", - description="A", - priority="urgent", - related_system="CRM", - assignee=current_user, - created_by=current_user, - ) - Ticket.objects.create( - ticket_no="TK202607010102", - title="Other medium ERP", - description="B", - priority="medium", - related_system="ERP", - ) - - response = admin_client.get( - "/api/tickets?priority=urgent&related_system=CRM&view=assigned_to_me" - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["total"] == 1 - assert payload["items"][0]["ticket_no"] == "TK202607010101" - - -def test_transition_ticket_requires_non_empty_remark(admin_client, db): - ticket = Ticket.objects.create( - ticket_no="TK202607010103", - title="A", - description="A", - status="pending", - ) - - response = admin_client.post( - f"/api/tickets/{ticket.id}/status", - data={"status": "in_progress", "remark": ""}, - content_type="application/json", - ) - - assert response.status_code == 400 -``` - -- [x] **Step 2: Run tests to verify RED** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_api.py -v -``` - -Expected: fails because list response is currently a list, display users are missing, advanced filters are missing, and empty transition remark is allowed. - -- [x] **Step 3: Implement schemas and serialization** - -Add schemas to `backend/apps/tickets/schemas.py`: - -```python -class TicketUserOut(Schema): - id: int - username: str - display_name: str - - -class TicketListOut(Schema): - items: list[TicketOut] - page: int - page_size: int - total: int - total_pages: int -``` - -Extend `TicketOut` with: - -```python -requester_id: int | None -requester: TicketUserOut | None = None -assignee: TicketUserOut | None = None -``` - -Implement serializer helpers in `backend/apps/tickets/api.py`: - -```python -def serialize_ticket_user(user): - if user is None: - return None - return { - "id": user.id, - "username": user.username, - "display_name": user.get_full_name() or user.username, - } - - -def serialize_ticket(ticket: Ticket) -> dict: - return { - "id": ticket.id, - "ticket_no": ticket.ticket_no, - "title": ticket.title, - "description": ticket.description, - "status": ticket.status, - "priority": ticket.priority, - "impact_scope": ticket.impact_scope, - "environment": ticket.environment, - "related_system": ticket.related_system, - "requester_id": ticket.requester_id, - "assignee_id": ticket.assignee_id, - "requester": serialize_ticket_user(ticket.requester), - "assignee": serialize_ticket_user(ticket.assignee), - "created_at": ticket.created_at, - "updated_at": ticket.updated_at, - } -``` - -- [x] **Step 4: Implement paginated list and filters** - -Change `list_tickets` to return `TicketListOut` and accept: - -```python -page: int = 1 -page_size: int = 20 -status: str | None = None -priority: str | None = None -q: str | None = None -related_system: str | None = None -assignee_id: int | None = None -view: str = "all" -``` - -Filter rules: - -- `status`: exact status. -- `priority`: exact priority. -- `q`: `ticket_no`, `title`, or `description` contains keyword. -- `related_system`: contains text. -- `assignee_id`: exact assignee. -- `view=assigned_to_me`: `assignee=request.user`. -- `view=created_by_me`: `created_by=request.user`. -- `view=high_priority`: `priority in ["high", "urgent"]`. - -Clamp: - -```python -page = max(page, 1) -page_size = min(max(page_size, 1), 100) -``` - -- [x] **Step 5: Require transition remarks** - -In `update_ticket_status`, reject blank remarks: - -```python -if not payload.remark.strip(): - raise HttpError(400, "Transition remark is required") -``` - -Import `HttpError` from `ninja.errors`. - -- [x] **Step 6: Update existing tests for new list shape** - -Change existing list assertions in `test_list_tickets_filters_by_status`: - -```python -payload = response.json() -assert payload["total"] == 1 -assert payload["items"][0]["ticket_no"] == "TK202606300001" -``` - -- [x] **Step 7: Verify GREEN** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests/test_ticket_api.py apps/tickets/tests/test_ticket_export.py apps/accounts/tests/test_permission_enforcement.py -v -python -m ruff check . -``` - -Expected: all selected tests pass and ruff has no errors. - -- [x] **Step 8: Commit** - -```bash -git add backend/apps/tickets -git commit -m "feat: add ticket list visual support api" -``` - -## Task 2: Frontend Ticket API And List Redesign - -**Files:** -- Modify: `frontend/src/features/tickets/types.ts` -- Modify: `frontend/src/features/tickets/api.ts` -- Modify: `frontend/src/features/tickets/TicketListPage.tsx` -- Create: `frontend/src/features/tickets/TicketMetricCards.tsx` -- Create: `frontend/src/features/tickets/TicketFilterBar.tsx` - -- [x] **Step 1: Refresh shadcn docs** - -Run: - -```bash -cd frontend -npx shadcn@latest docs tabs table card badge select input pagination skeleton sonner -``` - -Use installed components first. Do not overwrite existing locally patched components. - -- [x] **Step 2: Update ticket frontend types** - -In `frontend/src/features/tickets/types.ts`, add: - -```ts -export type TicketUser = { - id: number - username: string - display_name: string -} - -export type TicketListResponse = { - items: Ticket[] - page: number - page_size: number - total: number - total_pages: number -} -``` - -Extend `Ticket`: - -```ts -requester_id: number | null -requester: TicketUser | null -assignee: TicketUser | null -``` - -Add list query type: - -```ts -export type TicketListParams = { - page?: number - page_size?: number - status?: string - priority?: string - q?: string - related_system?: string - assignee_id?: number - view?: 'all' | 'assigned_to_me' | 'created_by_me' | 'high_priority' -} -``` - -- [x] **Step 3: Update ticket API** - -In `frontend/src/features/tickets/api.ts`, update `listTickets` to return `TicketListResponse` and include all query fields: - -```ts -export function listTickets(params: TicketListParams = {}) { - const query = new URLSearchParams() - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { - query.set(key, String(value)) - } - }) - const suffix = query.toString() ? `?${query}` : '' - return apiRequest(`/tickets${suffix}`) -} -``` - -- [x] **Step 4: Create TicketMetricCards** - -Create `frontend/src/features/tickets/TicketMetricCards.tsx` with compact shadcn `Card` metrics for: - -```text -待处理 -处理中 -待确认 -高优先级 -``` - -Props: - -```ts -type TicketMetricCardsProps = { - tickets: Ticket[] -} -``` - -- [x] **Step 5: Create TicketFilterBar** - -Create `frontend/src/features/tickets/TicketFilterBar.tsx` using `AppFilterBar`, `Input`, `Select`, and `Button`. - -Props: - -```ts -type TicketFilterBarProps = { - q: string - status: string - priority: string - relatedSystem: string - onQChange: (value: string) => void - onStatusChange: (value: string) => void - onPriorityChange: (value: string) => void - onRelatedSystemChange: (value: string) => void - onReset: () => void -} -``` - -- [x] **Step 6: Redesign TicketListPage** - -Update `TicketListPage`: - -- Use shadcn `Tabs` for `全部工单`, `我的待办`, `我创建的`, `高优先级`. -- Use `TicketMetricCards`. -- Use `TicketFilterBar`. -- Use backend pagination fields. -- Use `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationPrevious`, `PaginationNext`. -- Table columns must be Chinese and include requester/assignee display when available. -- Empty/error/loading states must be Chinese. - -- [x] **Step 7: Verify frontend GREEN** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both pass. - -- [x] **Step 8: Commit** - -```bash -git add frontend/src/features/tickets -git commit -m "feat: redesign ticket list experience" -``` - -## Task 3: Frontend Ticket Detail Redesign - -**Files:** -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` -- Modify: `frontend/src/features/tickets/TicketComments.tsx` -- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` -- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx` -- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx` - -- [x] **Step 1: Refresh shadcn docs** - -Run: - -```bash -cd frontend -npx shadcn@latest docs dialog textarea card badge separator tabs sonner -``` - -- [x] **Step 2: Create TicketTransitionDialog** - -Create a dialog component with: - -```ts -type TicketTransitionDialogProps = { - open: boolean - status: TicketStatus | null - onOpenChange: (open: boolean) => void - onSubmit: (status: TicketStatus, remark: string) => Promise -} -``` - -Visible text: - -```text -处理说明 -请说明本次流转的原因、处理动作或待确认事项。 -取消 -确认提交 -``` - -The submit button is disabled until `remark.trim()` is non-empty. - -- [x] **Step 3: Create TicketMetaPanel** - -Create `TicketMetaPanel` with shadcn `Card`, `Badge`, `Separator`, `AppField`, `AppStatusBadge`, and `AppPriorityBadge`. - -Display: - -```text -当前状态 -优先级 -处理人 -请求人 -关联系统 -运行环境 -影响范围 -``` - -- [x] **Step 4: Update TicketStatusTimeline** - -Make every event label Chinese: - -```ts -const EVENT_LABELS: Record = { - created: '创建工单', - updated: '更新工单', - status_change: '状态变更', - commented: '新增评论', - attachment_added: '新增附件', -} -``` - -- [x] **Step 5: Update TicketComments** - -Use Chinese labels and distinguish: - -```text -内部备注 -用户可见回复 -``` - -Use shadcn `Tabs` or `Select` to choose visibility. Existing API can still receive: - -```ts -{ comment_type: 'internal_note' | 'user_reply', visibility: 'internal' | 'public' } -``` - -- [x] **Step 6: Redesign TicketDetailPage** - -Update page: - -- Loading title: `正在加载工单` -- Error title: `无法加载工单` -- Header actions open `TicketTransitionDialog`. -- Main column contains `问题描述`, `处理记录`, `评论与协作`. -- Side column uses `TicketMetaPanel`, `SolutionLinkPanel`, and restrained AI placeholder. - -- [x] **Step 7: Verify frontend GREEN** - -Run: - -```bash -cd frontend -npm run typecheck -npm run build -``` - -Expected: both pass. - -- [x] **Step 8: Commit** - -```bash -git add frontend/src/features/tickets -git commit -m "feat: redesign ticket detail experience" -``` - -## Task 4: Frontend Ticket Create Redesign And Final Verification - -**Files:** -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` - -- [x] **Step 1: Refresh shadcn docs** - -Run: - -```bash -cd frontend -npx shadcn@latest docs card input select textarea badge alert -``` - -- [x] **Step 2: Redesign create page** - -Update `TicketCreatePage` into grouped sections: - -```text -基础信息 -影响与优先级 -环境与系统 -问题描述 -提交确认 -``` - -Visible copy must be Chinese. Use `Card`, `Input`, `Select`, `Textarea`, `Badge`, and `Button`. - -- [x] **Step 3: Use Chinese error feedback** - -On create failure, show: - -```text -创建工单失败,请检查必填信息或稍后重试。 -``` - -- [x] **Step 4: Run full verification** - -Run: - -```bash -cd backend -python -m pytest apps/tickets/tests apps/accounts/tests/test_permission_enforcement.py -v -python -m ruff check . - -cd ../frontend -npm run typecheck -npm run build -npx shadcn@latest info -``` - -Expected: all commands pass. - -- [x] **Step 5: Browser visual smoke test** - -With local backend/frontend running: - -- Login as seeded admin. -- Open `#/tickets` and verify Chinese tabs, metrics, filters, table, and pagination. -- Open a ticket detail page and verify transition dialog requires handling note. -- Open `#/tickets/new` and verify grouped create form has no obvious overlap. - -- [x] **Step 6: Commit** - -```bash -git add frontend/src/features/tickets -git commit -m "feat: redesign ticket create experience" -``` - -## Final Review Checklist - -- [x] Ticket list uses shadcn tabs, cards, table, filters, and pagination. -- [x] Ticket detail uses a Chinese transition dialog and no English loading/error copy. -- [x] Ticket comments distinguish internal notes and user-visible replies. -- [x] Ticket create page uses grouped Chinese form sections. -- [x] Backend ticket list supports pagination and visual filters. -- [x] Ticket responses include requester and assignee display fields. -- [x] Empty transition remarks are rejected. -- [x] Backend ticket tests pass. -- [x] Backend ruff passes. -- [x] Frontend typecheck passes. -- [x] Frontend build passes. -- [x] shadcn info still reports Vite/Tailwind v3 and installed components. diff --git a/docs/superpowers/plans/2026-07-02-stage-1-8-shadcn-admin-ui-standardization.md b/docs/superpowers/plans/2026-07-02-stage-1-8-shadcn-admin-ui-standardization.md deleted file mode 100644 index f66843f..0000000 --- a/docs/superpowers/plans/2026-07-02-stage-1-8-shadcn-admin-ui-standardization.md +++ /dev/null @@ -1,280 +0,0 @@ -# Stage 1.8 Shadcn Admin UI Standardization 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:** Rebuild the frontend UI foundation around shadcn/ui so future pages inherit a consistent enterprise admin layout, mobile navigation, form standard, select composition, and visual baseline. - -**Architecture:** shadcn/ui remains the source component library. Project `App*` components become thin composition wrappers around shadcn primitives, while feature pages consume those wrappers instead of hand-rolled layout. The first implementation batch fixes shell navigation, field primitives, select composition, and high-frequency forms. - -**Tech Stack:** React 19, Vite, TypeScript, Tailwind v3, shadcn/ui `radix-nova`, Radix primitives, lucide-react. - ---- - -## File Structure - -- Modify: `frontend/src/app/layout/AppShell.tsx` - passes mobile menu state into topbar/sidebar sheet. -- Modify: `frontend/src/components/app/AppSidebar.tsx` - extracts reusable navigation content and adds sheet-friendly rendering. -- Modify: `frontend/src/components/app/AppTopbar.tsx` - adds mobile menu button and keeps desktop actions. -- Create: `frontend/src/components/ui/field.tsx` - shadcn field primitive from CLI, merged without losing local component changes. -- Modify: `frontend/src/components/app/AppField.tsx` - thin wrapper over shadcn field primitives. -- Modify: `frontend/src/features/**` select call sites - wrap `SelectItem` with `SelectGroup`. -- Modify: `frontend/src/features/auth/LoginPage.tsx` - migrate login form fields. -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` - migrate main create form fields. -- Modify: `frontend/src/features/solutions/SolutionCreatePage.tsx` - migrate solution form fields. -- Modify: `frontend/src/features/system/SystemUsersPage.tsx`, `SystemRolesPage.tsx`, `SystemUserGroupsPage.tsx` - migrate quick-create forms where practical. -- Test/verify: `npm run typecheck`, `npm run build`, grep scans, desktop/mobile browser checks. - -## Implementation Progress - -As of 2026-07-02, the Stage 1.8 foundation has been implemented and extended with core ticket page polish: - -- Mobile sidebar navigation is available through a shadcn `Sheet`. -- shadcn `Field` primitives are installed and `AppField` is the project wrapper. -- Select menus have been swept to use `SelectContent > SelectGroup > SelectItem`. -- Login, ticket, solution, and system quick-create forms have been migrated toward `AppField`. -- `AppPage`, `AppDataTable`, `AppFilterBar`, and system governance page shells have been standardized. -- Enterprise visual foundation, light/dark/system theme switching, and sidebar theme tokens are in place. -- Ticket center and ticket detail have been refined as core Stage 1 operational screens. - ---- - -### Task 1: Mobile Shell Navigation - -**Files:** -- Modify: `frontend/src/app/layout/AppShell.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` -- Modify: `frontend/src/components/app/AppTopbar.tsx` - -- [x] **Step 1: Inspect current shell boundaries** - -Run: - -```powershell -Get-Content frontend/src/app/layout/AppShell.tsx -Get-Content frontend/src/components/app/AppSidebar.tsx -Get-Content frontend/src/components/app/AppTopbar.tsx -``` - -Expected: `AppShell` renders fixed sidebar and topbar; sidebar is desktop-only. - -- [x] **Step 2: Add mobile menu state in shell** - -Implement `mobileNavOpen` state in `AppShell`, pass `onMobileMenuClick` into topbar, and pass `mobileOpen/onMobileOpenChange` into sidebar. - -- [x] **Step 3: Extract sidebar navigation content** - -In `AppSidebar`, extract grouped nav rendering into `SidebarNavContent({ onNavigate })`. Use the same component for desktop and mobile sheet. - -- [x] **Step 4: Render mobile Sheet** - -Use existing shadcn `Sheet`, `SheetContent`, `SheetHeader`, and `SheetTitle`. The sheet title must be present for accessibility. Menu item clicks call `onNavigate`. - -- [x] **Step 5: Add topbar mobile trigger** - -Add a `Button size="icon" variant="ghost"` visible below `lg`, with `Menu` icon using `data-icon` and `aria-label="打开导航"`. - -- [x] **Step 6: Verify** - -Run: - -```powershell -npm run typecheck -``` - -Expected: exit code 0. - ---- - -### Task 2: Install and Merge Shadcn Field Primitive - -**Files:** -- Create: `frontend/src/components/ui/field.tsx` -- May modify: `frontend/src/components/ui/label.tsx` -- May modify: `frontend/src/components/ui/separator.tsx` -- Modify: `frontend/src/components/app/AppField.tsx` - -- [x] **Step 1: Preview CLI changes** - -Run: - -```powershell -$env:NODE_USE_ENV_PROXY = "1"; $env:HTTP_PROXY = "http://127.0.0.1:7897"; $env:HTTPS_PROXY = "http://127.0.0.1:7897"; cd frontend; npx shadcn@latest add field --dry-run -``` - -Expected: creates `field.tsx`, previews updates to `label.tsx` and `separator.tsx`. - -- [x] **Step 2: Inspect diffs before applying** - -Run: - -```powershell -$env:NODE_USE_ENV_PROXY = "1"; $env:HTTP_PROXY = "http://127.0.0.1:7897"; $env:HTTPS_PROXY = "http://127.0.0.1:7897"; cd frontend; npx shadcn@latest add field --diff field.tsx -$env:NODE_USE_ENV_PROXY = "1"; $env:HTTP_PROXY = "http://127.0.0.1:7897"; $env:HTTPS_PROXY = "http://127.0.0.1:7897"; cd frontend; npx shadcn@latest add field --diff label.tsx -$env:NODE_USE_ENV_PROXY = "1"; $env:HTTP_PROXY = "http://127.0.0.1:7897"; $env:HTTPS_PROXY = "http://127.0.0.1:7897"; cd frontend; npx shadcn@latest add field --diff separator.tsx -``` - -Expected: understand local changes before applying. - -- [x] **Step 3: Add the field component** - -Apply the CLI result, then inspect changed files. If `label.tsx` or `separator.tsx` overwrite local customizations, restore needed local behavior manually. - -- [x] **Step 4: Rebuild AppField** - -Make `AppField` render shadcn `Field`, `FieldLabel`, and optional `FieldDescription`. Support `htmlFor`, `required`, `invalid`, and `description` props. - -- [x] **Step 5: Verify** - -Run: - -```powershell -npm run typecheck -``` - -Expected: exit code 0. - ---- - -### Task 3: Select Composition Sweep - -**Files:** -- Modify: `frontend/src/features/tickets/TicketFilterBar.tsx` -- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` -- Modify: `frontend/src/features/tickets/TicketComments.tsx` -- Modify: `frontend/src/features/solutions/SolutionListPage.tsx` -- Modify: `frontend/src/features/solutions/SolutionCreatePage.tsx` -- Modify: `frontend/src/features/design-system/DesignSystemPreview.tsx` - -- [x] **Step 1: Add SelectGroup imports** - -For each file importing select primitives, add `SelectGroup`. - -- [x] **Step 2: Wrap select items** - -Change: - -```tsx - - 全部状态 - -``` - -to: - -```tsx - - - 全部状态 - - -``` - -- [x] **Step 3: Scan** - -Run: - -```powershell -rg "SelectContent>[\\s\\S]*SelectItem|\\s*\\s* **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:** Allow support staff to search published solutions, adopt one, write it into the ticket resolution, and close the ticket in one action. - -**Architecture:** Keep the adoption workflow server-side so ticket status, solution link, solution usage count, resolution fields, and timeline events stay consistent. The frontend remains a shadcn/App-component panel that calls a single action endpoint and refreshes ticket context. - -**Tech Stack:** Django, Django Ninja, PostgreSQL migrations already present, React, TypeScript, Tailwind, shadcn/ui project components. - ---- - -### Task 1: Backend Adopt-And-Close Endpoint - -**Files:** -- Modify: `backend/apps/solutions/schemas.py` -- Modify: `backend/apps/solutions/api.py` -- Test: `backend/apps/solutions/tests/test_solution_api.py` - -- [ ] **Step 1: Write the failing API test** - -Add a test that creates a published solution and pending ticket, posts to `/api/solutions/{solution_id}/adopt-ticket`, and asserts: - -```python -assert response.status_code == 200 -ticket.refresh_from_db() -assert ticket.status == "closed" -assert ticket.is_resolved is True -assert ticket.replied_to_customer is True -assert ticket.resolution_summary == solution.solution -assert ticket.events.filter(event_type="solution_adopted").exists() -``` - -- [ ] **Step 2: Implement the schema and endpoint** - -Add a payload with `ticket_id`, `remark`, and `replied_to_customer`. The endpoint should: - -```python -link, _ = TicketSolutionLink.objects.get_or_create( - ticket=ticket, - solution=solution, - link_type="adopted", - defaults={"created_by": request.user if request.user.is_authenticated else None}, -) -ticket.status = "closed" -ticket.resolution_summary = solution.solution -ticket.is_resolved = True -ticket.replied_to_customer = payload.replied_to_customer -ticket.resolved_at = ticket.resolved_at or timezone.now() -ticket.closed_at = timezone.now() -ticket.save(update_fields=[...]) -TicketEvent.objects.create(event_type="solution_adopted", ...) -``` - -- [ ] **Step 3: Verify backend** - -Run: - -```powershell -python -m pytest backend\apps\solutions\tests backend\apps\tickets\tests -python -m ruff check backend\apps\solutions backend\apps\tickets -``` - -Expected: all tests pass and ruff reports no issues. - -### Task 2: Frontend Search And Adopt UI - -**Files:** -- Modify: `frontend/src/features/solutions/api.ts` -- Modify: `frontend/src/features/solutions/SolutionLinkPanel.tsx` -- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` - -- [ ] **Step 1: Add API client** - -Add: - -```ts -export function adoptSolutionAndCloseTicket(solutionId: number, ticketId: number) { - return apiRequest(`/solutions/${solutionId}/adopt-ticket`, { - method: 'POST', - body: JSON.stringify({ - ticket_id: ticketId, - replied_to_customer: true, - remark: '采纳解决方案并关闭工单' - }) - }) -} -``` - -- [ ] **Step 2: Add search and action button** - -In `SolutionLinkPanel`, add a search input filtering by `title`, `problem_signature`, and `solution`. Rename the adoption action to `采纳并关闭`, call the new endpoint, refresh linked solutions, and notify the parent ticket detail page to reload. - -- [ ] **Step 3: Verify frontend** - -Run: - -```powershell -npm run typecheck -npm run build -``` - -Expected: TypeScript and Vite build both succeed. - -### Task 3: Commit - -**Files:** -- Stage only the backend/frontend files touched by this feature. - -- [ ] **Step 1: Inspect git status** - -Run: - -```powershell -git status --short -git diff --stat -``` - -Expected: existing local `.gitignore` and local lock/compose files remain unstaged. - -- [ ] **Step 2: Commit** - -Run: - -```powershell -git add backend/apps/solutions frontend/src/features/solutions frontend/src/features/tickets/TicketDetailPage.tsx docs/superpowers/plans/2026-07-03-adopt-solution-close-ticket.md -git commit -m "feat: adopt solution and close ticket" -``` diff --git a/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md b/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md deleted file mode 100644 index 826db27..0000000 --- a/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md +++ /dev/null @@ -1,1142 +0,0 @@ -# Stage 2 Knowledge Vector Retrieval 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:** Build the first usable knowledge base layer: knowledge bases, documents, structured knowledge items, chunks, deterministic local vectors, and retrieval APIs/UI that later Agents can consume. - -**Architecture:** Add a new Django `apps.knowledge` module with normalized knowledge models and a service layer for parsing, chunking, embedding, and retrieval. Use a deterministic local embedding function in Stage 2 so retrieval contracts are stable before Stage 3 model gateway integration replaces the embedding provider. Add React shadcn-based knowledge pages using the existing `App*` component layer and Chinese UI text. - -**Tech Stack:** Python 3.12, Django, Django Ninja, PostgreSQL, JSON fields for MVP embedding vectors, pytest, ruff, React, Vite, TypeScript, Tailwind CSS, shadcn/ui, lucide-react. - ---- - -## Scope - -Stage 2 implements the knowledge layer only. It does not call real LLMs, does not configure model providers, and does not run autonomous Agents. The retrieval API returns citable chunks from manual knowledge items, uploaded document text, and published reusable solutions. - -## File Structure - -Backend: - -- Create: `backend/apps/knowledge/__init__.py` -- Create: `backend/apps/knowledge/apps.py` -- Create: `backend/apps/knowledge/models.py` -- Create: `backend/apps/knowledge/schemas.py` -- Create: `backend/apps/knowledge/services.py` -- Create: `backend/apps/knowledge/api.py` -- Create: `backend/apps/knowledge/tests/test_knowledge_api.py` -- Create: `backend/apps/knowledge/tests/test_retrieval.py` -- Modify: `backend/config/settings/base.py` -- Modify: `backend/config/urls.py` - -Frontend: - -- Create: `frontend/src/features/knowledge/types.ts` -- Create: `frontend/src/features/knowledge/api.ts` -- Create: `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` -- Create: `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` -- Create: `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` - -Docs: - -- Modify: `README.md` -- Modify: `docs/frontend/design-system.md` - ---- - -### Task 1: Backend Knowledge Models And CRUD API - -**Files:** -- Create: `backend/apps/knowledge/models.py` -- Create: `backend/apps/knowledge/schemas.py` -- Create: `backend/apps/knowledge/api.py` -- Create: `backend/apps/knowledge/apps.py` -- Create: `backend/apps/knowledge/tests/test_knowledge_api.py` -- Modify: `backend/config/settings/base.py` -- Modify: `backend/config/urls.py` - -- [x] **Step 1: Write failing CRUD tests** - -Create `backend/apps/knowledge/tests/test_knowledge_api.py`: - -```python -from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem - - -def test_create_and_list_knowledge_base(admin_client, db): - response = admin_client.post( - "/api/knowledge/bases", - data={ - "name": "税务采集知识库", - "description": "税局采集、日志和常见问题", - "scope": "数据采集", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["name"] == "税务采集知识库" - assert payload["enabled"] is True - - response = admin_client.get("/api/knowledge/bases") - - assert response.status_code == 200 - assert response.json()[0]["name"] == "税务采集知识库" - - -def test_create_knowledge_item_builds_chunks(admin_client, db): - base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集") - - response = admin_client.post( - "/api/knowledge/items", - data={ - "knowledge_base_id": base.id, - "title": "税局采集进度查询", - "problem": "客户询问采集进度长时间不更新", - "applicable_scope": "生产环境税局采集", - "error_code": "", - "root_cause": "税局服务响应不稳定", - "troubleshooting_steps": "查询 Python 日志并确认采集任务状态", - "solution": "告知客户税局不稳定,任务仍在采集中,稍后复查。", - "risk_note": "不要重复触发采集任务。", - "status": "published", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - item = KnowledgeItem.objects.get(id=response.json()["id"]) - assert item.status == "published" - assert KnowledgeChunk.objects.filter(item=item).count() >= 1 - - -def test_ingest_document_text_creates_document_and_chunks(admin_client, db): - base = KnowledgeBase.objects.create(name="日志知识库", scope="技术支持") - - response = admin_client.post( - "/api/knowledge/documents", - data={ - "knowledge_base_id": base.id, - "file_name": "tax-log.md", - "file_type": "markdown", - "content": "税局接口不稳定时,采集任务会停留在处理中。需要查询 Python 日志确认。", - }, - content_type="application/json", - ) - - assert response.status_code == 200 - document = KnowledgeDocument.objects.get(id=response.json()["id"]) - assert document.parse_status == "success" - assert KnowledgeChunk.objects.filter(document=document).count() >= 1 -``` - -- [x] **Step 2: Run tests to verify RED** - -Run: - -```powershell -python -m pytest backend\apps\knowledge\tests\test_knowledge_api.py -v -``` - -Expected: fails because `apps.knowledge` and `/api/knowledge/*` routes do not exist. - -- [x] **Step 3: Create models** - -Create `backend/apps/knowledge/models.py`: - -```python -from django.conf import settings -from django.db import models - - -class KnowledgeBase(models.Model): - name = models.CharField(max_length=160) - description = models.TextField(blank=True) - scope = models.CharField(max_length=120, blank=True) - enabled = models.BooleanField(default=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-updated_at"] - - def __str__(self) -> str: - return self.name - - -class KnowledgeDocument(models.Model): - knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="documents") - file_name = models.CharField(max_length=255) - file_type = models.CharField(max_length=80, blank=True) - storage_path = models.CharField(max_length=500, blank=True) - content_text = models.TextField(blank=True) - parse_status = models.CharField(max_length=40, default="pending") - parse_error = models.TextField(blank=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-created_at"] - - -class KnowledgeItem(models.Model): - knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="items") - title = models.CharField(max_length=200) - problem = models.TextField() - applicable_scope = models.TextField(blank=True) - error_code = models.CharField(max_length=120, blank=True) - root_cause = models.TextField(blank=True) - troubleshooting_steps = models.TextField(blank=True) - solution = models.TextField() - risk_note = models.TextField(blank=True) - status = models.CharField(max_length=40, default="draft") - source_type = models.CharField(max_length=40, default="manual") - source_ticket_id = models.BigIntegerField(null=True, blank=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_knowledge_items", - ) - reviewed_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="reviewed_knowledge_items", - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-updated_at"] - - -class KnowledgeChunk(models.Model): - knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="chunks") - document = models.ForeignKey( - KnowledgeDocument, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="chunks", - ) - item = models.ForeignKey( - KnowledgeItem, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="chunks", - ) - chunk_index = models.PositiveIntegerField(default=0) - content = models.TextField() - embedding = models.JSONField(default=list, blank=True) - embedding_model = models.CharField(max_length=120, default="local-hash-v1") - metadata = models.JSONField(default=dict, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ["knowledge_base_id", "document_id", "item_id", "chunk_index"] - indexes = [ - models.Index(fields=["knowledge_base", "chunk_index"]), - ] -``` - -- [x] **Step 4: Register app and router** - -Create `backend/apps/knowledge/apps.py`: - -```python -from django.apps import AppConfig - - -class KnowledgeConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.knowledge" -``` - -Modify `backend/config/settings/base.py`: - -```python -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "corsheaders", - "apps.accounts", - "apps.audit", - "apps.system", - "apps.tickets", - "apps.solutions", - "apps.knowledge", -] -``` - -Modify `backend/config/urls.py`: - -```python -from apps.knowledge.api import router as knowledge_router - -api.add_router("/knowledge", knowledge_router) -``` - -- [x] **Step 5: Add schemas** - -Create `backend/apps/knowledge/schemas.py`: - -```python -from datetime import datetime - -from ninja import Schema - - -class KnowledgeBaseIn(Schema): - name: str - description: str = "" - scope: str = "" - enabled: bool = True - - -class KnowledgeBaseOut(Schema): - id: int - name: str - description: str - scope: str - enabled: bool - created_at: datetime - updated_at: datetime - - -class KnowledgeDocumentIn(Schema): - knowledge_base_id: int - file_name: str - file_type: str = "" - content: str - - -class KnowledgeDocumentOut(Schema): - id: int - knowledge_base_id: int - file_name: str - file_type: str - parse_status: str - parse_error: str - created_at: datetime - - -class KnowledgeItemIn(Schema): - knowledge_base_id: int - title: str - problem: str - applicable_scope: str = "" - error_code: str = "" - root_cause: str = "" - troubleshooting_steps: str = "" - solution: str - risk_note: str = "" - status: str = "draft" - - -class KnowledgeItemOut(Schema): - id: int - knowledge_base_id: int - title: str - problem: str - applicable_scope: str - error_code: str - root_cause: str - troubleshooting_steps: str - solution: str - risk_note: str - status: str - source_type: str - created_at: datetime - updated_at: datetime -``` - -- [x] **Step 6: Implement CRUD API** - -Create `backend/apps/knowledge/api.py`: - -```python -from ninja import Router - -from apps.accounts.api import require_permission -from apps.knowledge.models import KnowledgeBase, KnowledgeDocument, KnowledgeItem -from apps.knowledge.schemas import ( - KnowledgeBaseIn, - KnowledgeBaseOut, - KnowledgeDocumentIn, - KnowledgeDocumentOut, - KnowledgeItemIn, - KnowledgeItemOut, -) -from apps.knowledge.services import ingest_document_text, rebuild_item_chunks - -router = Router(tags=["knowledge"]) - - -@router.post("/bases", response=KnowledgeBaseOut) -def create_knowledge_base(request, payload: KnowledgeBaseIn): - require_permission(request, "knowledge.manage") - return KnowledgeBase.objects.create( - name=payload.name, - description=payload.description, - scope=payload.scope, - enabled=payload.enabled, - created_by=request.user if request.user.is_authenticated else None, - ) - - -@router.get("/bases", response=list[KnowledgeBaseOut]) -def list_knowledge_bases(request, enabled: bool | None = None): - require_permission(request, "knowledge.view") - queryset = KnowledgeBase.objects.all() - if enabled is not None: - queryset = queryset.filter(enabled=enabled) - return queryset[:100] - - -@router.post("/items", response=KnowledgeItemOut) -def create_knowledge_item(request, payload: KnowledgeItemIn): - require_permission(request, "knowledge.manage") - item = KnowledgeItem.objects.create( - knowledge_base_id=payload.knowledge_base_id, - title=payload.title, - problem=payload.problem, - applicable_scope=payload.applicable_scope, - error_code=payload.error_code, - root_cause=payload.root_cause, - troubleshooting_steps=payload.troubleshooting_steps, - solution=payload.solution, - risk_note=payload.risk_note, - status=payload.status, - created_by=request.user if request.user.is_authenticated else None, - ) - rebuild_item_chunks(item) - return item - - -@router.get("/items", response=list[KnowledgeItemOut]) -def list_knowledge_items(request, knowledge_base_id: int | None = None, status: str | None = None): - require_permission(request, "knowledge.view") - queryset = KnowledgeItem.objects.all() - if knowledge_base_id: - queryset = queryset.filter(knowledge_base_id=knowledge_base_id) - if status: - queryset = queryset.filter(status=status) - return queryset[:100] - - -@router.post("/documents", response=KnowledgeDocumentOut) -def create_knowledge_document(request, payload: KnowledgeDocumentIn): - require_permission(request, "knowledge.manage") - return ingest_document_text( - knowledge_base_id=payload.knowledge_base_id, - file_name=payload.file_name, - file_type=payload.file_type, - content=payload.content, - operator=request.user if request.user.is_authenticated else None, - ) -``` - -- [x] **Step 7: Create migrations** - -Run: - -```powershell -cd backend -python manage.py makemigrations knowledge -``` - -Expected: creates `backend/apps/knowledge/migrations/0001_initial.py`. - -- [x] **Step 8: Verify GREEN** - -Run: - -```powershell -python -m pytest backend\apps\knowledge\tests\test_knowledge_api.py -v -python -m ruff check backend\apps\knowledge backend\config -``` - -Expected: CRUD tests pass and ruff has no issues. - -- [x] **Step 9: Commit** - -Run: - -```powershell -git add backend/apps/knowledge backend/config/settings/base.py backend/config/urls.py -git commit -m "feat: add knowledge base core api" -``` - ---- - -### Task 2: Chunking, Deterministic Embedding, And Retrieval - -**Files:** -- Create/Modify: `backend/apps/knowledge/services.py` -- Create: `backend/apps/knowledge/tests/test_retrieval.py` -- Modify: `backend/apps/knowledge/schemas.py` -- Modify: `backend/apps/knowledge/api.py` -- Modify: `backend/apps/solutions/api.py` - -- [x] **Step 1: Write failing retrieval tests** - -Create `backend/apps/knowledge/tests/test_retrieval.py`: - -```python -from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeItem -from apps.knowledge.services import retrieve_knowledge -from apps.solutions.models import Solution - - -def test_retrieve_knowledge_prefers_relevant_chunk(db): - base = KnowledgeBase.objects.create(name="采集知识库", scope="数据采集") - item = KnowledgeItem.objects.create( - knowledge_base=base, - title="税局采集进度", - problem="采集进度不更新", - solution="查询 Python 日志,确认税局状态。", - status="published", - ) - KnowledgeChunk.objects.create( - knowledge_base=base, - item=item, - chunk_index=0, - content="税局采集进度不更新时,需要查询 Python 日志。", - embedding=[0.1, 0.2], - metadata={"source_type": "knowledge_item", "title": item.title}, - ) - KnowledgeChunk.objects.create( - knowledge_base=base, - chunk_index=1, - content="登录失败通常需要检查账号密码。", - embedding=[0.9, 0.1], - metadata={"source_type": "document", "title": "登录问题"}, - ) - - results = retrieve_knowledge("税局采集进度怎么查询", limit=1) - - assert len(results) == 1 - assert results[0]["title"] == "税局采集进度" - assert "Python 日志" in results[0]["content"] - - -def test_published_solution_can_be_indexed_for_retrieval(db): - solution = Solution.objects.create( - title="税局不稳定采集处理中", - problem_signature="采集进度长时间不更新", - root_cause="税局服务不稳定", - solution="回复客户税局不稳定,任务仍在采集中。", - status="published", - ) - - from apps.knowledge.services import index_published_solution - - chunks = index_published_solution(solution) - - assert len(chunks) == 1 - assert chunks[0].metadata["source_type"] == "solution" - assert chunks[0].metadata["solution_id"] == solution.id -``` - -- [x] **Step 2: Run tests to verify RED** - -Run: - -```powershell -python -m pytest backend\apps\knowledge\tests\test_retrieval.py -v -``` - -Expected: fails because retrieval services do not exist. - -- [x] **Step 3: Implement chunking and embedding services** - -Create `backend/apps/knowledge/services.py`: - -```python -import hashlib -import math - -from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem -from apps.solutions.models import Solution - -DEFAULT_CHUNK_SIZE = 600 - - -def split_text(content: str, chunk_size: int = DEFAULT_CHUNK_SIZE) -> list[str]: - normalized = "\n".join(line.strip() for line in content.splitlines() if line.strip()) - if not normalized: - return [] - return [normalized[index : index + chunk_size] for index in range(0, len(normalized), chunk_size)] - - -def embed_text(content: str, dimensions: int = 16) -> list[float]: - digest = hashlib.sha256(content.encode("utf-8")).digest() - values = [digest[index] / 255 for index in range(dimensions)] - length = math.sqrt(sum(value * value for value in values)) or 1 - return [round(value / length, 6) for value in values] - - -def keyword_score(query: str, content: str) -> float: - terms = [term for term in query.lower().split() if term] - lowered = content.lower() - return sum(1 for term in terms if term in lowered) - - -def vector_score(left: list[float], right: list[float]) -> float: - return sum(a * b for a, b in zip(left, right, strict=False)) - - -def rebuild_item_chunks(item: KnowledgeItem) -> list[KnowledgeChunk]: - item.chunks.all().delete() - content = "\n".join( - [ - item.title, - item.problem, - item.applicable_scope, - item.error_code, - item.root_cause, - item.troubleshooting_steps, - item.solution, - item.risk_note, - ] - ) - chunks = [] - for index, chunk_text in enumerate(split_text(content)): - chunks.append( - KnowledgeChunk.objects.create( - knowledge_base=item.knowledge_base, - item=item, - chunk_index=index, - content=chunk_text, - embedding=embed_text(chunk_text), - metadata={ - "source_type": "knowledge_item", - "title": item.title, - "item_id": item.id, - }, - ) - ) - return chunks - - -def ingest_document_text( - knowledge_base_id: int, - file_name: str, - file_type: str, - content: str, - operator=None, -) -> KnowledgeDocument: - document = KnowledgeDocument.objects.create( - knowledge_base_id=knowledge_base_id, - file_name=file_name, - file_type=file_type, - content_text=content, - parse_status="success", - created_by=operator, - ) - for index, chunk_text in enumerate(split_text(content)): - KnowledgeChunk.objects.create( - knowledge_base=document.knowledge_base, - document=document, - chunk_index=index, - content=chunk_text, - embedding=embed_text(chunk_text), - metadata={ - "source_type": "document", - "title": document.file_name, - "document_id": document.id, - }, - ) - return document - - -def index_published_solution(solution: Solution) -> list[KnowledgeChunk]: - base, _ = KnowledgeBase.objects.get_or_create( - name="解决方案知识库", - defaults={"description": "从已发布解决方案生成的检索知识", "scope": "solutions"}, - ) - KnowledgeChunk.objects.filter(metadata__solution_id=solution.id).delete() - content = "\n".join( - [ - solution.title, - solution.problem_signature, - solution.related_system, - solution.environment_scope, - solution.root_cause, - solution.solution, - solution.risk_note, - ] - ) - chunks = [] - for index, chunk_text in enumerate(split_text(content)): - chunks.append( - KnowledgeChunk.objects.create( - knowledge_base=base, - chunk_index=index, - content=chunk_text, - embedding=embed_text(chunk_text), - metadata={ - "source_type": "solution", - "title": solution.title, - "solution_id": solution.id, - }, - ) - ) - return chunks - - -def retrieve_knowledge(query: str, knowledge_base_id: int | None = None, limit: int = 5) -> list[dict]: - query_embedding = embed_text(query) - queryset = KnowledgeChunk.objects.select_related("knowledge_base", "item", "document") - if knowledge_base_id: - queryset = queryset.filter(knowledge_base_id=knowledge_base_id) - scored = [] - for chunk in queryset[:1000]: - score = keyword_score(query, chunk.content) * 10 + vector_score(query_embedding, chunk.embedding) - if score > 0: - scored.append((score, chunk)) - scored.sort(key=lambda pair: pair[0], reverse=True) - return [ - { - "id": chunk.id, - "knowledge_base_id": chunk.knowledge_base_id, - "title": chunk.metadata.get("title", chunk.knowledge_base.name), - "source_type": chunk.metadata.get("source_type", "chunk"), - "content": chunk.content, - "score": round(score, 4), - "metadata": chunk.metadata, - } - for score, chunk in scored[:limit] - ] -``` - -- [x] **Step 4: Add retrieval schemas and API** - -Append to `backend/apps/knowledge/schemas.py`: - -```python -class KnowledgeSearchIn(Schema): - query: str - knowledge_base_id: int | None = None - limit: int = 5 - - -class KnowledgeSearchResultOut(Schema): - id: int - knowledge_base_id: int - title: str - source_type: str - content: str - score: float - metadata: dict -``` - -Append to `backend/apps/knowledge/api.py`: - -```python -from apps.knowledge.schemas import KnowledgeSearchIn, KnowledgeSearchResultOut -from apps.knowledge.services import retrieve_knowledge - - -@router.post("/search", response=list[KnowledgeSearchResultOut]) -def search_knowledge(request, payload: KnowledgeSearchIn): - require_permission(request, "knowledge.view") - return retrieve_knowledge( - query=payload.query, - knowledge_base_id=payload.knowledge_base_id, - limit=min(max(payload.limit, 1), 20), - ) -``` - -- [x] **Step 5: Index published solutions on create** - -Modify `backend/apps/solutions/api.py` in `create_solution`: - -```python -solution = Solution.objects.create( - title=payload.title, - problem_signature=payload.problem_signature, - related_system=payload.related_system, - environment_scope=payload.environment_scope, - root_cause=payload.root_cause, - solution=payload.solution, - risk_note=payload.risk_note, - status=payload.status, - created_by=request.user if request.user.is_authenticated else None, -) -if solution.status == "published": - from apps.knowledge.services import index_published_solution - - index_published_solution(solution) -return solution -``` - -- [x] **Step 6: Verify GREEN** - -Run: - -```powershell -python -m pytest backend\apps\knowledge\tests backend\apps\solutions\tests -v -python -m ruff check backend\apps\knowledge backend\apps\solutions -``` - -Expected: tests pass and ruff has no issues. - -- [x] **Step 7: Commit** - -Run: - -```powershell -git add backend/apps/knowledge backend/apps/solutions/api.py -git commit -m "feat: add knowledge retrieval service" -``` - ---- - -### Task 3: Frontend Knowledge Pages - -**Files:** -- Create: `frontend/src/features/knowledge/types.ts` -- Create: `frontend/src/features/knowledge/api.ts` -- Create: `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` -- Create: `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` -- Create: `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` - -- [x] **Step 1: Create frontend types** - -Create `frontend/src/features/knowledge/types.ts`: - -```ts -export type KnowledgeBase = { - id: number - name: string - description: string - scope: string - enabled: boolean - created_at: string - updated_at: string -} - -export type KnowledgeItemInput = { - knowledge_base_id: number - title: string - problem: string - applicable_scope: string - error_code: string - root_cause: string - troubleshooting_steps: string - solution: string - risk_note: string - status: 'draft' | 'published' | 'disabled' -} - -export type KnowledgeSearchResult = { - id: number - knowledge_base_id: number - title: string - source_type: string - content: string - score: number - metadata: Record -} -``` - -- [x] **Step 2: Create frontend API** - -Create `frontend/src/features/knowledge/api.ts`: - -```ts -import { apiRequest } from '@/lib/api' - -import type { KnowledgeBase, KnowledgeItemInput, KnowledgeSearchResult } from './types' - -export function listKnowledgeBases() { - return apiRequest('/knowledge/bases') -} - -export function createKnowledgeBase(payload: { - name: string - description: string - scope: string - enabled?: boolean -}) { - return apiRequest('/knowledge/bases', { - method: 'POST', - body: JSON.stringify({ ...payload, enabled: payload.enabled ?? true }) - }) -} - -export function createKnowledgeItem(payload: KnowledgeItemInput) { - return apiRequest('/knowledge/items', { - method: 'POST', - body: JSON.stringify(payload) - }) -} - -export function searchKnowledge(query: string, knowledgeBaseId?: number) { - return apiRequest('/knowledge/search', { - method: 'POST', - body: JSON.stringify({ - query, - knowledge_base_id: knowledgeBaseId, - limit: 8 - }) - }) -} -``` - -- [x] **Step 3: Create knowledge base list page** - -Create `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` using `AppPage`, `AppPageHeader`, `AppSection`, `AppDataTable`, `AppField`, `Input`, and `Button`. - -Required Chinese UI: - -```text -知识库 -新建知识库 -名称 -范围 -描述 -启用状态 -创建知识库 -暂无知识库 -``` - -The quick-create form posts to `createKnowledgeBase`, reloads `listKnowledgeBases`, and clears the form. - -- [x] **Step 4: Create knowledge item editor** - -Create `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` using existing `AppField`, `Input`, `Textarea`, `Select`, `SelectGroup`, and `Button`. - -Required initial form: - -```ts -const initialForm: KnowledgeItemInput = { - knowledge_base_id: 0, - title: '', - problem: '', - applicable_scope: '', - error_code: '', - root_cause: '', - troubleshooting_steps: '', - solution: '', - risk_note: '', - status: 'draft' -} -``` - -Required Chinese sections: - -```text -新建知识条目 -基础信息 -问题与范围 -原因与处理 -发布状态 -保存知识条目 -``` - -On success, navigate to `#/knowledge/search`. - -- [x] **Step 5: Create knowledge search page** - -Create `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` using `AppPage`, `AppPageHeader`, `AppSection`, `Input`, `Button`, `Badge`, and `Card`. - -Search behavior: - -```ts -const [query, setQuery] = React.useState('') -const [results, setResults] = React.useState([]) - -async function handleSearch(event: React.FormEvent) { - event.preventDefault() - if (!query.trim()) return - setResults(await searchKnowledge(query.trim())) -} -``` - -Required Chinese UI: - -```text -知识检索 -输入问题、错误码或处理场景 -检索 -引用来源 -相似度 -暂无检索结果 -``` - -- [x] **Step 6: Wire routes** - -Modify `frontend/src/app/router.tsx`: - -```tsx -import { KnowledgeBaseListPage } from '@/features/knowledge/KnowledgeBaseListPage' -import { KnowledgeItemEditorPage } from '@/features/knowledge/KnowledgeItemEditorPage' -import { KnowledgeSearchPage } from '@/features/knowledge/KnowledgeSearchPage' - -if (route === '/knowledge') { - return -} - -if (route === '/knowledge/items/new') { - return -} - -if (route === '/knowledge/search') { - return -} -``` - -- [x] **Step 7: Add sidebar navigation** - -Modify `frontend/src/components/app/AppSidebar.tsx` to add Chinese knowledge menu entries under the existing navigation group: - -```ts -{ label: '知识库', href: '#/knowledge', icon: BookOpen } -{ label: '知识检索', href: '#/knowledge/search', icon: Search } -``` - -Use lucide icons already available in `lucide-react`. - -- [x] **Step 8: Verify frontend** - -Run: - -```powershell -npm run typecheck -npm run build -``` - -Expected: both commands pass. - -- [x] **Step 9: Commit** - -Run: - -```powershell -git add frontend/src/features/knowledge frontend/src/app/router.tsx frontend/src/components/app/AppSidebar.tsx -git commit -m "feat: add knowledge management pages" -``` - ---- - -### Task 4: Final Verification And Documentation Sync - -**Files:** -- Modify: `README.md` -- Modify: `docs/frontend/design-system.md` -- Modify: `docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md` - -- [x] **Step 1: Run backend verification** - -Run: - -```powershell -python manage.py makemigrations --check --dry-run -python -m pytest backend\apps\knowledge\tests backend\apps\solutions\tests backend\apps\tickets\tests -python -m ruff check backend\apps\knowledge backend\apps\solutions backend\apps\tickets backend\config -``` - -Expected: - -```text -No changes detected -all selected tests pass -All checks passed! -``` - -- [x] **Step 2: Run frontend verification** - -Run: - -```powershell -npm run typecheck -npm run build -``` - -Expected: TypeScript and Vite build succeed. - -- [x] **Step 3: Run database migration locally** - -Run: - -```powershell -cd backend -python manage.py migrate -``` - -Expected: `knowledge.0001_initial` is applied. - -- [x] **Step 4: Update README current status** - -Modify `README.md` current status sentence to include: - -```text -knowledge base and retrieval baseline -``` - -- [x] **Step 5: Update frontend design system** - -Modify `docs/frontend/design-system.md` under `Current Core Screens`: - -```markdown -- Knowledge pages use the same `AppPage`, `AppSection`, `AppDataTable`, `AppField`, and shadcn form primitives as ticket and system pages. -- Knowledge search results must show source type, title, score, and citable content snippets. -``` - -- [x] **Step 6: Mark plan complete** - -In this plan file, mark completed task checkboxes as `[x]` after successful execution. - -- [x] **Step 7: Commit** - -Run: - -```powershell -git add README.md docs/frontend/design-system.md docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md -git commit -m "docs: sync stage 2 knowledge progress" -``` - ---- - -## Final Review Checklist - -- [x] `apps.knowledge` exists and is registered in Django settings. -- [x] Knowledge bases can be created and listed through API. -- [x] Knowledge items create chunks. -- [x] Document text ingestion creates chunks. -- [x] Published solutions can be indexed as knowledge chunks. -- [x] Retrieval returns citable chunk results with source metadata. -- [x] Frontend has Chinese knowledge base, item creation, and search pages. -- [x] Sidebar exposes knowledge navigation. -- [x] Backend tests pass. -- [x] Backend ruff passes. -- [x] Frontend typecheck passes. -- [x] Frontend build passes. -- [x] README and design-system docs reflect Stage 2 behavior. diff --git a/docs/superpowers/plans/2026-07-06-stage-2-5-global-frontend-prototype.md b/docs/superpowers/plans/2026-07-06-stage-2-5-global-frontend-prototype.md deleted file mode 100644 index ca6ae08..0000000 --- a/docs/superpowers/plans/2026-07-06-stage-2-5-global-frontend-prototype.md +++ /dev/null @@ -1,1661 +0,0 @@ -# Stage 2.5 Global Frontend 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:** Build a Chinese, shadcn-based, interactive frontend prototype that exposes the full AI ticket platform product map before implementing Stage 3 to Stage 5 backend APIs. - -**Architecture:** Keep existing real API-backed pages for tickets, solutions, knowledge, and system management. Add a frontend-only prototype module with typed mock contracts, shared App components, and mock-backed pages for dashboard, model hub, Agents, Skills, and MCP tools. Route everything through the current hash router and sidebar without adding backend models or APIs. - -**Tech Stack:** React, Vite, TypeScript, Tailwind CSS, shadcn/ui, Radix UI, lucide-react, existing project-owned `App*` components. - ---- - -## Source Spec - -- `docs/superpowers/specs/2026-07-06-stage-2-5-global-frontend-prototype-design.md` - -## File Structure - -Create: - -- `frontend/src/features/prototype/types.ts` - Typed mock contracts that document future API payloads. -- `frontend/src/features/prototype/mockData.ts` - Realistic Chinese mock data for dashboard, model hub, Agent, Skill, and MCP pages. -- `frontend/src/components/app/AppMetricCard.tsx` - Reusable metric card for workbench/dashboard/model summaries. -- `frontend/src/components/app/AppRiskBadge.tsx` - Reusable risk badge for MCP and Agent safety states. -- `frontend/src/components/app/AppTraceTimeline.tsx` - Reusable execution trace timeline for Agent/tool/model steps. -- `frontend/src/components/app/AppReferenceCard.tsx` - Reusable citable reference card for knowledge/AI panels. -- `frontend/src/features/dashboard/DashboardPage.tsx` - Mock-backed operational dashboard. -- `frontend/src/features/modelhub/ModelProviderPage.tsx` - Mock-backed model provider list and edit dialog. -- `frontend/src/features/modelhub/ModelTestingPage.tsx` - Mock-backed model comparison/testing page. -- `frontend/src/features/agents/AgentListPage.tsx` - Mock-backed Agent configuration list. -- `frontend/src/features/agents/AgentRunsPage.tsx` - Mock-backed Agent run list. -- `frontend/src/features/agents/AgentRunDetailPage.tsx` - Mock-backed Agent run detail and step trace. -- `frontend/src/features/skills/SkillListPage.tsx` - Mock-backed Skill list and editing/testing surface. -- `frontend/src/features/tools/McpToolsPage.tsx` - Mock-backed MCP server/tool/risk page. - -Modify: - -- `frontend/src/app/router.tsx` - Add Stage 2.5 routes. -- `frontend/src/components/app/AppSidebar.tsx` - Expose full product map through built-in prototype navigation when backend menus do not include it. -- `frontend/src/features/workbench/WorkbenchPage.tsx` - Add AI/knowledge/model/tool overview blocks using real tickets plus prototype metrics. -- `frontend/src/features/tickets/TicketCreatePage.tsx` - Add a future Agent-assisted creation panel. -- `frontend/src/features/tickets/TicketDetailPage.tsx` - Add consistent AI analysis/reference prototype sections. -- `docs/frontend/design-system.md` - Document frontend-first prototype rules. -- `README.md` - Mention Stage 2.5 frontend prototype status. -- `docs/superpowers/plans/2026-07-06-stage-2-5-global-frontend-prototype.md` - Mark checkboxes during execution. - -Do not modify backend files in this stage. - ---- - -### Task 1: Prototype Contracts And Shared App Components - -**Files:** -- Create: `frontend/src/features/prototype/types.ts` -- Create: `frontend/src/features/prototype/mockData.ts` -- 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` - -- [ ] **Step 1: Create typed mock contracts** - -Create `frontend/src/features/prototype/types.ts`: - -```ts -export type PrototypeStatus = 'success' | 'warning' | 'danger' | 'muted' - -export type DashboardMetric = { - id: string - label: string - value: string - helper: string - status: PrototypeStatus -} - -export type ModelProvider = { - id: number - name: string - provider_type: 'openai_compatible' | 'azure_openai' | 'local' - base_url: string - enabled: boolean - model_count: number - default_chat_model: string - default_embedding_model: string - last_test_status: 'passed' | 'failed' | 'untested' - updated_at: string -} - -export type ModelTestResult = { - id: number - provider_name: string - model_name: string - prompt: string - response: string - latency_ms: number - input_tokens: number - output_tokens: number - status: 'success' | 'failed' - error_message: string -} - -export type AgentDefinition = { - id: number - name: string - agent_type: 'ticket_create' | 'support_analysis' | 'knowledge_distill' - description: string - enabled: boolean - model_name: string - knowledge_scope: string - tool_scope: string - requires_human_confirm: boolean - updated_at: string -} - -export type AgentStepType = 'retrieval' | 'skill' | 'model_call' | 'tool_call' | 'human_confirm' | 'artifact' - -export type AgentStep = { - id: number - step_type: AgentStepType - step_name: string - status: 'success' | 'failed' | 'waiting_confirm' | 'skipped' - input_summary: string - output_summary: string - latency_ms: number - created_at: string -} - -export type AgentRun = { - id: number - agent_name: string - run_type: 'ticket_create' | 'support_analysis' | 'knowledge_distill' | 'test' - ticket_no: string - status: 'running' | 'success' | 'failed' | 'waiting_confirm' - trigger_user: string - duration_ms: number - created_at: string - output_summary: string - steps: AgentStep[] -} - -export type SkillDefinition = { - id: number - name: string - scenario: 'field_extract' | 'missing_info' | 'solution_recommend' | 'reply_draft' | 'knowledge_draft' - description: string - enabled: boolean - version: string - prompt_template: string - input_schema: string - output_schema: string - updated_at: string -} - -export type McpServer = { - id: number - name: string - server_url: string - auth_type: 'none' | 'token' | 'basic' - connection_state: 'connected' | 'disconnected' | 'error' - tool_count: number - last_sync_at: string -} - -export type McpTool = { - id: number - server_id: number - name: string - description: string - risk_level: 'low' | 'medium' | 'high' - enabled: boolean - requires_human_confirm: boolean -} - -export type ToolCall = { - id: number - tool_name: string - run_id: number - input_summary: string - output_summary: string - status: 'success' | 'failed' | 'blocked' | 'waiting_confirm' - confirmed_by: string - created_at: string -} - -export type KnowledgeReference = { - id: number - title: string - source_type: 'knowledge_item' | 'solution' | 'document' | 'historical_ticket' - content: string - score: number -} -``` - -- [ ] **Step 2: Create realistic mock data** - -Create `frontend/src/features/prototype/mockData.ts`: - -```ts -import type { - AgentDefinition, - AgentRun, - DashboardMetric, - KnowledgeReference, - McpServer, - McpTool, - ModelProvider, - ModelTestResult, - SkillDefinition, - ToolCall -} from './types' - -export const dashboardMetrics: DashboardMetric[] = [ - { id: 'pending', label: '待处理工单', value: '18', helper: '其中 6 个超过 4 小时未更新', status: 'warning' }, - { id: 'knowledge', label: '知识命中率', value: '72%', helper: '近 7 天解决方案和知识引用', status: 'success' }, - { id: 'agent', label: 'Agent 运行', value: '46', helper: '3 次等待人工确认', status: 'muted' }, - { id: 'risk', label: '高风险工具', value: '2', helper: '均处于人工确认模式', status: 'danger' } -] - -export const modelProviders: ModelProvider[] = [ - { - id: 1, - name: 'OpenAI 兼容渠道', - provider_type: 'openai_compatible', - base_url: 'https://api.example.com/v1', - enabled: true, - model_count: 4, - default_chat_model: 'gpt-4.1', - default_embedding_model: 'text-embedding-3-large', - last_test_status: 'passed', - updated_at: '2026-07-06T09:20:00Z' - }, - { - id: 2, - name: '本地推理服务', - provider_type: 'local', - base_url: 'http://model-gateway.internal/v1', - enabled: true, - model_count: 2, - default_chat_model: 'qwen-support-32b', - default_embedding_model: 'bge-m3', - last_test_status: 'failed', - updated_at: '2026-07-05T16:10:00Z' - } -] - -export const modelTestResults: ModelTestResult[] = [ - { - id: 1, - provider_name: 'OpenAI 兼容渠道', - model_name: 'gpt-4.1', - prompt: '客户反馈税局采集进度长时间不更新,请给出排查建议。', - response: '建议先查询采集任务状态和 Python 日志,确认税局接口是否返回超时或限流,再向客户说明任务仍在采集中。', - latency_ms: 1860, - input_tokens: 62, - output_tokens: 88, - status: 'success', - error_message: '' - }, - { - id: 2, - provider_name: '本地推理服务', - model_name: 'qwen-support-32b', - prompt: '客户反馈税局采集进度长时间不更新,请给出排查建议。', - response: '', - latency_ms: 30000, - input_tokens: 62, - output_tokens: 0, - status: 'failed', - error_message: '模型服务响应超时' - } -] - -export const agents: AgentDefinition[] = [ - { - id: 1, - name: '建单辅助 Agent', - agent_type: 'ticket_create', - description: '从自然语言和截图中提取工单字段,并生成缺失信息追问。', - enabled: true, - model_name: 'gpt-4.1', - knowledge_scope: '工单分类、历史相似问题', - tool_scope: '只读知识检索', - requires_human_confirm: true, - updated_at: '2026-07-06T08:30:00Z' - }, - { - id: 2, - name: '支持分析 Agent', - agent_type: 'support_analysis', - description: '结合工单上下文、知识库和解决方案生成排查建议。', - enabled: true, - model_name: 'qwen-support-32b', - knowledge_scope: '全部已发布知识库', - tool_scope: '日志查询、监控查询', - requires_human_confirm: true, - updated_at: '2026-07-06T10:05:00Z' - } -] - -export const agentRuns: AgentRun[] = [ - { - id: 1001, - agent_name: '支持分析 Agent', - run_type: 'support_analysis', - ticket_no: 'TK202607060018', - status: 'waiting_confirm', - trigger_user: '技术支持-张敏', - duration_ms: 4200, - created_at: '2026-07-06T10:12:00Z', - output_summary: '已生成排查步骤和客户回复草稿,等待人工确认是否采纳。', - steps: [ - { - id: 1, - step_type: 'retrieval', - step_name: '检索相似知识', - status: 'success', - input_summary: '查询:税局采集进度长时间不更新', - output_summary: '命中 3 条知识引用和 1 个解决方案', - latency_ms: 120, - created_at: '2026-07-06T10:12:01Z' - }, - { - id: 2, - step_type: 'model_call', - step_name: '生成支持分析', - status: 'success', - input_summary: '工单详情、知识引用、解决方案', - output_summary: '输出可能原因、排查步骤、客户回复草稿', - latency_ms: 1830, - created_at: '2026-07-06T10:12:03Z' - }, - { - id: 3, - step_type: 'human_confirm', - step_name: '等待人工确认', - status: 'waiting_confirm', - input_summary: '是否采纳客户回复草稿', - output_summary: '等待处理人确认', - latency_ms: 0, - created_at: '2026-07-06T10:12:05Z' - } - ] - } -] - -export const skills: SkillDefinition[] = [ - { - id: 1, - name: '字段抽取', - scenario: 'field_extract', - description: '从用户描述中抽取标题、系统、环境、优先级和问题分类。', - enabled: true, - version: 'v1.0.0', - prompt_template: '你是技术支持建单助手,请从用户描述中抽取结构化工单字段。', - input_schema: '{ "description": "string", "attachments": "array" }', - output_schema: '{ "title": "string", "category": "string", "priority": "string" }', - updated_at: '2026-07-06T09:00:00Z' - }, - { - id: 2, - name: '客户回复草稿', - scenario: 'reply_draft', - description: '根据处理结论生成面向客户的简洁回复。', - enabled: true, - version: 'v1.1.0', - prompt_template: '请根据工单处理记录生成礼貌、准确、可发送给客户的回复。', - input_schema: '{ "ticket": "object", "resolution": "string" }', - output_schema: '{ "reply": "string", "risk_note": "string" }', - updated_at: '2026-07-05T18:30:00Z' - } -] - -export const mcpServers: McpServer[] = [ - { - id: 1, - name: '日志平台 MCP', - server_url: 'https://logs.internal/mcp', - auth_type: 'token', - connection_state: 'connected', - tool_count: 6, - last_sync_at: '2026-07-06T08:50:00Z' - }, - { - id: 2, - name: '配置中心 MCP', - server_url: 'https://config.internal/mcp', - auth_type: 'token', - connection_state: 'error', - tool_count: 4, - last_sync_at: '2026-07-05T14:20:00Z' - } -] - -export const mcpTools: McpTool[] = [ - { - id: 1, - server_id: 1, - name: 'search_python_logs', - description: '按工单编号、客户名称或任务 ID 查询 Python 服务日志。', - risk_level: 'medium', - enabled: true, - requires_human_confirm: false - }, - { - id: 2, - server_id: 2, - name: 'restart_collector_job', - description: '重启指定采集任务,仅允许人工确认后执行。', - risk_level: 'high', - enabled: false, - requires_human_confirm: true - } -] - -export const toolCalls: ToolCall[] = [ - { - id: 1, - tool_name: 'search_python_logs', - run_id: 1001, - input_summary: '查询任务 TASK-20260706-18 的最近 30 分钟日志', - output_summary: '发现税局接口连续 3 次超时', - status: 'success', - confirmed_by: '', - created_at: '2026-07-06T10:12:02Z' - }, - { - id: 2, - tool_name: 'restart_collector_job', - run_id: 1001, - input_summary: '尝试重启采集任务 TASK-20260706-18', - output_summary: '高风险工具已阻止,需人工确认', - status: 'blocked', - confirmed_by: '', - created_at: '2026-07-06T10:12:04Z' - } -] - -export const knowledgeReferences: KnowledgeReference[] = [ - { - id: 1, - title: '税局采集进度查询', - source_type: 'knowledge_item', - content: '采集进度长时间不更新时,先查询 Python 日志确认税局接口状态,再判断是否需要客户等待。', - score: 0.91 - }, - { - id: 2, - title: '税局不稳定采集处理中', - source_type: 'solution', - content: '回复客户税局服务不稳定,当前任务仍在采集中,稍后复查结果。', - score: 0.88 - } -] -``` - -- [ ] **Step 3: Create `AppMetricCard`** - -Create `frontend/src/components/app/AppMetricCard.tsx`: - -```tsx -import type * as React from 'react' - -import { Badge } from '@/components/ui/badge' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' - -type AppMetricCardProps = { - label: string - value: string | number - helper?: string - icon?: React.ReactNode - status?: 'success' | 'warning' | 'danger' | 'muted' -} - -const statusLabels = { - danger: '需关注', - muted: '正常', - success: '良好', - warning: '预警' -} - -export function AppMetricCard({ label, value, helper, icon, status = 'muted' }: AppMetricCardProps) { - return ( - - - {label} - {icon} - - -
-

{value}

- {statusLabels[status]} -
- {helper ?

{helper}

: null} -
-
- ) -} -``` - -- [ ] **Step 4: Create `AppRiskBadge`** - -Create `frontend/src/components/app/AppRiskBadge.tsx`: - -```tsx -import { Badge } from '@/components/ui/badge' - -type AppRiskBadgeProps = { - risk: 'low' | 'medium' | 'high' -} - -const riskLabels = { - high: '高风险', - low: '低风险', - medium: '中风险' -} - -export function AppRiskBadge({ risk }: AppRiskBadgeProps) { - return {riskLabels[risk]} -} -``` - -- [ ] **Step 5: Create `AppTraceTimeline`** - -Create `frontend/src/components/app/AppTraceTimeline.tsx`: - -```tsx -import { CheckCircle2, CircleDashed, Clock, XCircle } from 'lucide-react' - -import { Badge } from '@/components/ui/badge' -import { Card, CardContent } from '@/components/ui/card' -import { cn } from '@/lib/utils' - -export type AppTraceTimelineItem = { - id: number | string - title: string - type: string - status: 'success' | 'failed' | 'waiting_confirm' | 'skipped' | 'running' - input: string - output: string - latencyMs?: number -} - -const statusLabels = { - failed: '失败', - running: '运行中', - skipped: '已跳过', - success: '成功', - waiting_confirm: '待确认' -} - -function getStatusIcon(status: AppTraceTimelineItem['status']) { - if (status === 'success') return CheckCircle2 - if (status === 'failed') return XCircle - if (status === 'waiting_confirm') return Clock - return CircleDashed -} - -export function AppTraceTimeline({ items }: { items: AppTraceTimelineItem[] }) { - return ( -
- {items.map((item, index) => { - const Icon = getStatusIcon(item.status) - return ( - - -
-
-
- -
-
-

{index + 1}. {item.title}

-

{item.type}

-
-
-
- {statusLabels[item.status]} - {typeof item.latencyMs === 'number' ? {item.latencyMs} ms : null} -
-
-
-
-

输入

-

{item.input}

-
-
-

输出

-

{item.output}

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

{title}

- {sourceLabels[sourceType] ?? sourceType} - {typeof score === 'number' ? 匹配度 {score.toFixed(2)} : null} -
-

{content}

-
-
- ) -} -``` - -- [ ] **Step 7: Verify Task 1** - -Run: - -```powershell -npm run typecheck -``` - -Expected: TypeScript completes without errors. - -- [ ] **Step 8: Commit Task 1** - -Run: - -```powershell -git add frontend/src/features/prototype frontend/src/components/app/AppMetricCard.tsx frontend/src/components/app/AppRiskBadge.tsx frontend/src/components/app/AppTraceTimeline.tsx frontend/src/components/app/AppReferenceCard.tsx -git commit -m "feat: add frontend prototype contracts" -``` - ---- - -### Task 2: Global Navigation, Dashboard, And Workbench Overview - -**Files:** -- Create: `frontend/src/features/dashboard/DashboardPage.tsx` -- Modify: `frontend/src/app/router.tsx` -- Modify: `frontend/src/components/app/AppSidebar.tsx` -- Modify: `frontend/src/features/workbench/WorkbenchPage.tsx` - -- [ ] **Step 1: Create dashboard page** - -Create `frontend/src/features/dashboard/DashboardPage.tsx`: - -```tsx -import { Activity, Bot, Database, ShieldAlert } from 'lucide-react' - -import { AppMetricCard } from '@/components/app/AppMetricCard' -import { AppPage } from '@/components/app/AppPage' -import { AppPageHeader } from '@/components/app/AppPageHeader' -import { AppSection } from '@/components/app/AppSection' -import { Badge } from '@/components/ui/badge' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { dashboardMetrics, agentRuns, mcpTools, modelProviders } from '@/features/prototype/mockData' - -const icons = [Activity, Database, Bot, ShieldAlert] - -export function DashboardPage() { - return ( - - - -
- {dashboardMetrics.map((metric, index) => { - const Icon = icons[index] ?? Activity - return } /> - })} -
- -
- -
- {['税局采集进度', '登录失败', '数据同步延迟', '发票下载异常'].map((name, index) => ( -
-
- {name} - {32 - index * 6} 单 -
-
-
-
-
- ))} -
- - -
- - - 模型健康 - - - {modelProviders.map((provider) => ( -
- {provider.name} - - {provider.last_test_status === 'passed' ? '通过' : '失败'} - -
- ))} -
-
- - - - 待确认事项 - - -

Agent 待确认:{agentRuns.filter((run) => run.status === 'waiting_confirm').length} 项

-

高风险工具:{mcpTools.filter((tool) => tool.risk_level === 'high').length} 个

-
-
-
-
- - ) -} -``` - -- [ ] **Step 2: Add dashboard route** - -Modify `frontend/src/app/router.tsx`: - -```tsx -import { DashboardPage } from '@/features/dashboard/DashboardPage' -``` - -Add before the workbench route: - -```tsx - if (route === '/dashboard') { - return - } -``` - -- [ ] **Step 3: Expand built-in sidebar prototype navigation** - -Modify `frontend/src/components/app/AppSidebar.tsx`: - -Add lucide imports if absent: - -```tsx - Activity, - BrainCircuit, - Boxes, - Cpu, - FlaskConical, - PlugZap, - Wrench -``` - -Add icon mappings: - -```tsx - activity: Activity, - agents: BrainCircuit, - boxes: Boxes, - cpu: Cpu, - flask: FlaskConical, - plug: PlugZap, - wrench: Wrench -``` - -Replace the current single built-in knowledge group with a `prototypeNavGroups` array: - -```tsx -const prototypeNavGroups: AuthMenu[] = [ - { - id: -100, - code: 'prototype_workspace', - title: '工作台', - path: '#/', - icon: 'home', - permission_code: 'workspace.view', - is_visible: true, - is_enabled: true, - children: [ - { id: -101, code: 'workspace_today', title: '今日工作台', path: '#/', icon: 'home', permission_code: 'workspace.view', is_visible: true, is_enabled: true, children: [] }, - { id: -102, code: 'workspace_dashboard', title: '运营总览', path: '#/dashboard', icon: 'activity', permission_code: 'dashboard.view', is_visible: true, is_enabled: true, children: [] } - ] - }, - { - id: -200, - code: 'prototype_knowledge', - title: '知识与方案', - path: '#/knowledge', - icon: 'book_open', - permission_code: 'knowledge.view', - is_visible: true, - is_enabled: true, - children: [ - { id: -201, code: 'knowledge_bases', title: '知识库', path: '#/knowledge', icon: 'book_open', permission_code: 'knowledge.view', is_visible: true, is_enabled: true, children: [] }, - { id: -202, code: 'knowledge_search', title: '知识检索', path: '#/knowledge/search', icon: 'search', permission_code: 'knowledge.view', is_visible: true, is_enabled: true, children: [] }, - { id: -203, code: 'solutions', title: '解决方案', path: '#/solutions', icon: 'library', permission_code: 'solutions.view', is_visible: true, is_enabled: true, children: [] } - ] - }, - { - id: -300, - code: 'prototype_agents', - title: 'Agent', - path: '#/agents', - icon: 'agents', - permission_code: 'agents.view', - is_visible: true, - is_enabled: true, - children: [ - { id: -301, code: 'agents', title: 'Agent 配置', path: '#/agents', icon: 'agents', permission_code: 'agents.view', is_visible: true, is_enabled: true, children: [] }, - { id: -302, code: 'agent_runs', title: '运行记录', path: '#/agents/runs', icon: 'activity', permission_code: 'agents.view', is_visible: true, is_enabled: true, children: [] }, - { id: -303, code: 'skills', title: 'Skill 配置', path: '#/skills', icon: 'wrench', permission_code: 'skills.view', is_visible: true, is_enabled: true, children: [] } - ] - }, - { - id: -400, - code: 'prototype_model_tools', - title: '模型与工具', - path: '#/modelhub/providers', - icon: 'cpu', - permission_code: 'modelhub.view', - is_visible: true, - is_enabled: true, - children: [ - { id: -401, code: 'model_providers', title: '模型供应商', path: '#/modelhub/providers', icon: 'cpu', permission_code: 'modelhub.view', is_visible: true, is_enabled: true, children: [] }, - { id: -402, code: 'model_testing', title: '模型测试', path: '#/modelhub/testing', icon: 'flask', permission_code: 'modelhub.view', is_visible: true, is_enabled: true, children: [] }, - { id: -403, code: 'mcp_tools', title: 'MCP 工具', path: '#/tools/mcp', icon: 'plug', permission_code: 'tools.view', is_visible: true, is_enabled: true, children: [] } - ] - } -] -``` - -Then derive `groups` like this: - -```tsx - const menuGroups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled) - const existingPaths = new Set(menuGroups.flatMap((group) => visibleChildren(group).map((item) => item.path))) - const supplementalGroups = prototypeNavGroups - .map((group) => ({ - ...group, - children: group.children.filter((item) => !existingPaths.has(item.path)) - })) - .filter((group) => group.children.length > 0) - const groups = [...menuGroups, ...supplementalGroups] -``` - -- [ ] **Step 4: Enhance workbench overview** - -Modify `frontend/src/features/workbench/WorkbenchPage.tsx`: - -Add imports: - -```tsx -import { Bot, Database, ShieldAlert } from 'lucide-react' -import { AppMetricCard } from '@/components/app/AppMetricCard' -import { dashboardMetrics, agentRuns, mcpTools } from '@/features/prototype/mockData' -``` - -Replace the current metric card grid with: - -```tsx -
- {metrics.map((metric) => ( - - ))} -
- -
- } /> - run.status === 'waiting_confirm').length} helper="AI 建议默认等待人工确认" status="warning" icon={} /> - tool.risk_level === 'high').length} helper="高风险工具不会自动执行" status="danger" icon={} /> -
-``` - -- [ ] **Step 5: Verify Task 2** - -Run: - -```powershell -npm run typecheck -npm run build -``` - -Expected: both commands pass. - -- [ ] **Step 6: Commit Task 2** - -Run: - -```powershell -git add frontend/src/features/dashboard frontend/src/app/router.tsx frontend/src/components/app/AppSidebar.tsx frontend/src/features/workbench/WorkbenchPage.tsx -git commit -m "feat: add prototype dashboard navigation" -``` - ---- - -### Task 3: Model Hub Prototype Pages - -**Files:** -- Create: `frontend/src/features/modelhub/ModelProviderPage.tsx` -- Create: `frontend/src/features/modelhub/ModelTestingPage.tsx` -- Modify: `frontend/src/app/router.tsx` - -- [ ] **Step 1: Create model provider page** - -Create `frontend/src/features/modelhub/ModelProviderPage.tsx` with: - -```tsx -import * as React from 'react' -import { Plus, ServerCog } 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 { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { formatDateTime } from '@/lib/format' -import { modelProviders } from '@/features/prototype/mockData' -import type { ModelProvider } from '@/features/prototype/types' - -const columns: AppDataTableColumn[] = [ - { key: 'name', header: '供应商', cell: (provider) => {provider.name} }, - { key: 'type', header: '类型', cell: (provider) => {provider.provider_type === 'local' ? '本地模型' : 'OpenAI 兼容'} }, - { key: 'models', header: '模型数量', cell: (provider) => provider.model_count }, - { key: 'default', header: '默认模型', cell: (provider) => {provider.default_chat_model} }, - { key: 'status', header: '测试状态', cell: (provider) => {provider.last_test_status === 'passed' ? '通过' : provider.last_test_status === 'failed' ? '失败' : '未测试'} }, - { key: 'updated_at', header: '更新时间', cell: (provider) => {formatDateTime(provider.updated_at)} } -] - -export function ModelProviderPage() { - const [open, setOpen] = React.useState(false) - - return ( - - - - - - - - 新增模型供应商 - -
- - - - - - -
-
- - } - /> - provider.id} emptyTitle="暂无模型供应商" /> -
- ) -} -``` - -- [ ] **Step 2: Create model testing page** - -Create `frontend/src/features/modelhub/ModelTestingPage.tsx` with prompt input, model selector, parameter selector, and result cards: - -```tsx -import * as React from 'react' -import { FlaskConical, Play } from 'lucide-react' - -import { AppField } from '@/components/app/AppField' -import { AppPage } from '@/components/app/AppPage' -import { AppPageHeader } from '@/components/app/AppPageHeader' -import { AppSection } from '@/components/app/AppSection' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Textarea } from '@/components/ui/textarea' -import { modelProviders, modelTestResults } from '@/features/prototype/mockData' - -export function ModelTestingPage() { - const [running, setRunning] = React.useState(false) - - function runMockTest() { - setRunning(true) - window.setTimeout(() => setRunning(false), 600) - } - - return ( - - - -
- -