# P0-09 桥接收敛：三代合一 + 契约统一

> 写在最前：本机 bridge_daemon.py 幽灵进程 PID 22580 **已死**（最后心跳 12:12:30，本任务执行时已 3 小时无活动）。`find_orphan_bridge.ps1` 仍会做完整检测。

---

## 1. 三代桥对照表（逐行核对）

| 代 | 文件 | 行数 | 入口方式 | 信箱根目录 | 状态来源 | 业务字段覆盖 |
|---|---|---|---|---|---|---|
| **A** | `$env:LOCALAPPDATA\NxCamToolkit\bridge\nx_bridge_dotnet.cs` | 447 | C# DLL，`public static void Main(string[] args)`，靠 `UGII_USER_DIR` 自动加载 | `$env:LOCALAPPDATA\NxCamToolkit\bridge`（v1） | `status.json` | connected/pid/ts/nx_open/part_name（**缺 nx_version/state/units/op_count**） |
| **B** | `C:\NxCamToolkit\nx\nx_bridge.template.cs` | 1125 | 同 A，但由 `launcher.py` 动态生成（**全工程无调用者**） | 硬编码 `"C:/NxCamToolkit/bridge"` | 丰富：Mutex 单例、失败字典、全字段 | 全字段（但**从未接线**） |
| **C** | `$env:LOCALAPPDATA\NxCamToolkit\bridge\bridge_daemon.py` | 71 | Python `while True` 心跳，`run_journal.exe` 拉起 | 同 A（v1） | `status.json`（C# 版格式） | 无业务字段，只转发 cmd→resp |
| **D（P0-08 新基线）** | `D:\NX_AI\nx\NxBridge.cs` | 599 | 同 A | `$env:LOCALAPPDATA\NxCamToolkit\bridge\v2`（**v2 独立目录**） | `status.json` + `hello.json` | 全字段（Section 9 + 7 + 8），协议 v2 |

### 四代致命差异

| 问题 | A 代 | B 代 | C 代 | D（P0-08 基线） |
|---|---|---|---|---|
| 信箱路径 | ✅ v1 | ❌ 硬编码 C: | ✅ v1 | ✅ v2 独立 |
| 协议版本 | ❌ 无 | ❌ 无 | ❌ 无 | ✅ proto="2" |
| nx_version 字段 | ❌ 无 | ✅ 有 | ❌ 无 | ✅ 有 |
| state 状态机 | ❌ 无 | ✅ 有 | ❌ 无 | ✅ idle/busy/modal/error |
| 长任务进度 | ❌ 无 | ✅ 有 | ❌ 无 | ✅ progress/prog_<id>.json |
| 取消 | ❌ 无 | ✅ 有 | ❌ 无 | ✅ cancel/cancel_<id>.json |
| 心跳周期 | 5s（代码） | - | 3s（日志） | 2s |
| JSON 编码 | ASCII？ | UTF-8？ | 默认 cp936 | UTF-8 no-BOM |
| 并发安全 | ❌ 无 | ✅ Mutex | ❌ 无 | ✅ locks/send_<uuid>.lock |

### 三个致命后果（任务书原文 + 实测）

| 后果 | 实测 |
|---|---|
| **双信箱错位** | A 代写 v1 目录；D 代写 v2 目录；旧 Python 主控读 v1，新主控读 v2 —— 两版同时跑会互相"看不见" |
| **契约错位** | A 代 status.json 只有 5 字段；GUI 监控读 12 字段 —— 永远显示"未连接 / 工序 0" |
| **编码静默丢命令** | C 代 bridge_daemon.py `open()` 无 encoding，cp936 下中文工序名 → UnicodeDecodeError → 吞异常 → 删 cmd 文件 |

---

## 2. 唯一基线选择：D（P0-08 NxBridge.cs）

**理由**：

1. **已编译通过**：2212/2412 两份 DLL 真实编译成功
2. **协议版本化**：proto="2"，新老桥不会静默混跑
3. **v2 独立目录**：`bridge/v2/` 与旧 `bridge/` 并存，零互相干扰
4. **完整契约**：Section 9 hello.json + Section 7 status.json 全字段
5. **极薄原则**：只做收发 + 读部件 + 受控 NX 操作，业务字段一律移外部主控
6. **版本隔离**：两份 DLL 各编译一份，不会"一份打天下"

**B 代 template.cs 为什么被放弃**：1125 行的"全字段实现"违背极薄原则，里面的 Mutex 单例、失败字典等业务逻辑属于外部主控职责，不属于桥接。

---

## 3. 唯一字段契约表

### 3.1 status.json（NX 桥每 2s 更新）

| 字段 | 类型 | 单位 | 谁写 | 谁读 | 缺失默认值 | 说明 |
|---|---|---|---|---|---|---|
| `proto` | string | - | NX 桥 | 主控 | `"2"` | 协议版本，不匹配主控报 BridgeProtocolError |
| `pid` | int | - | NX 桥 | 主控 / find_orphan | 0 | 当前桥接进程 pid |
| `state` | string | - | NX 桥 | 主控 | `"unknown"` | idle / busy / modal / error |
| `ts` | float | Unix 秒 | NX 桥 | 主控 | 0.0 | 最后心跳时间 |
| `nx_version` | string | - | NX 桥 | 主控 / GUI | `"unknown"` | NX 版本号（2212 / 2412 / ...） |
| `nx_open` | bool | - | NX 桥 | 主控 / GUI | false | NX 主窗口是否打开 |
| `part` | string | - | NX 桥 | 主控 | `"(none)"` | 当前部件名 |
| `last_cmd` | string? | UUID | NX 桥 | 主控 | null | 最近一条命令 id（busy 时） |
| `last_cmd_ts` | float? | Unix 秒 | NX 桥 | 主控 | null | 最近命令开始时间 |

### 3.2 hello.json（NX 桥启动时写一次）

| 字段 | 类型 | 谁写 | 谁读 | 缺失默认值 |
|---|---|---|---|---|
| `proto` | string | NX 桥 | 主控 | `"2"` |
| `nx_version` | string | NX 桥 | 主控 | `"unknown"` |
| `nx_full_version` | string | NX 桥 | 主控 | `""` |
| `nx_open` | bool | NX 桥 | 主控 | false |
| `pid` | int | NX 桥 | 主控 / find_orphan | 0 |
| `capabilities.long_task_progress` | bool | NX 桥 | 主控 | true |
| `capabilities.cancel` | bool | NX 桥 | 主控 | true |
| `capabilities.version` | string | NX 桥 | 主控 | `"1.0.0"` |
| `supported_commands` | string[] | NX 桥 | 主控 | `[]` |
| `started_at` | float | NX 桥 | 主控 | 0.0 |

### 3.3 Resp（outbox/resp_<id>.json）

| 字段 | 类型 | 谁写 | 谁读 | 缺失默认值 |
|---|---|---|---|---|
| `ok` | bool | NX 桥 | 主控 | **必须有** |
| `status` | string | NX 桥 | 主控 | `"done"` |
| `id` | string | NX 桥 | 主控 | **必须有** |
| `proto` | string | NX 桥 | 主控 | `"2"` |
| `ts` | float | NX 桥 | 主控 | **必须有** |
| `data` | object? | NX 桥 | 主控 | null |
| `error` | string? | NX 桥 | 主控 | null |

### 3.4 Progress（progress/prog_<id>.json）

| 字段 | 类型 | 谁写 | 谁读 | 缺失默认值 |
|---|---|---|---|---|
| `step` | int | NX 桥 | 主控 | 0 |
| `total` | int | NX 桥 | 主控 | 1 |
| `message` | string | NX 桥 | 主控 | `""` |
| `ts` | float | NX 桥 | 主控 | **必须有** |
| `done` | bool | NX 桥 | 主控 | false |
| `ok` | bool? | NX 桥 | 主控 | null |
| `error` | string? | NX 桥 | 主控 | null |

### 3.5 Cmd（inbox/cmd_<id>.json）

| 字段 | 类型 | 谁写 | 谁读 | 缺失默认值 |
|---|---|---|---|---|
| `proto` | string | 主控 | NX 桥 | `"2"` |
| `id` | string (UUID) | 主控 | NX 桥 | **必须有** |
| `type` | string | 主控 | NX 桥 | **必须有** |
| `params` | object | 主控 | NX 桥 | `{}` |
| `ts` | float | 主控 | NX 桥 | **必须有** |
| `deadline` | float? | 主控 | NX 桥 | ts + 60 |
| `trace` | string? | 主控 | NX 桥 | null |

---

## 4. 收敛方案（不动旧文件）

### 4.1 目录布局

```
D:\NX_AI\
├── core\
│   ├── bridge_v2.py              ← 主控客户端（P0-07）
│   ├── contracts.py               ← 本任务新增：契约校验 + 适配层
│   ├── nx_versions.ps1           ← 三源探测
│   └── nx_launch.ps1             ← 进程级环境注入启动器
├── nx\
│   ├── NxBridge.cs               ← C# 桥接源码（P0-08）
│   ├── build_bridge.ps1          ← 编译流水线
│   ├── startup\                  ← 三件套
│   ├── dist\
│   │   ├── 2212\NxBridge.dll
│   │   └── 2412\NxBridge.dll
│   └── deploy_bridge.ps1         ← 本任务新增：自动检测 NX 版本 + 选对应 DLL 部署
├── tools\
│   ├── find_orphan_bridge.ps1    ← 本任务新增：孤儿进程检测
│   └── cleanup_residue.ps1       ← P0-05
└── 状态\
```

### 4.2 收敛动作（只做新增，不删旧文件）

| 动作 | 脚本 | 效果 |
|---|---|---|
| 字段契约校验 | `contracts.py` | 读 status.json/hello.json/resp.json，缺失必填字段立即报错 |
| 适配层 | `contracts.py` `LegacyStatusAdapter` | 把旧 A 代 5 字段 status.json 适配成 D 代 9 字段（缺失补默认值） |
| NX 版本自动部署 | `nx\deploy_bridge.ps1` | 调 nx_versions.ps1 探测本机 NX → 选 dist/\<版本\>/NxBridge.dll → 调 deploy.ps1 部署到 vendor |
| 孤儿进程检测 | `find_orphan_bridge.ps1` | 只读扫描 python/run_journal/ugraf → 报告 PID/命令行/启动时间 |
| 幽灵进程清理 | `find_orphan_bridge.ps1 -Kill` | 强制结束（**需要显式 -Kill 参数**） |

### 4.3 版本切换策略

```
用户打开 NX2212（或 NX2412） → nx_launch.ps1 注入 UGII_BASE_DIR/UGII_ROOT_DIR 等进程级环境变量
    ↓
NX 启动 → 加载 <NX>\UGII\vendor\NX Copilot\startup\ 里的 NxBridge.dll + 三件套
    ↓
NxBridge.Main() 启动 → 写 hello.json + 100ms Timer 轮询 v2/inbox/
    ↓
主控（Python）：BridgeClient.read_hello() → 发现 proto="2" + nx_version="2212" → 版本协商通过
    ↓
主控发送 cmd → 桥接处理 → 写 resp → 主控读 resp
```

**关键**：进程级环境变量（nx_launch.ps1 注入），**不写**系统级/用户级 UGII_VENDOR_DIR（P0-05 清理掉后再也不写）。

---

## 5. 验收清单

| # | 验收 | 命令 |
|---|---|---|
| 1 | contracts.py 能对 P0-07 selftest 的 MockBridge 输出做契约校验 | `python contracts.py --selftest` |
| 2 | contracts.py 能抓到旧 A 代 status.json 的缺字段（nx_version/state 等） | `python contracts.py --legacy-status <旧 status.json>` |
| 3 | find_orphan_bridge.ps1 在本机能真实列出可疑进程 | `.\find_orphan_bridge.ps1` |
| 4 | 找不到进程时如实报告（不崩、不说假话） | `.\find_orphan_bridge.ps1`（期望：0 个 Python 进程在写 bridge.log） |
| 5 | 字段契约表与 contracts.py 校验项一一对应 | 人工核对 |

---

## 6. 不做的事（硬性约束）

- ❌ 不删除 C:\NxCamToolkit 里的旧三代源码
- ❌ 不删除 `$env:LOCALAPPDATA\NxCamToolkit\bridge\` 里的旧文件
- ❌ 不启动 NX GUI 做真机桥接测试
- ❌ 不修改 NX 安装目录下任何官方文件
