AI 辅助开发规范与自动化 Review Agent 实践
副标题:把规范工程化,把风险前置到审查,把 AI 变成可复用的工程资产
分享人:lh | 日期:2026-07-29
案例:TSpace 考试管控(tspace-exam-control)
TL;DR(先把结论说清楚)
- 把“规范”当成工程资产:写成可验证条目,放进默认工作流里自动执行,而不是靠口头与个人习惯。
- 把“实现/审查/修复/测试”拆成明确角色:每个 Agent 只做自己那一段,输入输出可交接、可复审。
- 把“高代价风险”前置到 Review:兼容性崩溃点、准入/权限漏校验、资源闭环、状态口径分裂。
- 把每次交付的差量回写知识库:下一个人(或下一个 Agent)不用从 Grep 开始重复探索。
- 这套闭环可以复用:OpenSpec 产物 → 开发/审查/修复 → 测试/部署 → 回写沉淀。
本次分享只回答三个问题:
- 规范如何工程化:从“零散文档/口头约定”变成“默认流程里必然会执行”?
- 执行如何标准化:开发/审查/修复三个 Agent 如何按提示词分工协作,而不是混在一次对话里全做?
- 风险如何前置:哪些问题属于“高代价风险”,如何在 Review 阶段就稳定暴露并修复?
一、背景与目标:把 AI 辅助研发做成可控流程
AI 辅助开发很容易停留在“写代码更快”。这次分享更关心两件事:
- 需求过程能回放:关键决策、边界、验收标准有落盘产物(不是靠口头同步与个人记忆)。
- 高代价风险能前置:把“代价高、排障难的问题”尽量提前暴露在 Review 阶段,而不是上线后排障。
这次分享会用一条可复用的闭环把这些落到流程里:OpenSpec 产物 → MCP 拉取知识库上下文 → 开发/审查/修复/测试分工 → 部署验证 → 回写知识库沉淀;后半部分用 TSpace 考试管控把这条闭环完整跑一遍。
二、分享结构:先讲工作流,再用案例走一遍
这一部分不只告诉大家“有哪些步骤”,而是明确每一章会带走什么工程产物:
- 第 3 章:把“闭环工作流”讲清楚(含总览流程图)
- OpenSpec 的产物长什么样(proposal/design/spec/tasks)
- MCP 如何从知识库拉取上下文(实体/流程/接口)避免重复探索
- 开发/审查/修复/测试 Agent 的分工边界与交接输入输出
- 第 4 章:用真实需求(TSpace 考试管控)把闭环跑一遍
- 重点看“关键决策”怎么写进 design/spec
- 重点看“硬约束”怎么写成可验证条目,最后落到 tasks 的完成定义(明确验收标准)
- 第 5 章:从执行结果回看“容易踩坑的地方”长什么样
- 不是纠结代码风格,而是复盘那些“测试时不一定马上发现、上线后代价很高”的问题:输入不规范就报错、权限/范围漏校验、资源没释放/没回收、改了规则但没同步到所有入口等
- 第 6 章:把复盘结论沉淀为可复用规范与清单
- 直接给可执行的开发硬规则与审查规则(R/RISK/P0)
- 每条规则配最小正例/反例代码片段,做到“看了就能照着改、照着审”
三、方法框架:OpenSpec 先行,Agent 按任务闭环交付
这篇分享想讲清楚一件事:在 VDI 这种遗留系统里,AI 的关键不是“写得快”,而是把它放进一个可审、可验收、可回放的工程流程里。
我们把闭环拆成 6 个步骤:先把需求过程落盘,再把实现交给开发 Agent,随后进入审查与修复,最后部署与测试。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
subgraph U["用户对话"]
direction TB
U0[需求/约束/范围确认]:::user
U1[确认 tasks 与验收]:::user
U2[验收反馈]:::user
end
subgraph O["OpenSpec(探索与门禁)"]
direction TB
M0[澄清 + 决策]:::main
KB[知识库(MCP)]:::kb
OS[proposal/design/spec/tasks]:::doc
G{允许进入 Apply?}:::main
end
U0 --> M0
M0 --> KB --> M0
M0 --> OS --> G
G -- 否 --> U0
G -- 是 --> U1
classDef user fill:#F8FAFC,stroke:#0F172A,color:#0f172a,stroke-width:2px;
classDef main fill:#E0E7FF,stroke:#3730A3,color:#0f172a,stroke-width:2px;
classDef doc fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;
classDef kb fill:#F1F5F9,stroke:#334155,color:#0f172a,stroke-width:2px;%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
DEV[vdi-developer
实现 + 自检]:::dev
REV[vdi-code-reviewer
并行审查]:::review
FIX[review-fixer
按清单修复]:::fix
TEST[测试
api-tester + test-runner]:::test
DEPLOY[部署
vdi-deploy]:::deploy
KB1[回写知识库(MCP)]:::kb
U2[用户验收反馈]:::user
DEV --> REV -->|问题清单| FIX --> REV
REV -->|通过| TEST
TEST -- 失败 --> FIX
TEST -- 通过 --> DEPLOY --> KB1 --> U2
classDef user fill:#F8FAFC,stroke:#0F172A,color:#0f172a,stroke-width:2px;
classDef dev fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px;
classDef review fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px;
classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px;
classDef deploy fill:#CCFBF1,stroke:#0F766E,color:#0f172a,stroke-width:2px;
classDef kb fill:#F1F5F9,stroke:#334155,color:#0f172a,stroke-width:2px;
classDef test fill:#E0F2FE,stroke:#0284C7,color:#0f172a,stroke-width:2px;3.0 上下文入口:用 Obsidian 知识库做单一事实源(SSOT),用 MCP 工具拉取上下文
我们把“上下文”当成一种资产管理:不要求每个 Agent 从代码库里重新探索一遍领域知识,而是先从 Obsidian 知识库拿到结构化的“领域上下文”,再去代码里做校验与落地。
知识库的组织方式是“以实体为核心,通过双向链接组织知识网络”:实体是节点,流程与接口是连接节点的边。知识库首页提供了三类最常用的入口(都适合 Agent 快速定位上下文):
- 实体索引:桌面/虚拟机/模板/授权等核心名词与模型边界
- 流程索引:服务桌面/教学场景/任务队列等端到端链路
- 接口索引:Console/Thor 两套接口的业务入口与路径速查
在工程化工作流里,我们把这一步从“反复 Grep/读文件补背景”替换为“调用 MCP 工具检索/读取知识库”,核心收益是:
- 避免重复探索:developer/reviewer/fixer 三个角色共享同一份领域上下文摘要
- 减少 token 与时间税:上下文以“索引→定位→精读”的方式获取,不靠大段代码贴进对话
- 更可复盘:上下文来自可维护的知识库节点,不依赖个人记忆
3.1 OpenSpec:先生成过程文档,再允许进入实现
OpenSpec 在这个项目里承担的不是“写文档”,而是把需求过程固定成稳定产物,并明确“什么时候可以开始改代码”:
proposal.md:Why/What(为什么做、改哪些能力边界)design.md:关键 Decisions(模型与边界取舍、约束与非目标)specs/*/spec.md:分能力契约(硬约束写成可验证条目)tasks.md:任务拆解 + 完成条件(完成定义:明确验收标准)
流程上,Explore 阶段只补齐上下文与决策,不直接改业务代码;当过程文档齐备后,才进入 Apply 阶段执行代码改动。
OpenSpec 的使用方式(命令级)
OpenSpec 在日常使用里不是“随便写几篇文档”,而是把一次变更拆成明确阶段,每个阶段都有对应命令与产物:
- Propose:创建变更包骨架
- 命令:
/openspec-propose - 产物:生成
proposal/design/tasks/specs的初版骨架(可立刻开始补齐上下文)
- 命令:
- Explore:调研与澄清(不改代码)
- 命令:
/openspec-explore - 行为边界:只做需求澄清、方案取舍、风险识别与任务调整;不进入代码实施
- 命令:
- Update:更新计划与规格(仍不改代码)
- 命令:
/openspec-update-change - 用途:当探索过程中出现新决策时,统一回写并保持
proposal/design/specs/tasks一致
- 命令:
- Apply:按 tasks.md 逐条实施(进入改代码阶段)
- 命令:
/openspec-apply-change - 约束:以
tasks.md为驱动逐条落地;实现执行交给 vdi-developer;每完成一项就回写完成状态
- 命令:
- Sync:规格收敛(可选)
- 命令:
/openspec-sync-specs - 用途:把变更包里演进的差量规格同步回主规格,避免主规格被手工改乱
- 命令:
- Archive:归档变更包
- 命令:
/openspec-archive-change - 产物:归档完整过程资产,便于复盘与同类需求复用
- 命令:
OpenSpec 的核心约束只有一句话:没有 Explore/Update 产物,就不进入 Apply;没有 tasks 完成条件,就不让“先写代码再补文档”。
OpenSpec vs GitHub Spec Kit:同样是 SDD,但“组织单位”不同
一句话(引用一篇更系统的对比):Spec Kit 更像 SDD 的“正步走”(严格、一致、可靠);OpenSpec 更像 SDD 的“自由舞”(灵活、轻量、高效) [1]。
抓住 3 个分野,基本就能选对:
- 阶段门禁 vs 迭代流动:Spec Kit 用固定阶段把跳步成本抬高;OpenSpec 允许围绕变更包反复迭代再进入实现。
- 宪法优先 vs 规范库治理:Spec Kit 用“宪法”做强前置统一约束;OpenSpec 更偏“轻量流程 + verify 校验”,统一性更依赖团队的规范库与审查门禁。
- 项目级产物链 vs 变更包治理:Spec Kit 更像“项目级产物链”;OpenSpec 以 change 为最小单位,强调 delta/sync/archive,更适合多人并行与存量项目增量演进。
| 维度 | OpenSpec(本分享使用) | GitHub Spec Kit(specify) |
|---|---|---|
| 默认流程 | Propose/Explore/Update → Apply → Sync → Archive [2] | Spec → Plan → Tasks → Implement [3] |
| 质量侧重点 | 偏“验证”:verify + 结果分级输出问题 [1:1] | 偏“预防”:宪法 + clarify/analyze/checklist [1:2] |
| 定制能力 | Schema/workflow/profile(工作流可编程) [1:3] | 工作流更固定,生态组件更像装配件 [3:1] |
| 协作与并行 | change/delta/sync/archive 天然适配多人并行 [1:4] | 更偏“单条产物链”的一致性治理 [1:5] |
| CI/CD 友好度 | 更强(命令与输出更适配流水线) [1:6] | 相对弱一些 [1:7] |
在 VDI 这种存量工程 + 强约束环境里,我更想要的是“每个变更包有清晰的完成定义(验收标准)、可回放的决策、可追溯的审查依据”,所以这里选 OpenSpec 做最小机制;但 Spec Kit 的“宪法 + 阶段门禁”思路值得借鉴:当你需要把团队流程进一步产品化时,它是一条更硬的路。
3.2 tasks.md:把任务与验收标准写成“可勾选”的完成条件
在实际落地里,tasks.md 的核心不是“分步骤”,而是把每一项都写成可验收条目:
- 任务描述:改什么(文件/接口/模型/迁移/脚本)
- 完成条件:怎么证明完成(返回字段、状态变化、数据落表、导出内容)
- 边界与非目标:明确不做什么,避免实现跑偏
这一步很关键:它决定了后续 vdi-developer 的实现输入、reviewer 的审查依据、以及最后的测试清单。
3.3 vdi-developer:按提示词执行“5步法”,只做实现不做设计
vdi-developer 的定位是“开发执行专员”:主对话完成分析与设计后,把明确的 tasks交给它执行。提示词原文见附录A。
它的核心工作流来自提示词的 5 步法:
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
S0[输入:主对话方案 + tasks]:::input --> S1[Step1:理解任务 + 拉取知识库上下文(MCP)]:::step
S1 --> S2[Step2:执行代码修改]:::step
S2 --> S3[Step3:语法自检]:::check
S3 --> S4[Step4:合规自检(R01-R24)]:::check
S4 --> S5[输出:修改报告(含风险提示)]:::output
classDef input fill:#EDE9FE,stroke:#6D28D9,color:#0f172a,stroke-width:2px;
classDef step fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px;
classDef check fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px;
classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;提示词要点(摘录):
- Step1 必须先补齐上下文:先用 MCP 工具从 Obsidian 知识库拉取“实体/流程/接口”背景与已有结论,再读完整文件、Grep 调用链/模型/常量,确认是否已有封装可复用
- Step2 强调最小修改、风格一致、Python2 兼容、常量引用
- Step3 每次改完做基础语法检查(py_compile),尽早发现低级错误
- Step4 按
vdi-coding-rules做合规自检(R01-R24 + 常量 + RISK 模式) - Step5 输出“修改报告”:包含变更详情、上下文搜索记录、合规自检结果与风险提示
关键约束(同样来自提示词的边界定义):
- 只执行代码修改,不做需求分析与方案设计
- 不重启服务、不操作数据库、不执行部署
- 不提交 git
- 修改前必须先理解上下文与调用链,修改后必须做合规自检
与 Superpowers(开源技能体系)的对比:同样“强纪律”,但落点不同
开源社区里比较流行的一个方向是 Superpowers(obra/superpowers):它不是“某一个 skill”,而是一套“给编码 Agent 的软件开发方法论 + 一组可组合的 skills + 会话启动时的 bootstrap 规则”。
基于本项目下载的原始内容(D:/vdi/local/opensource_docs/superpowers/),它的主张可以概括为一条默认工作流:
- 对话先澄清:不要直接写代码,先把需求聊成 spec
- 先出设计/计划:让用户能逐段确认,再进入实现
- 按计划执行 + 频繁审查:把实现拆成可独立验证的任务,用子代理逐条实现并审查
- 验证后再宣称完成:没有“新鲜证据”就不下结论
这种设计的关键点在于:Superpowers 把“流程纪律”做成了可复用的技能包,并通过 using-superpowers 在会话启动时强制要求“先触发技能再行动”(README 里也强调“skills trigger automatically”)。
using-superpowers 的这种“硬门禁”也有明显的优劣势:
- 优势
- 默认阻止跳步:把“先澄清/先用技能再行动”变成会话级默认行为,能显著减少“一上来就写代码”的偏航。
- 流程一致性强:把技能当作统一入口,输出更容易标准化(比如先 brainstorming,再 writing-plans)。
- 对新手更友好:对“不会提要求/不会拆任务”的使用者,技能包提供了比较固定的对话脚手架。
- 劣势
- 门禁过硬:它要求“哪怕 1% 可能适用也必须 invoke”,在业务沟通/信息收集/快速问答场景里容易显得臃肿,增加对话成本。
- 对环境适配要求高:强依赖 harness 支持“skill invocation / session-start hook”等机制;在企业内部强约束环境下,往往需要二次改造才能顺滑落地。
- 容易挤占领域工作:当项目真正难点在领域状态机、跨工程口径、部署限制等“非通用流程问题”时,过强的流程门禁可能会把注意力从关键领域风险上拉走。
下面按 Superpowers 原始 skills 目录,整理每个 skill 的典型调用时机(不是穷举所有实现细节,只抓它们的定位与触发点):
| skill | 典型调用时机(按 Superpowers SKILL.md 描述) |
|---|---|
| using-superpowers | 会话启动/开始任何对话:要求任何响应前先判断是否需要技能 |
| brainstorming | 任何“要做新功能/改行为”的创作型工作:先澄清→给方案→写设计→用户确认 |
| writing-plans | 已有 spec/需求,需要多步骤任务拆解:先写计划,再动代码 |
| using-git-worktrees | 开始功能开发或执行计划前:创建/确认隔离工作区 |
| subagent-driven-development | 有实施计划且任务相对独立:每任务一个 implementer 子代理 + 任务审查 + 最终总审 |
| executing-plans | 有实施计划但在另一个会话执行:加载计划→逐条执行→最后进入 finishing-a-development-branch |
| requesting-code-review | 每个任务完成后/大功能完成后/合入前:请求 reviewer 子代理做 review |
| receiving-code-review | 收到 review 反馈时:先理解/验证,再逐条实现(反对“表演式同意”) |
| test-driven-development | 做功能或 bugfix 前:先写会失败的测试,再写实现(TDD) |
| systematic-debugging | 遇到 bug/测试失败/异常行为时:先定位根因,再提修复 |
| dispatching-parallel-agents | 同时出现多个相互独立的问题域:并行派发多个代理分别处理 |
| verification-before-completion | 准备宣称“完成/修复/通过”前:必须跑验证命令并以输出作为证据 |
| finishing-a-development-branch | 实现完成且测试通过后:决定如何合入/清理工作区 |
| writing-skills | 需要写/改/验证一个 skill 本身时:把“写技能”当成 TDD 的文档化过程 |
把它放在一起对比,能更清楚 vdi-developer 的定位:
| 维度 | vdi-developer(本项目) | Superpowers(开源) |
|---|---|---|
| 角色定位 | 开发执行专员:主对话完成分析与设计后,它只做实现与自检 | 技能包驱动的全流程方法论:从对话澄清开始,贯穿 spec→plan→execute→review→verify |
| 触发方式 | Apply 阶段显式调度(按 tasks 驱动) | 会话 bootstrap + 技能自动触发:先走 brainstorming/writing-plans,再进入 executing/sdd |
| 约束来源 | 强绑定项目约束:Python2/Django1.11、R01-R24、状态机/联动/闭环、常量口径等 | 偏通用工程方法论:TDD、YAGNI、DRY、证据驱动验证、子代理分工 |
| 风险控制 | 把“不能做什么”写进边界:不重启/不写库/不部署/不提交 | 把“怎么做才算合规”写进技能:先设计/计划、频繁 review、验证后再宣称完成 |
| 产物形态 | 产出“修改报告 + 合规自检结果”,供 reviewer/fixer 接力 | 产出“设计文档 + 计划文档 + 执行/审查/验证证据”的过程链条(按其 docs 约定路径) |
一句话总结:Superpowers 解决的是“通用编码 Agent 容易跳过工程阶段”的问题;vdi-developer 解决的是“在遗留系统与强约束下,如何把实现执行标准化并可验收”的问题。两者不冲突:我们把全流程拆成 OpenSpec(负责 spec/plan/tasks)+ vdi-developer(负责实现)+ reviewer/fixer(负责审查与修复),等价于把“Superpowers 的方法论”做了 VDI 项目化落地。
3.4 vdi-code-reviewer:并行子 Agent 审查 → 汇总清单 → 调度修复 → 复审
审查协调者不直接下结论,而是调度子 Agent 分层覆盖风险面,然后汇总为统一清单。提示词原文见附录A。
与开源社区 Code Review 的对比:为什么我们要分多个子 Agent
开源社区里常见的 Code Review(GitHub PR / GitLab MR)通常是“以 diff 为中心的人审流程”:看变更、提意见、改到通过。它非常有效,但它默认了一些前提:
- 代码库相对现代(语言/框架不太会出现“版本语法地雷”)
- 业务状态机与资源闭环相对简单,或者团队对领域风险已经高度熟悉
- 审查者经验足够且注意力充足,能同时兼顾质量、业务正确性、并发、接口契约
VDI 这种遗留系统刚好相反:Python2 + Django1.11 + 三子项目耦合,事故型风险不只来自“写错一行”,更来自“跨层级契约被破坏”,例如状态机口径分裂、联动资源残留、缓存与数据库不一致、接口权限边界缺失。
因此我们的审查选择“分层 + 分工”,把一个人的注意力拆成可并行的专长视角,减少遗漏:
- review-quality(L1+L6):通用质量与兼容性哨兵(异常吞没、Py2 兼容、常量同步等)
- review-state(L2+L3+L4):核心业务正确性(状态机合法性、联动完整性、闭环一致性)
- review-concurrent(L5+IP):并发/缓存/IP 闭环(save(update_fields)、任务队列互斥、预占/回滚)
- review-api(API 专项):接口契约与权限(路由、入参出参、鉴权钩子、领域语义)
拆分成多个子 Agent 的直接收益:
- 覆盖面可控:每个子 Agent 只跑自己的一套清单,避免“看着看着就只盯风格”
- 信号更强:把事故型风险从 diff 中“提纯”出来,按严重度排序,便于决策
- 并行更快:同一份 diff 同时从多个角度审,不靠单人上下文切换
- 可复用沉淀:子 Agent 的发现可以稳定回写为规则(R/RISK),下次默认复用
一句话总结:开源社区的 review 偏“经验驱动的人审”;VDI 的 review 需要“规则驱动的分层审查”,用多个子 Agent 把复杂系统的风险面拆开覆盖。
flowchart TB R0[Step1:收集上下文(知识库 + diff + 调用链)] --> R1[Step2:并行审查调度] R1 --> Q["review-quality
L1+L6"] R1 --> S["review-state
L2+L3+L4"] R1 --> C["review-concurrent
L5+IP"] R1 --> A["review-api
API专项"] Q --> R2[Step3:收集结果] S --> R2 C --> R2 A --> R2 R2 --> R3["Step4:汇总清单
去重/排序/编号"] R3 --> R4[Step6:调度 review-fixer] R4 --> R5["Step7:修复后复审
至少复跑 review-state"] R5 --> R6["输出:结构化报告
写入 docs/code-review/"] classDef coord fill:#E0E7FF,stroke:#3730A3,color:#0f172a,stroke-width:2px; classDef sub fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef sub_core fill:#FDE68A,stroke:#B45309,color:#0f172a,stroke-width:3px; classDef review fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px; classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px; class R0,R1,R2,R3 coord; class Q,C,A sub; class S sub_core; class R4 fix; class R5 review; class R6 output;
提示词要点(摘录):
- Step1 明确要求读完整上下文(不是只看 diff 片段):先用 MCP 从知识库定位变更涉及的实体/流程/接口契约,再结合 diff/文件列表/调用链做变更分类
- Step2 “并行审查”有默认启动策略:
review-quality与review-state必启;其余按变更是否涉及并发/接口决定 - Step4 汇总时强调:去重、按严重度排序、统一编号(L1/L2/API/IP 等)
- Step6 修复不是“顺手改”,而是把汇总清单与修复建议传给 review-fixer 统一执行
- Step7 修复后必须复审(至少复跑核心业务层的 review-state)
这一步的价值在于:把“事故型风险”结构化前置,而不是靠人肉 review 时的注意力与经验。
3.5 review-fixer:只按清单修复,按优先级推进并做合规自检
修复 Agent 的边界同样明确:不审查、不决策,只对清单逐条修复并展示 diff。提示词原文见附录A。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
F0[Step1:理解修复清单]:::fix --> F1[Step2:拉取知识库上下文 + 定位代码点]:::step
F1 --> F2[Step3:执行修复
按 🔴→🟡→🔵]:::fix
F2 --> F3[Step4:自检合规
R01-R24]:::check
F3 --> F4[输出:修复摘要 + diff]:::output
classDef step fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px;
classDef check fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px;
classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px;
classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;提示词要点(摘录):
- 修复原则是最小变更、复用优先、风格一致,不做无关重构
- 自检阶段明确列出高频硬规则检查点(SoftDeleteManager、update_fields、ForeignKey on_delete、Python2 兼容、i18n 等)
- 输出必须给出“修复清单表 + 自检结果表 + 未修复项原因”,便于协调者验收
3.6 部署与测试:把“能跑”变成“可验证”
当修复完成并复审通过后,才进入部署与测试阶段:
vdi-test-runner:用真实数据做功能测试脚本执行,产出结构化结果vdi-api-tester:对相关接口做统一登录与接口测试(含边界条件)vdi-deploy:部署到服务器并记录变更(便于回溯)
3.6.1 可选增强:用 Hooks 把“提交后必须审查”工程化为守卫
如果团队希望把“闭环”从流程规范进一步升级为工程守卫,可以在 git commit 与会话停止前增加 hooks 约束:
git commit前必须显式确认本次提交类型(bugfix/feature)与目标分支(如 master/develop/release/x.y.z)git commit后自动登记待审查项,并要求在对应项目目录生成新的审查报告:<项目根>/docs/code-review/*.md
3.6.2 再加一层闭环:把接口契约写回知识库(让“交付”可复用)
很多团队的交付闭环止于“代码合入 + 部署可用”,但对遗留系统来说,后续维护成本更高:下一个需求往往要重新探索“这个功能有哪些入口、权限怎么控、边界行为是什么”。因此我们把接口文档当成一种工程资产,把“接口契约回写知识库”写进完成定义(Definition of Done),让每次交付都同步沉淀为可复用上下文。
建议把这条作为功能模块的完成定义条目:
- 本次新增/变更的接口,必须同步更新到 Obsidian:接口页 + 接口索引
- 接口页必须包含:领域语义、路由与方法、鉴权/权限边界、入参/出参、关键规则、错误码/固定文案、至少 1 个示例
- 接口索引必须可导航:能从“模块/场景”一跳定位到对应接口页
知识库落点建议保持最小可执行结构:
MOC/接口索引.md:仅做入口导航(模块 → 接口页链接 → 路径/方法速查)接口/<模块>接口.md:承载接口契约(对齐 review-api 的审查关注点)
流程示意(代码交付 + 接口知识沉淀的闭环):
本次闭环的全景流程见第 3 章开头的“方法框架总览图”;这里强调的关键点是:测试通过与部署完成后,必须把接口契约回写知识库,保证后续变更的 Step1 可以直接复用领域上下文与接口入口。
四、案例:TSpace 考试管控如何走完这条闭环
这里用两个例子说明 OpenSpec 的实际价值:边界钉死与硬约束可验证。
4.1 边界决策:Web 端以审计闭环替代强制锁定
TSpace 考试管控的核心难点在 Web:浏览器无法实现桌面端那种强制全屏锁定。OpenSpec 直接把“做不到/不做”的边界写进 Non-Goals,并把后端策略定为“考试模式标识 + 违规事件记录 + 统计/导出”构成闭环。
这样的决策带来的工程收益是:后续实现不会在不可实现的方向反复拉扯;审查与测试也有明确的目标对象(事件表、聚合查询、导出规则)。
4.2 关键硬约束:结束考试与禁止重复进入
规格明确了“结束考试”的行为后果:
- 结束后必须停止接收该学生该计划的新违规事件
- 结束后必须阻止再次进入同一考试桌面(并返回固定提示文案)
这种“必须发生/必须禁止”的约束,写成 spec 才能在后续形成稳定的校验点:开发有对照、审查有依据、测试能验证。
流程图(考试管控的审计闭环):
%%{init: {'theme':'base','themeVariables': {'fontFamily':'Inter, Arial, sans-serif','primaryTextColor':'#0f172a','lineColor':'#64748b','actorBkg':'#DBEAFE','actorBorder':'#1D4ED8','activationBkgColor':'#E0E7FF','activationBorderColor':'#3730A3','noteBkgColor':'#FEF3C7','noteBorderColor':'#D97706','tertiaryColor':'#ffffff'}}}%%
sequenceDiagram
autonumber
participant S as Student
participant API as API
participant DB as DB
S->>API: 进入考试桌面
API->>DB: 校验参与资格(部门/用户组/范围)
DB-->>API: 允许 / 拒绝
alt 允许
API-->>S: 进入成功
S->>API: 上报违规事件
API->>DB: 写入 ExamViolation
S->>API: 结束考试
API->>DB: 标记结束(停止接收新违规)
API-->>S: 返回结束成功
S->>API: 再次进入
API-->>S: 拒绝(固定提示文案)
else 拒绝
API-->>S: 拒绝进入
end五、执行后审查复盘:高风险问题的典型形态
下面摘取两份审查报告中的典型风险点。选择标准只有一个:它们更像线上事故,而不是风格偏好。
5.1 Thor:兼容性、准入校验与资源闭环
1)Python2 兼容性:异常类型与字符串类型判断
审查指出使用了 json.JSONDecodeError(Python2 不存在),以及 isinstance(x, str) 可能漏掉 unicode,导致解析失败无法捕获或逻辑分支不生效,进而引发接口 500 或参数失效。
这类问题的特点是:平时测试用正常输入很难触发,但遇到非法输入就会直接崩,属于必须前置发现的风险。
2)学生侧写入接口准入缺失:可能污染违规数据与考试状态
审查指出学生侧 end/report 只校验登录/授权/时间窗等,但缺少“参与资格”校验(用户是否属于该考试计划范围或是否有合法准入),存在任意登录用户污染数据、结束不属于自己的考试的风险。
这类问题的特点是:功能测试默认用正确用户,很容易漏掉;但一旦被滥用,影响的是数据可信度与审计口径。
3)资源闭环风险:缓存 key 拼接优先级
审查指出点数临时 key 的拼接表达式存在优先级疑似错误,可能造成 key 错乱或异常,进而引发点数占用/释放闭环异常。
这类问题的特点是:不是“某个接口报错”,而是系统运行一段时间后表现越来越异常,排障成本很高。
5.2 VDIDB:基线门禁、耦合风险与常量一致性
1)基线门禁:ForeignKey 必须显式 on_delete
审查指出多个 ForeignKey 未显式指定 on_delete,虽然 Django1.11 允许省略,但项目基线要求显式声明,避免未来升级或代码扫描门禁直接卡合入。
2)跨工程耦合:vdidb utils 依赖 thor 模块
审查指出 vdidb 的工具模块直接依赖 thor 的能力(language cache/license utils),这会增加耦合与循环依赖风险,影响 vdidb 单独运行/单测的稳定性。
3)一致性风险:常量与 migration 默认值
审查提出一个很典型但容易被忽略的问题:migration 中字段 default 往往是写死字符串,后续常量变更如果不同步检查 migration,会导致新旧数据混跑时状态识别不一致、查询漏数、口径分裂。
这类问题的特点是:上线初期不一定出错,但会在后续迭代中慢慢积累,最终变成“统计不准、数据修复困难”。
六、沉淀输出:把复盘变成可执行的团队规范
把问题复盘出来只是第一步;更重要的是把它们变成“可执行规则”,写进团队的 checklist 或审查模板。
这里给出四条最实用、也最容易落地的规范项(均来自本次审查结论):
- Python2 兼容性哨兵
- 捕获异常避免 Py3-only 类型(如
json.JSONDecodeError) - 字符串类型判断使用
basestring覆盖str/unicode
- 捕获异常避免 Py3-only 类型(如
- 学生侧写入接口必须做参与资格校验
end/report等写入类接口除登录外,必须校验“是否属于该考试计划参与范围/是否具备合法准入”
- 资源闭环(缓存 key / 点数占用释放)必须覆盖边界
- key 拼接表达式加括号,避免优先级坑
- 对
client_id=None等边界路径有最小验证
- 常量变更同步检查 migration default
- 尤其是 default 写死字符串的字段:常量改了,迁移默认值与查询口径要同步核对
这些条目不依赖“AI 很强”,它们依赖的是:每次变更都能稳定执行同一套检查。
6.1 Python2 兼容性:basestring 与 JSON 解析兜底
审查要点:
- Python2 下字符串类型判断用
basestring,不要用str(会漏掉unicode)。 json.loads的失败兜底捕获(TypeError, ValueError);不要写json.JSONDecodeError(Py2 不存在)。
正例:开放计划表单对 client_range 做类型与异常兜底(Py2 兼容):
client_range = self.data.get("client_range", {})
# 解析 client_range
if isinstance(client_range, basestring):
try:
import json
client_range = json.loads(client_range)
except (TypeError, ValueError):
client_range = {}
if not isinstance(client_range, dict):
client_range = {}
正例:兼容旧版本“不规范字段值”,basestring + dict/str 分支处理:
if (no_assigned_key in result and not result[no_assigned_key]) or (
ipv4_key in result and isinstance(result[ipv4_key], basestring) and result[ipv4_key] != 'no_assigned'
):
ipv4_uuid = result[ipv4_key].get('uuid') if isinstance(result[ipv4_key], dict) else None
result[ipv4_key] = result[ipv4_key] if ipv4_uuid and Subnet.objects.filter(uuid=ipv4_uuid).exists() else {}
反例:直接 json.loads,缺少异常兜底,遇到脏数据会抛异常:
snapshot_volumes = json.loads(snap.snapshot_volume) if snap.snapshot_volume else {}
6.2 并发与数据一致性:save() 必须考虑 update_fields
审查要点:Django 的 save() 默认会写入所有字段;在并发或“同线程多实例对象”场景下,容易把其他路径刚写入的字段覆盖回去。原则是:只改的字段,用 save(update_fields=...)。
正例:表单层统一按 changed_data 计算 update_fields,只写入变更字段:
update_fields = list(set(self.changed_data or []) & cls.model_fields) or None
logger.info(
'%s.save: update_fields=%s changed_data=%s',
self.__class__.__name__,
update_fields,
self.changed_data
)
self.instance.save(update_fields=update_fields)
正例:业务代码里显式控制写入范围(cluster/teacher 相关):
service_desktop.cluster = cluster
service_desktop.save(update_fields=['cluster'])
service_desktop.enable_teacher_desktop = bool(data.get('enable_teacher_desktop'))
teacher_user_id = data.get('teacher_user_id')
service_desktop.teacher_user_id = teacher_user_id if service_desktop.enable_teacher_desktop else None
service_desktop.save(update_fields=['enable_teacher_desktop', 'teacher_user'])
反例:save() 不带 update_fields,改动范围不受控:
service_desktop.all_admin_usable = False
if open_state:
service_desktop.open_state = consts.OPEN_STATE_OFF
service_desktop.save()
反例复盘:同线程出现“两个实例对象”导致覆盖(save() 全字段写回):
# clientobj = Client.obj.get(id=client.id) # 这里执行client.id,已经从数据库把client的信息从SQL里查到了
# clientobj.fixed_ip = None
# ...
# clientobj.save()
#
# client.user_id = None
# ...
# client.save() # 此时client的fixed_ip实际上是有值的,又没有指定update_fields,所以就把clientobj所做的修改又覆盖了
6.3 Django 模型基线:ForeignKey 必须显式 on_delete
审查要点:在 Django1.11 项目里也要求显式写 on_delete,避免未来升级/静态检查门禁失败,同时让级联语义更明确。
正例(模型层):ForeignKey 显式 on_delete:
image = models.ForeignKey('vdidb.Image', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
flavor = models.ForeignKey('vdidb.HardwareConfig', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
cluster = models.ForeignKey('vdidb.TSpaceDesktopCluster', blank=True, null=True, on_delete=models.SET_NULL, related_name='service_desktop')
teacher_user = models.ForeignKey('vdidb.User', blank=True, null=True, on_delete=models.SET_NULL, related_name='teacher_service_desktops')
正例(迁移层):迁移里显式 CASCADE/SET_NULL:
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='authgroupmembers', to='vdidb.User', verbose_name='User'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='file_objects', to='vdidb.User', verbose_name='created_by'),
反例:缺失 on_delete 的 ForeignKey(同文件多处):
host = models.ForeignKey('vdidb.Node', to_field='uuid') # 磁盘 volume 所在节点
diskless_desktop = models.ForeignKey('vdidb.DisklessDesktopParam', related_name='diskless_disks')
client = models.ForeignKey('vdidb.Client')
6.4 异常处理:避免无声吞异常
审查要点:
- 能“忽略失败”的兜底逻辑:必须留下可定位日志,并返回明确的降级结果。
- 关键路径不要
except: pass静默吞掉;否则问题会在后续以更隐蔽、更难定位的方式爆炸。
反例:迁移里执行 DDL 失败被 pass 静默忽略:
try:
cursor.execute('ALTER TABLE vdidb_tspacedesktops_instance_share_url DROP FOREIGN KEY vdidb_tspacedesktops_instance_id_32d51988_fk_instance_')
except Exception:
pass
反例:通用工具函数里 except Exception: pass,一旦失败会把环境问题转成“默认路径继续跑”,不易察觉:
try:
dt = timezone.make_aware(dt, timezone.get_current_timezone())
except Exception:
pass
正例:失败时记录异常并抛出明确业务错误(调用方可感知):
try:
lazyobject.cloudosclient.volume.reset_volume(self.volume_uuid)
except Exception as e:
self.logger.exception('reset volume failed, err=%s, volume_uuid=%s', e, self.volume_uuid)
raise exception.BadRequest(_('Clear data failed'))
需谨慎:捕获 BaseException 仅记录日志、不抛出也不返回错误信号,适用范围需严格限定:
def ignore_exception(func):
def __func(*args, **wargs):
try:
return func(*args, **wargs)
except BaseException:
LOG.exception("error accours")
return __func
6.5 状态与枚举:优先常量口径,避免硬编码字符串集合
审查要点:状态值是天然的“跨模块契约”。硬编码字符串集合短期可读,但口径变化时容易漏改,形成统计/过滤的静默偏差。原则是:状态判断优先统一常量口径。
反例:硬编码 'alive'(同文件后面又用了常量集合,口径不一致风险更高):
if voipersonaldesktop_status == 'alive':
send_status_desc = _(u'已下发')
is_issued = True
反例:硬编码状态集合 ['ACTIVE', 'SHUTOFF']:
if 'status' in inst and inst['status'] not in ['ACTIVE', 'SHUTOFF']:
return False
反例:硬编码 'error' 写入状态字段:
modepool.status = 'error'
modepool.save(update_fields=('status',))
正例:同类判断使用常量 consts.VM_RUN_ACTIVE/VM_RUN_SHUTOFF:
if 'status' in inst and inst['status'] not in [consts.VM_RUN_ACTIVE, consts.VM_RUN_SHUTOFF]:
if inst['status'] == consts.VM_RUN_ERROR:
redis_cli = lazyobject.cache.client.get_client()
redis_cli.set(consts.VM_SET_ROLLBACK_STATUS_ERROR_KEY % instance_id, 1, 60 * 3)
return True
return False
正例:业务字段写入使用常量(open_state):
if open_state:
service_desktop.open_state = consts.OPEN_STATE_OFF
6.6 进程级副作用:避免修改 TZ 污染 Worker
审查要点:进程级修改 os.environ['TZ'] + time.tzset() 属于“全局副作用”,一旦异常路径未还原,会污染同一 worker 后续请求的时间口径,排障成本极高。原则是:数据层做 UTC→目标时区转换,避免修改进程时区。
正例:数据层 UTC→目标时区转换(不改进程 TZ):
zone = json.loads(setting.value).get(self.default_key)
from_zone = tz.gettz('UTC')
to_zone = tz.gettz(zone)
utc = utils.utcnow().replace(tzinfo=from_zone)
local = utc.astimezone(to_zone)
反例:通过 TZ 环境变量 + time.tzset() 修改进程级时区:
os.environ['TZ'] = '/etc/localtime'
time.tzset()
os.environ['TZ'] = self.get_time_zone(zone)
time.tzset()
os.environ['TZ'] = zone_city_str
time.tzset() # 重新加载时区
6.7 开发规范与审查规范速查(把“复盘结论”固化为可执行清单)
这份速查清单来自 Obsidian 知识库沉淀的两类资产:
- 《VDI项目开发规范》:R01-R24 的编码/提交/部署/业务硬规则
- 《禅道Bug审查规则归纳》:从 25000+ Bug 根因模式里提炼的审查“必拦截项”(P0/P1)
目标只有一个:让 developer/reviewer/fixer/tester 在每次变更中稳定执行同一套检查,而不是靠个人经验临场发挥。
A)开发硬规则(节选:写法要统一、代码要“可被审查”)
R01:SoftDeleteManager 不可重复过滤 deleted=False/0(ManyToMany/默认 Manager 已过滤)
# ❌ 冗余:Manager 已过滤
service_desktop.user.filter(deleted=False)
User.objects.filter(deleted=False)
# ✅ 正确:Manager 已自动过滤
service_desktop.user.all()
User.objects.all()
R05:save() 必须考虑 update_fields(避免并发覆盖)
update_fields = list(set(self.changed_data or []) & cls.model_fields) or None
self.instance.save(update_fields=update_fields)
R07:ForeignKey 必须显式 on_delete
image = models.ForeignKey('vdidb.Image', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
cluster = models.ForeignKey('vdidb.TSpaceDesktopCluster', blank=True, null=True, on_delete=models.SET_NULL, related_name='service_desktop')
R09:Python2 全量兼容(basestring + json.loads 捕获 (TypeError, ValueError))
if isinstance(client_range, basestring):
try:
client_range = json.loads(client_range)
except (TypeError, ValueError):
client_range = {}
R12:避免 N+1 查询(列表页/聚合页优先用 select_related/prefetch_related)
# ❌ 反例:N+1(循环里触发关联查询)
users = User.objects.filter(id__in=user_ids)
for u in users:
dept_name = u.department.name
# ✅ 正例:一次性把关联带出来
users = User.objects.filter(id__in=user_ids).select_related('department')
for u in users:
dept_name = u.department.name
R22b:国际化规范(英文作为 msgid):写入 DB 的文案用 ugettext_noop 存英文 msgid(不要写入 _('中文'))
# ✅ 正例:写入英文 msgid(可翻译)
instance_name=ugettext_noop(u'Batch create %(host_count)s virtual machines (%(host_name)s)') % {
'host_count': host_count,
'host_name': host_name
}
# ❌ 反例:把中文作为 msgid 写入 DB(后续无法稳定 i18n/检索/对齐口径)
instance_name=_(u'批量创建 %(host_count)s 台虚拟机(%(host_name)s)') % {
'host_count': host_count,
'host_name': host_name
}
B)高风险模式库(RISK-01~11)
| RISK | 模式 | 典型业务后果 |
|---|---|---|
| RISK-01 | 硬编码状态字符串 | 常量改动后口径分裂,统计/过滤静默失效 |
| RISK-02 | 改业务状态不改缓存 | 显示/调度口径不一致,出现“越跑越不对劲” |
| RISK-03 | 异常中遗漏状态回退 | 资源/任务卡死,必须人工修复状态 |
| RISK-04 | 跳过前置状态检查 | 走到非法状态组合,后续联动全崩 |
| RISK-05 | 联动操作不完整 | 主状态变了,但关联资源残留(典型线上事故) |
| RISK-06 | 新增状态未入分组集合 | 新状态永远不会被调度/被统计到 |
| RISK-07 | save() 并发覆盖 | 同一对象多路径写入互相覆盖,产生“偶现 bug” |
| RISK-08 | 忽略无 choices 约束 | 任意值写入状态字段,后续查询口径不可控 |
| RISK-09 | 任务取消不调回调 | 资源释放/回滚链路断裂,留下脏数据 |
| RISK-10 | 模板更新不联动桌面 | 模板变更未传播,导致“配置改了但不生效” |
| RISK-11 | N+1 查询 | 线上慢查询与超时,放大锁与队列堆积,排障代价高 |
C)禅道 Bug 归纳的 P0 审查规则(建议纳入 review-* 默认清单)
- A01:增量收敛(缓存/策略刷新禁止 ClearAll+Rebuild)
- A02:任务标记原子化(业务成功 + 标记失败 = 重复执行)
- C01:保守取整(资源分配必须用 Floor,禁 Round/Ceil)
- B02:修改路径校验(修改操作必须重新校验业务约束)
这四条的共同点:它们都不是“写错一行”,而是“中间态/重复执行/超卖/漏校验”导致的线上事故型风险。
D)审查分工与覆盖面(对应 Agent)
| Agent | 覆盖层级 | 核心关注 |
|---|---|---|
| review-quality | L1 + L6 | 通用质量、异常处理、Python2 兼容、常量同步、i18n msgid 口径、N+1 查询 |
| review-state | L2 + L3 + L4 | 状态机合法性、联动完整性、闭环一致性 |
| review-concurrent | L5 + IP | 并发/缓存一致性、任务队列、IP 分配释放闭环 |
| review-api | API 专项 | 路由/权限/输入输出契约、领域语义与边界行为 |
七、总结:收益与边界
7.1 收益
- OpenSpec 把需求过程变成可追溯资产:决策、约束、任务清单都可回放复用。
- 自动化分层审查能稳定暴露事故型风险:兼容性崩溃点、数据污染点、资源闭环点、基线门禁点。
- 审查报告反哺规范与清单,让收益可累积:下次同类需求默认更稳。
7.2 边界
- 自动化审查不等于“没有 bug”,它解决的是“覆盖稳定性与风险前置”,不是替代业务验证。
- 规范与清单需要持续维护:每次审查发现的新风险模型,才是真正值得沉淀的部分。
附录A:材料路径速查
- 工作流与 Agent
- OpenSpec 工作流与变更包:
D:/vdi/openspec/ - vdi-developer 提示词:
D:/vdi/.trae/resources/agents/vdi-developer.md - vdi-code-reviewer 提示词:
D:/vdi/.trae/resources/agents/vdi-code-reviewer.md - review-fixer 提示词:
D:/vdi/.trae/resources/agents/review-fixer.md - Obsidian 知识库:
D:/vdi/vdi-kb/(推荐入口:MOC/首页.md、MOC/实体索引.md、MOC/流程索引.md、MOC/接口索引.md)
- OpenSpec 工作流与变更包:
- 案例(TSpace 考试管控)
- OpenSpec 变更包:
D:/vdi/openspec/changes/tspace-exam-control/proposal.mddesign.mdspecs/tasks.md
- Thor 审查报告:
D:/vdi/thor/docs/code-review/tspace-exam-control-thor-code-review-20260728.md - VDIDB 审查报告:
D:/vdi/vdidb/docs/code-review/tspace-exam-control-vdidb-code-review-20260728.md
- OpenSpec 变更包:
附录B:开源资料与对照分析(附件)
- Superpowers(obra/superpowers)
- README(本地):refs/README
- skills 索引(本地):refs/superpowers-skills
- 原始链接:https://github.com/obra/superpowers/tree/main/skills
- GitHub Copilot Code Review(官方文档)
- 对照分析(本地附件)
- Superpowers skills 不适配点(本地附件)
附录C:MCP 调用知识库的最小模式(避免重复探索)
在实际执行里,建议把“知识库上下文获取”规范成一个可复制的最小套路:先检索,再按需精读(避免一次性读完整大文件)。
- 检索入口(定位到文件)
mcp_obsidian.search_simple(query="服务桌面 发布 取消发布")mcp_obsidian.search_simple(query="TSpace 考试 管控")
- 拿结构(定位到章节)
mcp_obsidian.vault_get_document_map(path="MOC/首页.md")
- 按章节精读(只读需要的那一段)
mcp_obsidian.vault_read(path="MOC/首页.md", targetType="heading", target=["VDI 知识库","🔌 接口业务流程"], scope="content")
- 带着领域上下文回到代码做核对
- 再进入 Read/Grep 调用链、模型字段、常量口径与权限边界的代码级验证
- 把本次接口变更回写知识库(形成闭环)
- 更新
MOC/接口索引.md(新增/变更接口入口导航) - 更新对应接口页
接口/<模块>接口.md(补齐权限边界、关键规则、错误码与示例)
- 更新
https://www.cnblogs.com/helloHKTK/p/20051075 (Spec-Kit vs OpenSpec:深度对比文章) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
https://raw.githubusercontent.com/Fission-AI/OpenSpec/refs/heads/main/docs/commands.md (OpenSpec commands:/opsx 工作流) ↩︎
https://github.github.io/spec-kit/ (Spec Kit 主页:核心流程与生态组件说明) ↩︎ ↩︎