# 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 变化;
- 生产发布和回滚流程变化;
- 能力从“模拟 / 原型”变为“真实已验收”。
文档里的完成状态必须由代码、测试或生产验收记录支撑。