diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..93b9d1b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# 仓库内文本统一以 LF 存储,避免 Windows/WSL 混用产生全量行尾差异。 +# 行尾差异会淹没真实改动,也会让 gitea.env 之类的配置在 bash 中带上 \r。 +* text=auto eol=lf + +*.py text eol=lf +*.md text eol=lf +*.json text eol=lf +*.ps1 text eol=crlf + +*.png binary +*.jpg binary +*.zip binary diff --git a/.gitea/issue_template/task.md b/.gitea/issue_template/task.md index 8d7d785..d85b925 100644 --- a/.gitea/issue_template/task.md +++ b/.gitea/issue_template/task.md @@ -1,9 +1,6 @@ ## 基本信息 - 类型:需求 / 缺陷 / 重构 -- 任务类型:单项目 / 协同 -- 主项目:Sense / Brain / Bell / contracts / 根级 -- 主 agent: - 所属 Epic:# - 所属 MVP / 版本:# - 阶段: @@ -21,20 +18,8 @@ - 仅影响的子项目 / 交付单元: - 是否跨子项目:是 / 否 - 是否修改共享接口或契约:是 / 否;唯一事实来源: -- write_paths: - 各子项目需要执行的验证: -## 协同接口 - - - -- 生产者: -- 消费者: -- 契约/共享事实源: -- 兼容策略:不适用 / 向后兼容 / 发布新版本 -- 被阻塞或需要适配的工单: -- 集成顺序: - ## 原始需求 - 来源:用户对话 / Gitea / 其他 @@ -68,6 +53,22 @@ |---|---|---|---| | | | | 是 / 否 | +## 设计与原型门禁 + + + +- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug +- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据 +- 可编辑设计源、线上原型链接和访问检查: +- 审核版本、revision、复制版本或确认日期及识别方式: +- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求 +- 本地 HTML 路径、版本和资源检查(仅显式导出时填写): +- 状态:无 / 草稿 / 已确认 / 已废弃 +- 确认人、确认时间和覆盖范围: +- 无需 UI 原型或无需任何原型的原因: + + + ## 文档影响 @@ -88,6 +89,15 @@ - [ ] 新增交付文档,受众与页面: - [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式: +## 任务记录与可选快照 + +- 单次任务事实来源:当前 Gitea 工单正文与评论 +- [ ] 默认不创建任务快照 +- [ ] 用户明确要求专项快照;用途和范围: +- [ ] 项目专用规则要求任务快照;规则入口: + + + ## 验收标准 - [ ] diff --git a/.gitignore b/.gitignore index 0125a16..9c058d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .codex/gitea.env +gitea.env .codex/*.log .codex/config.toml __pycache__/ diff --git a/AGENTS.md b/AGENTS.md index 78bffb2..4da1ae1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,25 @@ # Agent 开发规则 -本仓库采用 DevHarness 工作流:Gitea 工单是任务过程的事实来源,Gitea Wiki 是长期开发文档和任务归档的事实来源,Git 是代码与版本绑定资料的变更记录,`docs/` 只保存 Wiki 的只读镜像。人负责确认方案与验收,Agent 负责检查、实现、测试和留下证据。 +本仓库采用 DevHarness 工作流:Gitea 工单是单次任务需求、变化、实现、测试、提交和验收的事实来源,Gitea Wiki 是长期开发文档的事实来源,Git 是代码与版本绑定资料的变更记录。`docs/` 默认保存核心 Wiki 的只读镜像,`docs/task/` 只保存人工明确要求的专项或历史兼容快照。人负责确认方案与验收,Agent 负责检查、实现、测试和留下证据。 -开始工作前先阅读 [项目档案](docs/00-project-profile.md) 和任务涉及目录中的 `AGENTS.md`。目录越深的规则越具体,但不得削弱上级安全规则。 +开始工作前先阅读任务涉及目录中的 `AGENTS.md`。目录越深的规则越具体,但不得削弱上级安全规则。 + +[项目档案](docs/00-project-profile.md) 按需阅读,不作为每次任务的固定前置。出现下列情况之一时必须读:需要环境、配置或凭据来源;需要确认目录边界;需要判断子项目与交付单元划分;需要 DevHarness 来源与基线;需要项目专用验收要求。只为查命令不必打开项目档案。 + +## 常用命令 + +所有命令默认从仓库根目录执行,与项目档案保持一致。 + +| 用途 | 命令 | +|---|---| +| 查看工作区 | `git status --short --branch` | +| 检查模板结构 | `python dev_scripts/harness.py check --strict` | +| 运行单元测试 | `python -m unittest discover -s tests -v` | +| 导出核心 Wiki 镜像 | `python dev_scripts/harness.py sync` | +| 检查核心 Wiki 镜像 | `python dev_scripts/harness.py sync --check` | +| 创建可选任务快照 | `python dev_scripts/harness.py archive 123 "修复登录超时"` | +| 增量导出已有快照 | `python dev_scripts/harness.py export` | +| 全量导出已有快照 | `python dev_scripts/harness.py export --all` | ## 1. 永久规则 @@ -18,14 +35,15 @@ 新功能、缺陷修复、重构以及任何用户可感知或改变程序行为的修改,必须先建立单元任务工单。 -以下小改动可以直接提交,不要求工单和任务归档: +以下小改动可以直接提交,不要求工单: - 只改错别字、注释或文档措辞; - 只做格式化、导入排序或不跨文件的内部变量改名; - 补充类型标注或文档字符串且不改变行为; - 删除已经确认无人使用的死代码。 +- 只修改用户看到的界面显示文案,并且满足本文件“工单与设计证据双门禁”的全部豁免条件。 -只要涉及接口、数据库、状态、权限、安全、并发、用户界面,或者无法确定是否改变行为,就必须建工单。 +除严格符合界面显示文案豁免的修改外,只要涉及接口、数据库、状态、权限、安全、并发、用户界面,或者无法确定是否改变行为,就必须建工单。代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案,不适用豁免。 ## 3. 需求到实施 @@ -37,24 +55,61 @@ 6. 开始实施前检查分支和工作区,明确哪些现有改动不属于本任务。 7. 严格按工单范围实现;新发现的问题先记录,不顺手混入当前任务。 8. 执行与风险相称的测试,把关键结果和未验证部分更新到工单。实施过程中出现计划外、当前无法解除的问题时才标记“阻塞”。 -9. 长期文档必须先修改 Wiki、读取确认,再运行 `python dev_scripts/sync_wiki_docs.py` 导出本地镜像;不得直接编辑 `docs/` 后反向覆盖 Wiki。 +9. 只有长期事实变化时才修改 Wiki、读取确认,再运行 `python dev_scripts/harness.py sync` 导出本地镜像;没有长期文档影响时在工单说明原因并跳过 Wiki 同步。不得直接编辑镜像后反向覆盖 Wiki。 + +工单正文保存用户确认的任务基线;根因、范围、方案、风险或阻塞发生重要变化时追加评论。完成实现后用一条评论集中记录最终差异、测试、未验证内容、提交哈希和文档影响;用户验收后再追加验收时间与结论,不重复覆盖或抄写已有证据。 Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明确授权,不得默认绕过建单。 +### Gitea 交互与工单最小读取 + +- 所有 Gitea 工单和 Wiki 的查询、创建、更新、评论、状态变更及关闭操作,优先使用项目已配置的 Gitea MCP。 +- MCP 不可用或不支持所需操作时才回退 Gitea API,并在当前工单记录回退原因;初始化阶段尚无工单时记录到初始化工单草稿,建单后补回。凭据只从环境或 MCP 安全配置读取。 +- 首次接手任务时读取工单确认基线和完成当前判断所需的评论,不因节省 Token 跳过范围、依赖、安全、验收或重要变更。 +- 同一任务、同一会话且关键前提未变化时,复用仍有效的工单事实,优先关注当前状态、最新评论和首个未完成步骤,不重复分析已经确认且仍有效的内容。 +- 会话、代码、配置、依赖、凭据、远端状态或关键前提变化,任务基线不清楚,或最新评论声明历史需求、方案、范围、风险或验收发生变化时,重新读取必要历史;无法判断影响范围时读取完整工单。 +- 连接器不支持评论分页或增量读取时允许读取完整工单,但不得把“已读取全文”误当成需要重新分析全部历史,也不得为规避完整读取而新增本地工单、缓存或第二事实来源。 +- 正确性、安全规则和已确认范围优先于 Token 优化;读取边界存在不确定时补读必要证据。 + +### 新项目 Wiki 初始化门禁 + +- 从本模板创建新项目时,先创建 Gitea 远端仓库并启用工单和 Wiki,再配置 `wiki-docs.json`;不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据。 +- 优先使用项目已配置的 Gitea MCP 查询和写入 Wiki;MCP 不可用或不支持所需写操作时,才使用 Gitea API,并在初始化工单记录回退原因。凭据只从环境或 MCP 安全配置读取。 +- 先查询线上页面列表;`Home` 不存在时必须先创建 `Home`,在线回读正文并记录 revision,然后再逐页创建或更新其他核心映射页面。 +- 每个核心页面写入后必须在线回读并取得 revision。页面缺失、回读失败或没有 revision 时停止初始化,不得开始产品代码。 +- 产品编码前必须运行 `python dev_scripts/harness.py sync --verify`;全部成功才表示线上 Wiki 和核心镜像初始化完成。 + +### 工单与设计证据双门禁 + +- 纯界面显示文案只有在不改变业务含义、流程、权限、状态、接口、数据、法律/安全/支付/单位等高风险含义、国际化键、程序标识符、布局和可访问性,且没有任何不确定时,才免工单和原型;修改后执行最小界面检查。 +- 新页面、独立用户功能、重大交互或导航变化,必须先用 Quant-UX 或其他合适工具制作可审阅原型;用户确认原型、文字需求和覆盖范围后,才能建立或放行实现工单并编写生产代码。 +- 上述完整原型形成待审核版本后,默认直接通过 Quant-UX 或其他设计工具的线上链接审核,不要求每次导出本地 HTML。工单必须记录可访问链接、版本/revision 或确认日期、审核版本识别方式、确认人、确认时间和覆盖范围;线上链接无法访问或无法区分版本时停止审核,等待用户确认等效方案。 +- 只有用户明确要求 `导出原型 #N`、`导出全部原型`,或项目专用规则明确要求离线交付时,才导出到 `prototypes/<工单号>/<版本>/index.html`。已确认的本地快照不得原位覆盖;版本目录内资源使用相对路径,导出后检查入口、主要交互和资源完整性,但不自动提交。 +- 现有界面的小范围样式或布局调整使用标注截图、低保真图或明确复用的现有规范;新组件记录状态、错误和边界。两者只要不符合纯文案豁免就必须建单。 +- 后端、接口、数据处理和定时任务不强制 UI 原型,但必须先确认架构、API、数据、状态或流程设计;恢复既有确认行为的 Bug 可以复用原设计、截图、复现步骤或已有验收证据。 +- 需要设计证据的工单记录链接或路径、版本/revision 或日期、状态、确认人、确认时间和覆盖范围;没有 UI 原型时记录替代技术设计或原因。 +- 页面结构、主要流程、状态、权限、异常处理或验收结果变化时,必须更新原型或文字需求并重新确认后再继续正式编码。 +- 草稿原型可以用于讨论;写入 Git/Wiki、多人协作或单独实施时建立设计任务。草稿和经明确授权的隔离技术验证都不得直接作为生产实现。 +- 设计工具无法生成用户明确要求的可用 HTML 时,在工单记录限制并停止该导出或离线交付,等待用户确认等效方案;线上原型可访问且版本明确时不因此阻塞线上审核。纯显示文案、小范围 UI、非 UI 需求和恢复既有行为的 Bug 不强制建立完整线上原型或导出 HTML。 + ### 自然语言快捷指令 快捷指令只是本工作流的自然语言别名,不得绕过方案确认、前置依赖、安全规则、工单范围、Wiki 主源、必要验证或人工验收: - `只分析`:只读检查并给出方案;不建单、不修改,停在等待确认。 - `建工单`:根据已确认方案创建单元任务工单;建单后停止,不修改代码。 -- `执行工单 #N`:检查工单和依赖,实施、测试、提交、归档、推送并回写证据;停在“待验收”。 +- `执行工单 #N`:检查工单和依赖,实施、测试、提交、推送并回写证据;仅在明确要求时创建任务快照;停在“待验收”。 - `建工单并做`:依次建单和执行,`建工单,做`、`建工单,做` 含义相同;停在“待验收”。 -- `继续工单 #N`:核对工单、Git 和 Wiki 证据,从首个未完成步骤继续,不重复仍然有效的检查。 +- `继续工单 #N`:优先核对当前状态、最新评论、Git 和必要 Wiki 证据,从首个未完成步骤继续;关键前提未变化时不重复读取和分析仍有效的内容。 - `检查工单 #N`:只读核对范围、验收、测试和证据并输出报告;不自动修复。 -- `同步文档`:读取 Wiki、导出 `docs/` 并检查一致性;不修改 Wiki、不自动提交。 -- `#N 验收通过`:仅在用户明确验收后,更新归档、同步并提交镜像、推送、同步父工单并关闭任务。 +- `同步文档`:读取 Wiki、导出核心 `docs/` 并检查一致性,不处理任务归档;不修改 Wiki、不自动提交。 +- `导出原型 #N`:人工触发导出指定工单已确认的原型版本,按工单和版本写入 `prototypes/`;不扩展范围、不自动提交。 +- `导出全部原型`:人工触发导出当前项目已明确范围内的全部已确认原型;不自动提交。 +- `导出任务归档`:人工触发 `python dev_scripts/harness.py export`,只导出新增或 revision 已变化的任务归档;不删除本地文件、不自动提交。 +- `导出全部任务归档`:人工触发 `python dev_scripts/harness.py export --all`,读取并导出全部线上任务归档;不删除本地文件、不自动提交。 +- `#N 验收通过`:仅在用户明确验收后,在工单追加验收结论;按需更新真实变化的长期 Wiki,推送、同步父工单并关闭任务;不创建或导出任务归档。 -方案未确认或前置依赖未满足时,实施类指令必须停在对应门禁;除 `#N 验收通过` 外,快捷指令不得关闭待验收工单。Gitea 工单不导出全文,本地只保存 Wiki 任务归档镜像。详细语义见 [开发工作流](docs/01-workflow.md)。 +方案未确认或前置依赖未满足时,实施类指令必须停在对应门禁;除 `#N 验收通过` 外,快捷指令不得关闭待验收工单。原型和任务快照的导出必须由用户明确提出或项目专用规则明确要求,其他指令不得隐式执行。Gitea 工单不导出全文,`docs/task/` 只是可能不完整的专项或历史兼容快照。详细语义见 [开发工作流](docs/01-workflow.md)。 ### 需求记录与流转 @@ -62,13 +117,13 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明 - 工单中的目标、非目标、已确认方案、验收标准和文档影响构成确认后的正式任务需求。 - 影响范围、接口、数据、风险或验收的需求变化必须记录日期、内容、原因和用户确认;会改变已确认结果时先更新工单并等待再次确认。 - 不复制完整聊天,不保存 Agent 内部推理,不写入密码、令牌、个人数据或生产数据;包含敏感信息的原话必须删除敏感部分或改写为脱敏摘要。 -- 长期有效的产品需求、业务规则和系统边界进入对应 Wiki 主题页并导出 `docs/`;完成结果进入 Wiki 任务归档并导出 `docs/task/`。Gitea 工单全文不导出到仓库。 +- 长期有效的产品需求、业务规则和系统边界进入对应 Wiki 主题页并导出核心 `docs/`;单次任务的完成结果和验收保留在工单正文与评论。只有用户明确要求专项快照或项目专用规则要求时才创建 Wiki 任务快照并按需导出 `docs/task/`。Gitea 工单全文不导出到仓库。 详细记录边界见 [开发工作流](docs/01-workflow.md) 与 [业务规则和术语](docs/03-business-rules-and-glossary.md)。 ### 效率与范围控制 -本节只用于减少无关工作和重复检查,不得削弱安全规则、已确认方案、工单范围、必要测试、Wiki 同步、Git 提交和验收归档要求。 +本节只用于减少无关工作和重复检查,不得削弱安全规则、已确认方案、工单范围、必要测试、必要的长期文档同步、Git 提交和人工验收要求。 #### 严格控制范围 @@ -116,28 +171,26 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明 ## 6. Git 与验证 -- `explore` 是旧实现的只读聚合快照,不接受新功能、修复或文档演进;需要恢复旧行为时从其来源提交读取证据,不在该分支续写产品。 -- `main` 是用户审核基线,禁止直接开发、直接提交或未经用户明确审核的合并。 -- 新任务分支必须从当前 `dev` 创建,完成后通过 PR 合回 `dev`;不得把功能分支直接合入 `main`。 -- `dev` 完成集成测试后仍不能自动进入 `main`;只有用户明确表示审核/验收通过,Agent 才能执行 `dev → main`。 -- 紧急修复也必须建单并取得用户对分支与合并路径的明确授权,不默认绕过 `dev`。 - 提交只包含当前工单相关文件。 - 实现提交信息引用工单号,例如:`fix: 修复登录超时 (#123)`。 - 不为流程制造空提交。 - 优先运行项目档案中记录的格式检查、静态检查、单元测试和必要的集成测试。 -- 不能验证的真机、生产、迁移或并发行为必须写入工单和归档。 +- 不能验证的真机、生产、迁移或并发行为必须写入工单;存在明确要求的任务快照时再同步记录。 +- Windows 环境优先使用当前已配置的 PowerShell;可选择时优先 PowerShell 7 `pwsh.exe`,不得仅为设置编码重复启动一层 PowerShell。 +- 文本文件读写在命令支持时显式指定 UTF-8;文件解码和控制台输出分别处理,只有出现真实乱码或已知宿主非 UTF-8 时才设置当前进程的输出编码或 Python UTF-8 环境变量。 +- 不得默认使用 `-ExecutionPolicy Bypass`;只有可信 `.ps1`确实被执行策略阻止且没有更小替代方案时,才对该次进程使用并在工单记录原因。 -## 7. 完成、验收和归档 +## 7. 完成和验收 -1. 逐项完成验收、测试和实现提交,并把最终方案、差异、结果、提交及遗留问题写回工单。 +1. 逐项完成验收、测试和实现提交,在工单追加最终证据评论,记录最终方案、差异、结果、提交、遗留问题和文档影响。 2. 工单保持“待验收”,用户没有明确验收通过前不得关闭。 -3. 运行 `python dev_scripts/new_task_archive.py <编号> "<短标题>"`,先创建 Wiki 归档,再登记并导出本地镜像。 -4. 读取确认 Wiki,运行 `python dev_scripts/sync_wiki_docs.py --check`;归档镜像单独提交,并把页面、revision、路径和提交哈希写回工单。 -5. 用户验收通过后关闭单元工单,并同步更新 MVP 和 Epic。 +3. 有长期文档影响时,读取确认 Wiki,运行 `python dev_scripts/harness.py sync --check` 检查核心镜像,并把页面 revision 和镜像提交哈希写回工单;没有长期文档影响时不运行 Wiki 同步。 +4. 用户验收通过后,在工单追加验收时间和结论,关闭单元工单,并同步更新 MVP 和 Epic;没有真实变化的 Wiki 不重复更新或检查。 +5. 默认不创建任务归档。只有用户明确要求专项快照或项目专用规则明确要求时,才运行 `python dev_scripts/harness.py archive <编号> "<短标题>"`;`export` 和 `export --all` 同样必须显式触发。 MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才能关闭 MVP。Epic 的全部范围完成后才能关闭 Epic。 -详细归档顺序和字段见 [开发工作流](docs/01-workflow.md)。 +详细证据回写、可选快照和文档同步边界见 [开发工作流](docs/01-workflow.md)。 ## 8. 可维护性 @@ -158,8 +211,9 @@ MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才 - 每个单元任务必须在工单中选择“无长期文档影响并说明原因”或列出需要更新的 Wiki 页面。 - 启动、测试、部署、排错命令,模块入口、目录职责、主要调用路径,配置、API、数据结构、状态、业务规则、安全边界、日志位置发生变化时,必须更新对应 Wiki。 +- 部署命令变化时,有常驻服务的项目更新自己的 `Deployment-and-Operations` 页面(由 [部署文档模板](docs/templates/deployment.md) 复制建立);没有常驻服务的项目记录为无部署文档影响,不创建空的部署页。 - 普通内部重构只有在入口、行为、配置和验证方式均未改变时,才可以记录为不影响长期文档。 -- 必需核心页面及结构以 `python dev_scripts/check_harness.py --strict` 和 [新项目文档初始化](docs/07-new-project-documentation-setup.md) 为准;稳定文档与任务归档的分工见 [开发工作流](docs/01-workflow.md)。 +- 必需核心页面及结构以 `python dev_scripts/harness.py check --strict` 和 [新项目文档初始化](docs/07-new-project-documentation-setup.md) 为准;稳定文档与可选历史快照的分工见 [开发工作流](docs/01-workflow.md)。 ## 9. 引导提交例外 @@ -169,11 +223,20 @@ MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才 ## 10. 项目专用规则 +### YoVision 分支治理 + +- `explore` 是旧实现的只读聚合快照,不接受新功能、修复或文档演进;需要恢复旧行为时从其来源提交读取证据,不在该分支续写产品。 +- `main` 是用户审核基线,禁止直接开发、直接提交或未经用户明确审核的合并。 +- 新任务分支必须从当前 `dev` 创建,完成后通过 PR 合回 `dev`;不得把功能分支直接合入 `main`。 +- `dev` 完成集成测试后仍不能自动进入 `main`;只有用户明确表示审核/验收通过,Agent 才能执行 `dev → main`。 +- 紧急修复也必须建单并取得用户对分支与合并路径的明确授权,不默认绕过 `dev`。 + -- 长期开发文档以 Gitea Wiki 为事实来源,`docs/` 是显式映射生成的只读镜像。 -- Wiki 与镜像的固定顺序是:修改 Wiki → 读取确认 → 导出 `docs` → 校验差异 → 提交镜像。 -- 同步配置只允许写入 `docs/` 下的 Markdown;发现镜像有未提交修改时必须停止。 +- 长期开发文档以 Gitea Wiki 为事实来源,单次任务证据以 Gitea 工单为事实来源;`docs/` 默认保存显式映射生成的核心只读镜像,`docs/task/` 是人工明确要求的专项或历史兼容快照,可能不完整或不是最新状态。 +- 核心 Wiki 与镜像的固定顺序是:修改 Wiki → 读取确认 → 导出核心 `docs` → 校验差异 → 提交镜像。 +- 默认不创建任务归档;`archive`、`导出任务归档` 或 `导出全部任务归档` 必须由用户明确提出或项目专用规则明确要求,且不得自动传播删除或重命名。 +- 同步配置只允许写入 `docs/` 下的 Markdown;发现核心镜像有未提交修改时必须停止。 - Gitea 凭据只通过进程环境或 MCP 安全配置提供,不得写入仓库。 - `dev_scripts/` 只存放 DevHarness 自身工具;业务项目的通用脚本必须使用独立目录,不得混放。 - 本仓库包含 `Sense/`、`Brain/`、`Bell/` 三个独立开发与交付单元;Sense 与 Bell 是认证、数据、部署和发布完全独立的销售产品,Brain 是无界面推理交付单元。 diff --git a/README.md b/README.md index bdbedba..fa953b9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,48 @@ YoVision 是一个单仓多项目的智能视频事件平台,包含三个可 Sense 与 Bell 是账户、数据、部署和发布边界完全独立的两个销售产品;Brain 是无界面的独立推理交付单元,默认随 Sense 部署。跨项目协作只通过 `contracts/` 中的版本化契约。 -开始工作前阅读 [AGENTS.md](AGENTS.md) 和 [文档中心](docs/README.md)。本仓库采用 DevHarness:Gitea 工单记录任务过程,Gitea Wiki 保存长期文档,`docs/` 是 Wiki 的只读镜像。 +开始工作前阅读 [AGENTS.md](AGENTS.md) 和 [文档中心](docs/README.md)。本仓库采用 DevHarness:Gitea 工单是单次任务唯一事实来源,Gitea Wiki 保存长期文档,`docs/` 默认保存核心 Wiki 的只读镜像。 + +## 工作闭环 + +```text +讨论需求或缺陷 + -> 阅读代码并提出方案 + -> 人工确认方案 + -> 创建 Epic / MVP / 单元任务工单 + -> Agent 实现并测试 + -> 提交代码并集中回写工单证据 + -> 人工验收后记录结论并关闭工单 + -> 长期事实变化时才同步 Wiki +``` + +默认不创建任务归档;`docs/task/` 只保存人工明确要求的专项或历史兼容快照。 + +## 首次初始化顺序 + +1. 创建 Gitea 远端仓库并推送当前引导提交,启用工单和 Wiki。 +2. 配置 `wiki-docs.json` 和安全访问方式;优先使用已配置的 Gitea MCP,MCP 不可用时才使用 Gitea API并记录原因。 +3. 查询线上 Wiki;`Home` 不存在时先创建并回读 `Home`,取得 revision 后再创建其他核心页面。 +4. 本地 `docs/` 的存在不能证明线上 Wiki 已初始化。 +5. 开始产品编码前运行: + + ```powershell + python dev_scripts/harness.py sync --verify + ``` + +YoVision 已完成远端与 Wiki 初始化,上述步骤用于维护者理解门禁和以后建立新仓库。 + +## 常用命令 + +```powershell +python dev_scripts/harness.py check --strict +python dev_scripts/harness.py sync +python dev_scripts/harness.py sync --check +python -m unittest discover -s tests -v +git diff --check +``` + +只有明确要求时才运行 `python dev_scripts/harness.py archive <编号> "<标题>"`、`export` 或 `export --all`。 ## 分支模型 @@ -18,3 +59,15 @@ Sense 与 Bell 是账户、数据、部署和发布边界完全独立的两个 - 只有用户明确审核通过,才能将 `dev` 合入 `main`。 Sense 与 Bell 的新实现必须从根 `goadmin-baseline.json` 冻结的 go-admin 和 go-admin-ui 完整提交派生,开发时必须核对对应 commit 的 go-admin-doc。仅参考 GoAdmin 的技术栈、布局或视觉不属于本项目认可的二次开发。 + +## Harness 目录 + +```text +AGENTS.md Agent 通用规则与 YoVision 专用门禁 +CLAUDE.md Claude Code 入口和三项目角色路由 +.gitea/issue_template/ Epic、MVP、单元任务模板 +docs/ 核心 Wiki 只读镜像及可选历史快照 +wiki-docs.json 核心 Wiki 页面显式映射 +dev_scripts/harness.py check / sync / archive / export 单一入口 +dev_scripts/wiki_docs.py Gitea Wiki 客户端与镜像生成库 +``` diff --git a/dev_scripts/check_harness.py b/dev_scripts/harness.py similarity index 55% rename from dev_scripts/check_harness.py rename to dev_scripts/harness.py index b1c2d54..bed92a6 100644 --- a/dev_scripts/check_harness.py +++ b/dev_scripts/harness.py @@ -1,15 +1,38 @@ -"""检查 DevHarness 必需文件、核心文档和任务归档的基本结构。""" +"""DevHarness 单一命令行入口。 + +子命令: + check 检查必需文件、核心文档和已有任务快照结构 + sync 从 Gitea Wiki 单向导出或校验核心 docs 镜像 + archive 显式在 Gitea Wiki 创建可选任务快照 + export 人工按需把已有 Wiki 任务快照导出到 docs/task + +各子命令的实现逻辑取自原来的 check_harness.py、sync_wiki_docs.py、 +new_task_archive.py 和 export_task_archives.py,行为未改变。 +""" from __future__ import annotations import argparse import json import re +from datetime import date from pathlib import Path +from typing import Any -from wiki_docs import WikiDocsError, load_config, parse_mirror +from wiki_docs import ( + DEFAULT_CONFIG, + WikiClient, + WikiDocsError, + dirty_paths, + load_config, + parse_mirror, + sync_all, + write_mirror, +) +# ---------------------------------------------------------------- 结构检查 + ROOT = Path(__file__).resolve().parents[1] CORE_PAGE_PATHS = { "Home": "docs/README.md", @@ -28,10 +51,20 @@ CORE_PAGE_PATHS = { "Existing-Project-Adoption-Guide": ( "docs/08-existing-project-adoption.md" ), + "Product-Requirements": ( + "docs/09-product-requirements.md" + ), + "Requirements-Migration-Matrix": "docs/10-requirements-migration-matrix.md", + "Multi-Agent-Collaboration": "docs/11-multi-agent-collaboration.md", + "Product-Roadmap": "docs/12-product-roadmap.md", "Delivery-Documentation-Guide": "docs/delivery/README.md", "Audience-Document-Template": ( "docs/delivery/audience-document-template.md" ), + "Deployment-and-Operations": ( + "docs/delivery/deployment-and-operations.md" + ), + "Deployment-Template": "docs/templates/deployment.md", "Task-Archive-Template": "docs/templates/task-archive.md", } CORE_DOCUMENT_REQUIREMENTS = { @@ -43,6 +76,7 @@ CORE_DOCUMENT_REQUIREMENTS = { ), "docs/00-project-profile.md": ( "## 基本信息", + "## DevHarness 来源与基线", "## 子项目与交付单元", "## 技术栈与运行环境", "## 阅读入口", @@ -51,11 +85,17 @@ CORE_DOCUMENT_REQUIREMENTS = { "## Sense/Bell GoAdmin 固定技术基线", ), "docs/01-workflow.md": ( + "## Gitea 交互与工单最小读取", + "## 新项目 Wiki 初始化门禁", + "## 工单与设计证据双门禁", + "### 先判断是否需要工单", + "### 再判断设计证据", + "### 线上原型审核与按需导出", + "### 记录和重新确认", "## 面向初级维护者的修改边界", - "## go-admin / go-admin-ui 开发约束", "## 每个任务的文档影响", "## 需求记录与流转", - "## 稳定文档与任务归档", + "## 稳定文档与可选历史快照", "## 自然语言快捷指令", "## 效率与范围控制", "### 严格控制范围", @@ -77,6 +117,7 @@ CORE_DOCUMENT_REQUIREMENTS = { ), "docs/04-local-development-and-verification.md": ( "## 环境要求", + "## Windows PowerShell 与 UTF-8", "## Sense/Bell 固定工具链与只读参考源", "## 第一次运行", "## 常用调试方式", @@ -94,8 +135,13 @@ CORE_DOCUMENT_REQUIREMENTS = { ), "docs/07-new-project-documentation-setup.md": ( "## 初始化顺序", - "### 2. 识别子项目与交付单元", - "### 6. 确定交付对象和文档", + "#### 需求总览启用条件", + "### 2. 选择建设基线", + "#### 工程基线裁剪", + "#### 判断案例", + "### 3. 识别子项目与交付单元", + "### 7. 确定交付对象和文档", + "#### 在线创建与回读门禁", "## 完成标准", ), "docs/08-existing-project-adoption.md": ( @@ -107,6 +153,9 @@ CORE_DOCUMENT_REQUIREMENTS = { "### 可以考虑拆仓", "### 保持单仓库时的最小规则", "## 增量接入顺序", + "## 后续升级", + "### 升级步骤", + "### 可复制升级指令", "## 冲突处理和停止条件", "## 可复制 Agent 指令", "### 只分析", @@ -114,6 +163,19 @@ CORE_DOCUMENT_REQUIREMENTS = { "## 最小验收清单", "## 回退原则", ), + "docs/09-product-requirements.md": ( + "## 本页用途", + "## 事实来源边界", + "## 当前需求索引", + "## 登记规则", + "## 原型与设计资产", + "### 原型门禁", + "### 线上原型与按需 HTML 快照", + "### 原型确认记录", + "## 状态规则", + "## 更新时机", + "## 最小验收清单", + ), "docs/delivery/README.md": ( "## 什么时候需要交付文档", "## 受众与文档选择", @@ -140,12 +202,12 @@ REQUIRED_FILES = ( "README.md", "docs/00-project-profile.md", "docs/01-workflow.md", + "docs/templates/deployment.md", "docs/templates/task-archive.md", *CORE_DOCUMENT_REQUIREMENTS, "wiki-docs.json", - "goadmin-baseline.json", "dev_scripts/wiki_docs.py", - "dev_scripts/sync_wiki_docs.py", + "dev_scripts/harness.py", ".gitea/issue_template/epic.md", ".gitea/issue_template/mvp.md", ".gitea/issue_template/task.md", @@ -181,6 +243,15 @@ def check_archives(errors: list[str]) -> None: if not re.match(r"^\d+-.+\.md$", path.name): errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}") content = path.read_text(encoding="utf-8") + try: + metadata, _ = parse_mirror(content) + except WikiDocsError as exc: + errors.append(f"{path.name} 的任务镜像无效:{exc}") + continue + if re.fullmatch(r"Task-\d+-.+", metadata.get("wiki_page", "")) is None: + errors.append(f"{path.name} 的 wiki_page 不是任务归档页面") + if re.fullmatch(r"[0-9a-f]{40,64}", metadata.get("wiki_revision", "")) is None: + errors.append(f"{path.name} 的 wiki_revision 无效") for heading in ARCHIVE_HEADINGS: if heading not in content: errors.append(f"{path.name} 缺少章节:{heading}") @@ -213,9 +284,6 @@ def check_task_template(errors: list[str], root: Path = ROOT) -> None: return content = path.read_text(encoding="utf-8") required = ( - "- 任务类型:单项目 / 协同", - "- 主项目:Sense / Brain / Bell / contracts / 根级", - "- 主 agent:", "## 依赖与并行", "- 前置工单:无 / #编号", "- 是否允许与前置工单并行:是 / 否", @@ -224,21 +292,23 @@ def check_task_template(errors: list[str], root: Path = ROOT) -> None: "- 仅影响的子项目 / 交付单元:", "- 是否跨子项目:是 / 否", "- 是否修改共享接口或契约:是 / 否;唯一事实来源:", - "- write_paths:", "- 各子项目需要执行的验证:", - "## 协同接口", - "- 生产者:", - "- 消费者:", - "- 契约/共享事实源:", - "- 兼容策略:不适用 / 向后兼容 / 发布新版本", - "- 被阻塞或需要适配的工单:", - "- 集成顺序:", "## 原始需求", "- 来源:用户对话 / Gitea / 其他", "- 提出时间:", "- 关键原话或脱敏摘要:", "## 需求变化记录", "| 日期 | 变化内容 | 原因 | 用户确认 |", + "## 设计与原型门禁", + "- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug", + "- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据", + "- 可编辑设计源、线上原型链接和访问检查:", + "- 审核版本、revision、复制版本或确认日期及识别方式:", + "- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求", + "- 本地 HTML 路径、版本和资源检查(仅显式导出时填写):", + "- 状态:无 / 草稿 / 已确认 / 已废弃", + "- 确认人、确认时间和覆盖范围:", + "- 无需 UI 原型或无需任何原型的原因:", "## 文档影响", "- [ ] 不影响长期文档,原因:", "- [ ] 更新架构与代码地图", @@ -249,6 +319,11 @@ def check_task_template(errors: list[str], root: Path = ROOT) -> None: "- [ ] 更新已有交付文档,受众与页面:", "- [ ] 新增交付文档,受众与页面:", "- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:", + "## 任务记录与可选快照", + "- 单次任务事实来源:当前 Gitea 工单正文与评论", + "- [ ] 默认不创建任务快照", + "- [ ] 用户明确要求专项快照;用途和范围:", + "- [ ] 项目专用规则要求任务快照;规则入口:", ) for section in missing_sections(content, required): errors.append(f"单元任务模板缺少:{section}") @@ -268,12 +343,30 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: "单元任务是唯一正式实施单位", "高风险修改必须停止", "用户没有明确验收通过前不得关闭", - "长期文档必须先修改 Wiki", + "只有长期事实变化时才修改 Wiki", + "Gitea 工单是单次任务需求、变化、实现、测试、提交和验收的事实来源", + "默认不创建任务归档", + "### Gitea 交互与工单最小读取", + "查询、创建、更新、评论、状态变更及关闭操作", + "优先关注当前状态、最新评论和首个未完成步骤", + "连接器不支持评论分页或增量读取时允许读取完整工单", + "不得为规避完整读取而新增本地工单、缓存或第二事实来源", + "### 新项目 Wiki 初始化门禁", + "`Home` 不存在时必须先创建 `Home`", + "不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据", "提交只包含当前工单相关文件", + "不得仅为设置编码重复启动一层 PowerShell", + "文件解码和控制台输出分别处理", + "不得默认使用 `-ExecutionPolicy Bypass`", + "### 工单与设计证据双门禁", + "新页面、独立用户功能、重大交互或导航变化", + "`prototypes/<工单号>/<版本>/index.html`", + "默认直接通过 Quant-UX 或其他设计工具的线上链接审核", + "已确认的本地快照不得原位覆盖", + "`导出原型 #N`", + "`导出全部原型`", + "代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案", "### 自然语言快捷指令", - "### 三项目并行建单顺序", - "建单顺序不等于实施顺序", - "主 agent / dispatcher", "`只分析`", "`建工单`", "`执行工单 #N`", @@ -281,6 +374,10 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: "`继续工单 #N`", "`检查工单 #N`", "`同步文档`", + "`导出原型 #N`", + "`导出全部原型`", + "`导出任务归档`", + "`导出全部任务归档`", "`#N 验收通过`", "### 需求记录与流转", "不得臆造用户原话", @@ -291,6 +388,31 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: errors.append(f"AGENTS.md 缺少:{section}") +def check_repository_readme(errors: list[str], root: Path = ROOT) -> None: + """检查快速开始包含线上 Wiki 初始化顺序和产品编码门禁。""" + + path = root / "README.md" + if not path.is_file(): + return + content = path.read_text(encoding="utf-8") + required = ( + "创建 Gitea 远端仓库并推送当前引导提交,启用工单和 Wiki", + "优先使用已配置的 Gitea MCP", + "`Home` 不存在时先创建并回读 `Home`", + "本地 `docs/` 的存在不能证明线上 Wiki 已初始化", + "python dev_scripts/harness.py sync --verify", + "Gitea 工单是单次任务唯一事实来源", + "默认不创建任务归档", + ) + for section in missing_sections(content, required): + errors.append(f"README.md 缺少:{section}") + + remote_index = content.find("创建 Gitea 远端仓库") + wiki_index = content.find("`Home` 不存在时先创建") + if remote_index < 0 or wiki_index < 0 or remote_index > wiki_index: + errors.append("README.md 必须先创建 Gitea 远端,再创建 Wiki Home") + + def check_go_admin_ui_rules(errors: list[str], root: Path = ROOT) -> None: """检查 Sense、Bell 共用的框架精简和组件复用规则。""" @@ -455,6 +577,7 @@ def check_goadmin_baseline(errors: list[str], root: Path = ROOT) -> None: errors.append(f"{relative_path} 缺少 GoAdmin 基线规则:{section}") + def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None: """检查 Claude Code 入口直接复用共同 Agent 规则。""" @@ -471,14 +594,11 @@ def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None: "只修改 `AGENTS.md`", "## 模型路由", "## Agent 交接", - "## 三项目角色路由", "## Haiku 只读约束", "当前模型足以完成任务时不升级模型", "Opus 输出方案后必须等待用户确认", "不让 Haiku 决定最终根因", "只读必须通过子 Agent 工具权限实现", - "主 Claude 作为 dispatcher", - "coordination agent", ) for section in missing_sections(content, required): errors.append(f"CLAUDE.md 缺少:{section}") @@ -495,7 +615,7 @@ def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]: def check_wiki_mirrors(errors: list[str]) -> None: - """检查每份本地文档都有显式映射和可追踪的镜像头。""" + """检查核心映射与镜像头;任务快照由 check_archives 单独检查。""" try: config = load_config() @@ -509,6 +629,7 @@ def check_wiki_mirrors(errors: list[str]) -> None: mapped_paths = {mapping.path for mapping in config.mappings} actual_paths = { path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md") + if path.parent != ROOT / "docs" / "task" } for path in sorted(actual_paths - mapped_paths): errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}") @@ -534,15 +655,121 @@ def check_wiki_mirrors(errors: list[str]) -> None: errors.append(f"{mapping.path} 缺少 synchronized_at") -def main() -> int: - parser = argparse.ArgumentParser(description="检查 DevHarness 项目结构") - parser.add_argument( - "--strict", - action="store_true", - help="项目档案有占位内容时返回失败", - ) - args = parser.parse_args() +# ---------------------------------------------------------------- 任务归档 +def safe_title(title: str) -> str: + """把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。""" + + cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip()) + cleaned = re.sub(r"\s+", "-", cleaned) + cleaned = re.sub(r"-+", "-", cleaned) + return cleaned.strip(".-") + + +def build_archive( + template: str, + issue_number: str, + title: str, + page_name: str, + issue_url: str, +) -> str: + content = template.replace("<工单号>", issue_number, 1) + content = content.replace("<标题>", title.strip(), 1) + content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1) + content = content.replace("<链接>", issue_url, 1) + return content.replace("<页面名>", page_name, 1) + + +# ---------------------------------------------------------------- 归档导出 + +TASK_PAGE_PATTERN = re.compile(r"^Task-(?P\d+)-(?P.+)$") + + +def task_revision(metadata: dict[str, Any], page_name: str) -> str: + last_commit = metadata.get("last_commit") + revision = last_commit.get("sha") if isinstance(last_commit, dict) else None + if not isinstance(revision, str) or not revision: + raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}") + return revision + + +def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]: + """按镜像头匹配已有文件,兼容历史自定义文件名。""" + + mirrors: dict[str, Path] = {} + task_dir = root / "docs" / "task" + if not task_dir.is_dir(): + return mirrors + for path in task_dir.glob("*.md"): + try: + metadata, _ = parse_mirror(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, WikiDocsError) as exc: + raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc + page_name = metadata.get("wiki_page", "") + if not TASK_PAGE_PATTERN.fullmatch(page_name): + raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}") + if page_name in mirrors: + raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}") + mirrors[page_name] = path + return mirrors + + +def task_target(page_name: str, root: Path = ROOT) -> Path: + match = TASK_PAGE_PATTERN.fullmatch(page_name) + if match is None: + raise WikiDocsError(f"不是任务归档页面:{page_name}") + title = safe_title(match.group("title")) + if not title: + raise WikiDocsError(f"任务归档标题无效:{page_name}") + return root / "docs" / "task" / f"{match.group('number')}-{title}.md" + + +def export_task_archives( + client: WikiClient, *, export_all: bool = False, root: Path = ROOT +) -> list[str]: + """增量或全量读取任务归档;绝不删除本地文件。""" + + dirty = dirty_paths(["docs/task"], root) + if dirty: + raise WikiDocsError( + "本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty) + ) + + existing = existing_task_mirrors(root) + pages = [] + for metadata in client.list_pages(): + title = metadata.get("title") + if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title): + pages.append((int(title.split("-", 2)[1]), title, metadata)) + pages.sort(key=lambda item: (item[0], item[1])) + + messages: list[str] = [] + targets: set[Path] = set() + for _, page_name, metadata in pages: + target = existing.get(page_name, task_target(page_name, root)) + if target in targets: + raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}") + targets.add(target) + revision = task_revision(metadata, page_name) + if not export_all and target.is_file(): + local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8")) + if ( + local_metadata.get("wiki_page") == page_name + and local_metadata.get("wiki_revision") == revision + ): + messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}") + continue + page = client.get_page_from_metadata(metadata, page_name) + changed = write_mirror(target, page) + action = "已导出" if changed else "无变化" + messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}") + return messages + + +# ---------------------------------------------------------------- 子命令入口 + + +def run_check(args: argparse.Namespace) -> int: errors: list[str] = [] warnings: list[str] = [] check_required_files(errors) @@ -551,6 +778,7 @@ def main() -> int: check_core_documents(errors) check_task_template(errors) check_agent_efficiency_rules(errors) + check_repository_readme(errors) check_go_admin_ui_rules(errors) check_goadmin_baseline(errors) check_claude_code_entry(errors) @@ -568,5 +796,132 @@ def main() -> int: return 0 +def run_sync(args: argparse.Namespace) -> int: + """--verify 依次执行导出、结构检查和一致性校验,替代原来的三条命令。""" + + if args.verify: + steps = ( + ("同步", lambda: run_sync( + argparse.Namespace(check=False, verify=False, config=args.config))), + ("结构检查", lambda: run_check(argparse.Namespace(strict=True))), + ("一致性校验", lambda: run_sync( + argparse.Namespace(check=True, verify=False, config=args.config))), + ) + for name, step in steps: + code = step() + if code != 0: + print(f"错误:{name}未通过,已停止") + return code + return 0 + + try: + config = load_config(Path(args.config).resolve()) + messages = sync_all(config, WikiClient(config), check=args.check) + except WikiDocsError as exc: + print(f"错误:{exc}") + return 1 + for message in messages: + print(message) + print("Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成") + return 0 + + +def run_archive(args: argparse.Namespace) -> int: + short_title = safe_title(args.title) + if not args.issue_number.isdigit(): + print("错误:工单号必须是数字") + return 1 + if not short_title: + print("错误:标题不能为空") + return 1 + + try: + config = load_config(Path(args.config).resolve()) + page_name = f"Task-{args.issue_number}-{short_title}" + client = WikiClient(config) + if any(item.get("title") == page_name for item in client.list_pages()): + raise WikiDocsError(f"任务归档已经存在:{page_name}") + template = client.get_page("Task-Archive-Template").text + issue_url = ( + f"{config.gitea_url}/{config.owner}/{config.repository}/issues/" + f"{args.issue_number}" + ) + content = build_archive( + template, args.issue_number, args.title, page_name, issue_url + ) + page = client.create_page( + page_name, + content, + f"docs: 创建任务 #{args.issue_number} 归档草稿", + ) + except WikiDocsError as exc: + print(f"错误:{exc}") + return 1 + + print(f"已创建 Wiki:{page.html_url}") + print("未导出本地任务归档;需要时运行 harness.py export") + return 0 + + +def run_export(args: argparse.Namespace) -> int: + try: + config = load_config(Path(args.config).resolve()) + messages = export_task_archives(WikiClient(config), export_all=args.all) + except WikiDocsError as exc: + print(f"错误:{exc}") + return 1 + for message in messages: + print(message) + print("任务归档全量导出完成" if args.all else "任务归档增量导出完成") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="DevHarness 检查、同步与归档工具") + sub = parser.add_subparsers(dest="command", required=True) + + p_check = sub.add_parser("check", help="检查 DevHarness 项目结构") + p_check.add_argument( + "--strict", action="store_true", help="项目档案有占位内容时返回失败" + ) + p_check.set_defaults(func=run_check) + + p_sync = sub.add_parser("sync", help="从 Gitea Wiki 单向同步核心 docs 镜像") + p_sync.add_argument( + "--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件" + ) + p_sync.add_argument( + "--verify", + action="store_true", + help="依次执行导出、check --strict 和一致性校验", + ) + p_sync.add_argument( + "--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件" + ) + p_sync.set_defaults(func=run_sync) + + p_archive = sub.add_parser("archive", help="显式在 Gitea Wiki 创建可选任务快照") + p_archive.add_argument("issue_number", help="Gitea 工单号,例如 123") + p_archive.add_argument("title", help="简短任务标题") + p_archive.add_argument( + "--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置" + ) + p_archive.set_defaults(func=run_archive) + + p_export = sub.add_parser("export", help="人工按需导出 Gitea Wiki 任务归档") + p_export.add_argument( + "--all", + action="store_true", + help="全量读取全部线上任务归档;默认按 revision 增量", + ) + p_export.add_argument( + "--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置" + ) + p_export.set_defaults(func=run_export) + + args = parser.parse_args() + return args.func(args) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/dev_scripts/new_task_archive.py b/dev_scripts/new_task_archive.py deleted file mode 100644 index a1a01ad..0000000 --- a/dev_scripts/new_task_archive.py +++ /dev/null @@ -1,101 +0,0 @@ -"""先在 Gitea Wiki 创建任务归档,再登记并导出本地镜像。""" - -from __future__ import annotations - -import argparse -import re -from datetime import date -from pathlib import Path - -from wiki_docs import ( - DEFAULT_CONFIG, - Mapping, - WikiClient, - WikiDocsError, - append_mapping, - load_config, - sync_all, -) - - -def safe_title(title: str) -> str: - """把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。""" - - cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip()) - cleaned = re.sub(r"\s+", "-", cleaned) - cleaned = re.sub(r"-+", "-", cleaned) - return cleaned.strip(".-") - - -def build_archive( - template: str, - issue_number: str, - title: str, - page_name: str, - issue_url: str, -) -> str: - content = template.replace("<工单号>", issue_number, 1) - content = content.replace("<标题>", title.strip(), 1) - content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1) - content = content.replace("<链接>", issue_url, 1) - return content.replace("<页面名>", page_name, 1) - - -def main() -> int: - parser = argparse.ArgumentParser( - description="在 Gitea Wiki 创建任务归档并导出 docs/task 镜像" - ) - parser.add_argument("issue_number", help="Gitea 工单号,例如 123") - parser.add_argument("title", help="简短任务标题") - parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置") - args = parser.parse_args() - - short_title = safe_title(args.title) - if not args.issue_number.isdigit(): - print("错误:工单号必须是数字") - return 1 - if not short_title: - print("错误:标题不能为空") - return 1 - - try: - config = load_config(Path(args.config).resolve()) - page_name = f"Task-{args.issue_number}-{short_title}" - local_path = f"docs/task/{args.issue_number}-{short_title}.md" - mapping = Mapping(page=page_name, path=local_path) - if any( - item.page == mapping.page or item.path == mapping.path - for item in config.mappings - ): - raise WikiDocsError(f"任务归档已经登记:{page_name}") - - client = WikiClient(config) - template = client.get_page("Task-Archive-Template").text - issue_url = ( - f"{config.gitea_url}/{config.owner}/{config.repository}/issues/" - f"{args.issue_number}" - ) - content = build_archive( - template, args.issue_number, args.title, page_name, issue_url - ) - page = client.create_page( - page_name, - content, - f"docs: 创建任务 #{args.issue_number} 归档草稿", - ) - append_mapping(config, mapping) - updated_config = load_config(config.path) - messages = sync_all(updated_config, client) - except WikiDocsError as exc: - print(f"错误:{exc}") - return 1 - - print(f"已创建 Wiki:{page.html_url}") - for message in messages: - print(message) - print(f"已登记镜像:{local_path}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/dev_scripts/sync_wiki_docs.py b/dev_scripts/sync_wiki_docs.py deleted file mode 100644 index e44b9d6..0000000 --- a/dev_scripts/sync_wiki_docs.py +++ /dev/null @@ -1,33 +0,0 @@ -"""从 Gitea Wiki 单向导出本地 docs 镜像。""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -from wiki_docs import DEFAULT_CONFIG, WikiClient, WikiDocsError, load_config, sync_all - - -def main() -> int: - parser = argparse.ArgumentParser(description="从 Gitea Wiki 单向同步 docs 镜像") - parser.add_argument( - "--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件" - ) - parser.add_argument( - "--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件" - ) - args = parser.parse_args() - try: - config = load_config(Path(args.config).resolve()) - messages = sync_all(config, WikiClient(config), check=args.check) - except WikiDocsError as exc: - print(f"错误:{exc}") - return 1 - for message in messages: - print(message) - print("Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/dev_scripts/wiki_docs.py b/dev_scripts/wiki_docs.py index 5f111fd..6b1008c 100644 --- a/dev_scripts/wiki_docs.py +++ b/dev_scripts/wiki_docs.py @@ -210,6 +210,14 @@ class WikiClient: raise WikiDocsError( f"Wiki 页面不存在:{page_name};不会自动删除或重命名本地镜像" ) + return self.get_page_from_metadata(metadata, page_name) + + def get_page_from_metadata( + self, metadata: dict[str, Any], page_name: str | None = None + ) -> WikiPage: + """使用页面列表元数据读取正文,避免重复获取完整页面列表。""" + + resolved_name = page_name or _required_string(metadata, "title") sub_url = _required_string(metadata, "sub_url") page = self._request( "GET", @@ -218,20 +226,22 @@ class WikiClient: f"{quote(sub_url, safe='%')}", ) if not isinstance(page, dict): - raise WikiDocsError(f"Wiki 页面响应格式无效:{page_name}") + raise WikiDocsError(f"Wiki 页面响应格式无效:{resolved_name}") encoded_content = page.get("content_base64") if not isinstance(encoded_content, str): - raise WikiDocsError(f"Wiki 页面没有 content_base64:{page_name}") + raise WikiDocsError(f"Wiki 页面没有 content_base64:{resolved_name}") try: text = base64.b64decode(encoded_content, validate=True).decode("utf-8") except (ValueError, UnicodeDecodeError) as exc: - raise WikiDocsError(f"Wiki 页面不是有效的 UTF-8 Markdown:{page_name}") from exc + raise WikiDocsError( + f"Wiki 页面不是有效的 UTF-8 Markdown:{resolved_name}" + ) from exc last_commit = page.get("last_commit") revision = last_commit.get("sha") if isinstance(last_commit, dict) else None if not isinstance(revision, str) or not revision: - raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}") + raise WikiDocsError(f"Wiki 页面缺少 revision:{resolved_name}") title = page.get("title") - resolved_title = title if isinstance(title, str) and title else page_name + resolved_title = title if isinstance(title, str) and title else resolved_name html_url = ( f"{self.config.gitea_url}/{quote(self.config.owner, safe='')}/" f"{quote(self.config.repository, safe='')}/wiki/{quote(sub_url, safe='%')}" @@ -303,7 +313,14 @@ def render_mirror(page: WikiPage, existing: str | None = None) -> str: def dirty_mirror_paths(config: Config, root: Path = ROOT) -> list[str]: - paths = [mapping.path for mapping in config.mappings] + return dirty_paths([mapping.path for mapping in config.mappings], root) + + +def dirty_paths(paths: list[str], root: Path = ROOT) -> list[str]: + """返回指定路径中已有、修改或未跟踪的工作区条目。""" + + if not paths: + return [] result = subprocess.run( ["git", "status", "--porcelain", "--", *paths], cwd=root, @@ -329,6 +346,17 @@ def _write_atomic(path: Path, content: str) -> None: raise +def write_mirror(path: Path, page: WikiPage) -> bool: + """写入一份 Wiki 镜像;内容无变化时返回 False。""" + + existing = path.read_text(encoding="utf-8") if path.is_file() else None + rendered = render_mirror(page, existing) + if existing == rendered: + return False + _write_atomic(path, rendered) + return True + + def check_mirror(mapping: Mapping, page: WikiPage, path: Path) -> list[str]: if not path.is_file(): return [f"缺少镜像:{mapping.path}"] diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md index f60dc29..0c0576e 100644 --- a/docs/00-project-profile.md +++ b/docs/00-project-profile.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Project-Profile wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Project-Profile.- -wiki_revision: 84cb91d5fb87fcb711c04c1f94b5c09ebe8c22a1 -synchronized_at: 2026-08-15T06:45:18Z +wiki_revision: 5e6d18991d69aad15311aae811f35206aa0b5ee6 +synchronized_at: 2026-08-27T09:04:26Z <!-- gitea-wiki-mirror:end --> # 项目档案 @@ -22,6 +22,18 @@ synchronized_at: 2026-08-15T06:45:18Z | 当前阶段 | 进入 GoAdmin 源码派生重建:旧实现归档于 `explore`,`main` 为审核基线,`dev` 为集成开发分支 | | 历史来源 | `D:\OPC\yovision_old`,只读追溯 | +## DevHarness 来源与基线 + +| 项目 | 内容 | +|---|---| +| 上游仓库 | `https://git.ilapage.cn/OPC/dev_harness` | +| 本次升级前基线 | `f23c2cf81f9792495f696d49e88d79b16cf29810` | +| 当前目标基线 | `4bbacf4d7fb265984396bb5589c544105043fa0b` | +| 升级日期 | 2026-08-27 | +| 识别方式 | 初始导入文件 blob 与上游历史逐项对照;完整 commit 是基线标识 | + +YoVision 采用 DevHarness 的共同工作流、统一 `harness.py` 命令、Gitea 工单/Wiki 事实源边界和模板;项目适配保留三项目并行与写路径所有权、`explore/main/dev` 分支治理、Sense/Bell GoAdmin 固定基线、UI 精简复用门禁和现有产品文档结构。升级不得整页覆盖项目事实,也不得复制 DevHarness 自身任务状态。 + ## 子项目与交付单元 | 单元 | 职责 | 技术栈目标 | 独立构建/测试/发布 | 规则入口 | 共享边界 | @@ -57,9 +69,9 @@ synchronized_at: 2026-08-15T06:45:18Z 当前初始化阶段可执行: ```powershell -python dev_scripts/check_harness.py --strict +python dev_scripts/harness.py check --strict python -m unittest discover -s tests -v -python dev_scripts/sync_wiki_docs.py --check +python dev_scripts/harness.py sync --check git diff --check ``` diff --git a/docs/01-workflow.md b/docs/01-workflow.md index 7c48bca..88b0667 100644 --- a/docs/01-workflow.md +++ b/docs/01-workflow.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Development-Workflow wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Development-Workflow.- -wiki_revision: 2bb60b79073439babfda05140ce813d197de81d9 -synchronized_at: 2026-08-14T01:06:22Z +wiki_revision: b377b601b04074b834c4c9f6fbd37434b3d19d22 +synchronized_at: 2026-08-27T09:04:39Z <!-- gitea-wiki-mirror:end --> # 开发工作流 @@ -11,10 +11,94 @@ synchronized_at: 2026-08-14T01:06:22Z ## 事实来源边界 - Gitea 工单记录任务状态、讨论、阻塞、方案变化、验证和验收。 -- Gitea Wiki 保存架构说明、开发规范、操作手册和完成后的任务归档。 +- Gitea Wiki 保存长期架构、契约、业务规则、开发规范、操作手册和稳定需求;默认不重复保存单次任务归档。 - Git 保存源码、与特定代码版本强绑定的文档,以及 Wiki 的本地镜像。 - 本地 `docs/` 仅供浏览和审查,不是长期文档编辑入口。 +## Gitea 交互与工单最小读取 + +- 所有 Gitea 工单和 Wiki 的查询、创建、更新、评论、状态变更及关闭操作,优先使用项目已配置的 Gitea MCP。 +- MCP 不可用或不支持所需操作时才回退 Gitea API,并在当前工单记录回退原因;初始化阶段尚无工单时记录到初始化工单草稿,建单后补回。凭据只从环境或 MCP 安全配置读取。 +- 首次接手任务时读取工单确认基线和完成当前判断所需的评论,不因节省 Token 跳过范围、依赖、安全、验收或重要变更。 +- 同一任务、同一会话且关键前提未变化时,复用仍有效的工单事实,优先关注当前状态、最新评论和首个未完成步骤,不重复分析已经确认且仍有效的内容。 +- 会话、代码、配置、依赖、凭据、远端状态或关键前提变化,任务基线不清楚,或最新评论声明历史需求、方案、范围、风险或验收发生变化时,重新读取必要历史;无法判断影响范围时读取完整工单。 +- 连接器不支持评论分页或增量读取时允许读取完整工单,但不得把“已读取全文”误当成需要重新分析全部历史,也不得为规避完整读取而新增本地工单、缓存或第二事实来源。 +- 正确性、安全规则和已确认范围优先于 Token 优化;读取边界存在不确定时补读必要证据。 + +## 新项目 Wiki 初始化门禁 + +从 DevHarness 创建新项目时,本地 `docs/` 即使完整存在,也只能证明模板镜像存在,不能证明新项目的线上 Wiki 已初始化。开始任何产品代码前必须完成以下闭环: + +1. 先创建 Gitea 远端仓库并启用 Wiki,再把 `wiki-docs.json` 指向该仓库。 +2. 优先使用项目已配置的 Gitea MCP 查询 Wiki 页面;MCP 不可用或不支持所需写操作时,才使用 Gitea API,并在初始化工单记录回退原因。凭据只从环境或 MCP 安全配置读取。 +3. 查询线上页面列表;没有 `Home` 时先创建 `Home`,回读正文并记录 revision,然后再创建或更新其他核心映射页面。 +4. 每个核心页面写入后都要在线回读;页面可读取且取得 revision 才算创建成功,不能用本地 `docs/` 文件替代这项证据。 +5. 运行 `python dev_scripts/harness.py sync --verify`。任一映射页面不存在、无法回读或镜像不一致时,停止产品编码并完成初始化。 + +Gitea 暂时不可用时可以准备工单和 Wiki 草稿,但不得把本地草稿宣称为线上事实,也不得绕过此门禁开始产品功能开发。 + +## 工单与设计证据双门禁 + +开始正式实现前依次判断“是否需要工单”和“需要什么设计证据”。原型确认不能代替方案、工单、安全检查或技术验证;工单存在也不能绕过原型确认。 + +### 先判断是否需要工单 + +只有纯界面显示文案同时满足以下全部条件时,才可以免工单、免原型: + +- 只修改用户看到的组件显示名称、按钮文字、标题、提示语或其他文案; +- 不改变业务含义、操作流程、权限、状态、接口、数据和验收结果; +- 不涉及法律条款、安全提示、支付、金额、单位或其他高风险含义; +- 不修改国际化键、代码组件名、类名、变量、API 字段、数据库字段或其他程序标识符; +- 不造成明显布局、截断、换行、可访问性或支持平台问题; +- 有任何不确定时不使用豁免。 + +豁免修改只执行与受影响界面相称的最小检查,确认文字正确且没有明显布局或可访问性问题,然后停止。只要任一条件不满足,或涉及用户行为、样式布局、交互和导航,就建立单元任务工单。 + +### 再判断设计证据 + +| 修改类型 | 最低设计证据 | 正式编码门禁 | +|---|---|---| +| 纯显示文案且满足全部豁免条件 | 无原型 | 完成最小界面检查即可 | +| 现有界面的小范围样式或布局调整 | 标注截图或低保真线框图;没有设计不确定性时说明复用的现有规范 | 工单确认设计证据后编码 | +| 新组件但复用现有设计体系 | 组件状态、错误和边界说明;按需提供低保真图 | 工单确认状态和复用边界后编码 | +| 新页面、独立用户功能、重大交互或导航变化 | Quant-UX 或其他合适工具制作的可审阅原型 | 用户确认原型和文字需求后才能编写生产代码 | +| 后端、接口、数据处理或定时任务 | 架构、API、数据、状态或流程设计 | 用户确认技术方案后编码,不制作无意义的 UI 原型 | +| 恢复既有确认行为的 Bug | 原设计、已确认截图、复现步骤或现有验收证据 | 确认是恢复而不是改变行为后修复 | + +采用最低成本、足以让用户确认的证据,不为了形式制作高保真原型。草稿原型可以用于需求讨论;草稿需要写入 Git/Wiki、多人协作或单独实施时,应建立设计任务。草稿原型和临时技术验证都不能直接作为生产实现。 + +### 线上原型审核与按需导出 + +新页面、独立用户功能、重大交互或导航变化使用 Quant-UX 或等效工具形成待审核版本后,默认直接通过线上原型审核,不要求每次导出本地 HTML: + +- 可编辑设计源保存在 Quant-UX 或原设计工具;工单和 Wiki 只保存链接、版本与确认记录,不复制为第二份可编辑事实来源。 +- 线上链接必须能被确认人访问,并能通过版本、revision、复制版本或确认日期识别本次审核对象;无法访问或无法区分版本时停止审核,等待用户确认等效方案。 +- 提交审核前检查主要页面、流程、状态和交互可访问,并删除令牌、真实账号、个人信息和生产数据。 +- 页面结构、主要流程、状态、权限、异常处理或验收结果变化时,更新线上原型并重新确认;不得用旧确认覆盖新版本。 +- 纯显示文案、小范围现有 UI 调整、非 UI 需求和恢复既有行为的 Bug 仍只使用双门禁表规定的最低证据,不强制建立完整线上原型。 + +只有用户明确发出 `导出原型 #N`、`导出全部原型`,或项目专用规则明确要求离线交付时,才导出本地 HTML: + +- 指定工单的快照放入 `prototypes/<工单号>/<版本>/index.html`;全部导出时也按工单和版本分目录,先在工单明确导出范围。 +- 图片、样式、脚本和字体使用版本目录内的相对路径;需要网络资源才能显示时不得标记为可离线浏览。 +- 已确认的本地快照不得原位覆盖;新版本使用新目录,已有快照继续作为历史审核证据。 +- 导出后检查入口、主要交互和资源完整性;浏览器限制直接打开时,在工单记录最小本地静态服务命令和访问地址,不新增项目专用服务脚本。 +- 导出指令只生成或更新请求范围内的快照并报告结果,不自动提交;用户未明确要求时不得顺带导出其他原型。 +- 设计工具无法生成用户要求的可用 HTML 时,在工单记录限制并停止该导出或离线交付,等待用户确认等效方案;线上原型仍可访问且版本明确时,不因此阻塞线上审核。 + +### 记录和重新确认 + +需要设计证据的工单必须记录: + +- 原型或设计的线上链接、对应事实来源,以及链接可访问性; +- 版本、revision、复制版本或确认日期,以及审核版本的识别方式; +- 状态:无、草稿、已确认或已废弃; +- 只有显式导出时才记录本地 HTML 路径、版本和资源检查结果; +- 确认人和确认时间; +- 本次确认覆盖的页面、组件、流程和边界; +- 不需要 UI 原型时采用的技术设计,或无需任何原型的原因。 + +页面结构、主要流程、状态、权限、异常处理或验收结果变化时,先更新原型或文字需求并重新确认,再继续正式编码。只读技术检查可以在确认前进行;确需可行性代码验证时,必须由用户明确同意,隔离为不可进入生产的技术验证,不得悄悄扩展成正式实现。 ## 一次任务怎样完成 ### 1. 讨论 @@ -67,35 +151,50 @@ Agent 检查分支和工作区,只修改工单范围内的文件。发现新 - Git 提交哈希; - 相关 Wiki 页面及 revision。 -长期文档遵循唯一顺序: +工单正文保存用户确认的任务基线;根因、范围、方案、风险或阻塞发生重要变化时追加评论。完成实现后用一条评论集中记录最终差异、测试、未验证内容、提交哈希和长期文档影响,保留可追溯时间线,不在 Wiki 重抄同一份任务结果。 + +只有长期事实发生变化时才执行核心文档闭环: ```text -修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 +修改 Wiki → 读取确认 → 导出核心 docs → 校验差异 → 提交镜像 ``` -不得先编辑 `docs/` 再反向覆盖 Wiki。 +没有长期文档影响时,在工单写明原因并跳过 Wiki 更新和核心镜像同步;默认任务流程不创建任务归档。长期文档仍不得先编辑本地镜像再反向覆盖 Wiki。 ### 4. 待验收 实现和测试完成后,Agent 提交实现代码并将工单更新为“待验收”。用户验收前工单保持开启。 -### 5. 归档和关闭 +### 5. 待验收和关闭 -使用以下命令在 Wiki 创建任务归档页、登记显式映射并导出本地镜像: +实现、必要测试和提交完成后,在工单追加一条最终证据评论并保持“待验收”。评论至少记录最终差异、测试结果、未验证内容、提交哈希,以及长期 Wiki 页面和 revision,或“无长期文档影响”及原因。 + +用户明确验收通过后: + +1. 在工单追加验收时间和结论,不重复抄写已有测试与提交证据; +2. 关闭单元工单并勾选所属 MVP/Epic 子任务; +3. 只有验收结论改变长期需求状态或其他 Wiki 事实时,才更新 Wiki 并执行同步闭环;没有变化时不重复检查 Wiki; +4. 默认不创建或导出任务归档。 + +任务归档只保留为显式兼容能力。只有用户明确要求专项快照,或项目专用规则明确要求时才运行: ```powershell -python dev_scripts/new_task_archive.py 123 "修复登录超时" +python dev_scripts/harness.py archive 123 "修复登录超时" +python dev_scripts/harness.py export # 增量导出已有归档 +python dev_scripts/harness.py export --all # 全量导出已有归档 ``` -归档内容以 Wiki 页面为主源;本地 `docs/task/<编号>-<短标题>.md` 是镜像。归档镜像单独提交,再把 Wiki 页面、revision、镜像路径和提交哈希写回工单。用户明确验收通过后,关闭单元工单并勾选父工单中的任务。 +可选归档不得成为第二个日常维护入口;创建时以工单中的最终证据为来源,并记录工单链接。既有 Wiki 归档和 `docs/task/` 快照不自动删除、重命名或补齐。 ## 文档同步规则 -- 映射保存在 `wiki-docs.json`,每个 Wiki 页面对应唯一仓库路径。 -- 同步脚本只实现 Wiki → `docs/`,不提供反向同步。 +- 核心页面映射保存在 `wiki-docs.json`;普通同步只处理这些核心长期文档。 +- 可选任务归档不逐页登记映射;显式执行归档导出时,工具根据 `Task-<编号>-<标题>` 动态发现,已有镜像优先按镜像头匹配原页面。 +- 所有同步和导出只实现 Wiki → `docs/`,不提供反向同步。 - 镜像头必须记录页面名、页面地址、revision 和同步时间。 - 已跟踪镜像存在未提交改动时,同步必须停止;确认改动来源后再处理。 -- `--check` 只检查,不写文件;页面缺失、revision 不一致或正文不一致均失败。 +- 核心同步的 `--check` 只检查核心镜像,不要求线上任务归档全部存在于本地。 +- 已经导出的任务镜像仍必须具有来源页面、revision 和同步时间,并通过 Harness 格式检查。 - 页面删除和重命名不会自动传播,必须先更新工单并人工确认映射变化。 - Wiki 更新成功而导出失败时,在工单记录部分完成状态,不得把任务标为完成。 - 与具体代码版本强绑定的接口或迁移资料可直接随代码维护,但必须在 Wiki 提供入口或适用版本说明。 @@ -129,6 +228,8 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" - 业务规则、安全边界或权限变化; - 日志位置、错误定位或常见处理方式变化。 +部署命令的落点:有常驻服务的项目更新自己的 `Deployment-and-Operations` 页面(由[部署文档模板](Deployment-Template.-)复制建立);没有常驻服务的项目在工单记录“无部署文档影响”及原因,不要创建空的部署页。 + 普通内部重构如果入口、行为、配置和验证方式均未改变,可以记录“不影响长期文档”及原因。 ## 需求记录与流转 @@ -149,20 +250,20 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" | 关键原始需求、确认后的单次任务需求 | Gitea 单元任务工单 | 无 | | 讨论、决定和需求变化 | Gitea 工单正文或评论 | 无 | | 长期有效的产品需求、业务规则和系统边界 | 对应 Gitea Wiki 主题页 | `docs/` | -| 完成后的实现、验证和遗留问题 | Wiki 任务归档 | `docs/task/` | +| 完成后的实现、验证、遗留问题和验收 | Gitea 单元任务工单正文与评论 | 无;用户明确要求时可创建专项 Wiki 快照 | 任务产生长期结论时,先更新对应 Wiki 主题页,再导出本地镜像。Gitea 工单全文不导出到仓库,避免形成第二份任务过程记录。 -## 稳定文档与任务归档 +## 稳定文档与可选历史快照 - Home、项目档案、代码地图、业务规则、开发验证、常见修改和故障排查描述项目现在怎样工作。 -- 工单和任务归档解释某次为什么修改、实际改了什么以及如何验证。 -- 新人先读稳定主题页,只有追查历史原因时才读任务归档。 -- 任务产生的长期结论必须合并到主题页,不能只留在归档。 +- 工单正文和评论解释某次为什么修改、实际改了什么、如何验证以及怎样验收。 +- 新人先读稳定主题页,只有追查历史原因时才读工单;可选 Wiki 快照和本地任务快照只是专项或历史兼容资料,不是默认事实来源。 +- 任务产生的长期结论必须合并到对应主题页,不能只留在工单或可选快照。 ## 效率与范围控制 -本节用于减少无关工作和重复检查,不得削弱安全规则、已确认方案、工单范围、必要测试、Wiki 同步、Git 提交和验收归档要求。 +本节用于减少无关工作和重复检查,不得削弱安全规则、已确认方案、工单范围、必要测试、必要的长期文档同步、Git 提交和人工验收要求。 ### 严格控制范围 @@ -200,20 +301,26 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" |---|---|---| | `只分析` | 只读检查需求、代码、日志和文档,区分事实与假设并给出方案 | 输出方案并等待确认;不建单、不修改 | | `建工单` | 根据已经确认的方案创建单元任务工单 | 工单创建并记录完成;不修改代码 | -| `执行工单 #N` | 读取工单和前置依赖,实施、测试、提交、更新 Wiki、导出镜像、推送并回写证据 | 工单保持“待验收” | +| `执行工单 #N` | 读取工单和前置依赖,实施、测试、提交并回写证据;仅有长期文档影响时更新 Wiki 和镜像 | 工单保持“待验收” | | `建工单并做` | 依次执行“建工单”和“执行工单”;`建工单,做`、`建工单,做` 含义相同 | 工单保持“待验收” | | `继续工单 #N` | 核对工单、Git 和 Wiki 证据,从首个未完成步骤继续,不重复仍然有效的检查 | 到达该工单当前流程的停止条件 | | `检查工单 #N` | 只读对照范围、验收标准、测试和证据,报告通过项、缺失项及未验证部分 | 输出检查报告;不自动修复 | -| `同步文档` | 读取 Wiki,导出已映射的 `docs/` 镜像并检查一致性 | 显示结果和差异;不修改 Wiki、不自动提交 | -| `#N 验收通过` | 记录明确验收,更新 Wiki 归档为“已完成”,导出并提交镜像,推送、同步父工单并关闭任务 | 工单“已完成”并关闭 | +| `同步文档` | 读取 Wiki,导出核心长期文档镜像并检查一致性,不处理任务归档 | 显示结果和差异;不修改 Wiki、不自动提交 | +| `导出原型 #N` | 人工触发导出指定工单已确认的原型版本;按工单和版本写入 `prototypes/` | 显示路径和检查结果;不扩展范围、不自动提交 | +| `导出全部原型` | 人工触发导出当前项目明确范围内的全部已确认原型 | 显示导出范围和结果;不自动提交 | +| `导出任务归档` | 人工触发增量导出,只写入新增或 revision 已变化的任务归档 | 显示导出或跳过结果;不删除本地文件、不自动提交 | +| `导出全部任务归档` | 人工触发全量读取并导出线上全部任务归档 | 显示导出结果;不删除本地文件、不自动提交 | +| `#N 验收通过` | 在工单追加验收结论,按需更新真实变化的长期 Wiki,推送、同步父工单并关闭任务;不创建或导出任务归档 | 工单“已完成”并关闭 | 补充边界: - 方案未确认时,`建工单`、`建工单并做` 和 `执行工单 #N` 不得绕过确认;Agent 应停在方案确认。 - 前置依赖未满足且不允许并行时,实施类指令停在“待实施”。 - `#N 验收通过` 必须来自用户明确表达;其他快捷指令不得关闭待验收工单。 -- `同步文档` 发现镜像有未提交改动时停止,不覆盖现有修改。 -- Gitea 工单保留讨论和过程,不把工单全文导出到本地;`docs/task/` 只保存 Wiki 最终任务归档的镜像。 +- `同步文档` 或任务归档导出发现目标镜像有未提交改动时停止,不覆盖现有修改。 +- `导出原型 #N` 和 `导出全部原型` 必须由用户明确提出或项目专用规则明确要求;其他指令不隐式导出原型。 +- `导出任务归档` 和 `导出全部任务归档` 必须由用户明确提出,其他快捷指令不隐式执行。 +- Gitea 工单是单次任务唯一事实来源,不导出全文;`docs/task/` 只保存人工明确要求的专项或历史兼容快照。 ## 什么时候重新确认方案 @@ -235,8 +342,9 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" | 讨论过程和临时方案 | 是 | 否 | 否 | | 实施进度和阻塞 | 是 | 否 | 否 | | 长期有效的最终方案 | 链接 | 是 | 镜像 | -| 测试结果与未验证内容 | 是 | 任务归档 | 镜像 | -| 提交哈希 | 是 | 任务归档 | 镜像 | +| 测试结果与未验证内容 | 是 | 否 | 否 | +| 提交哈希和验收结论 | 是 | 否 | 否 | +| 用户明确要求的任务专项快照 | 提供来源 | 可选 | 可选导出 | | 与具体代码版本绑定的说明 | 可链接 | 提供入口 | 是 | ## go-admin / go-admin-ui 开发约束 diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index 78b4974..be60b3b 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.- -wiki_revision: 8df12aa3e136b1faee9c2dffb58ca39b045bc49f -synchronized_at: 2026-08-17T02:54:44Z +wiki_revision: ee5b5507bf8a8a834e07f223f1a90c2f67a70d6f +synchronized_at: 2026-08-27T09:14:31Z <!-- gitea-wiki-mirror:end --> # 架构与代码地图 @@ -35,9 +35,9 @@ Bell 也可接收合成事件、传感器平台或第三方系统事件;Sense | Brain 推理 | `Brain/` | `Brain/AGENTS.md`;后续包入口 | `Brain/` 内测试与契约测试 | 高:模型、GPU、隐私、事件语义 | | Bell 产品 | `Bell/` | `Bell/AGENTS.md`;待 GoAdmin 派生工单建立 README/入口 | 待新骨架建立 | 高:GoAdmin 派生、认证、事件与告警状态机 | | 共享契约 | `contracts/` | `contracts/AGENTS.md` | 三端消费者/生产者测试 | 高:兼容性与跨项目影响 | -| Harness | `dev_scripts/` | `check_harness.py` | `tests/` | 中 | +| Harness | `dev_scripts/` | `harness.py check --strict` | `tests/` | 中 | | 工单模板 | `.gitea/issue_template/` | `task.md` | Harness 严格检查 | 中 | -| Wiki 镜像 | `docs/` | `docs/README.md` | `sync_wiki_docs.py --check` | 低;禁止直接编辑 | +| Wiki 镜像 | `docs/` | `docs/README.md` | `harness.py sync --check` | 低;禁止直接编辑 | 旧 Sense、Bell 入口仅存在于 `explore` 快照,不是 `main` / `dev` 当前代码地图。三个项目的新入口必须随新骨架工单建立;不得把旧自研基础框架复制回 `dev`。 @@ -142,3 +142,11 @@ ONVIF 支持 Basic 与 MD5/SHA-256 Digest challenge,Profile 与无凭据 Strea 交付包同时提供检查、迁移、管理员初始化、停止、备份和恢复入口。停止脚本只操作当前包且监听配置端口的 Sense 进程树;备份密码只进入子进程环境;恢复要求数据库名和二次短语确认。包不包含 PostgreSQL、生产数据、默认管理员、默认密码或客户秘密。 <!-- sense-windows-delivery:end --> + + +## DevHarness 执行路径 + +- `dev_scripts/harness.py check --strict` 检查核心结构、YoVision GoAdmin 基线和项目规则。 +- `dev_scripts/harness.py sync` 只从 Gitea Wiki 导出 `wiki-docs.json` 映射的核心镜像;`sync --check` 只检查一致性。 +- `archive`、`export` 和 `export --all` 只在人工明确要求时处理可选任务快照,任务快照不进入核心映射。 +- `dev_scripts/wiki_docs.py` 负责 Gitea Wiki 读取、revision、镜像头、脏文件保护和安全路径校验。 diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index df741d8..265a734 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.- -wiki_revision: 2bcd90c508180eb9a2f9fe37b62115d7d9207e18 -synchronized_at: 2026-08-15T01:13:04Z +wiki_revision: 96fca4d2aa411725282bd3911c133efc75f7146a +synchronized_at: 2026-08-27T09:04:59Z <!-- gitea-wiki-mirror:end --> # 业务规则与术语 @@ -146,3 +146,11 @@ synchronized_at: 2026-08-15T01:13:04Z - 鼠标可点击/拖动顶点;键盘必须能添加、移动和删除顶点。错误在绘制区域附近以可被辅助技术感知的文字给出,并提供撤销、清空和未保存关闭确认。 - 区域配置是 Sense 内部事实;#69 不发布 Brain 契约。后续 Sense→Brain 配置协议必须由独立协调工单从当前版本投影生成,不能共享数据库模型。 <!-- sense-area:end --> + + +## DevHarness 任务证据边界 + +- Gitea 工单正文与评论是单次任务的需求、变化、实现、测试、提交和验收事实来源。 +- Gitea Wiki 只保存长期有效的项目事实;核心页面由 `wiki-docs.json` 显式映射。 +- `docs/task/` 是按人工明确要求形成的专项或历史兼容快照,可能不完整或不是最新状态,不得替代工单。 +- 默认不创建、导出或更新任务快照;导出过程不自动删除本地历史文件。 diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index 6f19378..6b7653b 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Local-Development-and-Verification wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.- -wiki_revision: 925a16a72bd5840697b5c61489a97018134b3930 -synchronized_at: 2026-08-27T08:07:04Z +wiki_revision: 7ede95108304cec08572d9da5ce1f41116e4970d +synchronized_at: 2026-08-27T09:05:09Z <!-- gitea-wiki-mirror:end --> # 本地开发与验证 @@ -19,6 +19,12 @@ synchronized_at: 2026-08-27T08:07:04Z Sense/Bell 的 Go、Node 与 pnpm 基线已冻结并记录于下文;Brain 的 Python/CUDA 以及 PostgreSQL、MediaMTX 精确版本仍将在对应骨架工单冻结。旧仓库环境不自动成为新项目事实。 +## Windows PowerShell 与 UTF-8 + +- Windows 环境优先使用当前已配置的 PowerShell;可选择时优先 PowerShell 7 `pwsh.exe`,不为设置编码重复启动一层 PowerShell。 +- 文本文件读写在命令支持时显式指定 UTF-8。文件解码与控制台输出分别处理,只有出现真实乱码或已知宿主非 UTF-8 时才调整当前进程输出编码或 Python UTF-8 环境变量。 +- 不默认使用 `-ExecutionPolicy Bypass`。只有可信脚本确实被策略阻止且没有更小替代方案时,才对该次进程使用并在工单记录原因。 + ## 第一次运行 1. 检查工作区: @@ -32,7 +38,7 @@ Sense/Bell 的 Go、Node 与 pnpm 基线已冻结并记录于下文;Brain 的 2. 检查 Harness: ```powershell - python dev_scripts/check_harness.py --strict + python dev_scripts/harness.py check --strict ``` 预期:输出“DevHarness 检查通过”。 @@ -48,7 +54,7 @@ Sense/Bell 的 Go、Node 与 pnpm 基线已冻结并记录于下文;Brain 的 4. 对照 Wiki: ```powershell - python dev_scripts/sync_wiki_docs.py --check + python dev_scripts/harness.py sync --check ``` 预期:全部映射一致。 @@ -81,8 +87,8 @@ Sense/Bell 的 Go、Node 与 pnpm 基线已冻结并记录于下文;Brain 的 ```powershell python -m unittest discover -s tests -v -python dev_scripts/check_harness.py --strict -python dev_scripts/sync_wiki_docs.py --check +python dev_scripts/harness.py check --strict +python dev_scripts/harness.py sync --check git diff --check git status --short ``` diff --git a/docs/05-common-changes.md b/docs/05-common-changes.md index 4f010b7..9172dd0 100644 --- a/docs/05-common-changes.md +++ b/docs/05-common-changes.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Common-Changes wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Common-Changes.- -wiki_revision: edfcd6dd2cd254d31e88e980558e467b5fe758a3 -synchronized_at: 2026-08-11T10:30:43Z +wiki_revision: becaa9f08fd07397da516c31e3d93362626df050 +synchronized_at: 2026-08-27T09:05:21Z <!-- gitea-wiki-mirror:end --> # 常见修改指南 @@ -20,15 +20,15 @@ synchronized_at: 2026-08-11T10:30:43Z | 中风险 | API、配置、依赖、跨模块逻辑、数据结构 | 由 Agent 实现,程序员理解差异并执行验证 | | 高风险 | 权限、安全、并发、迁移、支付、删除数据、不可逆操作 | 停止修改,由 Agent 分析并等待人工确认 | -“代码行数少”不等于低风险。 +“代码行数少”不等于低风险。风险等级只决定由谁实施和验证,不改变建单门禁:新功能、缺陷修复、重构及行为变化仍需工单;只有 AGENTS.md 明确列出的非行为修改和纯显示文案豁免可以直接提交。 ## 修改 Wiki 文案 1. 在相关工单确认目标。 2. 读取线上 Wiki 页面和当前 revision。 3. 修改线上 Wiki,不直接编辑 `docs/`。 -4. 运行 `python dev_scripts/sync_wiki_docs.py`。 -5. 运行 `python dev_scripts/sync_wiki_docs.py --check`。 +4. 运行 `python dev_scripts/harness.py sync`。 +5. 运行 `python dev_scripts/harness.py sync --check`。 6. 审查本地镜像差异并提交。 停止条件:页面需要删除、重命名或改变事实源边界。 @@ -45,7 +45,7 @@ synchronized_at: 2026-08-11T10:30:43Z ## 调整 Harness 检查 -1. 从 `dev_scripts/check_harness.py` 的 `main()` 开始读。 +1. 从 `dev_scripts/harness.py` 的 `run_check()` 开始读。 2. 新检查应输出具体文件和缺失内容。 3. 检查结构事实,不声称自动判断文档语义质量。 4. 在 `tests/` 添加成功和失败用例。 diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index 946db2e..5aab750 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting -wiki_revision: 1a452e9aafdfe01580f37f9179584b89516cf992 -synchronized_at: 2026-08-15T07:59:38Z +wiki_revision: 046624c1b2d7963ef733b4c9687d5dec14771388 +synchronized_at: 2026-08-27T09:05:32Z <!-- gitea-wiki-mirror:end --> # 故障排查 @@ -152,3 +152,12 @@ synchronized_at: 2026-08-15T07:59:38Z 如果迁移报告不支持的唯一性结构,不要手工删除约束或路由;在备份副本中核对实际约束和业务数据。正式迁移失败时保留错误并从迁移前备份恢复,不通过关闭唯一性绕过迁移。 <!-- sense-media-path-constraint:end --> + + +## DevHarness 同步排错 + +1. 先运行 `python dev_scripts/harness.py check --strict` 定位结构或项目规则问题。 +2. 镜像不一致时运行 `python dev_scripts/harness.py sync --check`;不得直接修改 `docs/` 后反向覆盖 Wiki。 +3. 若同步提示本地镜像有未提交修改,先核对改动归属并停止覆盖。 +4. Wiki 页面缺失、没有 revision、MCP/API 凭据不可用或映射准备删除/重命名时停止,由工单确认后处理。 +5. PowerShell 显示乱码时先区分文件编码和控制台输出编码,不默认另起 PowerShell 或使用 `-ExecutionPolicy Bypass`。 diff --git a/docs/07-new-project-documentation-setup.md b/docs/07-new-project-documentation-setup.md index a34ece9..32adf89 100644 --- a/docs/07-new-project-documentation-setup.md +++ b/docs/07-new-project-documentation-setup.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: New-Project-Documentation-Setup wiki_url: https://git.ilapage.cn/ila/yovision/wiki/New-Project-Documentation-Setup.- -wiki_revision: 8ac3aaf5a1cc5479f2a37a07fec6510b6dcc9cab -synchronized_at: 2026-08-11T10:30:50Z +wiki_revision: 66d63c1e43de4dc54bb7c16ec6eb462a07bab15c +synchronized_at: 2026-08-27T09:05:42Z <!-- gitea-wiki-mirror:end --> # 新项目文档初始化 @@ -26,7 +26,81 @@ synchronized_at: 2026-08-11T10:30:50Z 把项目专用红线写入根目录或子目录 `AGENTS.md`。 -### 2. 识别子项目与交付单元 +#### 需求总览启用条件 + +从模板创建项目时保留 Product-Requirements-Overview 这一核心页面。仅有探索性想法时可以只记录已确认目标和待确认项;形成 MVP、长期需求超过少量工单或开始制作原型时,必须建立并持续维护需求索引,把需求领域、状态、主题 Wiki、工单、原型和验收入口关联起来。不要复制完整工单或聊天记录。 + +### 2. 选择建设基线 + +确定技术方案前,优先评估是否存在功能和架构匹配、持续维护、许可证兼容且工程流程完善的开源项目。这里要求的是“先评估”,不是强制采用开源项目,也不能只根据知名度、Star 数量或演示效果决定。 + +至少检查: + +- 核心功能、架构和支持平台是否匹配,哪些能力可以直接保留; +- 许可证是否允许预期的使用、修改、分发和商业模式;不确定时交由负责人或法律专业人员确认; +- 最近维护活跃度、发布频率、Issue 处理和社区或维护团队的持续性; +- 已知安全问题、依赖健康度、供应链风险和安全响应方式; +- 自动化测试、CI、发布、升级、回退和文档是否足以支持长期维护; +- 定制、学习、迁移和后续跟踪上游的总成本是否低于从零开发; +- 是否能够固定上游仓库和基线版本,并建立合并上游更新、兼容验证和退出方案。 + +满足适配、许可证、安全、维护和总成本条件时,优先在该基线上二次开发。不存在合适基线,或引入后会增加不可接受的许可证、安全、架构或维护风险时,可以从零开发,但必须记录排除候选项目和选择从零开发的主要原因。 + +采用开源基线时,在 Project-Profile 的“技术栈与运行环境”记录上游项目名称、仓库地址、基线版本或提交、许可证、保留能力、定制范围和上游升级策略。尚未确认的候选和取舍先写入首个技术方案工单,不得把假设写成项目事实。 + +#### 工程基线裁剪 + +所有项目采用最小工程基线,不按项目规模免除事实和验收要求: + +- 用完整 Git commit 和核验日期固定“当前事实”的代码基线; +- 分开记录“当前已经实现什么”和“目标规范要求什么”,不得用目标描述宣称现有能力; +- 写明证据路径、可确认行为、未覆盖范围和证据不能证明什么; +- 明确目标、非目标、安全边界和可判定的验收标准; +- 跨子项目接口或契约指定唯一事实来源和各端验证命令。 + +当前事实以指定 commit 的代码、可执行测试和运行证据为依据;目标行为以人工批准的契约、ADR 和业务规则为依据。两者冲突时登记为带编号的差距或缺陷,不允许现有错误实现覆盖目标规范,也不允许目标设计冒充当前实现。 + +出现跨团队或跨仓库协作、外部交付、接口或状态机复杂、权限安全、迁移并发、明显文档漂移等情况时,采用增强工程基线:按需增加 GAP-ID 差距表、带状态的 ADR、接口与数据契约、需求追踪测试矩阵,以及 PR、RC、Definition of Done 分层门禁。SRS、SAD、安全、运维和测试文档按风险与读者选择,不强制小型单人项目建立完整文档集。 + +#### 判断案例 + +以下案例用于说明判断方式,不代表必须选择某种技术或具体开源项目。 + +##### 案例一:适合基于成熟项目二次开发 + +计划开发企业内部管理系统。候选项目已经具备用户、权限、审计日志、基础数据管理和自动化测试;功能与目标架构基本匹配,许可证允许预期使用,项目持续维护,发布与升级流程完整,预计只需修改业务模块和界面。 + +- 结论:优先基于该项目二次开发。 +- 原因:可以减少通用功能的开发和验证成本,定制范围可控。 +- 记录:上游仓库、基线版本、许可证、保留功能、定制模块和上游升级方式。 + +##### 案例二:项目成熟但许可证不兼容 + +候选项目功能完整、维护活跃、文档充分,但许可证与当前产品的闭源分发、商业模式或交付条件不兼容。 + +- 结论:不采用该项目作为建设基线。 +- 原因:技术成熟度不能消除许可证风险;不确定结论必须交由负责人或法律专业人员确认。 +- 记录:候选项目、许可证限制、确认人员和排除原因。 + +##### 案例三:功能相似但改造成本过高 + +候选项目表面上覆盖大部分功能,但数据模型、权限体系和部署结构与目标项目差异很大,需要大量删除模块、重写主要接口,并长期维护上游冲突。 + +- 结论:不直接基于完整项目二次开发,可以评估只复用合适的组件或设计思路。 +- 原因:二次开发的总成本、理解成本和长期维护风险已经高于自主实现核心业务。 +- 记录:主要结构差异、改造估算、长期维护风险和最终选择。 + +##### 案例四:只复用成熟框架或组件 + +没有功能高度匹配的完整开源产品,但存在成熟的应用框架、更新组件、日志组件或通信库。 + +- 结论:从零开发业务功能,同时复用经过评估的成熟框架或组件。 +- 原因:复用基础能力不等于必须采用完整产品,可以避免被不匹配的业务架构绑定。 +- 记录:每个依赖的用途、版本、许可证、安全边界、升级方式和可替换方案。 + +每个案例的实际评估都必须记录候选项目、判断依据、最终选择、未采用原因,以及升级或退出方式。 + +### 3. 识别子项目与交付单元 先判断仓库中有几个应用、服务、客户端、库或其他可独立交付的部分。对每个部分确认: @@ -40,19 +114,21 @@ synchronized_at: 2026-08-11T10:30:50Z 把结果写入 Project-Profile 的“子项目与交付单元”。单应用项目只填写一个交付单元;多应用单仓库为规则不同的目录增加子目录 `AGENTS.md`,但不因为技术栈不同自动拆仓,也不强制统一版本和发布周期。 -### 3. 建立 Gitea +### 4. 建立 Gitea -创建远端仓库并完成允许的初始引导提交。开启工单和 Wiki。任何产品功能开发在引导提交后都必须先有单元任务工单。 +创建远端仓库并完成允许的初始引导提交,开启工单和 Wiki;必须先有远端仓库,才能填写该仓库的线上 Wiki。配置项目已有的 Gitea MCP 和安全凭据;优先使用 MCP,MCP 不可用或不支持所需写操作时才回退到 Gitea API,并在初始化工单记录原因。凭据只通过环境或 MCP 安全配置提供。 -### 4. 修改镜像配置 +任何产品功能开发在引导提交后都必须先有单元任务工单,并且必须通过第 8 步的线上 Wiki 初始化门禁。 -把 `wiki-docs.json` 中的地址、owner 和 repository 改成新项目;核心主题映射保留。 +### 5. 修改镜像配置 -确认当前目录确实是新项目副本、且 DevHarness 历史归档不需要保留后,移除属于 DevHarness 的任务归档映射和对应 `docs/task/` 镜像。不要在原 DevHarness 仓库或已有业务项目中执行这项清理。 +把 `wiki-docs.json` 中的地址、owner 和 repository 改成新项目;只保留核心主题映射。可选任务快照不逐页登记,默认任务流程不创建。 + +确认当前目录确实是新项目副本、且 DevHarness 历史归档快照不需要保留后,可以移除对应 `docs/task/` 文件。不要在原 DevHarness 仓库或已有业务项目中执行这项清理。 不要把 PAT 写入配置。 -### 5. Agent 检查项目事实 +### 6. Agent 检查项目事实 Agent 只读检查: @@ -65,7 +141,7 @@ Agent 只读检查: 区分“代码中确认的事实”“负责人确认的业务规则”和“仍待确认的假设”。 -### 6. 确定交付对象和文档 +### 7. 确定交付对象和文档 由项目负责人确认哪些岗位或客户会实际使用、部署、管理、支持、集成或验收产品,并为每类对象确定: @@ -77,25 +153,36 @@ Agent 只读检查: 按照[交付文档指南](Delivery-Documentation-Guide.-)选择文档,使用[岗位文档模板](Audience-Document-Template.-)按需创建。没有明确读者的文档不创建,不预建空白的用户手册、管理员手册或运维手册。 -### 7. 先创建线上 Wiki +### 8. 先创建线上 Wiki + +#### 在线创建与回读门禁 + +1. 使用配置好的 Gitea MCP 查询目标仓库的 Wiki 页面列表;MCP 不可用时使用 Gitea API,并记录回退原因。 +2. 如果 `Home` 不存在,先创建 `Home`。创建后立即在线回读正文并记录 revision;`Home` 可读取后才能继续。 +3. 依照 `wiki-docs.json` 逐页创建或更新其他核心页面。每页写入后在线回读正文,记录页面名和 revision。 +4. 本地 `docs/` 是模板或 Wiki 镜像;本地文件存在、标题完整或 `harness.py check --strict` 通过,都不能单独证明线上 Wiki 已初始化。 +5. 页面缺失、回读失败或没有 revision 时停止初始化,不得开始产品代码;Gitea 恢复后从首个失败页面继续。 至少创建或填写: 1. Home; 2. Project-Profile; -3. Architecture-and-Code-Map; -4. Business-Rules-and-Glossary; -5. Local-Development-and-Verification; -6. Common-Changes; -7. Troubleshooting; -8. Development-Workflow; -9. Delivery-Documentation-Guide; -10. Audience-Document-Template; -11. Task-Archive-Template。 +3. Product-Requirements-Overview; +4. Architecture-and-Code-Map; +5. Business-Rules-and-Glossary; +6. Local-Development-and-Verification; +7. Common-Changes; +8. Troubleshooting; +9. Development-Workflow; +10. Delivery-Documentation-Guide; +11. Audience-Document-Template; +12. Task-Archive-Template。 -Home 给出建议阅读顺序;每个命令必须有预期结果;代码地图必须指出入口和测试位置。具体岗位文档仅按第 5 步确认的受众创建。 +Home 给出建议阅读顺序;每个命令必须有预期结果;代码地图必须指出入口和测试位置。具体岗位文档仅按第 7 步确认的受众创建。 -### 8. 人工确认 +部署页按需创建,不属于必需核心页面:项目负责人确认存在需要部署的常驻服务时,复制[部署文档模板](Deployment-Template.-)在本项目 Wiki 建立 `Deployment-and-Operations` 页面,并在本项目 `wiki-docs.json` 增加映射(建议镜像到 `docs/10-deployment-and-operations.md`);确认没有常驻服务时,在初始化工单记录原因,不创建该页面。 + +### 9. 人工确认 项目负责人至少确认: @@ -103,29 +190,33 @@ Home 给出建议阅读顺序;每个命令必须有预期结果;代码地图 - 关键业务规则和状态; - 权限、安全和数据边界; - 真实运行、测试和部署命令; +- 本项目是否有需要部署的常驻服务; - 哪些修改属于高风险; - 交付对象、文档可见范围和外部信息边界。 -### 9. 导出镜像并检查 +### 10. 导出镜像并检查 ```powershell -python dev_scripts/sync_wiki_docs.py -python dev_scripts/check_harness.py --strict -python dev_scripts/sync_wiki_docs.py --check +python dev_scripts/harness.py sync --verify python -m unittest discover -s tests -v ``` -只有线上 Wiki 确认后才导出 `docs/`。旧项目的任务归档不能带入新项目历史。 +只有线上 Wiki 确认后才导出核心 `docs/`。默认不创建或导出任务归档;用户明确要求专项快照时才运行 `archive`、`export` 或 `export --all`。旧项目的任务归档快照不能带入新项目历史。 + +`harness.py sync --check` 会在线读取全部显式映射页面;任一页面不存在、无法读取或 revision 与镜像不一致时,初始化不通过。只有上述命令全部成功后才允许开始产品代码。 ## 完成标准 初级程序员应能仅依靠 Home 和链接页面回答: - 项目解决什么问题; +- 当前有哪些长期需求、状态如何,详细规则、工单、原型和验收入口在哪里; - 怎样启动和运行测试; - 常用功能从哪个目录和入口开始读; - 一个简单修改通常要改哪里、验证什么; - 哪些情况必须停止并交给 Agent 或负责人; +- 项目采用哪个建设基线,为什么适合二次开发,或者为什么选择从零开发; +- 采用开源基线时,上游仓库、基线版本、许可证、定制范围和升级策略是什么; - 项目包含哪些子项目和独立交付单元,各自怎样构建、测试和发布; - 跨子项目共享什么接口或契约,其唯一事实来源在哪里; - 项目需要向哪些岗位交付什么文档,以及哪些内容不能对外提供。 diff --git a/docs/08-existing-project-adoption.md b/docs/08-existing-project-adoption.md index 8bca003..9417f7d 100644 --- a/docs/08-existing-project-adoption.md +++ b/docs/08-existing-project-adoption.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Existing-Project-Adoption-Guide wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Existing-Project-Adoption-Guide.- -wiki_revision: dfb92b4551c317a08f49e68d0262258157da6d04 -synchronized_at: 2026-08-11T10:30:53Z +wiki_revision: 54e7c774b679f582d210c08179de31d6e12ed9b4 +synchronized_at: 2026-08-27T09:05:54Z <!-- gitea-wiki-mirror:end --> # 已有项目接入 DevHarness 指南 @@ -23,7 +23,7 @@ synchronized_at: 2026-08-11T10:30:53Z | 文档 | 创建核心主题页 | 逐页判断保留、迁移、合并或停止维护 | | 工单和 Wiki | 新建并开始使用 | 先检查已有工单、Wiki 和状态体系 | | Git 历史 | 允许一次引导提交 | 保留全部历史,不使用引导提交例外 | -| 任务归档 | 从新项目任务开始 | 不复制 DevHarness 或其他项目的历史归档 | +| 任务证据 | Gitea 工单;任务快照仅显式按需创建 | 保留已有工单;不复制 DevHarness 或其他项目的历史归档 | | 接入方式 | 一次建立最小骨架 | 分阶段增量接入并逐步验收 | 从模板创建全新仓库时使用[新项目文档初始化](New-Project-Documentation-Setup.-);项目已有业务提交、用户或维护历史时使用本页。 @@ -71,7 +71,7 @@ Agent 在提出方案前只读检查: - 多个应用共同完成一条产品或业务链路; - 由同一团队维护,仓库权限基本一致; - 接口变更需要在一个工单中同步修改或验证多端; -- 共享契约、业务规则和任务归档放在一起更容易保持一致; +- 共享契约和业务规则由同一项目维护并指定唯一事实来源; - 仓库体积、测试时间和工具性能尚未明显影响开发; - 初级维护者和 Agent 能通过目录、子目录 `AGENTS.md` 和文档入口清楚定位。 @@ -131,7 +131,42 @@ Agent 在提出方案前只读检查: ### 7. 提交和验收 -提交只包含当前接入工单相关文件。记录测试、未验证部分、Wiki revision 和提交哈希,创建任务归档并保持工单“待验收”,等待用户明确验收后再关闭。 +提交只包含当前接入工单相关文件。在工单评论集中记录测试、未验证部分、提交哈希,以及真实变化的 Wiki revision 或“无长期文档影响”;保持工单“待验收”,等待用户明确验收后再关闭。默认不创建任务归档。 + +## 后续升级 + +已接入的项目必须以 Project-Profile 中记录的 DevHarness 来源和当前基线为起点升级,不得重新复制整个模板,也不得用“最新版本”代替可复现的目标提交。 + +### 升级步骤 + +1. 读取目标项目的 Project-Profile,确认 DevHarness 来源仓库、当前基线完整提交、最后升级日期和项目适配说明;字段缺失时先补齐可验证事实,无法确认则停止。 +2. 选择一个明确、已审阅的 DevHarness 目标提交,记录旧基线和新基线。先比较两个上游提交之间的变化,再判断这些变化如何作用于目标项目。 +3. 只读比较与 Harness 有关的 `AGENTS.md`、`CLAUDE.md`、工单模板、`dev_scripts/`、Harness 测试和核心 Wiki 结构,把差异分为“直接采用、按项目改写、冲突待确认、不采用”。不得把 DevHarness 的项目事实、工单或任务归档带入目标项目。 +4. 在目标项目建立单元任务工单,写明升级范围、差异分类、项目专用规则、风险、回退、验证和文档影响。会改变产品行为的内容必须拆成独立任务。 +5. 按工单最小合并,保留目标项目更具体的业务、安全、权限和目录规则,以及 Git 历史和无关工作区修改。无法判断哪一方规则有效时停止并等待负责人确认。 +6. 长期文档先更新目标项目 Wiki,读取确认后再同步目标项目的核心 `docs/` 镜像;不得用 DevHarness 的本地镜像覆盖目标项目文档。 +7. 执行目标项目规定的必要检查和受影响测试,提交并回写证据。工单保持“待验收”。 +8. 用户验收通过后,确认目标项目 Project-Profile 已记录新 DevHarness 基线完整提交和升级日期,再关闭工单。升级失败或回退时保留旧基线。 + +### 升级停止条件 + +除本页已有的冲突停止条件外,来源仓库与记录不一致、旧基线不存在、目标提交未明确、差异跨越过大而无法可靠分类,或升级需要覆盖项目专用安全规则时,都必须停止并请求确认。可以把升级拆成多个单元任务,但每个任务都要声明最终采用的同一目标基线。 + +### 可复制升级指令 + +```text +请把当前项目从 Project-Profile 记录的 DevHarness 基线升级到 +<DevHarness 目标完整提交哈希>。 + +先只读比较来源仓库中“旧基线..目标基线”的 Harness 变化和当前项目 +适配,列出直接采用、按项目改写、冲突待确认和不采用的内容,以及 +风险、回退、验证和文档影响。不要覆盖项目专用规则、业务文档、Git +历史或无关改动,不复制 DevHarness 工单和任务归档。方案确认后在 +当前项目建单并实施;长期文档先改当前项目 Wiki,再同步本地镜像。 +工单保持待验收,验收通过后确认 Project-Profile 已记录新基线。 +``` + +路径和目标完整提交哈希必须替换为真实值;目标提交未明确时只分析,不实施。 ## 冲突处理和停止条件 @@ -175,8 +210,8 @@ Agent 在提出方案前只读检查: 严格按工单范围增量接入 DevHarness,保留当前项目已有规则、历史、 任务状态和无关改动。长期文档先更新 Gitea Wiki,读取确认后再导出 -本地 docs 镜像。执行必要测试,提交实现和任务归档,然后把工单保持 -为“待验收”;未经我明确验收,不关闭工单。 +本地 docs 镜像。执行必要测试,提交实现并把最终证据回写工单,然后 +保持“待验收”;默认不创建任务归档,未经我明确验收不关闭工单。 ``` 路径、仓库地址和项目名称必须替换为当前环境的真实值。第二段指令只有在第一段方案已经明确确认后使用。 @@ -192,7 +227,7 @@ Agent 在提出方案前只读检查: - [ ] 已为每类长期文档明确事实来源和迁移状态。 - [ ] Wiki-first 页面已经读取确认并具有显式镜像映射。 - [ ] Harness 检查已按目标项目调整并通过。 -- [ ] 必要测试、未验证部分、提交和归档证据已记录。 +- [ ] 必要测试、未验证部分、提交和最终证据已记录。 - [ ] 工单处于待验收,未提前关闭。 ## 回退原则 diff --git a/docs/09-product-requirements.md b/docs/09-product-requirements.md index 28a678b..ea7df60 100644 --- a/docs/09-product-requirements.md +++ b/docs/09-product-requirements.md @@ -2,12 +2,63 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Product-Requirements wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Product-Requirements.- -wiki_revision: e6c6e0d7f658a7c1040fe569026e7d0ec581701a -synchronized_at: 2026-08-17T02:54:53Z +wiki_revision: e912a1ca1410e01680a0f11f6199ccb42cd8fe8f +synchronized_at: 2026-08-27T09:06:08Z <!-- gitea-wiki-mirror:end --> # 产品需求 +## 本页用途 + +本页同时承担 YoVision 产品需求正文与需求总览索引,保留已经确认的 Sense、Brain、Bell 和跨项目要求,不因 DevHarness 升级重命名或拆分事实源。 + +## 事实来源边界 + +- 长期产品目标、边界和稳定需求记录在本页。 +- 单次任务的范围、变化、实现和验收记录在对应 Gitea 工单。 +- 原型用于确认页面、流程与交互,不替代正式需求和验收标准。 + +## 当前需求索引 + +- 产品边界:PR-BND-001~PR-BND-003。 +- Sense:SEN-001 起,按 P0、P1/P2 管理。 +- Brain:BRN-001 起,按 P0、P1/P2 管理。 +- Bell:BEL-001 起,按 P0、P1/P2 管理。 +- 跨项目与非功能要求:见本页第 6 节及对应协调工单。 + +## 登记规则 + +新增或变化的长期需求必须有来源、状态、所属产品、优先级和验收边界;会改变已确认结果时先更新工单并重新取得用户确认。 + +## 原型与设计资产 + +### 原型门禁 + +新页面、独立用户功能、重大交互或导航变化必须先形成可审阅原型;小范围 UI 使用标注截图、低保真图或明确复用规范;非 UI 任务使用架构、API、数据、状态或流程设计。 + +### 线上原型与按需 HTML 快照 + +默认使用可访问且版本明确的线上原型审核。只有用户明确要求或项目规则要求时,才导出 `prototypes/<工单号>/<版本>/index.html`;已确认快照不得原位覆盖。 + +### 原型确认记录 + +实现工单记录设计链接或路径、版本/revision、访问检查、确认人、确认时间和覆盖范围。结构、流程、状态、权限或异常处理发生实质变化时必须重新确认。 + +## 状态规则 + +需求使用拟议、已确认、实施中、已交付、已废弃等状态;工单状态仍按待确认、待实施、进行中、阻塞、待验收、已完成管理,两者不得混用。 + +## 更新时机 + +产品边界、长期业务规则、需求优先级或验收边界变化时更新本页;单次实现细节、测试日志和提交哈希只写工单。 + +## 最小验收清单 + +- 需求有稳定编号、所属产品、优先级、来源和验收边界。 +- 跨项目需求只有一个契约或协调事实源。 +- 需要原型的变更已有可访问、可识别版本并完成确认。 +- 不包含密码、令牌、生产数据或完整聊天记录。 + ## 1. 产品范围与优先级 YoVision 首个可交付目标是在民办寄宿学校以默认 16 路高风险点位形成完整闭环: diff --git a/docs/README.md b/docs/README.md index 96865af..f74f456 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,55 +2,100 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Home wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Home -wiki_revision: 1a2dadc746ce76e182150ca93fc4a1ec192c6b6a -synchronized_at: 2026-08-11T10:30:24Z +wiki_revision: 8ba2aba01f287ec5ab9cafda3d719bfd9e7fc459 +synchronized_at: 2026-08-27T09:04:13Z <!-- gitea-wiki-mirror:end --> # YoVision 文档中心 -YoVision 是单仓三项目的智能视频事件平台。Sense 负责设备与媒体,Brain 负责推理与事件生成,Bell 负责事件预警和处置。Sense 与 Bell 是可独立销售、部署和验收的产品;Brain 是独立构建的推理交付单元。 +DevHarness 是一个以 Gitea 工单管理开发任务、以 Wiki 管理长期开发文档、以 Git 记录代码变更的 AI 辅助开发模板。目标是让初级程序员能够理解项目、运行验证,并在 Claude/Codex Agent 协助下处理简单 Bug 和需求。 ## 第一次阅读 -1. [项目档案](Project-Profile.-):目标、子项目、技术栈和边界。 -2. [产品需求](Product-Requirements.-):功能、质量、安全和非目标。 -3. [架构与代码地图](Architecture-and-Code-Map.-):职责、数据流和代码入口。 -4. [业务规则与术语](Business-Rules-and-Glossary.-):不可破坏的领域规则。 -5. [需求迁移矩阵](Requirements-Migration-Matrix.-):旧项目需求怎样进入新仓库。 -6. [多 Agent 协作](Multi-Agent-Collaboration.-):三名主 agent 和协调任务的写路径规则。 -7. [本地开发与验证](Local-Development-and-Verification.-):当前可执行命令和待补门禁。 -8. [开发工作流](Development-Workflow.-):建单、确认、实施、验收和归档。 +建议按以下顺序,用 10~20 分钟建立整体认识: + +1. [项目档案](Project-Profile.-):项目目标、环境、命令和目录边界。 +2. [产品需求总览](Product-Requirements-Overview.-):长期需求、状态、原型和验收入口。 +3. [架构与代码地图](Architecture-and-Code-Map.-):功能从哪里开始读、测试在哪里。 +4. [业务规则与术语](Business-Rules-and-Glossary.-):重要名词、状态和不能破坏的规则。 +5. [本地开发与验证](Local-Development-and-Verification.-):怎样运行、测试和排错。 +6. [常见修改指南](Common-Changes.-):简单修改的步骤和停止条件。 +7. [故障排查](Troubleshooting):遇到错误时按什么顺序检查。 +8. [开发工作流](Development-Workflow.-):完整建单、实施、验收和可选快照流程。 + +从模板创建新项目时先阅读[新项目文档初始化](New-Project-Documentation-Setup.-);向已有项目增量接入本流程时阅读[已有项目接入 DevHarness 指南](Existing-Project-Adoption-Guide.-)。需要为客户或其他岗位准备说明时,阅读[交付文档指南](Delivery-Documentation-Guide.-),再按需使用[岗位文档模板](Audience-Document-Template.-)。 ## 五分钟开始 +在仓库根目录执行: + ```powershell git status --short --branch -python dev_scripts/check_harness.py --strict +python dev_scripts/harness.py check --strict python -m unittest discover -s tests -v -python dev_scripts/sync_wiki_docs.py --check +python dev_scripts/harness.py sync --check ``` -预期:工作区变更归属清楚,Harness 与测试通过,Wiki 镜像一致。业务代码尚未初始化时,不应臆造 Sense、Brain 或 Bell 的构建命令。 +预期结果: + +- 工作区没有不属于当前任务的修改; +- Harness 输出“DevHarness 检查通过”; +- 所有单元测试通过; +- 所有 Wiki 映射显示“一致”。 + +如果失败,先看[故障排查](Troubleshooting),不要直接重置工作区或覆盖本地文档。 ## 简单修改从哪里开始 -| 修改类型 | 先读 | 主要验证 | +| 想做什么 | 先读哪里 | 主要验证 | |---|---|---| -| Sense 单项目 | Project-Profile、`Sense/AGENTS.md` | Sense 自身测试 | -| Brain 单项目 | Product-Requirements、`Brain/AGENTS.md` | Brain 自身测试与事件契约测试 | -| Bell 单项目 | Business-Rules、`Bell/AGENTS.md` | Bell 自身测试 | -| 共享契约 | Multi-Agent-Collaboration、`contracts/AGENTS.md` | 三端消费者/生产者契约测试 | -| 长期文档 | 对应 Wiki 页面 | Wiki 同步检查 | +| 修改文档 | 对应 Wiki 页面、Common-Changes | Wiki 同步检查 | +| 查看或更新产品需求 | Product-Requirements-Overview、对应主题 Wiki 和工单 | 状态、链接和事实来源核对 | +| 接入已有项目 | Existing-Project-Adoption-Guide | 只读盘点、差异确认和分阶段验证 | +| 准备交付文档 | Delivery-Documentation-Guide、Audience-Document-Template | 目标岗位验证和 Wiki 同步检查 | +| 调整工单字段 | `.gitea/issue_template/`、Development-Workflow | Harness 严格检查 | +| 修改同步行为 | `dev_scripts/wiki_docs.py`、Architecture-and-Code-Map | 单元测试和真实 Wiki 检查 | +| 增加结构检查 | `dev_scripts/harness.py` | 成功与失败测试 | +| 排查运行错误 | Troubleshooting、项目档案 | 最小复现命令 | + +权限、安全、并发、迁移、支付、删除数据或不可逆操作不属于简单修改,必须停止并交给 Agent 分析、等待人工确认。 ## 事实来源 | 信息 | 事实来源 | |---|---| -| 实时任务状态、方案确认和验收过程 | Gitea 工单 | -| 长期需求、架构、规则、操作与归档 | Gitea Wiki | -| API/Schema/迁移及代码版本事实 | Git 仓库 | -| 离线文档 | `docs/` Wiki 只读镜像 | -| 历史需求和旧证据 | `D:\OPC\yovision_old`,只读参考,不是当前状态 | +| 任务状态、讨论、阻塞、验收过程 | Gitea 工单 | +| 长期产品需求的统一导航和状态 | Gitea Wiki 的 Product-Requirements-Overview | +| 架构、业务规则、开发规范、操作手册和交付文档 | Gitea Wiki | +| 源码和与特定代码版本强绑定的文档 | Git 仓库 | +| 核心长期文档的离线浏览副本 | Git 仓库中的 `docs/` Wiki 镜像 | +| 单次任务需求、实现、测试和验收 | Gitea 工单;专项 Wiki 快照仅在人工明确要求时创建 | + +本地 `docs/` 不是编辑入口。长期文档必须先修改 Wiki,读取确认后再导出镜像。 + +## 项目入口 + +- [Gitea 工单](https://git.ilapage.cn/OPC/dev_harness/issues) +- [产品需求总览](Product-Requirements-Overview.-) +- [代码仓库](https://git.ilapage.cn/OPC/dev_harness) +- [已有项目接入 DevHarness 指南](Existing-Project-Adoption-Guide.-) +- [交付文档指南](Delivery-Documentation-Guide.-) +- [岗位文档模板](Audience-Document-Template.-) +- [可选任务归档模板(兼容)](Task-Archive-Template.-) + +## 同步原则 + +```text +修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 +``` + +- 核心页面和本地路径通过仓库中的 `wiki-docs.json` 显式映射;普通同步不处理任务归档。 +- 默认不创建任务归档;只有用户明确要求专项快照或项目专用规则要求时,才创建 Wiki 归档并按需导出到 `docs/task/`。 +- 镜像头记录来源页面、Wiki revision 和同步时间。 +- 已映射镜像存在未提交修改时同步必须停止。 +- 页面删除、重命名和映射变更必须人工确认。 +- 长期事实发生变化而核心 Wiki 或必要同步失败时,相关任务不能标记为完成;没有长期文档变化时不运行 Wiki 同步,未请求可选归档不阻止任务完成。 +- 凭据、个人数据和生产数据不得进入 Wiki 或镜像。 ## 项目入口 diff --git a/docs/delivery/README.md b/docs/delivery/README.md index e8ec8cf..402cc1d 100644 --- a/docs/delivery/README.md +++ b/docs/delivery/README.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Delivery-Documentation-Guide wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Delivery-Documentation-Guide.- -wiki_revision: 3d95c9d392e81cf59d2af1f6f21d8e67f580b68f -synchronized_at: 2026-08-15T03:02:47Z +wiki_revision: 5b295898612f86ac2301ea243df76fb284912317 +synchronized_at: 2026-08-27T09:06:55Z <!-- gitea-wiki-mirror:end --> # 交付文档指南 @@ -79,7 +79,6 @@ synchronized_at: 2026-08-15T03:02:47Z 开发任务怎样建单、实施和归档见[开发工作流](Development-Workflow.-);代码结构和维护入口见[架构与代码地图](Architecture-and-Code-Map.-)。本页不规定市场宣传、合同、法务或商务承诺。 -<!-- sense-mvp:start --> ## Sense MVP 岗位操作路径 Sense 面向网管、实施人员和非技术现场人员,菜单按日常任务组织:工作台 → 设备管理 → 视频接入 → 视频服务 → 实时监看 → 区域与警戒线。普通操作优先展示中文状态与下一步,不要求用户理解 ONVIF、RTSP 或 MediaMTX 内部模型。 @@ -134,3 +133,7 @@ Sense 面向网管、实施人员和非技术现场人员,菜单按日常任 <!-- sense-media:end --> <!-- sense-admission:end --> + +## 部署与运维文档 + +YoVision 已有 Sense Windows 交付包和本机 Supervisor 常驻实例,因此维护 `Deployment-and-Operations` 页面。部署命令、服务身份、配置来源、端口、日志、启动停止、回退或升级方式变化时必须先更新该 Wiki 页面,再同步本地镜像。 diff --git a/docs/delivery/deployment-and-operations.md b/docs/delivery/deployment-and-operations.md new file mode 100644 index 0000000..ccfcaa5 --- /dev/null +++ b/docs/delivery/deployment-and-operations.md @@ -0,0 +1,82 @@ +<!-- gitea-wiki-mirror:start --> +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Deployment-and-Operations +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Deployment-and-Operations.- +wiki_revision: 367c6aed6eaeece793622431c7d1b11b1b9dbec5 +synchronized_at: 2026-08-27T09:07:20Z +<!-- gitea-wiki-mirror:end --> + +# YoVision 部署与运维 + +## 文档信息 + +- 系统:YoVision +- 当前常驻实例:Sense +- 适用环境:Windows 本机开发/验收与 Sense Windows 交付包 +- 维护入口:工单 #108 后由长期 Wiki 持续维护 +- 安全边界:本文不记录数据库密码、管理员密码、摄像头凭据、JWT/会话密钥或 Gitea token + +## 部署范围与边界 + +当前可核对的常驻服务是 Sense。Brain 尚未初始化,Bell 新 GoAdmin 基线尚未完成,因此不在本页提供虚构的生产部署命令。三个交付单元保持独立配置、数据、身份、版本和发布边界;跨项目编排必须另建协调工单。 + +## 目录与入口 + +- Sense 项目目录启动入口:`Sense\start_sense.bat` +- Windows 交付包:`Sense\dist\sense-windows-amd64` +- 包内启动入口:`Sense\dist\sense-windows-amd64\start-sense.bat` +- 包内配置:`Sense\dist\sense-windows-amd64\config\sense.env` +- 项目配置源:`Sense\config\sense.env` +- 构建入口:`Sense\scripts\build\build-windows.bat` +- 本机 Supervisor 根目录:`D:\supervisor` + +`Sense\start_sense.bat` 只定位并调用交付包入口、透传参数和退出码;它不读取配置、不自动构建,也不直接启动 Go 或 Node 开发服务。MediaMTX 是否随包启动由 Sense 包内运行脚本和配置控制。 + +## 配置与秘密 + +- 生产模式必须提供有效 PostgreSQL 连接和应用安全配置;变量名及无敏感示例以项目或交付包中的 `.env.example` 为准。 +- 配置文件和进程环境中的秘密不得提交到 Git、工单、Wiki、日志或示例。 +- Sense、Bell 必须使用不同数据库角色、用户库、JWT/会话密钥和 Cookie;Brain 使用独立机器身份。 +- 修改服务账号、权限、端口、数据库、媒体二进制或持久化目录前必须建立相应工单并说明回退。 + +## 构建与启动 + +从 Sense 目录生成 Windows 包: + +```powershell +Sense\scripts\build\build-windows.bat +``` + +生成包并正确配置后,可从仓库根运行: + +```powershell +Sense\start_sense.bat +``` + +临时演示模式可透传 `demo` 参数;演示数据随进程停止而丢失,不得作为生产部署。 + +## Supervisor 常驻实例 + +本机 Supervisor 的 YoVision Sense 实例由工单 #106 建立。Supervisor 配置、启动停止命令、工作目录、环境文件和日志位置以 `D:\supervisor` 中的当前实例配置为准。修改该外部目录前必须建立工单并确认精确目标;仓库不得复制其中的秘密。 + +## 健康检查与日志 + +- 默认 Sense 访问地址以当前配置为准;已验收的本机默认地址为 `http://127.0.0.1:18080/`。 +- 先确认端口监听和 HTTP 页面,再检查 Sense 结构化日志、PostgreSQL 连接、数据库迁移以及 MediaMTX 进程和路径状态。 +- 日志不得输出数据库密码、摄像头凭据、会话 Cookie、JWT secret 或 token。 +- 具体视频、迁移和登录故障按 `Troubleshooting` 页面处理。 + +## 停止、升级与回退 + +- 手工前台启动时在原控制台正常终止进程;Supervisor 托管时使用该实例的受控停止方式,避免同时启动第二个占用相同端口的进程。 +- 升级前记录当前 Git 提交、交付包版本、数据库备份/恢复方案和配置差异。 +- 数据库迁移、凭据、权限或不可逆操作属于高风险,必须另建工单并等待确认。 +- 回退优先恢复上一已验收交付包和对应配置;涉及数据库迁移时只能使用该迁移工单确认的回退方案。 + +## 最小验收 + +- 启动入口、工作目录、配置来源和端口与实际一致。 +- Sense 页面与必要 API 可访问,数据库迁移成功。 +- MediaMTX 启用时进程、路径和播放链路状态可定位。 +- Supervisor 不会与手工进程重复占用端口。 +- 日志和文档没有秘密;未验证的 Brain、Bell、真机或生产行为明确标注。 diff --git a/docs/task/66-Sense视频接入与Profile.md b/docs/task/66-Sense视频接入与Profile.md index db9b259..0152ac3 100644 --- a/docs/task/66-Sense视频接入与Profile.md +++ b/docs/task/66-Sense视频接入与Profile.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Task-66-Sense视频接入与Profile wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-66-Sense%E8%A7%86%E9%A2%91%E6%8E%A5%E5%85%A5%E4%B8%8EProfile.- -wiki_revision: 322feb232fc03c3a9ba22f65504cf3e151fb0e57 -synchronized_at: 2026-08-14T09:15:26Z +wiki_revision: 87b3a59f3f27df1f1fb357f6c13a1e31cdde5e05 +synchronized_at: 2026-08-27T09:13:37Z <!-- gitea-wiki-mirror:end --> # 66 Sense视频接入与Profile @@ -32,6 +32,13 @@ synchronized_at: 2026-08-14T09:15:26Z - 复用 GoAdmin JWT/Casbin/操作审计、迁移和 go-admin-ui BasicLayout、Element Plus Form/Dialog/Table/Tag、动态菜单与权限按钮。 - implementation_operator、site_admin 可发现和探测,viewer 只读保存结果。 +## 修改文件 + +- 后端入口与业务:`Sense/server/app/admin/router/sense_admission.go`、`Sense/server/app/sense/admission/**`、`Sense/server/app/sense/onvif/**`、`Sense/server/app/sense/rtsp/**`。 +- 数据与验证:`Sense/server/cmd/migrate/migration/version/2026081417000_profile.go`、`Sense/server/tests/admission/postgres_test.go`。 +- 前端:`Sense/ui/src/api/sense/admission.js`、`Sense/ui/src/views/sense/admission/**`、`Sense/ui/src/views/sense/device/index.vue` 及对应单元测试。 +- 完整文件清单以实现提交 `2bb1614` 和 PR #84 的 Git diff 为准。 + ## 验收结果 | 标准 | 结果 | @@ -54,7 +61,7 @@ synchronized_at: 2026-08-14T09:15:26Z - PostgreSQL 17:迁移与重复迁移通过;migration=1、tables=2、menus=3、policies=7。 - SENSE_ADMISSION_TEST_DATABASE_URL 隔离测试:写入 Profile、重开连接、读取主子码流通过。 - Wiki 镜像检查:通过。 -- 未验证部分:未连接客户或实验室真实摄像机;真实厂商 Digest/RTSP 兼容、网络 ACL、设备校时和目标浏览器留待授权现场验收,不声称已通过。 +**未验证部分**:未连接客户或实验室真实摄像机;真实厂商 Digest/RTSP 兼容、网络 ACL、设备校时和目标浏览器留待授权现场验收,不声称已通过。 ## 回退 diff --git a/docs/task/67-Sense视频服务生命周期与状态对账.md b/docs/task/67-Sense视频服务生命周期与状态对账.md index 5f59a14..516454a 100644 --- a/docs/task/67-Sense视频服务生命周期与状态对账.md +++ b/docs/task/67-Sense视频服务生命周期与状态对账.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Task-67-Sense视频服务生命周期与状态对账 wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-67-Sense%E8%A7%86%E9%A2%91%E6%9C%8D%E5%8A%A1%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E4%B8%8E%E7%8A%B6%E6%80%81%E5%AF%B9%E8%B4%A6.- -wiki_revision: 4642924705d7a3406874ef532579fb2d6a25a88b -synchronized_at: 2026-08-14T10:13:23Z +wiki_revision: 718765b6b8eff5f4c81612ea8720272c97cd1a25 +synchronized_at: 2026-08-27T09:13:38Z <!-- gitea-wiki-mirror:end --> # 67 Sense视频服务生命周期与状态对账 @@ -30,6 +30,13 @@ synchronized_at: 2026-08-14T10:13:23Z - 接入成功后幂等建立路由;冷启动恢复 desired=running,明确 stopped 路径保持停止;稳定路径只刷新状态,不重复下发或增加版本。 - 复用 GoAdmin JWT/Casbin/操作审计、迁移、动态菜单和 go-admin-ui BasicLayout、Element Plus Descriptions/Table/Tag/Button/MessageBox。 +## 修改文件 + +- 后端入口与业务:`Sense/server/app/admin/router/sense_media.go`、`Sense/server/app/sense/media/**`、`Sense/server/app/sense/reconcile/**`。 +- 数据、配置与验证:`Sense/server/cmd/migrate/migration/version/2026081419000_media*`、`Sense/server/config/mediamtx/mediamtx.yml.example`、`Sense/server/tests/media/**`。 +- 前端:`Sense/ui/src/api/sense/media.js`、`Sense/ui/src/views/sense/media/**` 及对应单元测试。 +- 完整文件清单以实现提交 `19f9bfa` 和 PR #85 的 Git diff 为准。 + ## 验收结果 | 标准 | 结果 | @@ -53,7 +60,7 @@ synchronized_at: 2026-08-14T10:13:23Z - PostgreSQL 17 定向迁移:3 个菜单、12 条角色策略、1 条迁移记录通过。 - Wiki 镜像检查:通过。 - 已处理测试安全问题:MediaMTX 自动 TLS 文件的工作目录已固定到外部配置目录;临时证书/私钥未进入最终提交或远端。 -- 未验证部分:未连接客户真实摄像机和现场网络;真实上游持续拉流、reader 变化、端口 ACL 和目标浏览器留待授权现场验收。 +**未验证部分**:未连接客户真实摄像机和现场网络;真实上游持续拉流、reader 变化、端口 ACL 和目标浏览器留待授权现场验收。 - 已知相邻问题:空白 PostgreSQL 执行完整上游迁移链时,在到达 #67 前被旧 `sys_config` 初始化字段长度问题中止;#67 定向迁移已通过,空库安装链应由 #70/#71 单独复核,不在本工单混改上游初始化。 ## 回退 diff --git a/docs/templates/deployment.md b/docs/templates/deployment.md new file mode 100644 index 0000000..4c676ed --- /dev/null +++ b/docs/templates/deployment.md @@ -0,0 +1,282 @@ +<!-- gitea-wiki-mirror:start --> +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Deployment-Template +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Deployment-Template.- +wiki_revision: 75bd653ab2f3314a37eb9086e18152129941befa +synchronized_at: 2026-08-27T09:07:38Z +<!-- gitea-wiki-mirror:end --> + +# 部署文档模板 + +> 使用说明:本页是 DevHarness 模板,不描述任何真实服务。有常驻服务的项目复制本页,在自己的 Gitea Wiki 创建 `Deployment-and-Operations` 页面,并在本项目 `wiki-docs.json` 增加映射(建议镜像到 `docs/10-deployment-and-operations.md`)。填写时删除全部说明性文字,不得保留“待填写”后直接交付。没有常驻服务的项目不要创建部署页,在初始化工单记录原因即可。 +> +> 本模板面向项目内部维护者。面向客户或外部运维岗位的部署说明属于交付文档,使用[岗位文档模板](Audience-Document-Template.-)。 +> +> 标准栈为 nginx 反向代理 + supervisor 进程托管,示例按 Python 服务(gunicorn / uvicorn)编写。实际技术栈不同时替换命令,但保留章节结构和“每条命令写明预期结果”的要求。 + +## 本页用途 + +让维护者能够在一台干净的服务器上完成首次部署、日常运维、健康检查和回滚。所有命令默认以具备 sudo 权限的账号在服务器上执行,示例以 Linux 为主。 + +## 安全边界 + +- 本页只写配置项的**名称和来源**,不写任何真实密码、令牌、私钥、证书内容、生产数据库地址或个人数据。 +- 需要凭据的步骤写明“从哪里取”,例如运维密码库条目名或环境变量名。 +- 涉及删除数据、数据库迁移和不可逆操作的步骤必须给出醒目警告、影响范围和回退条件。 + +## 服务概览 + +| 项目 | 内容 | +|---|---| +| 服务名(supervisor program) | `<myapp>` | +| 代码部署目录 | `/srv/<myapp>` | +| 运行账号 | `<myapp>` | +| 运行时 | Python `<3.x>` | +| 应用服务器 | gunicorn / uvicorn | +| 本地监听地址 | `127.0.0.1:<8000>` | +| 进程数 | `<n>` | +| 对外域名与路径 | `https://<example.com>/` | +| 依赖的外部服务 | 数据库 / 缓存 / 对象存储 / 无 | +| 日志目录 | `/var/log/<myapp>/` | + +服务只监听 `127.0.0.1`,不直接对外暴露端口;所有外部访问经 nginx 转发。 + +## 环境要求 + +| 组件 | 版本要求 | 检查命令 | 预期结果 | +|---|---|---|---| +| 操作系统 | `<Ubuntu 22.04>` | `cat /etc/os-release` | 输出与要求一致 | +| Python | `<3.11+>` | `python3 --version` | 输出版本号且不低于要求 | +| nginx | `<1.18+>` | `nginx -v` | 输出版本号 | +| supervisor | `<4.2+>` | `supervisord --version` | 输出版本号 | + +未安装时: + +```bash +sudo apt update +sudo apt install -y nginx supervisor python3-venv +``` + +**预期结果**:`systemctl status nginx` 与 `systemctl status supervisor` 均为 `active (running)`。 + +## 首次部署 + +### 1. 创建运行账号与目录 + +```bash +sudo useradd --system --home /srv/<myapp> --shell /usr/sbin/nologin <myapp> +sudo mkdir -p /srv/<myapp> /var/log/<myapp> +sudo chown -R <myapp>:<myapp> /srv/<myapp> /var/log/<myapp> +``` + +**预期结果**:`id <myapp>` 输出该账号;两个目录存在且属主为 `<myapp>`。 + +服务账号使用 `nologin`,不允许直接登录。 + +### 2. 取得代码 + +```bash +sudo -u <myapp> git clone <仓库地址> /srv/<myapp>/app +cd /srv/<myapp>/app && sudo -u <myapp> git rev-parse HEAD +``` + +**预期结果**:输出本次部署的完整提交哈希,记录到部署记录中。 + +### 3. 安装依赖 + +```bash +sudo -u <myapp> python3 -m venv /srv/<myapp>/venv +sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r /srv/<myapp>/app/requirements.txt +``` + +**预期结果**:pip 以 `Successfully installed ...` 结束,无 ERROR。 + +### 4. 落位配置文件 + +```bash +sudo install -o <myapp> -g <myapp> -m 600 /dev/null /srv/<myapp>/app.env +sudo -u <myapp> vi /srv/<myapp>/app.env +``` + +**预期结果**:`ls -l /srv/<myapp>/app.env` 显示权限 `-rw-------` 且属主为 `<myapp>`。 + +配置项清单见下方“配置与凭据来源”。配置文件不进入 Git。 + +### 5. 数据库初始化或迁移 + +<!-- 无数据库时删除本节。 --> + +> **注意**:迁移可能不可逆。执行前必须先备份,并确认回退方式。 + +```bash +sudo -u <myapp> /srv/<myapp>/venv/bin/python -m <myapp>.manage migrate +``` + +**预期结果**:输出全部迁移已应用,无失败项。 + +## supervisor 配置 + +写入 `/etc/supervisor/conf.d/<myapp>.conf`: + +```ini +[program:<myapp>] +command=/srv/<myapp>/venv/bin/gunicorn <myapp>.wsgi:application --workers <n> --bind 127.0.0.1:<8000> --timeout 60 +directory=/srv/<myapp>/app +user=<myapp> +environment=PATH="/srv/<myapp>/venv/bin",APP_ENV_FILE="/srv/<myapp>/app.env" +autostart=true +autorestart=true +startsecs=5 +stopasgroup=true +killasgroup=true +stopwaitsecs=30 +stdout_logfile=/var/log/<myapp>/stdout.log +stderr_logfile=/var/log/<myapp>/stderr.log +stdout_logfile_maxbytes=50MB +stdout_logfile_backups=5 +``` + +> 异步框架使用 uvicorn 时把 `command` 换成: +> `/srv/<myapp>/venv/bin/uvicorn <myapp>.asgi:app --host 127.0.0.1 --port <8000> --workers <n>` + +不要在 `environment` 里写明文密码或令牌;敏感值放在 `app.env`,由应用读取。 + +加载配置并启动: + +```bash +sudo supervisorctl reread +sudo supervisorctl update +sudo supervisorctl start <myapp> +sudo supervisorctl status <myapp> +``` + +**预期结果**:`reread` 输出 `<myapp>: available`;`status` 显示 `RUNNING` 且 uptime 持续增长。出现 `BACKOFF` 或 `FATAL` 时查看 `stderr.log`。 + +## nginx 配置 + +写入 `/etc/nginx/sites-available/<myapp>.conf` 并软链到 `sites-enabled`: + +```nginx +server { + listen 80; + server_name <example.com>; + + access_log /var/log/nginx/<myapp>.access.log; + error_log /var/log/nginx/<myapp>.error.log; + + client_max_body_size <20m>; + + location /static/ { + alias /srv/<myapp>/app/static/; + expires 7d; + } + + location / { + proxy_pass http://127.0.0.1:<8000>; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_read_timeout <60s>; + } +} +``` + +<!-- WebSocket 需求存在时,在对应 location 增加 Upgrade 与 Connection 头;无此需求时删除本注释。 --> + +启用并生效: + +```bash +sudo ln -sf /etc/nginx/sites-available/<myapp>.conf /etc/nginx/sites-enabled/<myapp>.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +**预期结果**:`nginx -t` 输出 `syntax is ok` 与 `test is successful`;reload 无输出且 `systemctl status nginx` 仍为 `active (running)`。 + +`nginx -t` 未通过时不要 reload,先修正配置。 + +<!-- 需要 HTTPS 时在此记录证书来源、签发方式和续期检查命令;证书内容和私钥不得写入本页。 --> + +## 配置与凭据来源 + +| 配置项 | 用途 | 来源 | 是否敏感 | +|---|---|---|---| +| `APP_ENV_FILE` | 指向配置文件路径 | supervisor 配置 | 否 | +| `<DATABASE_URL>` | 数据库连接 | `/srv/<myapp>/app.env`,值取自运维密码库条目 `<条目名>` | 是 | +| `<SECRET_KEY>` | 会话与签名 | 同上 | 是 | +| `<LOG_LEVEL>` | 日志级别 | `/srv/<myapp>/app.env` | 否 | + +敏感值只记录取用位置,不在本页、工单、日志和提交中出现真实内容。 + +## 日常运维 + +| 操作 | 命令 | 预期结果 | +|---|---|---| +| 查看状态 | `sudo supervisorctl status <myapp>` | `RUNNING`,uptime 持续增长 | +| 重启服务 | `sudo supervisorctl restart <myapp>` | 输出 `stopped` 后 `started` | +| 停止服务 | `sudo supervisorctl stop <myapp>` | 输出 `stopped` | +| 实时日志 | `sudo supervisorctl tail -f <myapp> stderr` | 持续输出应用日志 | +| 应用日志 | `sudo tail -n 200 /var/log/<myapp>/stderr.log` | 输出最近日志 | +| 接入层日志 | `sudo tail -n 200 /var/log/nginx/<myapp>.error.log` | 输出 nginx 错误 | +| 重载 nginx | `sudo nginx -t && sudo systemctl reload nginx` | 测试通过后无中断生效 | + +修改 supervisor 配置后必须 `reread` + `update`,只 `restart` 不会加载新配置。 + +## 健康检查 + +每次部署、重启和回滚后必须全部执行: + +```bash +sudo supervisorctl status <myapp> +curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:<8000><健康检查路径> +curl -sS -o /dev/null -w "%{http_code}\n" https://<example.com><健康检查路径> +sudo tail -n 50 /var/log/<myapp>/stderr.log +``` + +**预期结果**:状态为 `RUNNING`;两个 `curl` 均返回 `200`;日志无新增异常堆栈。 + +任何一项不符合时不视为部署成功,按“升级与回滚”处理。 + +## 升级与回滚 + +### 升级 + +```bash +cd /srv/<myapp>/app +sudo -u <myapp> git rev-parse HEAD # 记录当前提交,回滚需要 +sudo -u <myapp> git fetch --all +sudo -u <myapp> git checkout <目标提交或标签> +sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r requirements.txt +sudo -u <myapp> /srv/<myapp>/venv/bin/python -m <myapp>.manage migrate # 无数据库时删除 +sudo supervisorctl restart <myapp> +``` + +**预期结果**:restart 后 `status` 为 `RUNNING`,随后健康检查全部通过。 + +升级前必须记录当前提交哈希;涉及数据库迁移时必须先备份。 + +### 回滚 + +```bash +cd /srv/<myapp>/app +sudo -u <myapp> git checkout <升级前记录的提交> +sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r requirements.txt +sudo supervisorctl restart <myapp> +``` + +**预期结果**:健康检查全部通过。 + +> **注意**:已执行的数据库迁移通常不能通过切回代码撤销。存在迁移时必须先确认迁移是否向后兼容;不兼容时按备份恢复流程处理,并停止自行操作、联系负责人。 + +### 备份与恢复 + +<!-- 记录备份对象、频率、保存位置、保留期和恢复步骤;无持久化数据时说明原因。 --> + +## 已知限制 + +<!-- 记录本环境无法验证的部分,例如未做过真实回滚演练、未验证高并发表现、灰度或多机部署尚未支持。不得留空,无限制时写“无”。 --> + +- <!-- 填写 --> diff --git a/tests/test_harness_docs.py b/tests/test_harness_docs.py index 2c0b360..aa760f3 100644 --- a/tests/test_harness_docs.py +++ b/tests/test_harness_docs.py @@ -3,21 +3,25 @@ from __future__ import annotations import sys import tempfile import unittest +import unittest.mock from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "dev_scripts")) -from check_harness import ( # noqa: E402 +import harness # noqa: E402 +from harness import ( # noqa: E402 CORE_DOCUMENT_REQUIREMENTS, CORE_PAGE_PATHS, REQUIRED_FILES, check_claude_code_entry, + check_required_files, check_core_documents, check_agent_efficiency_rules, check_go_admin_ui_rules, check_goadmin_baseline, + check_repository_readme, check_task_template, core_mapping_errors, missing_sections, @@ -45,6 +49,12 @@ class CoreDocumentTests(unittest.TestCase): for page, path in CORE_PAGE_PATHS.items(): self.assertEqual(mappings.get(page), path) + def test_task_archives_are_not_core_mappings(self) -> None: + config = load_config() + self.assertFalse( + any(mapping.path.startswith("docs/task/") for mapping in config.mappings) + ) + def test_existing_project_adoption_guide_is_core_document(self) -> None: path = "docs/08-existing-project-adoption.md" self.assertEqual( @@ -54,6 +64,102 @@ class CoreDocumentTests(unittest.TestCase): self.assertIn(path, CORE_DOCUMENT_REQUIREMENTS) self.assertIn("## 可复制 Agent 指令", CORE_DOCUMENT_REQUIREMENTS[path]) + def test_product_requirements_overview_is_core_document(self) -> None: + path = "docs/09-product-requirements.md" + self.assertEqual( + CORE_PAGE_PATHS.get("Product-Requirements"), + path, + ) + required = CORE_DOCUMENT_REQUIREMENTS[path] + self.assertIn("## 当前需求索引", required) + self.assertIn("## 原型与设计资产", required) + self.assertIn("### 原型门禁", required) + self.assertIn("### 线上原型与按需 HTML 快照", required) + self.assertIn("### 原型确认记录", required) + self.assertIn("## 更新时机", required) + + def test_workflow_requires_ticket_and_design_evidence_gates(self) -> None: + required = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] + self.assertIn("## 工单与设计证据双门禁", required) + self.assertIn("### 先判断是否需要工单", required) + self.assertIn("### 再判断设计证据", required) + self.assertIn("### 线上原型审核与按需导出", required) + self.assertIn("### 记录和重新确认", required) + + def test_workflow_requires_online_wiki_initialization_gate(self) -> None: + workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] + setup = CORE_DOCUMENT_REQUIREMENTS[ + "docs/07-new-project-documentation-setup.md" + ] + self.assertIn("## 新项目 Wiki 初始化门禁", workflow) + self.assertIn("#### 在线创建与回读门禁", setup) + + def test_workflow_requires_issue_only_task_record(self) -> None: + workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] + setup = CORE_DOCUMENT_REQUIREMENTS[ + "docs/07-new-project-documentation-setup.md" + ] + self.assertIn("## 稳定文档与可选历史快照", workflow) + self.assertIn("#### 工程基线裁剪", setup) + + def test_workflow_requires_gitea_mcp_and_minimal_issue_reading(self) -> None: + workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] + self.assertIn("## Gitea 交互与工单最小读取", workflow) + + errors: list[str] = [] + check_agent_efficiency_rules(errors) + self.assertEqual(errors, []) + + def test_existing_project_adoption_requires_upgrade_process(self) -> None: + required = CORE_DOCUMENT_REQUIREMENTS[ + "docs/08-existing-project-adoption.md" + ] + self.assertIn("## 后续升级", required) + self.assertIn("### 升级步骤", required) + self.assertIn("### 可复制升级指令", required) + + def test_project_profile_requires_dev_harness_baseline(self) -> None: + required = CORE_DOCUMENT_REQUIREMENTS["docs/00-project-profile.md"] + self.assertIn("## DevHarness 来源与基线", required) + + def test_new_project_setup_requires_baseline_selection(self) -> None: + required = CORE_DOCUMENT_REQUIREMENTS[ + "docs/07-new-project-documentation-setup.md" + ] + self.assertIn("### 2. 选择建设基线", required) + self.assertIn("#### 判断案例", required) + self.assertIn("### 3. 识别子项目与交付单元", required) + self.assertIn("#### 需求总览启用条件", required) + + def test_local_development_requires_powershell_utf8_boundaries(self) -> None: + required = CORE_DOCUMENT_REQUIREMENTS[ + "docs/04-local-development-and-verification.md" + ] + self.assertIn("## Windows PowerShell 与 UTF-8", required) + + errors: list[str] = [] + check_agent_efficiency_rules(errors) + self.assertEqual(errors, []) + + def test_deployment_template_is_required_and_mapped(self) -> None: + path = "docs/templates/deployment.md" + self.assertIn(path, REQUIRED_FILES) + config = load_config() + mappings = {mapping.page: mapping.path for mapping in config.mappings} + self.assertEqual(mappings.get("Deployment-Template"), path) + # 部署页按项目需要创建,不强制每个项目产出实例页。 + self.assertNotIn(path, CORE_DOCUMENT_REQUIREMENTS) + + def test_missing_deployment_template_is_reported(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + with unittest.mock.patch.object(harness, "ROOT", root): + errors: list[str] = [] + check_required_files(errors) + self.assertTrue( + any("docs/templates/deployment.md" in error for error in errors) + ) + def test_missing_or_wrong_core_mapping_is_reported(self) -> None: errors = core_mapping_errors({"Home": "docs/wrong.md"}) self.assertTrue(any("Home -> docs/README.md" in error for error in errors)) @@ -99,6 +205,42 @@ class TaskTemplateTests(unittest.TestCase): errors, ) + def test_task_template_requires_design_and_prototype_gate(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + template = root / ".gitea" / "issue_template" / "task.md" + template.parent.mkdir(parents=True) + template.write_text("## 基本信息\n", encoding="utf-8") + errors: list[str] = [] + check_task_template(errors, root) + self.assertIn("单元任务模板缺少:## 设计与原型门禁", errors) + self.assertIn( + "单元任务模板缺少:- 可编辑设计源、线上原型链接和访问检查:", + errors, + ) + self.assertIn( + "单元任务模板缺少:- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求", + errors, + ) + self.assertIn( + "单元任务模板缺少:- 本地 HTML 路径、版本和资源检查(仅显式导出时填写):", + errors, + ) + + def test_task_template_requires_optional_snapshot_policy(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + template = root / ".gitea" / "issue_template" / "task.md" + template.parent.mkdir(parents=True) + template.write_text("## 基本信息\n", encoding="utf-8") + errors: list[str] = [] + check_task_template(errors, root) + self.assertIn("单元任务模板缺少:## 任务记录与可选快照", errors) + self.assertIn( + "单元任务模板缺少:- [ ] 默认不创建任务快照", + errors, + ) + def test_task_template_requires_subproject_impact(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -117,23 +259,6 @@ class TaskTemplateTests(unittest.TestCase): errors, ) - def test_task_template_requires_coordination_fields(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - template = root / ".gitea" / "issue_template" / "task.md" - template.parent.mkdir(parents=True) - template.write_text("## 基本信息\n", encoding="utf-8") - errors: list[str] = [] - check_task_template(errors, root) - self.assertIn( - "单元任务模板缺少:- 任务类型:单项目 / 协同", - errors, - ) - self.assertIn("单元任务模板缺少:- 主 agent:", errors) - self.assertIn("单元任务模板缺少:## 协同接口", errors) - self.assertIn("单元任务模板缺少:- 生产者:", errors) - self.assertIn("单元任务模板缺少:- 消费者:", errors) - self.assertIn("单元任务模板缺少:- write_paths:", errors) def test_task_template_requires_dependency_fields(self) -> None: with tempfile.TemporaryDirectory() as directory: @@ -176,73 +301,44 @@ class TaskTemplateTests(unittest.TestCase): self.assertIn("单元任务模板缺少:## 需求变化记录", errors) -class AgentRuleTests(unittest.TestCase): - def test_required_agent_rules_are_present(self) -> None: - errors: list[str] = [] - check_agent_efficiency_rules(errors) - self.assertEqual(errors, []) - - def test_claude_code_entry_imports_shared_rules(self) -> None: - errors: list[str] = [] - check_claude_code_entry(errors) - self.assertEqual(errors, []) - - def test_go_admin_ui_rules_are_present(self) -> None: +class YoVisionBaselineTests(unittest.TestCase): + def test_go_admin_ui_rules_are_preserved(self) -> None: errors: list[str] = [] check_go_admin_ui_rules(errors) self.assertEqual(errors, []) - def test_go_admin_ui_rules_require_each_scope(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - (root / "Sense").mkdir() - (root / "Bell").mkdir() - (root / "AGENTS.md").write_text("", encoding="utf-8") - (root / "Sense" / "AGENTS.md").write_text("", encoding="utf-8") - (root / "Bell" / "AGENTS.md").write_text("", encoding="utf-8") - errors: list[str] = [] - check_go_admin_ui_rules(errors, root) - self.assertTrue(any(error.startswith("AGENTS.md ") for error in errors)) - self.assertTrue(any(error.startswith("Sense/AGENTS.md ") for error in errors)) - self.assertTrue(any(error.startswith("Bell/AGENTS.md ") for error in errors)) - - def test_go_admin_ui_rules_report_missing_scope_file(self) -> None: - with tempfile.TemporaryDirectory() as directory: - errors: list[str] = [] - check_go_admin_ui_rules(errors, Path(directory)) - self.assertIn("缺少 go-admin 规则文件:AGENTS.md", errors) - self.assertIn("缺少 go-admin 规则文件:Sense/AGENTS.md", errors) - self.assertIn("缺少 go-admin 规则文件:Bell/AGENTS.md", errors) - def test_goadmin_baseline_is_frozen(self) -> None: errors: list[str] = [] check_goadmin_baseline(errors) self.assertEqual(errors, []) - def test_goadmin_baseline_rejects_unpinned_source(self) -> None: + +class AgentRuleTests(unittest.TestCase): + def test_required_agent_rules_are_present(self) -> None: + errors: list[str] = [] + check_agent_efficiency_rules(errors) + self.assertEqual(errors, []) + + def test_repository_readme_requires_online_wiki_gate(self) -> None: + errors: list[str] = [] + check_repository_readme(errors) + self.assertEqual(errors, []) + + def test_repository_readme_rejects_missing_gate(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - (root / "goadmin-baseline.json").write_text( - '{"authority":"tag","runtimes":{},' - '"sources":{"go-admin":{}},"rules":{}}', + (root / "README.md").write_text( + "# 项目\n创建 Gitea 远端仓库\n", encoding="utf-8", ) errors: list[str] = [] - check_goadmin_baseline(errors, root) - self.assertTrue(any("go-admin.commit" in error for error in errors)) - self.assertIn( - "GoAdmin 技术基线必须以上游 URL 和 commit 为权威标识", - errors, - ) + check_repository_readme(errors, root) + self.assertTrue(any("README.md 缺少" in error for error in errors)) - def test_goadmin_baseline_reports_missing_file(self) -> None: - with tempfile.TemporaryDirectory() as directory: - errors: list[str] = [] - check_goadmin_baseline(errors, Path(directory)) - self.assertEqual( - errors, - ["缺少 GoAdmin 技术基线:goadmin-baseline.json"], - ) + def test_claude_code_entry_imports_shared_rules(self) -> None: + errors: list[str] = [] + check_claude_code_entry(errors) + self.assertEqual(errors, []) def test_claude_code_entry_requires_exact_import_line(self) -> None: with tempfile.TemporaryDirectory() as directory: diff --git a/tests/test_wiki_docs.py b/tests/test_wiki_docs.py index 4d8c19a..bc6b58e 100644 --- a/tests/test_wiki_docs.py +++ b/tests/test_wiki_docs.py @@ -12,7 +12,14 @@ from unittest.mock import Mock, patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "dev_scripts")) -from new_task_archive import build_archive, safe_title # noqa: E402 +from harness import ( # noqa: E402 + build_archive, + existing_task_mirrors, + export_task_archives, + main as harness_main, + safe_title, + task_target, +) from wiki_docs import ( # noqa: E402 Config, Mapping, @@ -158,6 +165,154 @@ class ArchiveTests(unittest.TestCase): self.assertIn("Task-12-login", result) self.assertNotIn("YYYY-MM-DD", result) + @patch("harness.WikiClient") + def test_create_archive_does_not_change_core_mapping(self, client_class) -> None: + with tempfile.TemporaryDirectory() as directory: + config_path = Path(directory) / "wiki-docs.json" + original = json.dumps( + { + "schema_version": 1, + "gitea_url": "http://gitea.example", + "owner": "o", + "repository": "r", + "mappings": [ + {"page": "Home", "path": "docs/README.md"} + ], + } + ) + config_path.write_text(original, encoding="utf-8") + client = client_class.return_value + client.list_pages.return_value = [] + client.get_page.return_value = WikiPage( + title="Task-Archive-Template", + sub_url="Task-Archive-Template.-", + text="# <工单号> <标题>\nYYYY-MM-DD\n<链接>\n<页面名>\n", + revision="a" * 40, + html_url="http://gitea.example/wiki/template", + ) + client.create_page.return_value = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="b" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + with patch.object( + sys, + "argv", + [ + "harness.py", + "archive", + "14", + "按需导出", + "--config", + str(config_path), + ], + ): + result = harness_main() + self.assertEqual(config_path.read_text(encoding="utf-8"), original) + self.assertEqual(result, 0) + client.create_page.assert_called_once() + + def test_task_target_uses_stable_safe_name(self) -> None: + with tempfile.TemporaryDirectory() as directory: + target = task_target("Task-14-修复:导出", Path(directory)) + self.assertEqual(target.name, "14-修复-导出.md") + + def test_existing_mirror_keeps_historical_custom_filename(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "docs" / "task" / "2-初级维护者文档体系.md" + path.parent.mkdir(parents=True) + page = WikiPage( + title="Task-2-Junior-Maintainer-Docs", + sub_url="Task-2-Junior-Maintainer-Docs.-", + text="# 2 文档\n", + revision="c" * 40, + html_url="http://gitea.example/wiki/task-2", + ) + path.write_text(render_mirror(page), encoding="utf-8") + mirrors = existing_task_mirrors(root) + self.assertEqual( + mirrors["Task-2-Junior-Maintainer-Docs"].name, + "2-初级维护者文档体系.md", + ) + + @patch("harness.dirty_paths", return_value=[]) + def test_incremental_export_skips_same_revision(self, _dirty) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "docs" / "task" / "14-按需导出.md" + path.parent.mkdir(parents=True) + page = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="d" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + path.write_text(render_mirror(page), encoding="utf-8") + client = Mock() + client.list_pages.return_value = [ + { + "title": page.title, + "sub_url": page.sub_url, + "last_commit": {"sha": page.revision}, + } + ] + messages = export_task_archives(client, root=root) + self.assertTrue(messages[0].startswith("跳过:")) + client.get_page_from_metadata.assert_not_called() + + @patch( + "harness.dirty_paths", + return_value=[" M docs/task/14-按需导出.md"], + ) + def test_export_stops_before_wiki_read_when_task_mirror_is_dirty( + self, _dirty + ) -> None: + client = Mock() + with self.assertRaisesRegex(WikiDocsError, "未提交改动"): + export_task_archives(client) + client.list_pages.assert_not_called() + + @patch("harness.dirty_paths", return_value=[]) + def test_full_export_reads_all_and_never_deletes_extra_file(self, _dirty) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + task_dir = root / "docs" / "task" + task_dir.mkdir(parents=True) + extra = task_dir / "99-历史快照.md" + extra_page = WikiPage( + title="Task-99-历史快照", + sub_url="Task-99.-", + text="# 99 历史快照\n", + revision="e" * 40, + html_url="http://gitea.example/wiki/task-99", + ) + extra.write_text(render_mirror(extra_page), encoding="utf-8") + page = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="f" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + client = Mock() + metadata = { + "title": page.title, + "sub_url": page.sub_url, + "last_commit": {"sha": page.revision}, + } + client.list_pages.return_value = [metadata] + client.get_page_from_metadata.return_value = page + messages = export_task_archives(client, export_all=True, root=root) + exported = root / "docs" / "task" / "14-按需导出.md" + self.assertTrue(exported.is_file()) + self.assertTrue(extra.is_file()) + self.assertTrue(messages[0].startswith("已导出:")) + client.get_page_from_metadata.assert_called_once_with(metadata, page.title) + if __name__ == "__main__": unittest.main() diff --git a/wiki-docs.json b/wiki-docs.json index ea0f9da..63745d1 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -68,101 +68,17 @@ "page": "Audience-Document-Template", "path": "docs/delivery/audience-document-template.md" }, + { + "page": "Deployment-and-Operations", + "path": "docs/delivery/deployment-and-operations.md" + }, + { + "page": "Deployment-Template", + "path": "docs/templates/deployment.md" + }, { "page": "Task-Archive-Template", "path": "docs/templates/task-archive.md" - }, - { - "page": "Task-1-三项目并行建单与协同工单顺序", - "path": "docs/task/1-三项目并行建单与协同工单顺序.md" - }, - { - "page": "Task-3-GoAdmin默认模块精简与UI组件复用", - "path": "docs/task/3-GoAdmin默认模块精简与UI组件复用.md" - }, - { - "page": "Task-5-冻结Sense与Bell-GoAdmin技术基线", - "path": "docs/task/5-冻结Sense与Bell-GoAdmin技术基线.md" - }, - { - "page": "Task-58-建立explore-main-dev分支治理并重置GoAdmin开发基线", - "path": "docs/task/58-建立explore-main-dev分支治理并重置GoAdmin开发基线.md" - }, - { - "page": "Task-28-三项目当前MVP交互原型", - "path": "docs/task/28-三项目当前MVP交互原型.md" - }, - { - "page": "Task-30-Sense原型对齐GoAdmin与Element-Plus", - "path": "docs/task/30-Sense原型对齐GoAdmin与Element-Plus.md" - }, - { - "page": "Task-32-Sense网管与非技术人员原型", - "path": "docs/task/32-Sense网管与非技术人员原型.md" - }, - { - "page": "Task-61-Sense冻结GoAdmin源码产品骨架", - "path": "docs/task/61-Sense冻结GoAdmin源码产品骨架.md" - }, - { - "page": "Task-64-Sense登录RBAC与审计", - "path": "docs/task/64-Sense登录RBAC与审计.md" - }, - { - "page": "Task-65-Sense设备台账与凭据边界", - "path": "docs/task/65-Sense设备台账与凭据边界.md" - }, - { - "page": "Task-92-Sense旧设备能力JSONB兼容迁移", - "path": "docs/task/92-Sense旧设备能力JSONB兼容迁移.md" - }, - { - "page": "Task-66-Sense视频接入与Profile", - "path": "docs/task/66-Sense视频接入与Profile.md" - }, - { - "page": "Task-67-Sense视频服务生命周期与状态对账", - "path": "docs/task/67-Sense视频服务生命周期与状态对账.md" - }, - { - "page": "Task-68-Sense单路实时监看与播放状态反馈", - "path": "docs/task/68-Sense单路实时监看与播放状态反馈.md" - }, - { - "page": "Task-69-Sense多边形区域与方向警戒线配置", - "path": "docs/task/69-Sense多边形区域与方向警戒线配置.md" - }, - { - "page": "Task-90-Sense项目根目录Windows启动脚本", - "path": "docs/task/90-Sense项目根目录Windows启动脚本.md" - }, - { - "page": "Task-70-Sense-Windows配置启动与打包交付", - "path": "docs/task/70-Sense-Windows配置启动与打包交付.md" - }, - { - "page": "Task-95-Sense旧媒体路由唯一约束兼容迁移", - "path": "docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md" - }, - { - "page": "Task-97-Sense免验证码登录与管理员密码重置", - "path": "docs/task/97-Sense免验证码登录与管理员密码重置.md" - }, - { - "page": "Task-101-Sense-GoAdmin-应用外壳", - "path": "docs/task/101-Sense-GoAdmin-应用外壳.md" - }, - { - "page": "Task-99-Sense登录页只读配置接口", - "path": "docs/task/99-Sense登录页只读配置接口.md" - }, - { - "page": "Task-104-Sense-30天登录有效期", - "path": "docs/task/104-Sense-30天登录有效期.md" - }, - { - "page": "Task-106-Sense-本机-Supervisor-实例", - "path": "docs/task/106-Sense-本机-Supervisor-实例.md" } ] }