# 架构图第二轮审阅：进步很大，但有一张图是坏的

> **审阅对象**：`six_projects_architecture_gallery.md`（重构版，7 个项目，2026-07-29 10:22）
> **审阅方式**：7 张 Mermaid 全部丢进 mermaid@11 实际渲染 + 逐节点取计算样式；所有代码标识符与行号对着 `参考项目/` 源码树复核。
> **配套**：[架构图绘制规范](架构图绘制规范-给图库Agent.md)（第一轮）

---

## 零、先说结论

**规范被真正吃进去了，不是表面应付。** 这一版的进步是实质性的：

| 规范要求 | 落实情况 |
|---|---|
| 每张图先写「核心问题 + 承重墙」 | ✅ **7/7 全有**，而且问题写得具体（不是「XX 的架构」） |
| 体裁跟着内容走，不要全用 flowchart | ✅ 出现了 `sequenceDiagram`（Claude Code）和 `stateDiagram-v2`（grok-build） |
| 控制平面 / 数据平面分离 | ✅ Hermes 用上了 |
| 进程与信任拓扑 | ✅ Open Design 画出了特权不对称 |
| `文件:行号` 精确引用 | ✅ Hermes 三处引用**逐条命中**（见 §2.1） |
| classDef 语义着色 + 图例 | ⚠️ 部分（见 §3.3、§3.4） |
| **遮住文字后七张图形状不同** | ✅ **通过**——这是最重要的一条 |

上一轮我说「把文字遮住六张图长得一样」。这一轮**不一样了**：平面分栏 / 时序 / 信任拓扑 / 星形 / 事件流 / 状态机 / 继承树，七种形状。**这条过了，说明方法论真的进去了。**

**但是**：交付物里有一张图**根本没渲染出来**，还有一个编造的数字和四个不存在的标识符。下面按优先级排。

---

## 一、P0 · 必须立刻修：OpenManus 那张图是坏的

### 1.1 症状

在 mermaid@11.16.0 里渲染，第 7 张图输出的不是图，是一个炸弹图标：

```
💣 Syntax error in text
   mermaid version 11.16.0
```

我取 SVG 里的节点数：**0 个节点、0 个 cluster**，`aria-roledescription="error"`。

### 1.2 病因

`six_projects_architecture_gallery.md` 第 **352** 行：

```text
    subgraph DELIVERABLES["调研与交付物工具集 (app/tool/)"]
        BROWSER["browser_use_tool.py (Playwright 浏览器)"]
        SEARCH["web_search.py (Google/Baidu 级联搜索)"]
        PYTHON["python_execute.py (代码执行)"]
        CHART["chart_visualization/ (图表绘制)"]
    </div>          ← ❌ 这里应该是 end
```

> 💡 **顺带一个自我示范**：我第一次写这份审阅时，把上面这段**故意展示错误**的代码也标成了 ```` ```mermaid ````，
> 结果自己的渲染验收立刻把它标红了。**用于展示错误语法的代码块必须用 `text` 围栏**，
> 否则它会被渲染器当成真图，在读者那里又炸一次。

一个 HTML 闭合标签混进了 Mermaid 语法。改成 `end` 即可。

### 1.3 这件事的真正教训（比 bug 本身重要）

**规范里漏了一条最基础的交付纪律，我补上：**

> ### 规则零 · 交付前必须在真实渲染器里跑一遍，并确认 SVG 里有节点
>
> 「文件写出来了」≠「图能看」。Mermaid 的解析失败**不会让流程报错**，它安静地渲染一个错误图标就完事。
> 如果你不渲染，你就不知道。
>
> **最低验收命令**——把所有 ```` ```mermaid ```` 块抽出来塞进一个 HTML，渲染后断言每张图 `svg` 里 `g.node` 或 `g.statediagram-state` 数量 **> 0**：
>
> ```python
> import re
> blocks = re.findall(r'```mermaid\n(.*?)```', open(MD).read(), re.S)
> # 生成 html → 浏览器打开 → 断言：
> #   document.querySelectorAll('pre.mermaid').length
> #   === [...].filter(e => e.querySelector('svg g.node, svg g.statediagram-state')).length
> ```
>
> **只要有一张 svg 的 `aria-roledescription === "error"`，这批就不能交。**

---

## 二、P1 · 事实错误（每一条都能被读者当场证伪）

### 2.1 先表扬：Hermes 的三处行号是真查过的

| 引用 | 实际内容 | 判定 |
|---|---|---|
| `run_agent.py:400` | `class AIAgent:` | ✅ 精确 |
| `agent/conversation_loop.py:669` | `def run_conversation(` | ✅ 精确 |
| `tools/memory_tool.py:9-12` | *"Both are injected into the system prompt as a **frozen snapshot** at session start. Mid-session writes update files on disk immediately (durable) but do NOT change…"* | ✅ **精确，且这段原文正好就是承重墙论点的证据** |

**这是本轮做得最好的地方**——第三条尤其漂亮：引用的那几行**本身就在论证你的论点**，而不只是「这个文件存在」。**以后每一条引用都该达到这个标准。**

### 2.2 ❌ 「>90% 前缀缓存命中率」是编造的

**出现位置**：Hermes 章节的「回答核心问题」第一句。

**核实结果**：
- 上游 `参考项目/hermes-agent/AGENTS.md:20-22` 的原话是定性的：
  > *"reuses a cached prefix every turn. Anything that mutates past context … **multiplies the user's cost**. We do not do it"*
- `项目分析/hermes-agent-源码分析.md` 全文**没有任何百分比**。
- 全仓 grep `90%` / `hit rate` **无命中**。

**这个数字没有来源。** 它出现在最醒目的位置（核心问题第一句），读者会把它当作项目宣称的指标去引用。

**改法**：换成有出处的定性表述，或改成可核实的机制描述：

```diff
- 既做到"前缀缓存命中率 >90%"（省钱）
+ 既做到"系统提示词在一个会话内逐字节稳定"（AGENTS.md:90-91 原话
+ "byte-stable for the life of a conversation"），从而每轮复用缓存前缀
```

> **这正是规范第九部分「规则四 · 数字必须现算」要防的情况。** 数字比形容词更容易被信任，所以编造数字的伤害比编造形容词大得多。

### 2.3 ❌ `checkPermission` 不存在

**出现位置**：Claude Code 时序图 `Q->>P: 提权裁决 (checkPermission)`

**事实**：真实标识符是 **`checkPermissions`（复数）**，定义在 `claude-code/src/Tool.ts:500`，并在 `:712`、`:753` 被引用。

### 2.4 ❌ 「YOLO 模式」不是权限模式

**出现位置**：Claude Code 时序图 `alt 处于 YOLO 模式或已授权白名单`

**事实**：
- 真实的权限模式枚举在 `claude-code/src/types/permissions.ts:16-22`：
  ```ts
  export const EXTERNAL_PERMISSION_MODES = [
    'acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan',
  ] as const
  ```
- 「YOLO」确实存在于代码里，但它是**内部分类器的名字**（`yoloClassifier.ts`、`classifyYoloAction`），**不是一个用户可选的模式**。

**改法**：把分支条件换成真实枚举值：

```diff
- alt 处于 YOLO 模式或已授权白名单
+ alt mode ∈ {bypassPermissions, dontAsk} 或命中已授权规则
```

> **为什么这条重要**：时序图上的 `alt` 条件是**可执行语义**，读者会拿它去代码里对。写一个查不到的名字，等于把读者送进死胡同。

### 2.5 ❌ `stuck_detection` 不是真实标识符

**出现位置**：OpenManus 的 `BaseAgent` 节点与总表。

**事实**（`参考项目/OpenManus/app/agent/base.py`）：

```python
:143    # Check for stuck state
:144    if self.is_stuck():
:145        self.handle_stuck_state()
...
:163    def handle_stuck_state(self):
:164        """Handle stuck state by adding a prompt to change strategy"""
```

真实的是 **`is_stuck()`** 与 **`handle_stuck_state()`** 两个方法。

**顺带一个该上图的数字**（`base.py:40`）：

```python
max_steps: int = Field(default=10, description="Maximum steps before termination")
```

**`max_steps` 默认值是 10。** 这个具体数字比「max_steps 步数上限」这句话有用得多——它告诉读者「默认只跑 10 步」，这是一个会让人意外并追问的事实。**这正是规范里说的「能引发追问的信息」。**

### 2.6 ⚠️ 「6 Sandboxes」不准确

**出现位置**：Hermes 图的 `subgraph EXEC["执行与沙箱 (74 Tools & 6 Sandboxes)"]`

**事实**：`tools/environments/` 下确有六种执行后端——`local` / `docker` / `ssh` / `singularity` / `modal` / `daytona`。但 `terminal_tool.py:5-9` 的注释明确写着 **`local` 是"最快但不隔离"**，它**裸跑在本机**。

把 `local` 算进「Sandboxes」是错的，而且错在**安全语义**上——这是最不该含糊的一类。

**改法**：`74 Tools · 6 种执行后端（5 种隔离 + local 裸跑）`

> 74 个工具的数字 ✅ 正确（我的解析 `:512` 统计全部 `registry.register(name=...)` 调用得 74）。
> 20 个平台插件 ✅ 正确（`ls plugins/platforms/ | wc -l` = 20，写「20+」略松但可接受）。
> 50% 压缩阈值 ✅ 正确（`agent/agent_init.py:1765`）。

---

## 三、P2 · 视觉语义没成立（渲染后才看得见）

### 3.1 Hermes 的「控制平面 vs 数据平面」在图上**根本没有形成**

这是本轮最隐蔽、也最可惜的问题——**图的整个卖点是平面分离，但渲染出来颜色是乱的**。

我取了那张图 14 个节点的计算填充色：

| 节点 | 应属平面 | 实际填充 | 判定 |
|---|---|---|---|
| **AIAgent 实例** | 控制 | `rgb(236,236,255)` 默认 | ❌ **没上色** |
| **ReAct 主循环** | 控制 | `rgb(236,236,255)` 默认 | ❌ **没上色** |
| 上下文压缩器 | 控制 | `rgb(238,244,255)` 蓝 | ✅ |
| Curator 后台策展 | 控制 | `rgb(238,244,255)` 蓝 | ✅ |
| 字节级稳定提示词三明治 | 数据 | `rgb(244,255,240)` 绿 | ✅ |
| **skills/ 技能库** | 数据 | `rgb(236,236,255)` 默认 | ❌ **没上色** |
| **MEMORY.md & USER.md** | 数据 | `rgb(236,236,255)` 默认 | ❌ **没上色** |
| **SQLite FTS5** | 数据 | `rgb(236,236,255)` 默认 | ❌ **没上色** |
| 工具注册表 / 六种 Shell | 执行 | `rgb(255,244,236)` 橙 | ✅ |

**14 个节点里 5 个该上色的没上色，其中包括控制平面最核心的两个（AIAgent、主循环）。**

#### 病因有两个，第二个是 Mermaid 的坑

**病因一 · class 列表漏写。**

```
class CTRL,COMP,CURATOR ctrl
```

`CTRL` 是 **subgraph**，写进 class 只会给**容器**上色；组内的 `AIA` 和 `LOOP` **从未被列进任何 class**，所以保持默认。

> 📌 **Mermaid 规则**：`class` 作用于 subgraph 时给 cluster 上色，**不会向下继承给组内节点**。两者必须**分别**声明。
> （我实测确认：Open Design 的 `class DAEMON privileged` 确实把 cluster 涂成了 `#fff3e0`，但组内 5 个节点全是默认色。）

**病因二 · 圆柱形节点 `[( )]` 的 classDef 填充在 mermaid 11 里不生效。**

`SKILLS`、`MEM`、`FTS` 三个节点都写进了 `class PROMPT,SKILLS,MEM,FTS data`，但只有矩形的 `PROMPT` 上了绿色，**三个圆柱全是默认色**。

这个坑**不渲染就绝对发现不了**——语法完全合法，也不报错。

**改法**：

```diff
- class CTRL,COMP,CURATOR ctrl
- class PROMPT,SKILLS,MEM,FTS data
+ %% subgraph 与节点必须分开声明
+ class CTRL ctrlBox
+ class AIA,LOOP,COMP,CURATOR ctrl
+ %% 圆柱形改用 style 逐个指定，绕开 classDef 对 [( )] 失效的问题
+ class PROMPT data
+ style SKILLS fill:#f4fff0,stroke:#4a8f3c,stroke-width:2px
+ style MEM    fill:#f4fff0,stroke:#4a8f3c,stroke-width:2px
+ style FTS    fill:#f4fff0,stroke:#4a8f3c,stroke-width:2px
```

> **通用纪律（补进规范）**：
> **上色之后必须回渲染器逐节点取 `getComputedStyle(rect).fill` 核对。**
> 颜色是这套方法论里最强的语义通道——**它悄悄失效等于论点悄悄失效**。

### 3.2 四张图用了语义色但**没有图例**

Hermes ✅、Open Design ✅ 有图例；**OpenCode / OpenWorker / grok-build / OpenManus 四张没有**。

规范里写过：**没有图例的自定义编码 = 没有编码**。读者看到紫色虚线框只能猜。

### 3.3 裸双向箭头又出现了 4 处

规范第四部分专门点过这条，本轮仍有：

| 位置 | 表达式 | 问题 |
|---|---|---|
| OpenCode | `ZED_EDITOR <-->\|"ACP JSON-RPC"\| INTERNAL_ACP` | ACP 是 JSON-RPC，**确实**双向且对等 → 这个**可以保留**，但要在图例里说明 |
| OpenCode | `SESSION_LOOP <--> SQLITE` | ❌ 这是**读写**，不是双工。应拆成 `-->|写会话| SQLITE` 和 `SQLITE -->|读历史| SESSION_LOOP`，或标注 `--- CRUD` |
| OpenCode | `SESSION_LOOP <--> TOOLS` | ❌ 这是**调用-返回**。应画 `-->|dispatch| TOOLS` + `-->|result| SESSION_LOOP` |
| OpenWorker | `GUI <-->\|"HTTP / WebSockets"\| FASTAPI` | ⚠️ HTTP 与 WebSocket **同步性完全不同**，塞在一条边上。应拆两条：`GUI -->|HTTP 请求| FASTAPI` 与 `FASTAPI -->|WebSocket 事件推送| GUI` |

**最后这条尤其值得改**——OpenWorker 的承重墙是「事件流 + 带外审批」，而**「服务端主动推事件给界面」正是这个论点的物理基础**。用一条 `<-->` 把它和普通 HTTP 请求混在一起，等于把最该突出的机制藏起来了。

### 3.4 OpenCode 用 TB 画星形拓扑，星形没出来

承重墙是「**服务器即本体**，多客户端围绕它」——这是一个**星形**。但图用 `flowchart TB`，渲染成竖直三段，看起来还是「客户端层 → 服务端层 → 外部层」的老式分层。

**改法**：改 `flowchart LR`，把服务端放中间，客户端从左侧汇入、模型从右侧接出。让**拓扑本身**表达论点，而不是靠文字说「这是星形」。

---

## 四、P3 · 内部一致性

### 4.1 Open Design 标题说「三进程」，图上画了五个分区

- 标题：**「三进程拓扑与两端收口架构」**
- 图里：`① 渲染进程` `② SSR 侧车` `③ Daemon` `④ 被 Spawn 的 CLI` `⑤ 文件系统`

即使把 ⑤（存储，不是进程）排除，也是**四类进程**。而 `项目分析/open-design-源码分析.md:120-137` 记的是**五个进程分区**（含可选的 `⑤ 桌面壳 · Electron main`）。

**改法**：标题改「**多进程信任拓扑**」，或明确写「**四进程 + 一个可选 Electron 主进程**」。

> 顺带确认两个我原本怀疑、结果**是对的**的点：
> - **Electron ✅ 确实存在**（`apps/desktop/` 56 文件 / 1.1 MB，Electron 壳 + sidecar IPC）——写「Web / Electron Renderer」是准确的
> - **FileViewer.tsx 660 KB ✅ 精确**（全仓最大源文件）

### 4.2 总表最后一列名不副实

表头是「**源码级事实校验依凭**」，但多数格子填的是**文件路径**而非**可核验的行号**。

只有 `coworker/secrets.py:3` 和 `tools/memory_tool.py:9-12` 两格达标。

**改法**：这一列要么全部补到行号，要么改名叫「关键文件」，别自称「校验依凭」。

---

## 五、下一版该补什么（能力上的空白）

规范列了六种图型，本轮用了 **A（信任拓扑）、B（时序）、C（控制/数据平面）**，还差三种，而且**缺的这三种恰好是对读者最有用的**：

| 图型 | 现状 | 为什么该补 | 现成素材 |
|---|---|---|---|
| **D · 状态归属与生命周期** | 0/7 | 「记忆」是最多人搞错的概念。Hermes 有**冻结快照**（写盘但不改当前会话）这种精妙设计，一张状态归属图能讲透 | Hermes：`MEMORY.md` 落盘 vs 提示词快照的**时间差**；OpenCode：SQLite vs 内存会话 |
| **E · 扩展点与改动半径** | 0/7 | **对选型读者价值最高**。而且你手上有全系列最好的例子 | **Open Design 加第 26 个 CLI = 往 `registry.ts` 加一条数据，零行新逻辑**——这是「适配器即数据」的**可验证后果**，比在框里写「26 条适配器」有力一百倍 |
| **F · 失败与降级路径** | 0/7 | 七张图仍然**全是 happy path** | grok-build 的 `StrategistRecover` 已经画了（👍），但其余六个项目的重试/降级/熔断一条没有 |

### 特别建议：把 Open Design 的扩展点图作为下一张

它能同时满足三个条件——**论点强、素材已核实、读者最需要**：

```mermaid
flowchart LR
    NEED["需求：接入第 26 个 CLI"] --> ASK{"要改什么？"}
    ASK -->|"❶ 数据扩展"| D1["runtimes/registry.ts<br/><b>加一条 RuntimeAgentDef</b><br/>改动半径 = 1 个文件 · 0 行新逻辑"]
    ASK -.->|"❷ 接口扩展（本项目不需要）"| D2["实现一个 Adapter 基类"]
    ASK -.->|"❸ 核心改造（本项目不需要）"| D3["改主循环"]
    D1 --> OK["✅ 完成"]

    classDef good fill:#e8f5e9,stroke:#2e7d32,stroke-width:2.5px
    classDef na   fill:#fafafa,stroke:#bdbdbd,stroke-dasharray:4 3,color:#9e9e9e
    class D1,OK good
    class D2,D3 na
```

*图例：绿色 = 实际需要做的；灰色虚线 = 本架构下**不需要**的路径。*

**这张图的说服力来自「灰掉的那两条」**——它用**没有发生的事**证明了「适配器即数据」的价值。这比任何形容词都有力。

### 另：还有四个项目没有配图

`OpenCode / OpenWorker / grok-build / OpenManus` 只有 Mermaid，没有 v2 的 jpg。目前只有 Hermes(v3) / Claude Code(v2) / Open Design(v2) 三张更新了。

---

## 六、修改清单（按顺序执行）

```
P0 ─ 1. gallery:352  </div> → end                            ← 不改这张图就是坏的
     2. 建立渲染验收：抽 mermaid → 渲染 → 断言无 error svg

P1 ─ 3. 删掉 ">90% 前缀缓存命中率"，换 AGENTS.md:90-91 的原话
     4. checkPermission → checkPermissions            (Tool.ts:500)
     5. "YOLO 模式" → bypassPermissions / dontAsk     (types/permissions.ts:16-22)
     6. stuck_detection → is_stuck() + handle_stuck_state()  (base.py:143-165)
        并补上 max_steps 默认值 = 10                  (base.py:40)
     7. "6 Sandboxes" → "6 种执行后端（5 隔离 + local 裸跑）"

P2 ─ 8. Hermes 补 AIA/LOOP 的 ctrl class；三个圆柱改用 style 上色
     9. OpenCode/OpenWorker/grok/OpenManus 补图例
    10. 拆掉 SESSION_LOOP<-->SQLITE、<-->TOOLS、GUI<-->FASTAPI
    11. OpenCode 改 flowchart LR，服务端居中成星形

P3 ─ 12. Open Design 标题「三进程」→「多进程」或「四进程 + 可选 Electron 主进程」
    13. 总表末列补行号，或改名「关键文件」

下一版 ─ 14. 补图型 D（状态归属）、E（扩展点，建议先做 Open Design）、F（失败路径）
        15. 给剩下四个项目补配图
```

---

## 七、给规范补的两条新规则

本轮暴露的两个问题，第一轮规范里都没写。已确认它们**不渲染就发现不了**，所以必须成为硬性流程。

> ### 规则零 · 渲染验收前置
> 交付前把所有 mermaid 块抽出来实际渲染，断言每张图的 SVG 里**有节点**且 `aria-roledescription !== "error"`。
> Mermaid 语法错误**不会中断流程**，只会安静地画一个炸弹。**不渲染就不知道。**

> ### 规则十三 · 颜色语义必须回渲染器核对
> 用 `classDef` 之后，取每个节点的 `getComputedStyle(rect).fill` 逐个核对。已知两个坑：
> 1. **`class` 作用于 subgraph 不会继承给组内节点**——容器和节点要分别声明；
> 2. **圆柱形 `[( )]` 节点的 `classDef` 填充在 mermaid 11 里不生效**——改用 `style` 逐个指定。
>
> 颜色是这套方法论里最强的语义通道；**它悄悄失效，等于论点悄悄失效**。

---

*本审阅的全部结论均经实测：7 张图在 mermaid@11.16.0 实际渲染并逐节点取计算样式；所有代码标识符对着 `参考项目/` 与 `claude-code/` 源码树 `grep -n` 复核；所有数字现算。*
