2155f65c76
- 驱动/技能执行/: oneshot + system 解释器. 读自带 技能/*.json -> 按 tier 选模型 -> 拼 prompt -> 调 OpenAI 兼容端点 -> 剥 JSON -> 逐条按档校验 -> 落盘 + 写 events main 档只锁外层 (plan 是数组 + 每步有 want); small 档锁死 (单值 want + 必须在契约 清单里 + args 键白名单 + confidence 只许 0/0.5/1) - 契约清单只读 PG 的 drivers 表拿 (不 import 别的驱动、不翻别人的文件夹); 也支持 EFI_SKILL_CONTRACTS 显式给一份 (手工跑 / PG 不在时) - 思考型模型把 max_tokens 吃光时, 把预算翻倍重试一次; 本机端点强制不走代理 - 驱动/Json解码: 补 provides ["Json解码:解析"] -- skill 样例里一直在用这个契约名, 之前没人提供 - 文档同步: README / 文档 02 / 文档 06 / 文档 10 (坑 27~30) / 设计 01 / 设计 05 (补记执行侧) - .gitignore: 盖住 驱动/*/输出/ (运行时产物不进 git)
12 KiB
12 KiB
06 · 架构与不变量(动手改底座前先读)
这份文档是给"要改这套底座的人(含以后的我)"看的:分层、必须守住的不变量、数据流、 以及几个"为什么故意不做"。改动如果违反下面任何一条不变量,就不是重构,是换架构 —— 要先跟老板对齐。
1. 分层(一层管一层,谁都不越级)
┌──────────────────────────────────────────┐
人 / 脚本 ───► │ 引导器 UEFI.boot.py (内核的管家) │
│ 体检 6 项 / venv 重建 / 包台账 / 记账 │
│ 管内核进程的: 启动 停止 重启 状态 日志 │
└───────────────────┬──────────────────────┘
│ 起/停/判活(走 进程.py)
┌───────────────────▼──────────────────────┐
│ 内核 内核/内核.py (常驻总调度, 甲) │
│ 命令消费 / 调用仲裁 / 依赖巡检 / 心跳 │
│ 扫描 + 校验 + 契约匹配 + 拓扑排序 │
└───────────────────┬──────────────────────┘
│ 启停/判活(同一个 进程.py)
┌───────────────────▼──────────────────────┐
│ 驱动 驱动/<名>/ (确定性代码能力接入层) │
│ 有 配置.efi.json 才算; 源码+venv 或 exec │
└───────────────────┬──────────────────────┘
│ 只经 PG 表(events 总线 / calls 调用)
┌───────────────────▼──────────────────────┐
│ 程序 / Skill (声明式, 模型驱动, v0.2) │ ← 本版不实现
└──────────────────────────────────────────┘
共享内存(唯一的"内核内存"):PostgreSQL 库 efi_kernel(8 张表,见 08)
职责边界(越界就是架构腐化):
| 层 | 只做 | 绝不做 |
|---|---|---|
| 引导器 | 环境体检 / 内核进程的生命周期 / 记账 | 不实现内核的子命令(只透传)、不改驱动配置、不自动修环境 |
| 内核 | 驱动注册与校验、契约匹配、启停与收尸、调用仲裁、巡检 | 不碰驱动内部(不解析 stdout)、不替引导器体检环境 |
| 驱动 | 声明契约、干活、往 PG 汇报 | 不 import 别的驱动、不写 drivers/driver_state/commands、不互相调用 |
2. 硬不变量(破了架构就塌)
| # | 不变量 | 为什么 | 在哪守 |
|---|---|---|---|
| 1 | 内存只有 PG 一处:不引入文件状态源、socket、消息队列、共享内存 | 两处真相 = 迟早不一致;"不用通信协议,直接用 pgsql 当内存"是老板定调 | 全项目(db.py 是唯一碰 SQL 的文件) |
| 2 | 判活一律回 /proc 复核,库里 pid 只当账 |
pid 会被系统复用,拿旧 pid 发信号会杀到别人 | 进程.判活 + 状态.复核(每次动作/渲染前都调) |
| 3 | 发信号前必须校验 cmdline;宁可不杀,不可误杀 | 老板的 GUI 程序被误杀过一次(点名批评) | 进程.停止(入口=…) 一律传入口 |
| 4 | 一个文件只装一种内容 | 混装过:命令输出混进日志文件,按级别筛不了 | 三条日志道 / 快照 / 配置 分家 |
| 5 | 一份实现不复制 | 两套实现 = 各有一套坑,还各自以为对方对 | 进程.py、日志.py、文本.py、状态.py 一份 |
| 6 | 驱动之间零耦合 | 耦合了就打架,而且"认识对方"会让替换/复用全断 | 只有 provides/needs;寻址在 calls 表里由内核做 |
| 7 | 驱动只许写 events / calls |
否则驱动能去停别人的进程,照样打架 | 约定 + 内核校验(角色级硬隔离留 v0.2) |
| 8 | 隔离失败:一个驱动坏只标它自己 | 一个坏配置不能让整台机器停摆 | 校验单个 就地失败、invalid + 原因 |
| 9 | 不吞错、不静默降级 | 出错必须留痕(last_error / events / 日志三处) |
内核对 PG 连不上直接退出(不降级到文件);引导器自己的命令才允许 WARN + 快照 |
| 10 | 状态取值固定 7 个(stopped/starting/running/exited/crashed/failed/invalid) |
状态是各方共用的词汇表,加一个要想清楚全链路 | 状态.py 常量 + 中文映射 |
| 11 | 一次只动一件事:启动与停止分两条命令,没停干净不启 | 反复起/杀会攒孤儿互抢(踩过 13 份隧道孤儿) | 内核 重启 先停干净、试跑引导器.py 每步复查 |
| 12 | 日志轮转只在拉起进程之前做 | 运行中改名 → 进程继续写老 inode = 日志丢 | 内核.拉起一个 / 引导器 内核启动 |
| 13 | 常驻内核独一份(PG 咨询锁 0x65666901,会话级) |
两个调度器 = 打架;会话级锁进程一死自动放,不留死锁 | db.试锁 |
| 14 | 契约匹配的确定性:同契约多提供方按名字取先、启动顺序按名字排 | 结果要可复现,不能靠文件系统顺序碰运气 | 扫描.取契约 / 扫描.排顺序 |
3. 操作纪律(破了不塌,但会难查)
| 纪律 | 说明 |
|---|---|
| 配置外置一份 | 环境.efi.json 是项目级唯一配置(引导器写、内核只读);内核不再自持配置文件 |
| 配置外置:驱动字段 | 驱动自己的配置只有 配置.efi.json 一份,内核只读 |
| 界面纯中文 | 库里存英文(值稳定),给人看的一律过 显示状态() 转中文 |
| CLI 可重定向 | 输出要能 > 文件;--json 只给机器读(人读的表格不混进去) |
| 改完代码必跑 | uvx pyright + 五份自测 + 试跑引导器.py(细节见 10-验收与质量门.md) |
| 注释写"为什么" | 代码里的 【…】 段落是决策记录;改逻辑时把理由一起改,别只改代码 |
4. 主流程(谁在什么时候碰哪张表)
| 流程 | 步骤 | 涉及 |
|---|---|---|
| 体检移交 | UEFI.boot.py <命令> → 体检 6 项 → 写 环境状态.efi.json + kernel_env → 透传内核(前台 跑并转发 / 后台 进程.启动 记账 kernel_runs) |
kernel_env、kernel_runs |
| 扫描 | 扫目录 → 9 条校验 → 收 provides → 查 needs → 查环 → 拓扑排序 → upsert drivers → 清幽灵 → 收尸(状态.复核)→ 写 driver_state → 落快照 → 记 scans + events(scan) |
drivers、driver_state、scans、events |
| 启动 | 填 commands(CLI) 或直接调 → 拼命令(解释器/args/env/EFI_DB)→ 轮转日志 → 进程.启动(独立会话、分隔头、探活)→ 写 driver_state(running) + events(start) → 失败则 failed + 日志尾巴 |
driver_state、events |
| 停止 | 校验 cmdline → SIGTERM 进程组 → 等 stop_timeout → 升 SIGKILL → 收子树 → 写 stopped + events(stop);级联停下游 |
同上 |
| 调用 | 驱动 INSERT calls(pending) → 内核领 → 六条仲裁 → 转发(running + pg_notify driver_<提供方>) → 提供方干活回填 done/failed;超时内核收权 timeout |
calls、events |
| 巡检(每 10s) | 收尸(复核 + 记 exit)→ 按 autostart/restart 重拉 → 级联标"依赖失效" → 收权超时 |
driver_state、calls、events |
| 心跳(每 300s) | 写一条 heartbeat(断电时间线的最后一格) |
events |
5. 双真相(最容易搞混的地方)
| 活真相 | 落盘快照 | |
|---|---|---|
| 位置 | PG driver_state / drivers |
驱动/<名>/运行.efi.json |
| 谁写 | 内核 | 内核(每次扫描/启停/收尸整份重写,原子替换) |
| 用途 | 一切判定与决策 | 文件夹自包含:拷走驱动目录、PG 不在时也能看上次状态 |
| 不要做 | — | 不要把快照当状态源去判活;驱动作者手改它没意义(下次刷新覆盖) |
同理配置:环境.efi.json(项目级,引导器写/内核读)与 驱动/<名>/配置.efi.json(驱动作者写/内核读)
是两份不同层级的配置,不要合并 —— 合并后"谁的配置"就说不清了。
6. 常驻调度(甲)在干什么,为什么必须常驻
四件事,都是"只有在运行时看着才知道"的:
- 命令消费:CLI 客户端把命令写进
commands(state=pending),常驻内核LISTEN/NOTIFY领走执行回填; CLI 自己执行过的命令落state=running,所以常驻内核不会重复领。 - 调用仲裁:
calls表的六条(越权/成环/无人提供/按需拉起/同锁排队/超时收权)。 - 依赖巡检(每 10s):崩了按策略重拉、上游崩了给下游标"依赖失效"(上游只是没启动不算)。
- 心跳(每 300s):断电时间线。
退出时不顺便停驱动("内核可以随时死"),下次启动先收尸认领。
7. 为什么故意不做
| 没做 | 理由(老板定调 / 工程判断) |
|---|---|
| TUI / 图形界面 | "内核要什么界面,肯定是纯 cli 啊和日志" —— 给脚本/自动化用的东西,输出要能 > 重定向 |
| 自造通信协议(socket / 消息格式) | "不用什么通信协议,直接用 pgsql 当内存";events 表就是总线,驱动直插 |
| supervisor / systemd / RPC 框架 | 内存层就是 PG;多一层框架 = 多一处真相与依赖(引导器本身是纯 stdlib,坏了还能报错) |
| 自动修环境 / 自动装包 / 自动重建 venv | "手动控制"口径:只有 环境 重建、包 安装 两个显式命令才动手 |
| 开机自启 | 网络/系统服务手动控制是老板偏好;爬虫管线那种长跑任务是例外 |
驱动自动重试 failed |
配置错了反复撞墙只会把日志刷满;restart=on-failure 只针对崩了的 |
| 日志落库 | 结构化汇报走 events;日志文件管"翻旧账 + tail";两份真相没意义(细节 设计/04) |
8. 已知薄弱点(诚实清单,不是 bug 但要知道)
| 项 | 现状 | 什么时候要处理 |
|---|---|---|
| 驱动权限隔离 | 约定 + 内核校验,不是 PG 角色级硬隔离:驱动拿到的连接串理论上能改 drivers |
v0.2(多用户/不可信驱动时) |
| 调用没有优先级/公平性 | 锁上按 id 先到先得,无权重、无抢占 |
出现"重要任务被长任务堵住"时 |
| 调用结果没有大小限制 | result 是 jsonb;大对象该走"提供方写自己的表 + result 只放引用" |
出现大结果时 |
| 依赖巡检 10s 粒度 | 崩了最多 10s 才被发现/重拉 | 需要更快时(但会更吵) |
| 单 PG 单库 | 没有分片/跨库 | 不在本版范围 |
| 程序/Skill 层由驱动执行 | skill 归驱动管:声明在 驱动/<名>/技能/*.json(两档样例 驱动/样板技能/,规范 设计/05),驱动自己读自己解释;内核不认识它 |
执行样例见 驱动/技能执行/(2026-09-17 落地:选模型 / 拼 prompt / 逐条校验 / 预算不够重试一次) |