# FlashOps 项目开发手册 (Project Manual)

本文档用于 FlashOps / STORAGE LABOS 项目的开发、测试、部署、运维与交接。内容以
2026-07-27 的实际源码和生产环境为准；目标架构与尚未实现的能力会明确标注，避免把原型能力
误认为真实硬件能力。

> **文档状态**：V1.2 · 已与当前代码、Git 仓库和云端部署边界核对
>
> **生产地址**：<https://flashops.imagebrewing.com/>
>
> **团队 Git**：<https://git.imagebrewing.com/yuanshuai/storage-labos>
>
> **后端代号**：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
```

打开：

- 总览：<http://localhost:8000/console/Dashboard.dc.html>
- 任务中心：<http://localhost:8000/console/Tasks.dc.html>
- 实时运行：<http://localhost:8000/console/LiveRun.dc.html>
- API 文档：<http://localhost:8000/docs>

### 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/<run_id>/
├── 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/<short-name>`：功能；
- `fix/<short-name>`：缺陷；
- `docs/<short-name>`：文档；
- `chore/<short-name>`：仓库、依赖、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 2>/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 变化；
- 生产发布和回滚流程变化；
- 能力从“模拟 / 原型”变为“真实已验收”。

文档里的完成状态必须由代码、测试或生产验收记录支撑。
