6.4 KiB
6.4 KiB
架构:五层 → 代码目录的逐层映射
实现状态(2026-07-27):本文描述目标架构。当前已落地控制平面的模型、 安全门禁、事件记录、Run 转移表、恢复决策、最小证据包、内置模拟执行器,以及 独立 Host Agent 的注册、token、心跳和 Host 状态老化链路;Step 租约、Adapter 执行、消息总线替换和生产对象存储仍未落地。
方案 §5.1 定义了五层架构。这里给出每一层落到哪个目录、边界在哪、谁不能调用谁。
┌─────────────────────────────────────────────────────────────┐
│ 交互与集成层 apps/console (Next.js) · CLI · Open API │
├─────────────────────────────────────────────────────────────┤
│ 控制平面 services/control-plane/flashops_control/ │
│ services/ 编排 safety/ 门禁 api/ 接口 │
├─────────────────────────────────────────────────────────────┤
│ 执行平面 services/host-agent/flashops_agent/ │
│ adapters/ 工具适配器 │
├─────────────────────────────────────────────────────────────┤
│ 带外平面 services/host-agent/flashops_agent/oob/ │
│ (独立进程/独立网络,模拟实现在 simulator/) │
├─────────────────────────────────────────────────────────────┤
│ 证据与智能层 control-plane/.../evidence/ · signatures/ │
│ events/ 事件总线 │
└─────────────────────────────────────────────────────────────┘
依赖方向(单向,不可逆)
console ──HTTP/SSE──▶ control-plane ◀──HTTP──── host-agent
│ │
│ ├─▶ adapters ─▶ 客户工具
▼ │
数据库 └─▶ oob ─▶ 带外控制器
硬规则:
host-agent不得导入control-plane的任何模块。两者只通过docs/agent-protocol.md定义的 HTTP 契约通信。理由:Agent 要能单文件分发到客户 Windows 机器, 控制平面代码不进客户测试主机。control-plane不得主动连接 Agent(Agent 是拉模式)。理由:客户测试主机在 内网/NAT 后面,反向连接在真实实验室里活不下来。engine/不得直接读写数据库。它只操作Snapshot值对象,返回Effect列表。 持久化由runtime.py施加。理由:状态机要能不起数据库单测。api/不得包含业务判断。路由只做校验 + 调用engine/services。- 任何危险动作(刷写、Format、Sanitize、断电)必须先过
safety/, 且safety/的拒绝发生在下发给 Agent 之前。
控制平面内部分层
flashops_control/
├─ config.py 配置(产品名、DSN、路径、超时策略)
├─ db.py 引擎/会话/建表
├─ models/ SQLAlchemy 表定义(贫血模型,只管持久化)
├─ schemas.py Pydantic 出入参(当前最小 API 契约)
├─ events/
│ ├─ bus.py 进程内事件总线(生产换 NATS/Redis,接口不变)
│ └─ recorder.py 事件溯源写入:唯一允许改 Run 状态的入口
├─ engine/
│ ├─ states.py Run/Step 状态枚举 + 转移表(纯数据)
│ ├─ recovery.py 五级恢复阶梯策略(纯函数)
│ └─ (目标)planner/runtime:独立 Agent 接入后补齐
├─ safety/
│ ├─ gates.py DUT 绑定 / 系统盘保护 / 破坏性白名单
│ └─ templates.py 签名命令模板注册表 + 参数 schema 校验
├─ signatures/ (目标)失败签名(8 要素哈希)与聚类
├─ evidence/ 最小 Evidence Bundle(manifest + 事件流)
├─ services/runs.py 当前内置模拟编排(后续拆成 Agent + runtime)
├─ seed.py 可重复执行的模拟资产与工作流
└─ api/ FastAPI 路由
engine/ 的纯度是刻意的。 当前 states.py / recovery.py
不导入 SQLAlchemy、不导入 FastAPI、不做 I/O、不看时钟
(时间从 Snapshot.now 传入)。这让状态机的全部分支都能用普通 pytest 覆盖,
不需要起库、不需要 sleep。后面几波会不断往状态机里加分支(新故障类型、
新恢复策略、多工位调度),这条边界是唯一能防止它烂掉的东西。
开发形态 vs 生产形态
| 组件 | 开发(make dev) |
生产(docker-compose) |
换的时候动哪 |
|---|---|---|---|
| 数据库 | SQLite (aiosqlite) | PostgreSQL 16 | 只改 DATABASE_URL |
| 队列/总线 | 进程内 asyncio | Redis / NATS | events/bus.py 换实现 |
| 对象存储 | var/objects/ 本地目录 |
MinIO | storage.py 换实现 |
| Agent | 内置模拟执行器 + 独立 Agent 心跳 | 客户主机上的 systemd/Windows 服务 | 补齐 Step lease/runtime |
| 带外 | 模拟控制器 | 树莓派 / JetKVM / 智能 PDU | oob/ 换实现 |
三处"换实现"都在接口后面,且开发态实现本身就是测试替身——不存在"生产才发现跑不通"。
为什么不用 Temporal / Airflow
见 ADR-0003。一句话:
恢复语义(带外断电、冻结现场、误盘防护)是本产品的差异化本身,
不能交给通用工作流引擎的重试模型。等到多工位跨节点调度成为瓶颈时再评估 Temporal,
runtime.py 是唯一需要替换的文件。