From 7c1ff5d82e9e14e415e44face7cf6a2e348603b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=91=E5=B8=85?= Date: Mon, 27 Jul 2026 21:07:48 +0800 Subject: [PATCH] docs(ai): make operating guide fully standalone --- CONTRIBUTING.md | 662 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 611 insertions(+), 51 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 494e808..1884a49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # STORAGE LABOS / FlashOps — AI 协作与系统操作指南 -> 文档版本:1.0 +> 文档版本:2.0 > > 核对日期:2026-07-27 > @@ -8,9 +8,13 @@ > > 目标:只把本文件交给 AI,也能让它安全地理解、运行、修改、验证和交付本系统 -本文件是 AI 接手项目时的第一入口。更完整的产品、架构、数据模型、状态机和生产运维说明见 -[ProjectManual.md](ProjectManual.md)。如果本文件与代码冲突,以代码和测试为准,并在本次变更中 -同步修正文档;如果生产信息与代码冲突,先做只读核验,不要凭文档直接修改服务器。 +本文件是可以单独分发给 AI 的完整系统说明书,不依赖仓库中的任何其他文档。它已经内嵌产品边界、 +架构、数据模型、Run/Step 状态机、恢复阶梯、API、Agent 协议、开发流程、生产运维、Git 和安全规则。 +即使 AI 只能看到这一份文件,也应能够正确查询线上系统、运行本地模拟、判断允许执行的动作,并在 +获得仓库或服务器权限后安全开发和运维。 + +如果 AI 同时能看到源码,以实际代码和测试结果为准;如果生产状态与本文不一致,先只读核验并报告 +差异,不要凭本文直接覆盖服务器。 --- @@ -21,12 +25,13 @@ ~~~text 你正在维护 STORAGE LABOS / FlashOps。 -请先完整阅读仓库根目录 CONTRIBUTING.md,再按其中的权限边界、架构红线、测试门禁和 -Git 交付流程工作。需要深入理解时再阅读 ProjectManual.md 及 flashops/docs/。 +你收到的这份文档已经包含操作本系统所需的产品、架构、状态机、API、Agent、生产运维、 +安全和 Git 规则。请完整阅读并只以本文作为任务上下文,不要要求用户再提供其他项目文档。 -先检查仓库状态并保护现有未提交改动。默认只操作本地开发环境;除非任务明确要求,不得修改 -生产服务器、生产数据、DNS、证书、Gitea 设置或调用生产环境写接口。任何真实硬件、固件刷写、 -Format、Sanitize、Reset、断电或 OOB 动作都必须停下并取得明确授权。 +如果任务同时提供仓库,先检查状态并保护现有未提交改动;如果只有本文,先使用公开 GET 接口进行 +只读核验。默认只操作本地开发环境;除非任务明确要求,不得修改生产服务器、生产数据、DNS、证书、 +Gitea 设置或调用生产环境写接口。任何真实硬件、固件刷写、Format、Sanitize、Reset、断电或 OOB +动作都必须停下并取得明确授权。 任务:<在这里写具体目标> @@ -50,7 +55,8 @@ Format、Sanitize、Reset、断电或 OOB 动作都必须停下并取得明确 | 本地 API 文档 | | | 后端 | Python 3.9+、FastAPI、SQLAlchemy | | 当前数据库 | SQLite,必须保持单 worker | -| 当前执行能力 | 模拟 Run 与 Agent dry-run;不代表真实硬件动作已交付 | +| 仓库代码能力 | 模拟 Run 与 Agent dry-run;不代表真实硬件动作已交付 | +| 当前线上能力 | API 版本 0.1.0,仅模拟 Run;Agent 路由尚未发布 | | 当前测试基线 | 25 项 | | 生产认证边界 | 预览站匿名开放,尚无应用级 RBAC | | Gitea | 公开仓库匿名可读;注册账号需管理员批准 | @@ -84,21 +90,20 @@ AI 应按以下顺序判断是否可以行动: --- -## 3. 信息源优先级 +## 3. 决策依据与证据优先级 AI 不应只根据文件名或旧说明猜测当前能力,按下面顺序取证: -1. 当前用户的明确任务与限制; -2. 当前工作树、Git diff 和实际源码; -3. 自动化测试及可重复的本地运行结果; -4. 本文件; -5. ProjectManual.md; -6. flashops/docs/ 下的架构、状态机和 ADR; -7. README、历史提交、部署模板; -8. 推测。 +1. 当前用户的明确任务、授权与限制; +2. 本文件写明的安全边界和不可执行动作; +3. 线上健康检查、API 返回、服务状态和日志等实际证据; +4. 如果 AI 获得仓库:当前工作树、Git diff、源码和自动化测试; +5. 如果 AI 获得服务器:生效配置、运行进程、数据路径和备份; +6. 历史提交或历史部署记录; +7. 推测。 -如果代码、测试和文档互相矛盾,必须在交付说明中指出。不得把“规划”“草案”“静态页面”描述成 -已经可用于真实硬件的能力。 +如果本文、代码、测试和生产状态互相矛盾,必须在交付说明中指出。不得把“规划”“草案”“静态 +页面”描述成已经可用于真实硬件的能力,也不得因为拥有技术访问能力就推断拥有写入授权。 --- @@ -132,20 +137,18 @@ git log -5 --oneline 先识别用户已有的未提交改动。不得使用 reset --hard、checkout 丢弃文件、clean -fd 或其他会 抹掉用户工作的命令。不得把不相关改动混进自己的提交。 -### 4.3 推荐阅读路线 +### 4.3 只有本文时 -按任务读取最少但足够的资料: +只有本文、没有仓库时,AI 仍可以: -| 任务 | 必读文件 | -|---|---| -| 普通开发 | 本文件、根 README、flashops/README.md | -| 状态机或恢复 | flashops/docs/state-machine.md、decisions/0003-* | -| 事件和数据一致性 | flashops/docs/domain-model.md、decisions/0002-* | -| 安全门禁 | flashops/docs/decisions/0004-*、safety/gates.py | -| Agent | flashops/docs/agent-protocol.md、services/host-agent/README.md | -| 工作流 | flashops/docs/workflow-spec.md | -| 适配器或真实工具 | flashops/docs/adapter-sdk.md | -| 部署、域名、备份 | ProjectManual.md 第 12 章、flashops/deploy/ | +- 使用第 6 节列出的公开 GET 接口查询健康、指标、Run、事件和 Host; +- 根据第 5 节解释状态、恢复、检查点和系统边界; +- 根据第 11 节指导获授权的人完成服务器检查、发布、备份和故障排查; +- 根据第 2 节拒绝未授权生产写入与危险硬件动作; +- 输出诊断结论、变更方案、验证清单和交接记录。 + +只有本文时,AI 不得声称已经修改代码、提交 Git、连接服务器或执行生产动作。只有在真正获得相应 +工具和授权后,才能执行这些操作。 --- @@ -153,18 +156,14 @@ git log -5 --oneline ~~~text STORAGE LABOS/ -├── CONTRIBUTING.md AI 与团队统一入口 -├── ProjectManual.md 完整产品、开发和运维手册 -├── README.md 项目总览 +├── CONTRIBUTING.md 本文:完整 AI 系统说明书 ├── .gitea/ PR 模板与未来 CI 工作流 ├── flashops/ │ ├── services/control-plane/ FastAPI 控制平面 │ ├── services/host-agent/ 拉模式 Host Agent -│ ├── docs/ 架构、协议、状态机、ADR │ ├── deploy/ FlashOps 与 Gitea 部署模板 │ ├── var/ 本地数据库和证据,禁止提交 -│ ├── Makefile 开发和验证入口 -│ └── README.md 可运行能力说明 +│ └── Makefile 开发和验证入口 └── 前端UI八页面完成/ FastAPI 同源挂载的静态控制台 ~~~ @@ -173,7 +172,8 @@ STORAGE LABOS/ - FastAPI 控制平面、14 张持久化表和 SQLite 数据库; - Run 事件溯源、状态投影、状态机和安全门禁; - 模拟回归、故障注入、L1–L3 恢复演示和 Evidence Bundle; -- Agent 注册、token、心跳、步骤租约、ACK、续租、事件、完成、超时回收和幂等链路; +- 仓库代码已具备 Agent 注册、token、心跳、步骤租约、ACK、续租、事件、完成、超时回收和幂等链路, + 但该增量尚未发布到当前公网环境; - 公开预览控制台及部分真实 API 接入页面。 当前不应声称已经具备: @@ -184,10 +184,217 @@ STORAGE LABOS/ - 多工位生产调度、PostgreSQL 高可用; - 已验收的失败聚类自动决策。 +### 5.1 五层架构与依赖方向 + +~~~text +浏览器控制台 ──HTTP──▶ FastAPI 控制平面 ◀──HTTP 长轮询── Host Agent + │ │ + ▼ ├──▶ 工具适配器 + SQLite / 证据 └──▶ 独立 OOB 控制器 +~~~ + +五个逻辑层: + +1. 产品交互层:控制台页面和 HTTP API; +2. 控制平面:工作流物化、状态机、安全门禁、资源锁、事件和恢复决策; +3. 数据面:客户测试主机上的拉模式 Agent,只执行控制平面批准的步骤; +4. 带外平面:独立于测试主机的 OOB 观测与电源控制,主机死亡后仍应可用; +5. 证据与分析层:事件时间线、Evidence Bundle、失败签名和审计。 + +依赖必须单向: + +- Agent 不依赖控制平面的 Python 模块,只通过 HTTP 契约通信; +- 控制平面不主动反连 Agent,Agent 必须主动注册、心跳和领取步骤; +- 状态机与恢复决策保持纯函数,不做数据库、网络或时钟 I/O; +- API 路由只校验请求并调用 service,不承载核心业务判断; +- 所有危险动作在下发 Agent 前先经过安全门禁; +- 带外通道不能依赖被测主机的 OS、网络栈或 Agent 进程。 + +### 5.2 核心数据模型 + +当前 SQLite 中的核心对象如下,AI 讨论数据时应使用这些名称: + +| 对象 | 作用 | +|---|---| +| TestHost | 测试主机、Agent 身份、心跳和工具链能力 | +| OobController | 独立带外控制器、电源和观测能力 | +| Dut | 被测盘及序列号、系统盘保护、状态 | +| FirmwareArtifact | 固件版本、型号适配和校验信息 | +| Workflow | 版本化 SOP 与步骤定义 | +| Run | 一次工作流执行及当前状态投影 | +| RunStep | Run 物化后的顺序步骤、attempt、租约和结果 | +| RunEvent | append-only 统一事件时间线 | +| RecoveryAction | 恢复级别、触发原因、结果和证据 | +| ResourceLock | DUT、Host 等独占资源锁 | +| FailureSignature | 归一化失败签名 | +| RunFailure | Run 与失败签名的关联 | +| EvidenceBundle | 证据包 URI、完整率、大小和哈希 | +| AuditLog | 人工或系统管理动作的审计记录 | + +RunEvent 是状态变化的权威事实;Run 和 RunStep 上的 state 是事件施加后的当前投影。修改投影时必须 +与事件处于同一数据库事务,不能先改状态后补事件。 + +### 5.3 Run 状态机 + +~~~text +QUEUED ──资源锁成功──▶ PREFLIGHT ──六项门禁通过──▶ RUNNING + │ │ │ + └──紧急停止──▶ ABORTED ├──门禁拒绝──▶ REJECTED ├──完成──▶ COMPLETED + └──紧急停止──▶ ABORTED ├──人工暂停──▶ PAUSED + │ │ + │ └──继续──▶ RUNNING + ├──故障──▶ RECOVERING + │ ├──恢复成功──▶ RUNNING + │ └──耗尽/不安全──▶ FROZEN + ├──冻结──▶ FROZEN + └──紧急停止──▶ ABORTED +~~~ + +状态含义: + +| 状态 | 含义 | +|---|---| +| QUEUED | 等待 DUT、Host 等独占资源 | +| PREFLIGHT | 执行安全门禁并冻结环境指纹 | +| RUNNING | 正在执行步骤或循环 | +| RECOVERING | 正在执行恢复阶梯 | +| PAUSED | 在安全检查点人工暂停 | +| COMPLETED | 正常结束,verdict 为 PASS 或 FAIL | +| FROZEN | 冻结现场,禁止覆盖性动作 | +| ABORTED | 紧急停止 | +| REJECTED | 安全门禁拒绝启动 | + +终态为 COMPLETED、FROZEN、ABORTED、REJECTED,终态不可继续转移。任何非法转移都应被视为代码 +缺陷,不能吞掉异常继续运行。任意非终态都可以因紧急停止进入 ABORTED;终态不能再次停止、暂停或继续。 + +### 5.4 Step 状态机与租约 + +~~~text +PENDING ──租给 Agent──▶ DISPATCHED ──ACK──▶ RUNNING + ▲ ├──▶ SUCCEEDED + │ ├──▶ FAILED + └──仅幂等且可重试时 attempt+1 ◀─────────────├──▶ TIMED_OUT + ├──▶ CANCELLED + └──▶ SKIPPED +~~~ + +- PENDING:等待调度; +- DISPATCHED:已分配 Agent 并创建 60 秒租约,等待 ACK; +- RUNNING:Agent 已确认接手,执行期间必须续租; +- SUCCEEDED、FAILED、TIMED_OUT、CANCELLED、SKIPPED:步骤终态; +- 每次派发增加 attempt,旧 attempt 的 ACK、续租、事件和完成上报必须返回 409; +- DISPATCHED 未 ACK 而租约到期时退回 PENDING; +- RUNNING 租约到期时,只有幂等且仍有重试额度的步骤能退回 PENDING; +- 非幂等步骤或重试耗尽时进入 TIMED_OUT,Run verdict 变为 INCONCLUSIVE 并冻结; +- retry 默认是 0;刷写、Format、Sanitize 等危险步骤默认非幂等且不得自动重试。 + +### 5.5 五级恢复阶梯 + +恢复动作根据可用通道逐级升级,不允许跳过安全判断: + +| 级别 | 动作 | 使用前提 | 典型触发 | +|---|---|---|---| +| L1_AGENT_SOFT | Agent 优雅停止或软重启进程 | Agent 仍在线 | 脚本卡死、进程僵死 | +| L2_OS_REBOOT | 通过 OS 通道重启主机 | 主机网络仍在线 | Agent 崩溃、驱动异常 | +| L3_OOB_RESET | OOB 触发主板 Reset | OOB 在线 | 心跳丢失、蓝屏 | +| L4_OOB_ATX_POWER | OOB 模拟 ATX 长按关机再开机 | OOB 在线 | Reset 无效 | +| L5_OOB_AC_CYCLE | AC 断电、安全等待、重新上电 | OOB 在线 | ATX 无效、DUT 掉盘最后手段 | +| FREEZE | 冻结现场并停止自动恢复 | 无 | 数据完整性失败或阶梯耗尽 | + +不可更改的恢复规则: + +1. 数据完整性不一致直接 FREEZE,不能先重启或重跑; +2. OOB 离线时 L3–L5 不可用,应标记 INFRA_FAILURE 并冻结,不能误判 DUT_DEFECT; +3. 每次恢复必须记录 trigger、level、outcome、时间和 evidence_uri; +4. 恢复成功只表示系统重新可执行,不表示原步骤成功; +5. 每循环最多恢复 3 次,每 Run 最多恢复 20 次; +6. L5 断电后默认安全等待 10 秒再上电; +7. 真实 L3–L5 当前没有交付,现有演示只证明状态和证据链路。 + +失败分类必须区分: + +- DUT_DEFECT:证据支持被测盘缺陷; +- INFRA_FAILURE:供电、网络、Host、OOB 等基础设施问题; +- SCRIPT_FAILURE:测试脚本或工具问题; +- DATA_INTEGRITY:数据校验失败,立即冻结; +- UNKNOWN:证据不足,不能强行归类。 + +### 5.6 检查点与断点续跑 + +检查点只在安全的步骤边界创建,不在无法证明幂等的步骤中间创建。 + +| 中断位置 | 恢复后的行为 | +|---|---| +| 步骤已 SUCCEEDED 且检查点已保存 | 从下一步继续 | +| RUNNING 步骤幂等 | 可重跑当前步骤 | +| RUNNING 步骤非幂等 | 不自动重跑,INCONCLUSIVE 并等待人工确认 | +| 循环体中的步骤 | 从当前 loop_index 的循环起点重跑整轮 | + +检查点至少要能确定 loop_index、下一步骤、当前固件/环境指纹和最后事件序号。保存检查点必须先于 +推进下一步骤;恢复时必须验证数据库投影、Agent attempt 和 DUT 当前状态一致。 + +### 5.7 心跳与双通道观测 + +~~~text +Agent 正常心跳:每 5 秒 +15 秒未收到:Host → DEGRADED,只记事件,不立刻做危险动作 +30 秒未收到:Host → OFFLINE,并结合 OOB 判断 + ├── OOB 在线且主机有电:可进入 L3 + ├── OOB 在线但主机无电:INFRA_FAILURE + └── OOB 也离线:INFRA_FAILURE + FREEZE +心跳恢复:核对 Run、Step、attempt、租约和 DUT 枚举后再决定续跑 +~~~ + +单一心跳、网络 ping 或 UI 状态都不能独立证明 DUT 故障。涉及恢复和缺陷归因时必须组合 Agent、 +Host 网络、OOB 电源/画面和 DUT 枚举至少两个通道的证据。 + +### 5.8 六项安全门禁 + +创建 Run 前至少检查: + +1. discovered_serial 与登记 DUT 序列号完全一致; +2. 目标不是系统盘,且没有系统分区或根文件系统; +3. 破坏性动作已有明确授权且授权尚未过期; +4. 固件型号、目标设备和校验值匹配; +5. Host 在线、未被其他 Run 占用且环境指纹满足要求; +6. 需要 OOB 的工作流必须确认 OOB 在线且能力匹配。 + +任一门禁失败都进入 REJECTED,不得通过 UI、API 参数或直接数据库写入绕过。真实危险命令还必须 +增加签名命令模板、参数 schema、allowlist、审批人、审批时间、可牺牲 DUT 和短期授权。 + +### 5.9 Evidence Bundle + +最小证据包包含 manifest.json 与按 seq 排序的 events.ndjson,并记录: + +- Run、Workflow、DUT、Firmware、Host 和环境指纹; +- 完整事件时间线和人工介入; +- 步骤结果、退出码、日志 URI; +- 恢复动作与恢复前后证据; +- 检查点、verdict、完整率、大小和哈希。 + +大日志和二进制产物存对象目录,数据库只保存索引、摘要和 URI。事件按控制平面分配的 seq 排序, +不能依赖不同主机的本地时间戳。任何人工暂停、继续、急停、审批或覆盖默认策略的动作都必须留下事件。 + --- ## 6. AI 如何安全使用正在运行的系统 +### 6.0 控制台页面 + +| 页面 | URL | 当前能力 | +|---|---|---| +| 总览 | /console/Dashboard.dc.html | 产品看板,部分指标仍为静态展示 | +| 任务中心 | /console/Tasks.dc.html | 已接安全预检和创建 Run | +| 实时运行 | /console/LiveRun.dc.html | 已接 Run、事件、演示、暂停、继续和急停 | +| 证据浏览器 | /console/Evidence.dc.html | 主要为静态样例,真实下载待接 | +| 失败中心 | /console/Failures.dc.html | 静态原型,失败聚类执行待实现 | +| 资产中心 | /console/Assets.dc.html | 静态原型,资产 CRUD 待实现 | +| 版本比较 | /console/Compare.dc.html | 静态原型,真实 A/B 查询待实现 | +| 工作流模板 | /console/Workflows.dc.html | 静态原型,编辑与发布 API 待实现 | + +控制台和 API 同源,动态请求使用 /api/v1 相对路径。页面展示不是后端能力证据;AI 必须通过 API +返回或测试确认功能是否真实接入。事件轮询使用 after_seq 增量读取。 + ### 6.1 公网只读检查 以下请求只读取公开信息,可用于健康诊断: @@ -196,10 +403,32 @@ STORAGE LABOS/ curl -fsS https://flashops.imagebrewing.com/api/v1/health curl -fsS https://flashops.imagebrewing.com/api/v1/dashboard/summary curl -fsS 'https://flashops.imagebrewing.com/api/v1/runs?limit=10' -curl -fsS https://flashops.imagebrewing.com/api/v1/agent/hosts ~~~ 不要在诊断任务中为了“试一下”调用生产 POST 接口。健康检查成功不代表业务能力或真实硬件已经验收。 +当前公网 OpenAPI 版本为 0.1.0,Agent 路径尚未发布,访问 /api/v1/agent/hosts 返回 404 是已知部署 +边界,不应误判成 Gitea、Nginx 或 Host 故障。 + +Run 查询的关键字段: + +| 字段 | 含义 | +|---|---| +| id、name | Run 标识和名称 | +| state、verdict | 当前状态与 PASS / FAIL / INCONCLUSIVE | +| execution_mode | simulated 或 agent_dry_run | +| workflow_key、workflow_version | 固定的工作流版本 | +| dut_id、test_host_id | 绑定资产 | +| loop_target、loop_done、progress | 循环目标与完成进度 | +| checkpoint | 最近安全恢复点 | +| environment | 创建时冻结的环境指纹 | +| recovery_count | 已执行恢复次数 | +| unattended_completion | 是否无人干预完成 | +| human_touches | 人工介入次数 | +| freeze_reason | 冻结原因 | +| evidence | 详情接口返回的证据包索引 | + +事件字段为 id、seq、ts、source、kind、severity、step_id、loop_index、message、payload。必须按 seq +消费;after_seq 传客户端最后确认的 seq。source 可能是 control_plane、agent、oob 或 human。 ### 6.2 本地启动 @@ -242,6 +471,8 @@ curl -fsS 'http://127.0.0.1:8000/api/v1/runs/RUN_ID/events?after_seq=0' ### 6.4 API 快速索引 +下表中的非 Agent 路径已在当前公网部署;Agent 路径属于仓库代码基线,当前只适合本地开发和测试。 + | 方法与路径 | 含义 | 默认安全级别 | |---|---|---| | GET /api/v1/health | 服务健康 | 只读 | @@ -258,11 +489,68 @@ curl -fsS 'http://127.0.0.1:8000/api/v1/runs/RUN_ID/events?after_seq=0' | POST /api/v1/runs/{id}/emergency-stop | 紧急停止 | 高影响写操作 | | POST /api/v1/agent/* | Agent 注册与执行协议 | 凭据操作;不得随意调用 | -请求体和响应模型以运行时 OpenAPI 为准。本地启动后优先读取 -,不要凭记忆构造字段。 +核心请求体已经内嵌如下;运行时 OpenAPI 只用于核验部署版本是否发生变化,不是阅读本文的前置条件。 + +Preflight 请求: + +~~~json +{ + "dut_id": "可选 DUT ID", + "host_id": "可选 Host ID", + "firmware_id": "可选固件 ID", + "discovered_serial": "探针实际发现的序列号" +} +~~~ + +Create Run 请求: + +~~~json +{ + "name": "固件 A/B 无人值守回归", + "workflow_id": null, + "dut_id": null, + "host_id": null, + "firmware_a_id": null, + "firmware_b_id": null, + "loops": 8, + "inject_failure": true, + "execution_mode": "simulated", + "created_by": "console-demo" +} +~~~ + +- loops 范围 1–500; +- execution_mode 只能是 simulated 或 agent_dry_run; +- simulated 会由控制平面启动内置模拟执行器; +- agent_dry_run 会物化步骤并等待独立 Agent 领取,但不允许真实主机命令。 + +暂停、继续和紧急停止使用: + +~~~json +{ + "actor": "操作者或 AI 任务标识", + "reason": "可审计的具体原因" +} +~~~ + +常见状态码: + +| 状态码 | 含义 | +|---:|---| +| 200 | 查询或动作成功 | +| 201 | Run、注册等资源创建成功 | +| 204 | Agent 当前没有可领取步骤 | +| 400 | 业务参数或资产关系无效 | +| 401 | Agent 注册口令或 bearer token 无效 | +| 404 | Run、Host、DUT 等对象不存在 | +| 409 | 非法状态、资源冲突、旧 attempt 或幂等键冲突 | +| 422 | 请求字段或 Idempotency-Key 校验失败 | +| 503 | 生产环境缺少 Agent 注册口令 | ### 6.5 Agent 使用规则 +- 本节描述仓库已经实现但尚未发布到公网的 Agent 协议;不要向当前生产域名调用这些端点; +- Agent 是拉模式,控制平面永远不主动连接测试主机; - 生产注册需要 X-FlashOps-Enrollment-Token; - 注册成功返回的原始 bearer token 只出现一次,只能保存到权限 0600 的本机状态文件; - 后续 Agent 请求使用 Authorization: Bearer; @@ -271,6 +559,60 @@ curl -fsS 'http://127.0.0.1:8000/api/v1/runs/RUN_ID/events?after_seq=0' - 未经授权不得代表真实 Host 注册、轮换 token 或领取生产步骤; - 日常开发优先使用 make dev-agent、make agent-demo 和测试夹具。 +Agent 端点和顺序: + +| 顺序 | 方法与路径 | 行为 | +|---:|---|---| +| 1 | POST /api/v1/agent/register | 注册或轮换 token,上报 Host 指纹 | +| 2 | POST /api/v1/agent/{agent_id}/heartbeat | 上报健康、工具链和设备探针 | +| 3 | POST /api/v1/agent/{agent_id}/lease | 领取步骤,最长等待 20 秒 | +| 4 | POST /api/v1/agent/{agent_id}/steps/{step_id}/ack | 确认当前 attempt | +| 5 | POST /api/v1/agent/{agent_id}/steps/{step_id}/renew | 续租,不超过步骤 timeout | +| 6 | POST /api/v1/agent/{agent_id}/steps/{step_id}/events | 批量上报 1–100 条事件 | +| 7 | POST /api/v1/agent/{agent_id}/steps/{step_id}/complete | 上报 SUCCEEDED 或 FAILED | + +注册体至少需要 agent_version,可选 host_id 或 host_name,并可携带 os_family、os_version、kernel、 +cpu、motherboard、bios_version、ip_address、toolchain 和 labels。心跳体可携带 healthy、 +agent_version、ip_address、toolchain 和 device_probe。 + +租约请求为: + +~~~json +{"wait_timeout_s": 0} +~~~ + +wait_timeout_s 范围 0–20。ACK 与 renew 请求为 {"attempt": 1}。事件请求示例: + +~~~json +{ + "attempt": 1, + "events": [ + { + "message": "step started", + "severity": "INFO", + "payload": {} + } + ] +} +~~~ + +完成请求示例: + +~~~json +{ + "attempt": 1, + "status": "SUCCEEDED", + "exit_code": 0, + "error_class": null, + "result": {}, + "log_uri": null +} +~~~ + +ACK、renew、events、complete 都必须带新的 UUID 格式 Idempotency-Key。相同操作重放返回当前投影 +并标记 replayed=true;同一个 key 用于其他步骤或其他动作时返回 409。重新注册会轮换 token,使旧 +token 立即失效。控制平面只保存 token 的 SHA-256 摘要。 + --- ## 7. 三条不可破坏的架构红线 @@ -428,16 +770,24 @@ git push -u origin docs/ai-operating-guide | 资源 | 当前值 | |---|---| | 云服务器 | 腾讯云 CVM,62.234.39.66 | +| 操作系统 | Ubuntu 24.04 | +| DNS | DNSPod A 记录,TTL 600 | | FlashOps 域名 | flashops.imagebrewing.com | | FlashOps 服务 | flashops.service | +| 当前公网 API | 0.1.0,仅模拟 Run,未发布 Agent 路由 | +| 应用用户 | ubuntu | | 应用目录 | /data/wangzhan/app/storage-labos | | 内部端口 | 127.0.0.1:18080 | | 应用数据 | /data/wangzhan/app/storage-labos/flashops/var | | 私有环境文件 | /etc/flashops/flashops.env | +| FlashOps Nginx | /www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf | +| FlashOps 证书 | /etc/letsencrypt/live/flashops.imagebrewing.com/ | | Gitea 域名 | git.imagebrewing.com | | Gitea 容器 | gitea,镜像版本固定为 1.27.0 | | Gitea 内部端口 | 127.0.0.1:13000 | | Gitea 实时数据 | /data/gitea/data | +| Gitea Nginx | /www/server/panel/vhost/nginx/git.imagebrewing.com.conf | +| Gitea 证书 | /etc/letsencrypt/live/git.imagebrewing.com/ | | COS 挂载 | /chucun | | Gitea 备份 | /chucun/wangzhan-production/backups/gitea/ | @@ -457,18 +807,228 @@ git push -u origin docs/ai-operating-guide FlashOps 当前是匿名预览环境。接入真实硬件或非公开数据前,必须先完成应用级 SSO / RBAC、 审批、审计和命令模板签名;不能只依赖 Nginx Basic Auth。 +### 11.1 FlashOps 服务管理与健康检查 + +只有获得服务器运维授权后才执行: + +~~~bash +sudo systemctl status flashops --no-pager -l +sudo systemctl is-enabled flashops +curl -fsS http://127.0.0.1:18080/api/v1/health +curl -fsS https://flashops.imagebrewing.com/api/v1/health +~~~ + +需要重启时先确认没有不可中断的 Run,再执行: + +~~~bash +sudo systemctl restart flashops +sudo systemctl status flashops --no-pager -l +curl -fsS http://127.0.0.1:18080/api/v1/health +~~~ + +服务必须只绑定 127.0.0.1:18080,不能把 Uvicorn 直接暴露公网。当前 SQLite 只允许运行一个 +worker;增加 worker 前必须先迁移 PostgreSQL,并处理跨进程事件总线与锁。 + +关键环境变量: + +| 变量 | 作用 | +|---|---| +| FLASHOPS_ENV | production 或 development | +| FLASHOPS_DATABASE_URL | 数据库 DSN | +| FLASHOPS_OBJECT_STORE_URL | 证据对象目录或对象存储 | +| FLASHOPS_BUS_URL | 外部事件总线,当前未启用 | +| FLASHOPS_AGENT_ENROLLMENT_TOKEN | Agent 首次注册口令 | +| FLASHOPS_API_HOST | API 监听地址 | +| FLASHOPS_API_PORT | API 端口 | +| FLASHOPS_CORS_ORIGINS | 允许的前端来源 | +| FLASHOPS_ENGINE_TICK_S | 引擎 tick | +| FLASHOPS_TIME_SCALE | 模拟时间倍率 | + +生产秘密只放在 root-only、权限 0600 的 /etc/flashops/flashops.env。不得把值写进 systemd unit、 +Nginx 注释、命令历史、聊天或 Git。 + +### 11.2 日志 + +~~~bash +sudo journalctl -u flashops -n 200 --no-pager +sudo journalctl -u flashops --since '30 minutes ago' --no-pager +sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.log +sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.error.log +~~~ + +持续跟踪日志只能用于短时诊断,结束后退出,避免长期占用会话。输出日志前先检查是否包含 token、 +客户数据、设备序列号或其他敏感信息。 + +### 11.3 Nginx、DNS 与证书 + +任何配置修改都必须先测试,成功后才能 reload: + +~~~bash +sudo nginx -t +sudo systemctl reload nginx +sudo certbot certificates +getent ahostsv4 flashops.imagebrewing.com +getent ahostsv4 git.imagebrewing.com +~~~ + +证书模拟续期: + +~~~bash +sudo certbot renew --dry-run --non-interactive --no-random-sleep-on-renew +~~~ + +两个域名应指向 62.234.39.66;HTTP 应 301 到 HTTPS;证书 SAN 必须匹配访问域名。ACME challenge +路径必须保持可达。证书续期后由 deploy hook reload Nginx。 + +FlashOps 当前配置不应包含 auth_basic、allow 或 deny 访问限制。若匿名访问出现 401/403,先检查 +浏览器旧认证缓存、Nginx 生效配置和上游安全产品;Agent API 因 token 无效返回 401 是另一回事。 + +### 11.4 标准发布流程 + +1. 确认目标 Git 提交、工作树和本次发布范围; +2. 在本地执行 make test 与 make demo; +3. 检查暂存和发布内容不含 .venv、var、缓存、凭据和运行产物; +4. 使用 SQLite 在线备份 API 备份当前生产数据库; +5. 保存当前可运行发布包、SHA-256 和回滚说明; +6. 将新版本上传到独立 incoming 目录,不覆盖正在运行的目录; +7. 校验上传包 SHA-256,安装或复用服务器虚拟环境; +8. 保留生产 flashops/var,绝不能用本地空 var 覆盖; +9. 校验 systemd unit 和私有环境文件权限; +10. 切换版本并重启 flashops; +11. 轮询 127.0.0.1:18080/api/v1/health; +12. 执行 nginx -t,必要时 reload; +13. 从服务器内部和外部各验证一次 HTTPS、最终跳转、health 和 dashboard JSON; +14. 检查最近日志无新 error,并记录提交号、时间、备份和回滚点。 + +不要在生产目录直接手工改源码。发布物必须能映射到 Git 提交。发布失败时优先切回已经验证的旧发布 +目录,并保留失败目录和日志用于排查,不能边修边覆盖证据。 + +### 11.5 SQLite 备份与恢复边界 + +运行中的 SQLite 应使用在线备份 API,不要直接复制可能正在写入的数据库: + +~~~bash +sudo /data/wangzhan/app/storage-labos/flashops/.venv/bin/python - <<'PY' +import datetime +import sqlite3 +from pathlib import Path + +source = Path('/data/wangzhan/app/storage-labos/flashops/var/flashops.db') +stamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S') +target = Path('/data/wangzhan/backups') / ('flashops-db-' + stamp + '.db') +target.parent.mkdir(parents=True, exist_ok=True) +with sqlite3.connect(source) as src, sqlite3.connect(target) as dst: + src.backup(dst) +print(target) +PY +~~~ + +备份后记录文件大小和 SHA-256。数据库恢复是高影响动作,本文不授权 AI 自动执行。获明确授权后也要: + +1. 停止 FlashOps; +2. 核对恢复目标、备份时间和校验值; +3. 再备份当前数据库; +4. 替换后修复 ubuntu:ubuntu 所有权和文件权限; +5. 启动服务; +6. 验证 health、表数量、最新 Run 和事件 seq 连续性; +7. 保留恢复前数据库,直到验收完成。 + +### 11.6 Gitea 运维与 COS 备份 + +Gitea 公开仓库允许匿名查看和 HTTPS 克隆;账号注册后需要管理员人工批准。当前不开放独立 Git SSH +端口,也没有生产 Actions Runner。 + +获授权后的只读检查: + +~~~bash +sudo docker ps --filter name=^gitea$ +sudo docker logs --tail 100 gitea +curl -fsS http://127.0.0.1:13000/api/healthz +sudo systemctl status flashops-gitea-backup.timer --no-pager +sudo systemctl list-timers flashops-gitea-backup.timer --no-pager +~~~ + +手工触发一次可恢复的 Gitea dump: + +~~~bash +sudo systemctl start flashops-gitea-backup.service +sudo systemctl status flashops-gitea-backup.service --no-pager +find /chucun/wangzhan-production/backups/gitea -maxdepth 1 -type f \ + -name 'gitea-*.zip' -ls +~~~ + +备份脚本使用 Gitea 自身 dump,生成 ZIP 和 SHA-256 sidecar,成功复制到 COS 后清理本机临时包。 +定时器每日约 03:20 运行并带随机延迟。实时仓库、Git 对象和 Gitea SQLite 必须留在 +/data/gitea/data 本地文件系统;COSFS 不提供数据库依赖的完整锁与原子语义,只能用于备份。 + +Gitea 恢复或升级是高影响操作:必须先验证备份、阅读目标版本迁移说明、安排维护窗口,并保留旧容器 +镜像和 /data/gitea/data 快照。镜像必须固定版本,不能使用 latest。 + +### 11.7 故障定位 + +公网 502: + +~~~bash +sudo systemctl status flashops --no-pager -l +sudo journalctl -u flashops -n 200 --no-pager +curl -v http://127.0.0.1:18080/api/v1/health +sudo nginx -t +sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.error.log +~~~ + +常见原因是服务未启动、Python 依赖错误、18080 端口不一致、var 不可写或 systemd 安全策略阻止路径。 + +SQLite locked: + +- 确认只有一个 Uvicorn worker; +- 检查是否有脚本绕过 session 直接写数据库; +- 所有状态写入应走同一事务和事件入口; +- 需要多进程时迁移 PostgreSQL,不能继续增加 SQLite 锁重试掩盖问题。 + +Gitea 页面不可访问: + +~~~bash +sudo docker ps --filter name=^gitea$ +sudo docker logs --tail 100 gitea +curl -fsS http://127.0.0.1:13000/api/healthz +getent ahostsv4 git.imagebrewing.com +sudo nginx -t +~~~ + +HTTPS 证书异常时核对 DNS、证书路径、证书域名、有效期和 Nginx 是否已 reload。不要在原因未确认时 +重复签发证书或删除现有证书目录。 + +### 11.8 生产验收清单 + +- [ ] flashops.service 为 active; +- [ ] Gitea 容器为 running,loopback health 为 pass; +- [ ] Nginx active 且 nginx -t 成功; +- [ ] 两个域名解析到 62.234.39.66; +- [ ] HTTP 301 到 HTTPS; +- [ ] 两张证书域名正确、未过期、自动续期可用; +- [ ] 未登录访问 FlashOps 控制台最终返回 200; +- [ ] FlashOps health 返回 status=ok; +- [ ] dashboard summary 返回合法 JSON; +- [ ] 未登录可以查看并克隆 public Git 仓库; +- [ ] 生产 var 数据可写,但应用源码目录不被随意写入; +- [ ] 最新日志没有新增 error; +- [ ] FlashOps 数据库备份有 SHA-256; +- [ ] Gitea COS dump 有 ZIP 和有效 SHA-256 sidecar; +- [ ] 发布提交号、时间和回滚点已经记录。 + --- ## 12. 常见任务剧本 ### 12.1 “看看项目并继续完善” -1. 阅读本文件、README 和相关源码; -2. 运行测试与演示建立基线; -3. 区分已实现、部分实现和待实现; -4. 优先选择一个能独立验收的纵向切片; -5. 实现、补测试、同步文档; -6. 报告实际证据,不把规划写成完成。 +1. 以本文的能力边界、状态机和安全规则建立基线; +2. 若获得仓库,检查工作树、源码和测试;只有本文时先做线上只读核验; +3. 运行可用的测试与模拟演示; +4. 区分已实现、部分实现和待实现; +5. 优先选择一个能独立验收的纵向切片; +6. 实现、补测试、同步本文; +7. 报告实际证据,不把规划写成完成。 ### 12.2 “排查网站打不开” @@ -480,12 +1040,12 @@ FlashOps 当前是匿名预览环境。接入真实硬件或非公开数据前 ### 12.3 “修改 API” -1. 阅读路由、schema、service 和相关测试; +1. 只有获得仓库后才能实施修改;先核对路由、schema、service 和测试; 2. 保持业务逻辑不堆在 route; 3. 状态写入走事件记录; 4. 为正常与错误路径增加测试; 5. 检查 OpenAPI; -6. 更新 ProjectManual 和本文件中的接口索引。 +6. 更新本文中的接口、状态码、权限和部署说明。 ### 12.4 “增加真实硬件能力”