# FlashOps 项目开发手册 (Project Manual) 本文档用于 FlashOps / STORAGE LABOS 项目的开发、测试、部署、运维与交接。内容以 2026-07-27 的实际源码和生产环境为准;目标架构与尚未实现的能力会明确标注,避免把原型能力 误认为真实硬件能力。 > **文档状态**:V1.2 · 已与当前代码、Git 仓库和云端部署边界核对 > > **生产地址**: > > **团队 Git**: > > **后端代号**:FlashOps > > **控制台品牌**:STORAGE LABOS > > **安全提醒**:本文不记录 SSH 密码、Gitea 管理员密码、私钥或其他明文凭据。 --- ## 目录 1. [项目概述](#1-项目概述) 2. [快速开始](#2-快速开始) 3. [系统架构](#3-系统架构) 4. [核心业务流程与状态机](#4-核心业务流程与状态机) 5. [安全门禁与危险动作](#5-安全门禁与危险动作) 6. [数据库与领域模型](#6-数据库与领域模型) 7. [API 接口设计](#7-api-接口设计) 8. [前端控制台](#8-前端控制台) 9. [工作流、Agent 与适配器](#9-工作流agent-与适配器) 10. [证据、失败分析与指标](#10-证据失败分析与指标) 11. [配置与环境变量](#11-配置与环境变量) 12. [腾讯云生产部署与运维](#12-腾讯云生产部署与运维) 13. [测试、质量门与开发规范](#13-测试质量门与开发规范) 14. [已知边界与演进路线](#14-已知边界与演进路线) 15. [故障排查](#15-故障排查) 16. [术语表与 FAQ](#16-术语表与-faq) 17. [变更记录](#17-变更记录) --- ## 1. 项目概述 ### 1.1 背景 固件回归测试通常跨越数小时甚至数天,期间可能发生 Agent 崩溃、操作系统蓝屏、测试盘掉盘、 网络中断或整机失联。传统做法依赖工程师通宵值守、手工重启、记录截图和拼装报告,成本高, 而且很难证明“当时到底发生了什么”。 FlashOps 的目标是把一条真实实验室 SOP 变成可执行、可恢复、可审计、可取证的状态机: - 自动执行固件 A/B 回归与循环测试; - 在破坏性操作前阻止错盘、系统盘和未授权 DUT; - 主机失联后通过独立带外通道逐级恢复; - 从安全检查点继续,而不是盲目重跑非幂等步骤; - 统一记录控制平面、Agent、OOB 和人工操作事件; - 生成可回放的 Evidence Bundle; - 用无人值守完成率和人工触碰次数量化节省的值守成本。 ### 1.2 主要用户 | 用户 | 主要任务 | |---|---| | 固件 / SSD 测试工程师 | 创建回归任务、查看实时进度、分析失败证据 | | 实验室管理员 | 管理 DUT、测试主机、带外控制器和危险动作授权 | | 研发工程师 | 对比固件版本、复现失败、读取时间线与环境指纹 | | 测试负责人 | 查看 UCR、证据完整率、阻断问题和工位利用情况 | | 运维人员 | 维护控制平面、数据库、证书、日志和备份 | ### 1.3 当前可运行基线 当前版本已经具备一条无需真实硬件即可端到端运行的纵向链路: ```text 创建任务 → 六项安全预检 → DUT / 测试主机资源加锁 → N 次模拟回归循环 → 注入 Agent 心跳丢失 → L1 Agent 软恢复失败 → L2 OS 重启失败 → L3 带外 Reset 恢复 → 校验环境指纹并从检查点续跑 → 生成 Evidence Bundle → 控制台展示状态与事件时间线 ``` ### 1.4 能力状态 | 能力 | 状态 | 说明 | |---|---|---| | FastAPI 控制平面 | 已实现 | 任务、预检、状态、人工操作、看板接口 | | 14 张持久化表 | 已实现 | SQLite 可直接建库和灌种子数据 | | Run 事件溯源 | 已实现 | 状态变化统一经 `events/recorder.py` 写入 | | Run 状态转移约束 | 已实现 | 非法转移抛出异常,终态不可继续 | | 六项安全门禁 | 已实现 | 序列号、授权、系统盘、固件、Host、OOB | | L1–L5 恢复决策 | 已实现 | 演示链路实际执行到 L3;L4/L5 为策略能力 | | 检查点续跑 | 模拟实现 | 当前保存循环边界检查点 | | Evidence Bundle | 最小实现 | `manifest.json` + `events.ndjson` | | 控制台 | 部分动态 | 任务中心和实时运行接 API,其余主要为产品原型 | | 生产部署 | 已实现 | 腾讯云 CVM + systemd + Nginx + HTTPS | | 团队 Git 服务 | 已实现 | Gitea 1.27 + HTTPS + PR/Issue + COS 定时备份 | | 独立 Host Agent | 部分实现 | 注册、token 摘要、主机探针、心跳及状态老化已实现;Step 租约待实现 | | 真实 NVMe / OOB 接入 | 未实现 | 尚未连接可牺牲 DUT、PDU 或 KVM | | 失败指纹聚类执行逻辑 | 未实现 | 数据模型和界面已定义,服务逻辑待接入 | | 应用级账号与 RBAC | 未实现 | FlashOps 预览站当前匿名开放;接真实硬件前必须恢复认证与审批 | ### 1.5 系统边界 当前版本是“可演示、可开发、可部署的控制平面基线”,不是已经可以对真实盘执行刷写和 Sanitize 的生产硬件平台。以下行为不得通过修改模拟数据来伪装完成: - 不得宣称 Step 租约、真实命令执行和真实 OOB 已经交付; - 不得把 `sim://` 的固件、OOB 和证据 URI 当作真实硬件结果; - 匿名预览环境不得接入真实硬件、客户数据或有效危险命令;接入前必须启用应用级认证、 审批和命令模板白名单; - 不得直接把生产部署的 SQLite 单机形态扩展为多 worker; - 不得绕过安全门禁向真实设备下发自由文本命令。 ### 1.6 技术栈 | 层 | 技术 | 当前用途 | |---|---|---| | 后端 | Python 3.9+ / FastAPI | HTTP API 与应用生命周期 | | ORM | SQLAlchemy 2.x Async | 领域模型、事务和查询 | | 开发数据库 | SQLite + aiosqlite | 零外部依赖启动 | | 目标数据库 | PostgreSQL 16 + asyncpg | 多工位生产形态,尚未切换 | | 服务进程 | Uvicorn | ASGI 运行时 | | 校验 | Pydantic 2.x | API 请求体校验 | | 工作流配置 | YAML / JSON | SOP 与步骤定义 | | 测试 | pytest / pytest-asyncio / httpx | 纯函数与 API 端到端测试 | | 前端 | 静态 HTML + Design System JS/CSS | 产品控制台原型和部分 API 接入 | | 生产守护 | systemd | 单 worker 服务、开机自启、故障重启 | | 入口 | 宝塔 Nginx | HTTPS、反代和安全响应头;FlashOps 预览站匿名开放 | | Git 协作 | Gitea 1.27 | 仓库、提交历史、分支、Issue、Pull Request 与成员管理 | | 证书 | Let's Encrypt / Certbot | HTTPS 与自动续期 | ### 1.7 目录结构 ```text STORAGE LABOS/ ├── .github/ PR 模板与 CI 工作流模板 ├── .gitignore / .gitattributes 忽略规则与跨平台文本规则 ├── README.md Git 仓库首页 ├── CONTRIBUTING.md 团队 Git 协作入口 ├── ProjectManual.md 本手册 ├── flashops/ 后端与技术文档 │ ├── Makefile 本地开发统一入口 │ ├── README.md 项目短说明和能力状态 │ ├── docker-compose.yml 目标生产拓扑草案 │ ├── deploy/ 当前腾讯云生产配置 │ │ ├── README.md │ │ ├── flashops.service │ │ ├── nginx-http.conf │ │ ├── nginx-https.conf │ │ └── certbot-reload-nginx.sh │ ├── docs/ 架构、协议、状态机、ADR │ │ ├── architecture.md │ │ ├── domain-model.md │ │ ├── state-machine.md │ │ ├── workflow-spec.md │ │ ├── agent-protocol.md │ │ ├── adapter-sdk.md │ │ └── decisions/ │ ├── services/control-plane/ │ │ ├── requirements.txt │ │ ├── flashops_control/ │ │ │ ├── api/ HTTP 路由 │ │ │ ├── engine/ 状态转移与恢复决策 │ │ │ ├── events/ 事件写入口 │ │ │ ├── evidence/ 证据包生成 │ │ │ ├── models/ 14 张表 │ │ │ ├── safety/ 六项安全门禁 │ │ │ ├── services/ Run 编排和模拟执行器 │ │ │ ├── cli.py 建库、种子、演示命令 │ │ │ ├── config.py 配置入口 │ │ │ ├── db.py 异步会话与 SQLite 并发保护 │ │ │ └── main.py FastAPI 应用入口 │ │ └── tests/ │ ├── services/host-agent/ │ │ ├── README.md 独立 Agent 运行与配置 │ │ ├── flashops_agent/ 注册、指纹、凭据和心跳客户端 │ │ └── tests/ │ └── var/ 本地数据库、对象和证据(不入库) └── 前端UI八页面完成/ 控制台静态页面与设计系统 ├── Dashboard.dc.html ├── Tasks.dc.html ├── LiveRun.dc.html ├── Evidence.dc.html ├── Failures.dc.html ├── Assets.dc.html ├── Compare.dc.html ├── Workflows.dc.html ├── ConsoleNav.dc.html ├── ds-base.js / support.js └── _ds/ 设计系统资源 ``` ### 1.8 服务地址与端口 | 环境 | 地址 / 端口 | 用途 | |---|---|---| | 本地开发 | `http://localhost:8000` | API 与控制台同源服务 | | 本地控制台 | `/console/Dashboard.dc.html` | 控制台入口 | | 本地 API 文档 | `/docs` | FastAPI OpenAPI UI | | 生产内部监听 | `127.0.0.1:18080` | Uvicorn,仅服务器本机可访问 | | 生产公网 | `https://flashops.imagebrewing.com` | Nginx HTTPS 入口 | | 团队 Git | `https://git.imagebrewing.com` | Gitea 页面、HTTPS clone、PR 与成员管理 | | 生产 HTTP | `80` | ACME challenge;其他请求跳转 HTTPS | | 生产 HTTPS | `443` | TLS + 反向代理;FlashOps 预览站无需登录 | --- ## 2. 快速开始 ### 2.1 环境准备 - macOS、Linux 或 WSL; - Python 3.9 或更高版本; - GNU Make; - 本地运行不需要 Node、Docker、PostgreSQL 或 Redis。 ### 2.2 首次初始化 ```bash cd "/Users/yuanshuai/Desktop/STORAGE LABOS/flashops" make setup ``` `make setup` 会执行以下动作: 1. 创建 `flashops/.venv`; 2. 安装 `services/control-plane/requirements.txt`; 3. 重建本地 `var/flashops.db`; 4. 创建 `var/objects/`; 5. 灌入模拟工作流、DUT、Host、OOB 和固件数据。 > `make setup` 内含 `db-reset`,会删除本地 `var/` 中的开发数据。不要在生产目录执行。 ### 2.3 启动开发服务 ```bash make dev ``` 打开: - 总览: - 任务中心: - 实时运行: - API 文档: ### 2.4 跑一条完整演示 ```bash make demo ``` 演示默认运行 8 个循环,并注入一次心跳丢失。完成后会输出 Run ID 和 Evidence Bundle 路径。 也可以在控制台“任务中心”先执行安全预检,再创建任务,然后进入“实时运行”观察事件时间线。 ### 2.5 运行测试 ```bash make test ``` 当前基线共有 25 项测试,覆盖: - Run 状态转移; - L1–L5 恢复阶梯选择; - 系统盘 / 分区保护; - 六项安全预检; - 完整模拟 Run、故障恢复、Evidence Bundle; - 暂停、继续、紧急停止与人工触碰审计; - Agent 注册、token 轮换、Bearer 鉴权和本地凭据权限; - 心跳状态老化:15 秒降级、30 秒离线。 ### 2.6 常用 Make 命令 | 命令 | 作用 | |---|---| | `make setup` | 创建虚拟环境、安装依赖、重建开发库 | | `make setup-py` | 只准备 Python 依赖 | | `make db-reset` | 删除本地数据库和对象目录后重建 | | `make seed` | 向空库灌入种子数据;已有工作流时不重复写 | | `make demo` | CLI 跑完整模拟链路 | | `make dev` | 启动 API 和同源控制台 | | `make dev-agent` | 注册 Agent 并发送一次真实进程心跳 | | `make dev-agent-loop` | 持续运行 Agent 心跳循环 | | `make test` | 执行完整测试集 | | `make e2e` | 只跑端到端纵向测试 | | `make clean` | 清理本地 `var/`、缓存和 `__pycache__` | --- ## 3. 系统架构 ### 3.1 五层架构 ```text ┌──────────────────────────────────────────────────────────────┐ │ 交互与集成层 静态控制台 · CLI · OpenAPI │ ├──────────────────────────────────────────────────────────────┤ │ 控制平面 FastAPI · Safety · Run Service · Event Store │ ├──────────────────────────────────────────────────────────────┤ │ 执行平面 当前:内置模拟器 + Agent 心跳;目标:Step 执行 │ ├──────────────────────────────────────────────────────────────┤ │ 带外平面 当前:模拟 OOB;目标:KVM / PDU / ATX / AC │ ├──────────────────────────────────────────────────────────────┤ │ 证据与智能层 统一事件流 · Evidence Bundle · Failure Signature│ └──────────────────────────────────────────────────────────────┘ ``` ### 3.2 依赖方向 ```text Browser / CLI ──HTTP──▶ Control Plane ◀──HTTP pull── Host Agent(部分落地) │ │ ├──▶ Database ├──▶ Adapters ├──▶ Object Store └──▶ OOB └──▶ Event Timeline ``` 硬规则: 1. Agent 不导入控制平面代码,只按 `docs/agent-protocol.md` 通信; 2. 控制平面不主动反连客户主机,Agent 采用拉模式; 3. 状态机纯逻辑不依赖 FastAPI、SQLAlchemy、文件系统或真实时钟; 4. API 路由只做校验与服务调用,不承载复杂业务决策; 5. 危险动作必须先过控制平面安全门禁; 6. Run 状态不得绕过 `events/recorder.py` 直接修改; 7. 日志正文和大文件进入对象存储,事件表只保存索引、摘要和 URI。 ### 3.3 当前运行架构 ```text Internet │ ▼ DNSPod: flashops.imagebrewing.com → 62.234.39.66 │ ▼ Nginx :443 ├── TLS 1.2 / 1.3 ├── 安全响应头 └── proxy_pass http://127.0.0.1:18080 │ ▼ systemd: flashops.service │ ▼ Uvicorn 单 worker / FastAPI ├── SQLite: var/flashops.db ├── Evidence: var/objects/ └── Console: ../前端UI八页面完成/ ``` ### 3.4 开发形态与目标形态 | 组件 | 当前开发 / 线上基线 | 目标多工位形态 | |---|---|---| | 数据库 | SQLite | PostgreSQL 16 | | 事务并发 | 应用级串行 SQLite session | 数据库行锁与队列 | | 进程数 | 1 worker | 多 worker / 多实例 | | 事件总线 | 进程内任务 | Redis / NATS | | 对象存储 | 本地目录 | MinIO / S3 / COS | | 执行器 | Run 仍由内置模拟器执行;独立 Agent 已能注册和心跳 | Agent 领取并执行 Step | | OOB | 模拟 | JetKVM / PDU / 主板控制器 | | 认证 | 匿名预览,无应用账号 | OIDC / SSO + RBAC + 审批 | ### 3.5 关键架构决策 `flashops/docs/decisions/` 记录已经接受的 ADR: - ADR-0001:Monorepo 与技术栈; - ADR-0002:Run 采用事件溯源; - ADR-0003:首版自研状态机,不依赖 Temporal / Airflow; - ADR-0004:安全门禁放在控制平面,Agent 保持简单; - ADR-0005:模拟器是一等产品能力,不是临时测试脚手架。 修改这些边界前,应新增 ADR,而不是只改代码。 --- ## 4. 核心业务流程与状态机 ### 4.1 创建任务 任务创建时会冻结以下信息: - 工作流 key、version 和 `spec_hash`; - DUT、Host、固件 A、固件 B; - 循环次数和是否注入故障; - OS、Kernel、BIOS、Agent、工具链、DUT 序列号和型号; - 环境指纹哈希; - 创建人和创建时间。 创建后依次写入 `RUN_CREATED` 和 `RUN_QUEUED` 事件,再启动模拟执行任务。 ### 4.2 Run 状态机 ```text QUEUED ──▶ PREFLIGHT ──▶ RUNNING ──▶ COMPLETED │ │ │ │ └──▶ REJECTED │ │ ├──▶ PAUSED ──▶ RUNNING │ ├──▶ RECOVERING ──▶ RUNNING │ │ └──▶ FROZEN └──────────────────────────┴──────────────▶ ABORTED ``` | 当前状态 | 允许转移到 | |---|---| | `QUEUED` | `PREFLIGHT`, `ABORTED` | | `PREFLIGHT` | `RUNNING`, `REJECTED`, `ABORTED` | | `RUNNING` | `RECOVERING`, `PAUSED`, `FROZEN`, `COMPLETED`, `ABORTED` | | `RECOVERING` | `RUNNING`, `FROZEN`, `ABORTED` | | `PAUSED` | `RUNNING`, `ABORTED` | | `FROZEN` | 无,终态 | | `COMPLETED` | 无,终态 | | `ABORTED` | 无,终态 | | `REJECTED` | 无,终态 | 非法转移由 `engine/states.py::assert_run_transition()` 拒绝。 ### 4.3 Step 状态机(目标契约) ```text PENDING → DISPATCHED → RUNNING → SUCCEEDED ├──→ FAILED ├──→ TIMED_OUT ├──→ SKIPPED └──→ CANCELLED ``` `run_steps` 表已经定义状态、attempt、租约、输入输出和错误字段,但当前内置模拟器主要在 Run 和循环层执行,完整 Step runtime / Agent 租约尚未接入。 ### 4.4 恢复阶梯 | 级别 | 名称 | 条件与动作 | |---|---|---| | L1 | `L1_AGENT_SOFT` | Agent 在线时优雅停止或重启执行进程 | | L2 | `L2_OS_REBOOT` | Host 网络仍在线时请求操作系统重启 | | L3 | `L3_OOB_RESET` | OOB 在线时触发主板 Reset | | L4 | `L4_OOB_ATX_POWER` | 模拟 ATX 长按关机并重新开机 | | L5 | `L5_OOB_AC_CYCLE` | 整机 AC 断电,等待安全间隔后上电 | | Freeze | `FREEZE` | 停止自动动作,保留现场并等待人工 | 数据完整性失败不走逐级恢复,直接 `FREEZE`,防止自动动作覆盖关键证据。 ### 4.5 检查点语义 当前模拟执行器在循环结束时保存: ```json { "loop_index": 3, "next_step_key": "regression-loop", "dut_fw": "B" } ``` 恢复成功后必须先确认环境指纹和物理状态仍符合预期,再写 `CHECKPOINT_RESUMED`。 刷写、Format、Sanitize 等非幂等步骤默认不得因为进程重启自动重复执行。 ### 4.6 暂停、继续与紧急停止 - 暂停:仅 `RUNNING → PAUSED`; - 继续:仅 `PAUSED → RUNNING`; - 紧急停止:任何非终态可进入 `ABORTED`; - 每次人工操作先写 `HUMAN_TOUCH`,递增 `human_touches`,并把 `unattended_completion` 设为 `false`; - 紧急停止会释放资源锁并取消内置模拟任务。 ### 4.7 SQLite 并发保护 模拟器与人工暂停可能同时写 `run_events`。SQLite 只允许一个 writer;如果两个会话同时读取 `event_seq` 再写入,会发生唯一键冲突。当前 `db.py` 对 SQLite 的完整 `session_scope` 事务按事件 循环串行化,保证事件序号和投影一致。 这项保护只适合单进程 / 单 worker 基线。切换 PostgreSQL 后不使用应用级锁,并需要增加 PostgreSQL 并发集成测试。 --- ## 5. 安全门禁与危险动作 ### 5.1 默认拒绝原则 安全判断发生在控制平面,并且必须早于命令下发。Agent 只能执行已批准模板和经过 schema 校验的参数,不能接收来自界面的自由文本 shell 命令。 ### 5.2 六项预检 | key | 门禁 | 通过条件 | |---|---|---| | `serial` | DUT 序列号白名单 | 期望序列号非空且等于现场发现序列号 | | `destructive` | 破坏性写入授权 | `duts.allow_destructive = true` | | `system_disk` | 系统盘 / 分区保护 | DUT 路径不命中系统盘及其任一分区 | | `firmware` | 固件适配型号 | DUT 型号在固件适用列表中,或列表为空 | | `host` | 测试主机与 Agent | Host 状态为 `ONLINE` | | `oob` | 带外控制器 | 绑定 OOB 存在且状态为 `ONLINE` | 六项全部通过,`passed` 才为 `true`。 ### 5.3 系统盘保护 `safety/gates.py::_same_device()` 同时比较完整设备名和分区归属。例如: - 目标 `/dev/nvme0n1`,系统盘 `/dev/nvme0n1`:拒绝; - 目标 `/dev/nvme0n1p2`,系统盘 `/dev/nvme0n1`:拒绝; - 目标 `/dev/nvme1n1`,系统盘 `/dev/nvme0n1`:可继续检查其他门禁。 真实 Agent 接入时还应补充:挂载点、root filesystem、启动标志、Windows Disk Number、BDF 和序列号的现场交叉校验。 ### 5.4 危险级别 | 级别 | 含义 | |---|---| | `none` | 只读、报告或普通控制动作 | | `low` | 有状态变化但可逆 | | `high` | 刷写、Format、Sanitize、断电等,需要审批与审计 | ### 5.5 进入真实实验室前必须补齐 1. 命令模板注册表与 JSON Schema 参数校验; 2. 不可绕过的系统盘识别; 3. 应用账号、RBAC、危险动作审批和双人复核; 4. Agent 身份、短期令牌、请求签名与重放保护; 5. 审批人、模板版本、参数、执行结果和撤销动作审计; 6. 真实 OOB 的安全间隔、最大恢复次数和硬件熔断; 7. 真实 DUT 的可牺牲测试环境,禁止先在业务盘验证。 --- ## 6. 数据库与领域模型 ### 6.1 当前数据库 - 本地:`flashops/var/flashops.db`; - 生产:`/data/wangzhan/app/storage-labos/flashops/var/flashops.db`; - 驱动:`sqlite+aiosqlite`; - 表由 SQLAlchemy `Base.metadata.create_all()` 创建; - 应用启动时会确保表和种子数据存在; - 当前无 Alembic 迁移,修改表结构时需要先补迁移方案。 ### 6.2 14 张表 | 领域 | 表 | 作用 | |---|---|---| | 资产 | `oob_controllers` | 带外控制器、能力、状态、电源和温度 | | 资产 | `test_hosts` | 操作系统、Agent、工具链、心跳和 OOB 绑定 | | 资产 | `duts` | 序列号、型号、BDF、设备路径、危险授权和健康信息 | | 资产 | `firmware_artifacts` | 版本、哈希、签名、适用型号和文件 URI | | 工作流 | `workflows` | SOP spec、版本、哈希、危险级别和审批人 | | 运行 | `runs` | 当前状态投影、环境指纹、进度、检查点和结论 | | 运行 | `run_steps` | 步骤状态、attempt、租约、输入输出和错误 | | 事件 | `run_events` | 每个 Run 内单调递增的 append-only 时间线 | | 恢复 | `recovery_actions` | 触发原因、恢复级别、结果、详情和证据 URI | | 调度 | `resource_locks` | DUT / Host 独占锁,防止并发踩踏 | | 分析 | `failure_signatures` | 失败聚类签名和统计 | | 分析 | `run_failures` | 单次失败实例及其分类 | | 证据 | `evidence_bundles` | 证据包 URI、manifest、大小和完整率 | | 审计 | `audit_log` | 操作者、动作、命令模板、参数、审批和结果 | ### 6.3 事件溯源 ```text run_events(顺序真相) │ ├──投影──▶ runs ├──投影──▶ run_steps └──关联──▶ recovery_actions / evidence_bundles / failures ``` `runs` 是为了查询效率保存的当前投影,`run_events` 才是完整顺序真相。事件的 `seq` 由控制平面 分配,不能用不同机器的时间戳代替排序。 ### 6.4 关键字段 - `runs.spec_hash`:任务使用的工作流不可变版本; - `runs.env_fingerprint` / `env_fingerprint_hash`:A/B 可比性和失败聚类输入; - `runs.event_seq`:每个 Run 的下一事件序号来源; - `runs.checkpoint`:安全续跑位置; - `runs.unattended_completion` / `human_touches`:UCR 与人工介入统计; - `duts.serial` / `device_path` / `allow_destructive`:危险动作三信号; - `firmware_artifacts.sha256` / `signature`:固件完整性与来源; - `evidence_bundles.completeness` / `missing_fields`:证据质量; - `resource_locks` 的资源类型和资源 ID 唯一约束:防止同一资产被重复占用。 ### 6.5 数据文件与 Git 以下内容属于运行产物,不进入 Git: ```text flashops/var/ flashops/.venv/ flashops/.pytest_cache/ *.db *.db-journal *.log .env .env.local ``` --- ## 7. API 接口设计 ### 7.1 基础约定 - 前缀:`/api/v1`; - 数据格式:JSON; - 时间:API 输出 ISO 8601 UTC 字符串; - 开发文档:`/docs`; - 当前应用内部没有用户认证中间件; - 当前公网预览站未启用 Nginx Basic Auth,页面和普通 API 可匿名访问; - 匿名预览只能使用模拟资产。进入真实硬件或多用户阶段前必须增加应用级认证、RBAC、 审批和审计上下文。 ### 7.2 接口清单 | 方法 | 路径 | 作用 | |---|---|---| | GET | `/api/v1/health` | 服务健康检查 | | POST | `/api/v1/preflight` | 对指定或默认资产执行六项预检 | | POST | `/api/v1/runs` | 创建 Run 并启动模拟执行 | | POST | `/api/v1/runs/demo` | 用默认参数创建演示 Run | | GET | `/api/v1/runs?limit=30` | 查询最近 Run,limit 限制 1–100 | | GET | `/api/v1/runs/{run_id}` | 查询 Run 当前投影和 Evidence Bundle | | GET | `/api/v1/runs/{run_id}/events?after_seq=0` | 增量查询事件时间线 | | POST | `/api/v1/runs/{run_id}/pause` | 人工暂停 RUNNING 任务 | | POST | `/api/v1/runs/{run_id}/resume` | 人工继续 PAUSED 任务 | | POST | `/api/v1/runs/{run_id}/emergency-stop` | 紧急停止非终态任务 | | GET | `/api/v1/dashboard/summary` | 汇总任务数、活动数、完成数和 UCR | | POST | `/api/v1/agent/register` | 首次注册或轮换 Agent token | | POST | `/api/v1/agent/{agent_id}/heartbeat` | Bearer 心跳、主机状态和探针上报 | | GET | `/api/v1/agent/hosts` | 查询 Host / Agent 在线投影,不返回 token | ### 7.3 请求模型 #### PreflightBody ```json { "dut_id": null, "host_id": null, "firmware_id": null, "discovered_serial": null } ``` 全部为空时使用第一组种子资产。`discovered_serial` 可用于模拟现场序列号不一致。 #### CreateRunBody ```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, "created_by": "console-demo" } ``` API 当前允许 `loops` 为 1–500;工作流种子 spec 的产品目标上限为 10000,两者尚未统一。 #### HumanActionBody ```json { "actor": "engineer-name", "reason": "检查暂停" } ``` ### 7.4 调用示例 本地: ```bash curl -sS http://localhost:8000/api/v1/health curl -sS -X POST http://localhost:8000/api/v1/preflight \ -H 'Content-Type: application/json' \ -d '{}' curl -sS -X POST http://localhost:8000/api/v1/runs \ -H 'Content-Type: application/json' \ -d '{"loops":8,"inject_failure":true,"created_by":"manual"}' ``` 公网预览环境可直接访问: ```bash curl https://flashops.imagebrewing.com/api/v1/health ``` ### 7.5 常见状态码 | 状态码 | 场景 | |---|---| | 200 | 查询或动作成功 | | 201 | Run 或 Agent 注册成功 | | 400 | 资产或工作流数据无法解析 | | 401 | Agent 注册口令或 Agent bearer token 无效 | | 404 | Run / DUT / Host 等资源不存在 | | 409 | 当前状态不允许暂停、继续或停止 | | 422 | Pydantic 请求参数校验失败 | | 503 | 生产环境尚未配置 Agent 注册口令 | | 500 | 应用未处理异常或数据约束冲突 | --- ## 8. 前端控制台 ### 8.1 挂载方式 `main.py` 把后端仓库的同级目录 `前端UI八页面完成/` 挂载到 `/console`: ```text STORAGE LABOS/ ├── flashops/ └── 前端UI八页面完成/ ``` 生产服务器必须保持同样的兄弟目录关系,否则根路径会跳到 `/docs`,控制台路径返回 404。 ### 8.2 核心页面 | 页面 | 文件 | 当前状态 | |---|---|---| | 总览 | `Dashboard.dc.html` | 产品级静态样例,看板 API 尚未全面接入 | | 任务中心 | `Tasks.dc.html` | 已接预检与创建 Run API | | 实时运行 | `LiveRun.dc.html` | 已接 Run、事件、演示、暂停、继续、急停 API | | 证据浏览器 | `Evidence.dc.html` | 主要为静态样例,真实 Evidence 下载待接 | | 失败中心 | `Failures.dc.html` | 静态产品原型,聚类服务待实现 | | 资产中心 | `Assets.dc.html` | 静态产品原型,资产 CRUD 待实现 | | 版本比较 | `Compare.dc.html` | 静态产品原型,真实 A/B 查询待实现 | | 工作流模板 | `Workflows.dc.html` | 静态产品原型,编辑与发布 API 待实现 | `ConsoleNav.dc.html` 提供导航与全局停止交互原型;`Deck.dc.html` 和 `_template/` 用于展示稿, 不属于正式业务路由。 ### 8.3 前端调用约定 - 页面与 API 同源,不配置独立 API Base URL; - 所有动态请求使用相对路径 `/api/v1/...`; - 前端代码不得保存任何生产密码或 Agent token; - Run ID 应通过 URL 或浏览器状态传递; - 事件增量读取使用 `after_seq`,不要每次重复拉取全部事件; - 页面中静态样例必须用“演示 / 示例”标识,避免与真实数据混淆。 ### 8.4 下一步前端接入优先级 1. 总览接 `/dashboard/summary` 与最近 Run; 2. 证据页接 Run Evidence Bundle 和真实事件; 3. 资产页增加只读 API,再增加受审批保护的写接口; 4. 工作流页面接版本、spec hash、发布和审批; 5. 失败中心接失败签名和人工分类; 6. 版本比较接真实环境指纹与指标; 7. 引入统一的 loading、empty、error、401、409 状态。 --- ## 9. 工作流、Agent 与适配器 ### 9.1 种子工作流 默认工作流: ```text key: fw-ab-regression name: 固件 A/B 电源状态回归 version: 3 danger_level: high ``` 步骤: 1. `preflight`:安全预检; 2. `flash-fw-b`:高危、非幂等固件刷写,完成后检查点; 3. `regression-loop`:fio、设备枚举、完整性校验,循环边界检查点; 4. `report`:汇总报告。 每个发布版本保存完整 spec 和稳定 `spec_hash`。同 key 的修改应创建新版本,不覆盖历史版本。 ### 9.2 工作流字段原则 - `key`:稳定机器标识; - `version`:不可变整数版本; - `danger_level`:工作流整体风险; - `params`:类型、默认值、范围; - `steps[].adapter`:调用的能力边界; - `steps[].template`:命令模板,而不是任意命令文本; - `idempotent`:是否允许安全重试; - `checkpoint`:是否允许从此边界恢复; - `on_fail`:retry / fail_run / continue / freeze / recover。 详见 `flashops/docs/workflow-spec.md`。 ### 9.3 Host Agent 当前实现与目标模型 独立 Agent 已作为 `services/host-agent/flashops_agent` 实现注册、主机指纹发现、 本地 `0600` 凭据文件、Bearer token 和心跳。控制平面只保存 token 的 SHA-256 摘要; 生产注册口令缺失时默认拒绝注册。心跳迟到 15 秒时 Host 自动进入 `DEGRADED`, 丢失 30 秒时进入 `OFFLINE`;安全预检会刷新状态后再判断 Host 是否可用。 当前 Run 执行仍由内置模拟器完成。 下一阶段继续按拉模式实现: ```text Agent 注册 / 心跳 → 拉取可执行 Step 租约 → ACK 接手 → 按模板执行 → 上报输出摘要与对象 URI → 完成 / 失败 / 超时 → 租约续期或到期回收 ``` 拉模式用于适配客户内网、NAT 和不允许控制平面反向连接的测试主机。 ### 9.4 Adapter 边界 目标适配器包括: - `nvme_cli`:识别设备、固件下载 / commit、SMART、Format / Sanitize; - `fio`:负载执行、指标和错误提取; - `shell`:只允许登记模板; - `oob`:Reset、ATX、AC、画面 / 电源状态; - `builtin`:预检、等待、报告和证据处理。 模拟适配器与真实适配器必须通过同一套契约测试。真实实现不得在接口外增加隐藏行为。 ### 9.5 幂等、租约和重放 - 每次 Step dispatch 带唯一 idempotency key; - Agent 重复上报不能重复产生危险动作; - 非幂等步骤中断后默认 `INCONCLUSIVE` 或人工确认; - 租约到期只代表执行权回收,不代表命令一定未执行; - 重派前必须根据模板类型和现场状态决定是否安全; - 任何重放都必须在事件流中留下原因和原始 dispatch 引用。 --- ## 10. 证据、失败分析与指标 ### 10.1 Evidence Bundle 当前内容 每个完成 Run 的对象目录: ```text var/objects/runs// ├── manifest.json └── events.ndjson ``` `manifest.json` 当前包含: - schema version; - Run ID; - workflow key / version / spec hash; - environment fingerprint; - checkpoint; - event count; - 文件路径和字节数。 `events.ndjson` 每行一条按 `seq` 排序的事件,包含时间、来源、kind、严重度、消息和 payload。 ### 10.2 目标证据包 真实硬件阶段应补充: - Agent stdout / stderr 和工具原始输出; - SMART、identify、PCIe、OS、驱动、BIOS 快照; - fio JSON、校验 hash、失败 LBA 范围; - 蓝屏 / panic dump、系统日志和屏幕证据; - OOB 电源状态与画面指纹; - 固件文件 hash 与签名验证结果; - 审批、命令模板和参数; - 文件级 SHA-256、缺失字段和生成器版本。 ### 10.3 失败分类 | 分类 | 含义 | |---|---| | `DUT_DEFECT` | DUT / 固件自身缺陷 | | `INFRA_FAILURE` | 主机、网络、供电、OOB 等基础设施问题 | | `SCRIPT_FAILURE` | 测试脚本、适配器或参数问题 | | `DATA_INTEGRITY` | 数据不一致,默认冻结现场 | | `UNKNOWN` | 证据不足或尚未分类 | 基础设施故障必须与 DUT 缺陷分开统计,否则会污染固件质量结论。 ### 10.4 失败签名目标 建议由以下稳定要素归一化后计算 hash: ```text workflow step + normalized error codes + normalized log templates + host state + DUT enumeration state + data integrity state + recovery outcome + environment fingerprint ``` 原始时间、随机 ID、绝对路径等高基数字段不应直接进入签名。 ### 10.5 核心产品指标 | 指标 | 定义 | |---|---| | UCR | 无人值守完成 Run / 已完成 Run | | Human Touches | Run 执行期间人工操作次数 | | Evidence Completeness | 已收集必需证据字段 / 应收集字段 | | Recovery Success Rate | 自动恢复成功次数 / 自动恢复尝试次数 | | Infra False Positive Rate | 被误判为 DUT 缺陷的基础设施故障比例 | | Reproduction Rate | 可由证据与最小步骤成功复现的失败比例 | 当前 `/dashboard/summary` 已提供总 Run、活动 Run、完成 Run 和 UCR 的最小聚合。 --- ## 11. 配置与环境变量 所有应用变量使用 `FLASHOPS_` 前缀。 | 变量 | 开发默认值 | 说明 | |---|---|---| | `FLASHOPS_ENV` | `development` | 环境标识 | | `FLASHOPS_DATABASE_URL` | `sqlite+aiosqlite:///.../var/flashops.db` | SQLAlchemy DSN | | `FLASHOPS_OBJECT_STORE_URL` | `.../var/objects` | 当前必须为本地路径 | | `FLASHOPS_BUS_URL` | 空 | 目标 Redis / NATS 地址,当前未启用 | | `FLASHOPS_AGENT_ENROLLMENT_TOKEN` | 空 | 控制面与 Agent 共享的首次注册口令;生产控制面缺失时拒绝注册 | | `FLASHOPS_API_HOST` | `0.0.0.0` | 监听地址 | | `FLASHOPS_API_PORT` | `8000` | 监听端口 | | `FLASHOPS_CORS_ORIGINS` | `http://localhost:3000` | 逗号分隔 CORS 来源 | | `FLASHOPS_ENGINE_TICK_S` | `0.5` | 引擎 tick 间隔 | | `FLASHOPS_TIME_SCALE` | `1.0` | 模拟时间倍率;测试使用更小值 | ### 11.1 时间策略默认值 | 配置 | 默认值 | |---|---:| | 心跳间隔 | 5s | | 心跳降级 | 15s | | 心跳丢失 | 30s | | Step 租约 TTL | 60s | | 租约拉取超时 | 20s | | Step 默认超时 | 600s | | 恢复后稳定等待 | 20s | | AC 断电安全间隔 | 10s | | 单循环最大恢复 | 3 | | 单 Run 最大恢复 | 20 | 当前这些时间策略在 `config.py::TimingPolicy` 中定义,还未全部开放为环境变量。 ### 11.2 Host Agent 运行变量 | 变量 | 默认值 | 说明 | |---|---|---| | `FLASHOPS_AGENT_SERVER` | `http://127.0.0.1:8000` | 控制面基础地址 | | `FLASHOPS_AGENT_HOST_ID` | 空 | 绑定已有 Test Host ID | | `FLASHOPS_AGENT_HOST_NAME` | 本机 hostname | 按名称绑定或新建 Test Host | | `FLASHOPS_AGENT_STATE_PATH` | `~/.flashops/agent-state.json` | 本地 bearer 凭据文件,权限固定为 `0600` | | `FLASHOPS_AGENT_ENROLLMENT_TOKEN` | 空 | 与控制面一致的首次注册口令 | | `FLASHOPS_AGENT_BASIC_USER` | 空 | 可选上游认证网关用户名;当前预览站未使用 | | `FLASHOPS_AGENT_BASIC_PASSWORD` | 空 | 可选上游认证网关密码;当前预览站未使用 | 用户名和密码必须同时配置。Agent 不会把 bearer token 输出到终端;首次注册响应中的原始 token 只写入本机状态文件。重新注册会轮换 token,使旧 token 立即失效。 ### 11.3 配置原则 - 凭据不得写进仓库、本文档、systemd unit 或 Nginx 注释; - 生产密码仅保存在受控凭据文件、密码管理器或服务器哈希文件; - 修改生产环境变量后必须重启 `flashops.service`; - 数据库和对象目录必须可写,其余应用目录应只读; - SQLite URL 使用四个斜杠表示绝对路径; - 多 worker 前必须先切换 PostgreSQL 和外部事件总线。 --- ## 12. 腾讯云生产部署与运维 ### 12.1 生产资源 | 项目 | 当前值 | |---|---| | 云厂商 | 腾讯云 CVM | | 操作系统 | Ubuntu 24.04 | | 公网 IP | `62.234.39.66` | | 域名 | `flashops.imagebrewing.com`、`git.imagebrewing.com` | | DNS | DNSPod A 记录,TTL 600 | | 反向代理 | 宝塔 Nginx | | 应用服务 | `flashops.service` | | 应用用户 | `ubuntu` | | 内部端口 | `127.0.0.1:18080` | | 应用根目录 | `/data/wangzhan/app/storage-labos` | | 持久化目录 | `/data/wangzhan/app/storage-labos/flashops/var` | | Nginx vhost | `/www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf` | | 证书 | `/etc/letsencrypt/live/flashops.imagebrewing.com/` | | 发布包备份 | `/data/wangzhan/backups/flashops-release-20260727.tar.gz` | | Git 服务 | Gitea 1.27.0 Docker 容器,回环端口 `127.0.0.1:13000` | | Git 实时数据 | `/data/gitea/data`,本地文件系统 | | Git 云端备份 | `/chucun/wangzhan-production/backups/gitea/`,每日定时 dump + SHA-256 | 初次证书签发于 2026-07-27,初始到期日为 2026-10-25。Certbot 已配置自动续期,并通过 `--dry-run` 验证;续期成功后会执行 `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh`。 > Agent 注册 / 心跳能力已在本地代码与进程联调中通过,但本节所述线上版本尚未发布该增量。 > 发布前需创建 root-only `/etc/flashops/flashops.env` 并配置 > `FLASHOPS_AGENT_ENROLLMENT_TOKEN`;不得把注册口令写进 systemd unit 或仓库。 ### 12.2 生产安全边界 - Uvicorn 仅监听回环地址,不直接暴露 18080; - Nginx 统一处理 TLS;FlashOps 当前作为匿名半成品预览开放; - ACME challenge 路径不要求认证,以支持自动续期; - systemd 启用 `NoNewPrivileges`、`PrivateTmp`、`ProtectSystem=full` 和 `ProtectHome`; - 仅 `flashops/var` 允许应用写入; - 当前 SQLite 生产基线固定一个 worker; - 公开预览不得绑定真实 DUT、客户数据、真实 OOB 或有效危险命令;接入前必须先启用应用级 账号、RBAC、审批与命令模板白名单。 ### 12.3 服务管理 ```bash sudo systemctl status flashops --no-pager -l sudo systemctl restart flashops sudo systemctl stop flashops sudo systemctl start flashops sudo systemctl enable flashops ``` 健康检查: ```bash curl -fsS http://127.0.0.1:18080/api/v1/health curl https://flashops.imagebrewing.com/api/v1/health ``` ### 12.4 日志 ```bash # 应用实时日志 sudo journalctl -u flashops -f # 最近 200 行 sudo journalctl -u flashops -n 200 --no-pager # Nginx 访问 / 错误日志 sudo tail -f /www/wwwlogs/flashops.imagebrewing.com.log sudo tail -f /www/wwwlogs/flashops.imagebrewing.com.error.log ``` ### 12.5 Nginx 与证书 ```bash sudo nginx -t sudo systemctl reload nginx sudo certbot certificates sudo certbot renew --cert-name flashops.imagebrewing.com \ --dry-run --non-interactive --no-random-sleep-on-renew ``` 任何 Nginx 修改都必须先 `nginx -t`,只有成功后才能 reload。 ### 12.6 发布流程 标准流程: 1. 本地 `make test`; 2. 确认前端和后端兄弟目录结构; 3. 打包时排除 `.venv`、`var`、缓存和 `__pycache__`; 4. 上传到服务器临时目录; 5. 解压到独立 incoming 目录; 6. 安装服务器虚拟环境和依赖; 7. 保留生产 `var/`,不要用本地空目录覆盖; 8. 安装 / 校验 systemd unit; 9. 重启服务并轮询内部 health; 10. `nginx -t` 后 reload; 11. 从服务器和外部网络各验证一次匿名 HTTPS 200 和 API health; 12. 保存带 SHA-256 的发布包到 `/data/wangzhan/backups/`。 ### 12.7 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') / f'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 ``` 恢复数据库是高影响操作:先停止服务,核对目标备份并额外备份当前数据库,再替换文件、修复 `ubuntu:ubuntu` 所有权、启动服务并检查事件序号连续性。不得在未确认备份内容时直接覆盖。 ### 12.8 公网预览访问策略 2026-07-27 起,`flashops.imagebrewing.com` 已取消 Nginx Basic Auth 和 IP 访问限制,匿名用户 可以查看控制台并调用模拟 API。取消前的 Nginx 配置保留在: ```text /www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf.before-public-20260727 ``` 这是临时产品演示策略,不是最终安全方案。接入真实硬件或非公开数据前,不能只恢复 Basic Auth; 必须优先实现应用账号、RBAC、危险动作审批和审计上下文。 ### 12.9 发布验收清单 - [ ] `flashops.service` 为 active; - [ ] Nginx 为 active 且 `nginx -t` 成功; - [ ] `getent ahostsv4 flashops.imagebrewing.com` 指向 `62.234.39.66`; - [ ] HTTP 返回 301 到 HTTPS; - [ ] HTTPS 证书域名正确且未过期; - [ ] 未登录访问控制台最终返回 200; - [ ] `/api/v1/health` 返回 `status=ok`; - [ ] `/api/v1/dashboard/summary` 返回合法 JSON; - [ ] 生产 `var/flashops.db` 和 `var/objects` 可写; - [ ] 应用最近日志无 error; - [ ] Certbot 模拟续期成功; - [ ] 发布包和数据库备份已记录。 ### 12.10 Gitea 团队 Git 服务 | 项目 | 当前值 | |---|---| | 页面与 clone 地址 | `https://git.imagebrewing.com` | | 项目仓库 | `https://git.imagebrewing.com/yuanshuai/storage-labos` | | 容器 | `gitea` / `gitea/gitea:1.27.0` | | HTTP 监听 | `127.0.0.1:13000`,不直接暴露公网 | | Git 传输 | HTTPS;当前不开放独立 SSH 端口 | | 匿名访问 | 可查看 public 仓库与最近提交 | | 注册 | 开放申请,但必须由管理员人工批准后才能登录 | | 实时数据 | `/data/gitea/data`,不得放在 COSFS 上 | | 备份 | 每日约 03:20 dump 到 `/chucun/wangzhan-production/backups/gitea/` | Gitea 管理员凭据只保存在本机被 `.gitignore` 排除的 `gitea-admin.env.local`,不写进手册、 仓库或服务器部署文件。团队成员先在网页注册,再由管理员在“站点管理 → 用户账户”中批准, 随后把成员加入仓库协作者。 ```bash # 服务与健康 sudo docker ps --filter name=gitea curl -fsS http://127.0.0.1:13000/api/healthz # 日志 sudo docker logs --tail 200 gitea # 备份定时器与手工备份 sudo systemctl status flashops-gitea-backup.timer --no-pager sudo systemctl start flashops-gitea-backup.service ``` 生产服务器没有启用 Gitea Actions Runner。原因是 Runner 需要执行仓库中的任意代码,直接挂在 同一台生产服务器会放大供应链风险;当前 PR 必须附本地 `make test` 结果。未来 CI 应部署到 隔离机器或一次性 Runner,而不是把 Docker socket 交给生产仓库任务。 --- ## 13. 测试、质量门与开发规范 ### 13.1 当前测试基线 截至 2026-07-27: - 测试总数:25; - 含 Agent 增量的完整测试集本地连续 20 轮全部通过; - 腾讯云生产环境依赖下连续 20 轮全部通过; - 服务器单轮约 1–2 秒; - 证书续期 dry-run 通过; - systemd 重启后 health 恢复通过; - FlashOps 匿名 HTTPS 200、Gitea HTTPS 200 与两个证书域名验证通过。 ### 13.2 测试分层 | 层 | 文件 | 重点 | |---|---|---| | 状态机 | `test_state_machine.py` | 合法 / 非法转移与终态 | | 恢复策略 | `test_recovery.py` | Agent、网络、OOB、完整性触发 | | 安全 | `test_safety.py` | 序列号、系统盘、分区和门禁组合 | | 端到端 | `test_e2e_vertical_slice.py` | 创建、恢复、证据、暂停、继续、急停 | | Agent 安全 | `test_agent_enrollment.py` | 生产缺少注册口令时默认拒绝 | | Agent 状态 | `test_agent_status.py` | 新鲜、迟到、失联和主动降级心跳 | | Agent 客户端 | `services/host-agent/tests/test_client.py` | HTTP 契约、401、凭据 `0600` | ### 13.3 提交前检查 ```bash cd "/Users/yuanshuai/Desktop/STORAGE LABOS/flashops" make test make demo ``` 如果改了部署配置,再检查: ```bash systemd-analyze verify deploy/flashops.service # 在 Linux 上执行 sudo nginx -t # 在目标服务器执行 ``` ### 13.4 开发规则 1. 状态变化必须追加事件; 2. 新状态必须更新枚举、转移表、文档和测试; 3. 新事件 kind 只增不改,历史事件已经落库; 4. 安全规则保持纯函数,拒绝发生在下发之前; 5. 危险步骤默认不可自动重试; 6. API 路由不堆业务逻辑; 7. 真实适配器与模拟适配器使用同一契约; 8. 大日志写对象存储,不塞进数据库 JSON; 9. 生产数据和凭据不进 Git; 10. 修改数据结构必须同时提供迁移和回滚说明; 11. 文档中的“已实现”必须能由代码或验收记录证明; 12. 生产发布必须保留可核验的归档包和数据库备份。 ### 13.5 Git 仓库与团队协作 当前 Git 基线: | 项目 | 当前值 | |---|---| | 本地仓库根 | `STORAGE LABOS/` | | 默认分支 | `main` | | 远程 | `https://git.imagebrewing.com/yuanshuai/storage-labos.git` | | 网页 | `https://git.imagebrewing.com/yuanshuai/storage-labos` | | 可见性 | public:匿名可读;提交、Issue 和 PR 写操作必须登录 | | 管理员 | `yuanshuai`;密码只在本机忽略文件中保存 | | 团队规则入口 | 根目录 `CONTRIBUTING.md` | 不要通过聊天工具反复传 ZIP 继续开发。团队以远程仓库为唯一代码与文档版本源,新成员执行: ```bash git clone https://git.imagebrewing.com/yuanshuai/storage-labos.git cd storage-labos/flashops make setup make test ``` 分支规则: - `main`:始终保持可测试、可发布,禁止直接开发和强制推送; - `feat/`:功能; - `fix/`:缺陷; - `docs/`:文档; - `chore/`:仓库、依赖、CI 和部署。 日常开发: ```bash git switch main git pull --ff-only git switch -c feat/agent-step-lease # 修改后先验证 cd flashops make test make demo cd .. # 只暂存本次相关文件;混合工作区不要直接 git add -A git status -sb git add <明确的文件或目录> git diff --cached --stat git diff --cached --check git commit -m "feat(agent): add step lease polling" git push -u origin "$(git branch --show-current)" ``` 采用 Conventional Commits:`type(scope): subject`。 | 类型 | 用途 | 示例 | |---|---|---| | `feat` | 新功能 | `feat(agent): add step lease polling` | | `fix` | 修复 | `fix(safety): reject stale host heartbeat` | | `docs` | 文档 | `docs(manual): add gitea operations` | | `test` | 测试 | `test(agent): cover token rotation` | | `refactor` | 不改变行为的重构 | `refactor(events): isolate projection writer` | | `perf` | 性能优化 | `perf(api): reduce timeline query cost` | | `chore` | 工具、依赖、部署 | `chore(repo): initialize collaboration repository` | 每项工作使用独立分支和 Pull Request。PR 必须说明改动、原因、影响、验证结果、风险和回滚; 至少一名团队成员评审后再合并。优先 Squash merge,合并后删除功能分支。只允许对自己的未合并 分支使用 `--force-with-lease`,严禁对 `main` 强推;已共享的错误提交使用 `git revert` 撤销。 团队成员在 Gitea 注册后需要管理员人工批准。批准后在仓库“设置 → 协作者”中授予写权限; 不要共用管理员账号。离职或设备丢失时立即移除成员、撤销 token 并检查审计记录。 以下内容永不入库:`.env`、密码、token、私钥、证书私钥、Agent state、SQLite、Evidence、日志、 生产备份、`.venv`、`node_modules` 和未脱敏客户数据。若凭据误入提交,先轮换凭据,再清理历史; 仅删除当前文件不能消除 Git 历史中的泄露。 ### 13.6 CI 与 Pull Request 质量门 根目录 `.github/workflows/ci.yml` 已提供 Python 3.9 / 3.12 测试矩阵模板,执行控制平面与 Host Agent 测试并编译全部 Python 模块。它可用于后续 GitHub 镜像或启用 Gitea Actions。 当前生产 Gitea 未配置 Actions Runner,因此这个工作流不会自动执行。这样做是刻意的:把 Runner 和 Docker socket 放在同一台生产服务器,会允许仓库代码获得过大的服务器权限。 现阶段 PR 必须附上本地 `make test` 与相关演示结果;下一阶段把 Runner 放到隔离机器或一次性 执行环境,再把检查设为 `main` 的必需状态。 仍待补充的质量门:PostgreSQL 集成、Ruff / mypy、工作流 schema、依赖漏洞与 secret scan、 Nginx / systemd 静态检查、模拟器长跑、发布包 SBOM 和签名。 --- ## 14. 已知边界与演进路线 ### 14.1 当前已知边界 | 边界 | 风险 | 处理方向 | |---|---|---| | SQLite 单 writer | 多并发会阻塞 | 当前单 worker;后续 PostgreSQL | | 无 Alembic | 表结构难升级 | 引入版本化 migration | | 无应用账号 / RBAC | 无细粒度审计与授权 | OIDC/SSO、角色、审批 | | 匿名公网预览 | 页面和模拟 API 可被任何人调用 | 不接真实资产;真实阶段启用 OIDC/RBAC/审批 | | Agent 无 Step 租约 | 只能注册和心跳,不能领取真实任务 | 实现 lease、ACK、续租和完成上报 | | OOB 为模拟 | 无法验证真实断电恢复 | 接可牺牲硬件和独立网络 OOB | | Step runtime 不完整 | 工作流只是部分执行 | planner/runtime/lease 落地 | | Evidence 最小 | 不足以支撑真实缺陷结论 | 接工具原始输出、hash、dump 和画面 | | 失败聚类未实现 | 失败中心为静态原型 | 归一化、签名、合并 / 拆分和复现任务 | | 多数页面静态 | UI 与真实数据可能不一致 | 按 8.4 顺序接 API | | 依赖范围较宽 | 重新安装可能拿到不同版本 | 生成并维护 production lock file | | 无定时 DB 备份 | 数据恢复点不稳定 | systemd timer + 备份校验 + 保留策略 | ### 14.2 建议演进顺序 #### Phase 1:独立 Agent 契约 - 已完成:注册、token 轮换、心跳、状态老化、主机探针和本地凭据; - 待完成:租约、ACK、续租、结果上报; - idempotency key 和重放保护; - 模拟 Agent 与控制平面完全分进程; - Agent 崩溃、网络断开、控制平面重启测试。 #### Phase 2:真实只读硬件接入 - NVMe identify / SMART / BDF / device path; - Host 环境探针; - OOB 只读电源和画面状态; - 不执行刷写、Format、Sanitize 或断电。 #### Phase 3:受控危险动作 - 命令模板白名单; - 审批、双人复核、短期授权; - 可牺牲 DUT; - 真实刷写、Reset、ATX、AC 阶梯; - 完整 Evidence Bundle。 #### Phase 4:多工位与生产化 - PostgreSQL、外部事件总线、对象存储; - 多工位调度与资源配额; - SSO / RBAC / 审计; - HA、监控、告警、备份和灾备; - 评估 Temporal 是否只替换 runtime。 #### Phase 5:失败智能与版本决策 - 失败签名聚类; - 环境可比性检查; - 自动裁剪最小复现工作流; - A/B 指标和证据门禁; - AI 只生成带证据引用的摘要,不替代工程师结论。 --- ## 15. 故障排查 ### 15.1 公网返回 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 ``` 常见原因:服务未启动、虚拟环境缺依赖、端口不一致、数据库目录不可写、systemd 安全策略阻止写入。 ### 15.2 公网返回 401 / 403 当前预览站允许匿名访问,首页和普通查询 API 不应由 Nginx 返回 401 / 403。遇到问题时: - 用无痕窗口访问,排除浏览器缓存的旧认证状态; - 检查 Nginx 生效配置中是否残留 `auth_basic`、`allow` 或 `deny`; - 区分普通页面与 Agent API:Agent 注册口令或 bearer token 无效仍会返回 401; - 执行 `nginx -t`,确认无误后 reload; - 查看站点 access/error 日志,确认状态码来自 Nginx、应用还是上游安全产品。 ### 15.3 HTTPS 证书错误 ```bash getent ahostsv4 flashops.imagebrewing.com sudo certbot certificates sudo nginx -T | grep -A8 -B3 flashops.imagebrewing.com openssl s_client -connect 62.234.39.66:443 \ -servername flashops.imagebrewing.com /dev/null \ | openssl x509 -noout -subject -issuer -dates ``` 检查 DNS 是否指向正确 IP、证书路径是否存在、Nginx 是否已经 reload。 ### 15.4 控制台 404 确认目录关系: ```text /data/wangzhan/app/storage-labos/ ├── flashops/ └── 前端UI八页面完成/ ``` `main.py` 通过 `REPO_ROOT.parent / "前端UI八页面完成"` 查找控制台。 ### 15.5 SQLite locked / 事件序号冲突 - 确认 Uvicorn 仍为一个 worker; - 不要从多个进程同时直接操作同一个 SQLite 文件; - 所有应用事务必须经 `session_scope()`; - 所有 Run 状态写入必须经 `append_event()`; - 检查是否有脚本绕过服务直接写库; - 若需要多进程并发,迁移 PostgreSQL,不要继续堆 SQLite 锁。 ### 15.6 服务不断重启 ```bash sudo systemctl show flashops -p NRestarts -p ExecMainStatus sudo journalctl -u flashops --since '30 minutes ago' --no-pager ``` 重点检查:`PYTHONPATH`、依赖安装、数据库 URL、目录权限、配置拼写和 18080 端口占用。 ### 15.7 测试本机通过、服务器失败 比较: ```bash .venv/bin/python --version .venv/bin/pip freeze ``` 当前 requirements 使用范围约束而非完整 lock,同一天不同环境可能安装不同 FastAPI、Starlette、 httpx 或 SQLAlchemy 版本。应先复现服务器依赖,再决定修代码或锁版本,不能只在本机降级掩盖并发缺陷。 ### 15.8 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 ``` - 公开仓库页面应允许匿名查看和克隆; - 新成员注册后需要管理员在 Gitea 后台批准,未批准时不能登录; - 推送必须使用已批准账号,不要把密码或访问令牌写入远程地址; - 若备份失败,检查 `gitea-backup.service` 日志和 `/chucun` 挂载状态; - Gitea 实时数据必须保留在 `/data/gitea/data`,不得迁移到 COSFS 挂载目录。 --- ## 16. 术语表与 FAQ ### 16.1 术语表 | 术语 | 含义 | |---|---| | DUT | Device Under Test,被测 SSD / NVMe 设备 | | Host | 连接 DUT 并运行测试工具的主机 | | Agent | 部署在 Host 上,拉取并执行已批准步骤的服务 | | OOB | Out-of-Band,主机失联后仍可观测 / 控制的带外通道 | | Run | 一次完整工作流执行实例 | | Step | Run 中最小可调度执行单元 | | Workflow | 版本化 SOP 和步骤定义 | | Checkpoint | 可安全恢复的执行边界 | | Evidence Bundle | 一次 Run 的证据清单与文件集合 | | Event Sourcing | 用 append-only 事件作为状态变化真相 | | Projection | 从事件派生的当前状态读模型 | | UCR | Unattended Completion Rate,无人值守完成率 | | Failure Signature | 归一化失败要素生成的聚类标识 | | Idempotency | 同一请求重复执行不会产生额外副作用 | | Lease | 控制平面临时授予 Agent 的步骤执行权 | | BDF | PCIe Bus:Device.Function 地址 | ### 16.2 FAQ #### 为什么生产还用 SQLite? 当前是单工位、单 worker、内部演示基线,SQLite 能降低部署摩擦。它不是多工位最终方案;增加并发前 必须切换 PostgreSQL。 #### 为什么不用 Airflow / Temporal? 恢复语义跨越 Agent、OS、OOB 和物理供电,且非幂等步骤默认不重跑。首版用纯函数状态机更容易 精确控制和测试。多工位跨节点调度成为瓶颈时,再评估用 Temporal 替换 runtime。 #### 为什么事件不能按时间戳排序? Host、OOB 和控制平面时钟可能不同,主机崩溃时还会丢失最后日志。控制平面分配的 `seq` 才能提供 稳定、可重放的总顺序。 #### 为什么数据完整性失败直接冻结? 自动重启、重跑或写盘可能覆盖最关键的现场。宁可暂停等待工程师,也不能为了完成率破坏证据。 #### 为什么 Agent 要用拉模式? 客户测试主机通常位于 NAT、内网或防火墙后面。由 Agent 主动向控制平面发起连接更容易部署, 也减少暴露入站端口。 #### 为什么控制台和后端放在一起? 当前静态控制台由 FastAPI 同源挂载,避免单独的前端构建和 CORS 配置,适合原型与演示。后续如果 切换 Next.js 或独立 SPA,可以保持 `/api/v1` 契约不变。 #### 管理凭据在哪里? 本文档不保存明文密码。Gitea 管理员、SSH 与云平台凭据通过本机受控文件、系统钥匙串或密码管理器 交接;这些文件必须命中 `.gitignore`,不得进入提交历史。 #### 可以直接把 18080 暴露公网吗? 不可以。该端口没有应用级认证和 TLS,必须只绑定 `127.0.0.1`,公网只走 Nginx 443。 --- ## 17. 变更记录 ### V1.2 — 2026-07-27 - 上线 `git.imagebrewing.com` Gitea 团队 Git 服务和公开项目仓库; - 增加分支、提交、评审、密钥和本地验证规范; - 使用 `/chucun/wangzhan-production/backups/gitea/` 保存每日 Gitea 校验备份; - FlashOps 预览站取消 Nginx Basic Auth 与 IP 限制,普通页面和查询 API 可匿名访问; - 明确匿名预览不得连接真实硬件、危险动作或非公开数据,正式生产前必须完成 SSO / RBAC。 ### V1.1 — 2026-07-27 - 新增独立拉模式 Host Agent; - 新增 Agent 注册、token 轮换、Bearer 心跳和 Host 状态查询 API; - 控制平面只保存 token SHA-256 摘要,Agent 凭据文件权限固定为 `0600`; - 生产环境缺少 Agent 注册口令时默认拒绝注册; - Agent 保留可选网关 Basic Auth 能力,当前匿名预览环境不启用; - 新增 Agent 客户端、注册安全、状态老化和进程联调测试;全量测试增至 22 项; - Step 租约、ACK、事件、完成与产物上传仍为下一阶段。 ### V1.0 — 2026-07-27 - 根据实际源码建立首版完整项目手册; - 记录控制平面、14 张表、状态机、六项门禁和 Evidence Bundle; - 记录 8 个核心控制台页面及真实 / 静态边界; - 记录腾讯云、DNSPod、systemd、Nginx 和 Let's Encrypt 部署; - 记录 SQLite 并发写入修复和单 worker 约束; - 记录本机与服务器连续 20 轮测试结果; - 给出生产发布、验收、备份、密码轮换和故障排查流程; - 明确独立 Agent、真实硬件、RBAC、PostgreSQL 和失败聚类为后续工作。 --- ## 文档维护要求 以下变化发生时,必须同步更新本文: - 新增 / 删除 API; - 新增状态、事件种类或恢复级别; - 修改表结构或迁移策略; - 修改环境变量; - 调整域名、端口、服务器路径或服务名; - 新增真实 Agent、Adapter 或 OOB; - 安全门禁、审批或认证方式变化; - Evidence Bundle schema 变化; - 生产发布和回滚流程变化; - 能力从“模拟 / 原型”变为“真实已验收”。 文档里的完成状态必须由代码、测试或生产验收记录支撑。