zcbot/windows-node/WPF_HOST_REFACTOR.md

775 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Windows Node Host 与 WPF 重构方案
> 状态:阶段 07 已落地,进入阶段 8 发布准备
> 日期2026-08-31
> 适用范围:`windows-node/Zcbot.WindowsNode` 及其测试、打包与运行文档
## 1. 背景
当前 Windows Node 已完成托盘常驻、节点注册、WebSocket 连接、任务持久化与恢复、Adapter 发现与执行、Workspace 管理、专业软件配置、受管 runtime 安装、本机任务查看、数据目录迁移和 ANSYS 真机验收。它已经从一个简单的内网托盘 MVP 演进为专业软件任务的稳定宿主。
当前实现仍是单个 .NET 10 WinForms `WinExe`,后台协议与执行能力总体边界正确,但 UI 和 Host 内部职责开始集中:
- `ConfigurationForm.cs` 约 1300 行,布局、状态、软件管理、任务展示、数据迁移和 ANSYS 验收混在同一窗口类中。
- `TrayApplicationContext.cs` 同时管理托盘、窗口、注册、连接、重连、迁移和安全退出。
- `NodeConnectionLoop.cs` 同时承担 WebSocket 会话、心跳、消息解析、任务编排、恢复、取消与终态回报。
- `JobInboxStore.cs` 同时承担协议接单、磁盘持久化、状态流转、恢复扫描和 UI 展示读取。
- `SoftwareRuntimeManager.cs` 以大体量静态类同时承担软件定义、位置存储、探测、环境注入和 runtime 安装。
- 注册、下载和上传分别管理 HTTP 客户端,网络错误分类、重试和认证注入缺少统一边界。
- 现有 Windows Node 测试中有较多源码字符串断言,能够保护若干安全约束,但难以验证真实状态转换和异步生命周期。
本次重构同时处理两个问题:
1. 将 WinForms 主窗口迁移为 WPF使用 XAML、数据绑定和 MVVM 组织界面。
2. 将 Node Host 重构为可测试、可组合、与 UI 无关的长期宿主架构。
本次不是服务端协议重写,也不是 Adapter 架构替换。
## 2. 目标
### 2.1 用户与运维目标
- 保持现有节点身份、数据目录、任务、Workspace 和 runtime 原位兼容。
- 保持解压发布包后直接运行的部署方式。
- 保持同一 EXE 的托盘、`enroll` 和 `run --headless` 三种入口。
- 提升 125%、150% 和 200% DPI 下的布局稳定性。
- 让节点状态、软件状态、任务状态和长时间操作在界面中保持一致。
- 让新增专业软件主要落在 Adapter 和软件定义,不再扩张单个巨型窗口。
- 在出现连接、传输、Worker 或恢复故障时提供可定位但不泄露 Token 的本机诊断。
### 2.2 工程目标
- 建立唯一 Composition RootGUI 与 headless 共用同一套 Host 组装。
- 将 UI、应用编排、领域状态、协议传输和基础设施分层。
- 以明确的 Host 生命周期替代托盘类直接管理连接任务。
- 以明确的 Job 状态机约束接单、下载、执行、上传、取消和恢复。
- 统一 HTTP、WebSocket、认证、错误分类和传输重试边界。
- 统一 Adapter 目录发现、合同加载、软件 probe 和进程监督边界。
- 对纯逻辑和关键异步流程增加 .NET 行为测试,减少脆弱的源码字符串测试。
- 每个实施阶段都可构建、可打包、可验收和可回退。
## 3. 非目标
以下事项不随本轮重构进入实现:
- 不改服务端 HTTP/WebSocket URL、消息类型、字段语义或认证方式。
- 不改 `node.json`、`jobs/`、`workspaces/`、`runtimes/` 的对外文件布局。
- 不清理、重置或隐式迁移已有节点数据。
- 不改 Adapter 的 `adapter.json + contract + worker + job directory` 协议。
- 不引入 Python GUI、Electron、WebView 或 WinUI 3。
- 不安装 Windows Service不拆 Service/DesktopRunner 双进程。
- 不增加自动更新平台、插件市场或在线 Adapter 覆盖。
- 不增加多 Job 并行或 per-capability slot继续保持整机单执行槽。
- 不把 Adapter probe、软件语义或具体软件分支移入通用 Host。
- 不在框架迁移时同时进行大规模视觉重设计。
## 4. 必须保持的兼容契约
| 契约 | 重构后要求 |
|---|---|
| 可执行文件 | 仍为 `Zcbot.WindowsNode.exe` |
| GUI 启动 | 无参数启动托盘;未注册时显示主窗口,已注册时后台连接 |
| CLI | `enroll` 参数、输出标签和退出码不变 |
| Headless | `run --headless` 不初始化 WPF、窗口或托盘 |
| 单实例 | 继续使用 `Local\\Zcbot.WindowsNode` Mutex |
| 节点身份 | 继续读取现有 DPAPI `LocalMachine` 加密 Token |
| 注册表 | 数据根目录、软件位置和当前账号 Run 项键名不变 |
| 数据目录 | 默认值、环境变量优先级和旧 `%ProgramData%` 兼容不变 |
| Job | 已落盘任务可由新版继续恢复、取消、上传和回报终态 |
| Workspace | `current`、`rollback` 和 head Job 语义不变 |
| Adapter | 继续从 EXE 同级 `adapters/*/adapter.json` 发现 |
| 发布 | 完整包和独立 Adapter 包的目录结构不变 |
| 服务端 | 无 DB migration、无 API 变化、无部署联动要求 |
任何需要改变以上契约的实现发现都必须暂停,单独形成兼容设计,不得借重构直接修改。
## 5. 总体架构
```mermaid
flowchart TD
Entry["Program / Composition Root"]
Tray["TrayHost"]
Wpf["WPF Views"]
VM["ViewModels"]
App["Application Services"]
Host["NodeHost"]
Session["NodeSession"]
Jobs["JobCoordinator"]
Transfers["TransferClient"]
Adapters["AdapterCatalog / AdapterExecutor"]
Persistence["Config / Job / Workspace Repositories"]
Runtime["SoftwareRuntimeService"]
Server["zcbot HTTP / WebSocket"]
Workers["Python or EXE Workers"]
Disk["Existing data root"]
Entry --> Tray
Entry --> Host
Tray --> Wpf
Wpf --> VM
VM --> App
Tray --> App
App --> Host
Host --> Session
Host --> Jobs
Jobs --> Transfers
Jobs --> Adapters
Jobs --> Persistence
App --> Runtime
Session --> Server
Transfers --> Server
Adapters --> Workers
Persistence --> Disk
Runtime --> Disk
```
### 5.1 分层规则
#### Presentation
包含 WPF View、ViewModel、托盘和对话框适配器。
- 可以引用 Application。
- 不直接引用 `ClientWebSocket`、`HttpClient`、Registry、文件系统或进程。
- XAML code-behind 只处理纯 View 行为,例如 PasswordBox 取值、窗口拖动和焦点。
- 不在 `MainWindow.xaml.cs` 中编排节点业务。
#### Application
包含用户操作和宿主生命周期的用例编排。
- 注册、重连、清除身份、迁移数据目录、安全退出。
- 软件位置变更、runtime 安装和固定验收。
- 组合领域结果为不可变 UI Snapshot。
- 不依赖 WPF 或 WinForms 类型。
#### Host / Domain
包含节点会话、Job 状态机、单槽执行、恢复和 Adapter 抽象。
- 不感知托盘、窗口、按钮或对话框。
- 不通过任意字符串隐式改变 Job 状态。
- 关键状态转换经过统一校验和持久化。
#### Infrastructure
包含 HTTP、WebSocket、JSON、DPAPI、Registry、文件、进程和软件探测实现。
- 向上提供明确的小接口或服务,不把 Win32/IO 类型扩散到 ViewModel。
- 原子写、路径约束、日志脱敏和进程树终止在此集中实现。
## 6. 目标项目结构
第一轮保持单一 `Zcbot.WindowsNode.csproj`,先形成目录和命名空间边界,避免同时承担程序集拆分、`internal` 可见性和发布路径变化。完成后再依据依赖图决定是否拆出测试友好的 Core 项目。
```text
Zcbot.WindowsNode/
├── Program.cs
├── App.xaml
├── App.xaml.cs
├── Bootstrap/
│ ├── NodeCompositionRoot.cs
│ ├── NodeCommandLine.cs
│ └── SingleInstanceGuard.cs
├── Application/
│ ├── NodeApplicationController.cs
│ ├── NodeApplicationSnapshot.cs
│ ├── NodeShutdownDecision.cs
│ ├── DiagnosticService.cs
│ ├── JobMonitorService.cs
│ ├── SoftwareManagementService.cs
│ └── AnsysAcceptanceService.cs
├── Host/
│ ├── NodeHost.cs
│ ├── NodeHostSnapshot.cs
│ ├── NodeSession.cs
│ ├── NodeSessionState.cs
│ └── NodeEvent.cs
├── Protocol/
│ ├── NodeProtocolClient.cs
│ ├── NodeProtocolCodec.cs
│ ├── ServerMessage.cs
│ ├── NodeMessage.cs
│ └── NodeProtocolException.cs
├── Jobs/
│ ├── JobCoordinator.cs
│ ├── JobExecutionGate.cs
│ ├── JobStateMachine.cs
│ ├── JobRepository.cs
│ ├── JobRecoveryService.cs
│ └── JobSnapshot.cs
├── Transfers/
│ ├── NodeHttpClient.cs
│ ├── JobInputTransfer.cs
│ ├── JobOutputTransfer.cs
│ └── TransferRetryPolicy.cs
├── Adapters/
│ ├── AdapterCatalog.cs
│ ├── AdapterDescriptor.cs
│ ├── AdapterContractLoader.cs
│ ├── AdapterExecutionService.cs
│ └── ProcessSupervisor.cs
├── Workspaces/
│ ├── WorkspaceRepository.cs
│ └── WorkspacePromotionService.cs
├── Runtime/
│ ├── SoftwareCatalog.cs
│ ├── SoftwareLocationService.cs
│ ├── SoftwareProbeService.cs
│ └── ManagedRuntimeInstaller.cs
├── Persistence/
│ ├── NodeConfigStore.cs
│ ├── NodeDataRootService.cs
│ ├── AtomicFile.cs
│ ├── PathGuard.cs
│ └── RotatingNodeLog.cs
├── Presentation/
│ ├── Tray/
│ ├── ViewModels/
│ ├── Views/
│ ├── Commands/
│ ├── Converters/
│ └── Themes/
└── Compatibility/
└── persisted model readers retained only where required
```
目录是目标职责图,不要求通过一次机械移动完成;重构期间优先保证逻辑边界,最后再统一物理目录。
## 7. Host 核心设计
### 7.1 Composition Root
`NodeCompositionRoot` 是唯一对象组装入口:
- 解析一次数据根目录并生成 `NodePaths`
- 创建唯一 `NodeConfigStore`、`JobRepository`、`WorkspaceRepository` 和 `AdapterCatalog`
- 按当前节点配置创建共享 `NodeHttpClient``NodeProtocolClient`
- 创建 `JobCoordinator`、`NodeSession` 和 `NodeHost`
- GUI 与 headless 只决定 Presentation 和生命周期宿主,不各自复制 Host 构造逻辑。
不引入通用依赖注入容器;当前规模使用显式构造函数组装更便于审计、打包和故障定位。
### 7.2 NodeHost 生命周期
`NodeHost` 对外只暴露少量稳定操作:
```csharp
Task RunAsync(CancellationToken cancellationToken);
Task ReconnectAsync(CancellationToken cancellationToken);
Task<NodeShutdownResult> StopAsync(NodeShutdownMode mode, CancellationToken cancellationToken);
NodeHostSnapshot Current { get; }
event EventHandler<NodeHostSnapshot>? SnapshotChanged;
```
生命周期约束:
- `RunAsync` 是结构化并发根拥有会话、恢复、Job 和传输子任务。
- 所有子任务都可追溯到 Host CancellationToken不允许 fire-and-forget Worker 管线。
- 重连先停止接收 offer再等待本地任务和上传到达安全点之后替换会话。
- 正常退出与取消任务退出使用同一 `StopAsync`GUI 和 headless 不复制收尾逻辑。
- `StopAsync` 幂等;重复点击退出或系统注销不会启动第二条取消链。
- Snapshot 是不可变值UI 只消费 Snapshot不读取 Host 内部集合。
### 7.3 WebSocket 会话拆分
`NodeConnectionLoop` 提取:
- `NodeSession`连接、hello、心跳、接收循环和退避重连。
- `NodeProtocolCodec`JSON 消息的严格解析、类型分发和序列化。
- `NodeProtocolClient`:并发安全发送和 WebSocket 关闭语义。
- `JobCoordinator`:处理 offer、cancel 和导出等业务消息。
要求:
- 一个会话只拥有一个接收循环。
- 所有发送通过同一发送锁,避免并发 `SendAsync`
- 认证拒绝、握手 HTTP 错误、网络中断和协议错误继续保持不同状态。
- 未知消息按现有兼容策略处理,不得把解析异常误报为身份失效。
- 心跳携带的能力、health、slot 和合同摘要来自同一个原子 Snapshot。
### 7.4 Job 状态机
新增 `JobStateMachine` 作为内部唯一状态转换入口,覆盖:
```text
offered
-> accepted
-> downloading
-> ready
-> software_running
-> promoting_workspace
-> uploading_preview / uploading_output
-> succeeded
任意可取消阶段 -> cancelling -> cancelled
任意执行阶段 -> failed
进程重启 -> recovered according to persisted markers
```
具体落盘字符串和 JSON 字段保持现状;状态机首先包裹现有格式,不强制迁移历史文件。
要求:
- offer 仍然先持久化并 flush/原子替换,再回复 accept。
- Job ID、lease ID 和 request digest 一致性校验保持不变。
- 终态不可逆;重复终态消息必须幂等。
- 单执行槽由本机 `JobExecutionGate` 再次强制,不只依赖云端 slot。
- Workspace promotion、预览上传和显式导出分别建模不能因上传重试重复执行专业软件。
- 进程重启恢复只依赖已落盘事实,不依赖 UI 内存状态。
### 7.5 Job 持久化拆分
`JobInboxStore` 拆为三个职责:
- `JobRepository`:原子读写 Job、marker 和 terminal。
- `JobRecoveryService`:启动扫描和恢复决策。
- `JobMonitorService`:面向 UI 生成只读展示 Snapshot。
统一 `AtomicFile`
- 同目录临时文件。
- 写入、flush、关闭后原子替换。
- 原文件存在时保持当前替换语义。
- 不用跨卷 rename。
- 临时文件命名不与 Worker 协议文件冲突。
统一 `PathGuard`
- 所有 Job、Workspace、Adapter 和 runtime 派生路径必须归一化后验证仍位于预期根目录。
- 不把请求内文件名直接拼入本机路径。
- 保持合同内输出 ID 与实际文件 manifest 的现有校验。
### 7.6 传输层
创建每个已加载节点配置唯一的 `NodeHttpClient`
- 统一 BaseAddress、Bearer Token、User-Agent、默认超时和错误摘要。
- 输入下载和输出上传复用连接池。
- 大文件继续流式传输,不整体读入内存。
- 重试只覆盖可安全重放的阶段,上传继续依赖服务端幂等校验。
- 认证失败不进入普通网络重试。
- 日志不记录 Authorization Header、Node Token、注册码或签名 URL 查询串。
`JobInputTransfer``JobOutputTransfer` 只负责传输;是否重试、何时回报终态由 `JobCoordinator` 决定。
### 7.7 Adapter 与进程监督
将当前 Adapter 发现和执行拆为:
- `AdapterCatalog`:启动时一次发现、合同加载和描述符缓存。
- `AdapterContractLoader`manifest、合同路径、Schema 和 SHA-256 校验。
- `AdapterExecutionService`:准备 Job 目录并调用固定 Adapter。
- `ProcessSupervisor`:启动、标准流采集、超时、取消、完整进程树终止和退出确认。
安全边界保持:
- Worker 入口必须来自已安装 manifest不来自任务请求。
- 入口归一化后必须位于对应 Adapter 目录。
- Python 解释器必须来自固定受管 runtime 解析。
- 请求不能注入命令行、环境变量、工作目录或本机路径。
- 进程退出后才读取 terminal取消后必须等待完整进程树退出。
- Host 不解析 Origin、ANSYS、Blender 业务语义。
### 7.8 Workspace
`WorkspaceRepository` 负责现有 metadata 和目录读取,`WorkspacePromotionService` 负责:
- 校验 source head。
- 在临时目录准备下一版。
- 成功后更新 `rollback``current`
- 失败时保持旧 `current` 可继续使用。
- 只保留一份 rollback。
重构不得增加按 Job 永久保存工程副本,也不得在 Node 间搬迁 Workspace。
### 7.9 软件与 runtime 管理
`SoftwareRuntimeManager` 拆为:
- `SoftwareCatalog`:固定的软件和 runtime 定义。
- `SoftwareLocationService`:注册表配置、自动检测和版本来源。
- `SoftwareProbeService`:通过 Adapter probe 汇总最终 health。
- `ManagedRuntimeInstaller`:固定 requirements、临时目录安装、验证和原子替换。
要求:
- 软件卡继续按 `runtime_id` 归并多个 capability。
- 环境变量优先级和现有兼容名称不变。
- runtime 有活动任务时继续拒绝替换。
- 安装命令、镜像地址和 requirements 不接受任务请求控制。
- Origin 软件位置识别与 COM 健康继续分离。
- ANSYS 执行门和固定验收继续由 Adapter/验收脚本给出事实结果。
## 8. WPF Presentation 设计
### 8.1 框架选择
- 继续使用 C# 和 .NET 10。
- 项目启用 WPF。
- 暂时保留 WinForms 引用,仅复用成熟的 `NotifyIcon`
- 不引入第三方 MVVM 或主题框架;先使用小型 `ObservableObject`、`RelayCommand`、`AsyncCommand` 和资源字典。
- 首版使用浅色主题,颜色、字体、间距和控件状态全部抽为资源。
### 8.2 View 与 ViewModel
```text
MainWindow
├── OverviewView / OverviewViewModel
├── SoftwareView / SoftwareViewModel
│ ├── SoftwareCardViewModel[]
│ └── AnsysAcceptanceViewModel
├── JobsView / JobsViewModel
└── SettingsView / SettingsViewModel
```
ViewModel 只接收 Application Service 和不可变 Snapshot不直接 new Adapter Registry、Job Store 或路径对象。
### 8.3 状态绑定
以下状态由 Snapshot 或专用 ViewModel 字段统一驱动:
- 节点连接状态、颜色、说明和操作可用性。
- 注册卡是否显示。
- 软件位置、runtime、probe 和 capability 状态。
- runtime 安装、验收和数据迁移进度。
- Job 列表、选中项、详情和刷新状态。
- 退出期间全局操作禁用。
不允许同一状态同时由多个页面各自推断。
### 8.4 托盘与窗口生命周期
`TrayHost` 只处理:
- 创建和释放 NotifyIcon。
- 将 Host Snapshot 映射为图标、悬浮文本和菜单摘要。
- 打开、隐藏和激活 WPF 主窗口。
- 收集退出选择并调用 `NodeApplicationController.StopAsync`
活动任务退出的三种选择、二次确认和等待提示保持现有语义。业务取消与进程收尾不在 TrayHost 内实现。
### 8.5 可访问性与 DPI
- 使用设备无关布局,不写依赖当前字体像素测量的固定页面宽度。
- 关键页面在 760×560 最小窗口和 100%200% DPI 下可访问。
- 长路径、长错误和任务标题允许换行、截断并提供完整详情。
- 设置合理的 Tab 顺序、AccessKey、默认按钮和取消行为。
- 不仅使用颜色表达在线、警告和失败状态。
- 长时间操作必须有文本状态、进度反馈和可用时的取消入口。
## 9. 诊断与可观测性
在不引入远程日志平台的前提下,增加受控本机 Host 日志:
- 路径位于现有 data root 的 `logs/`
- 使用 ASCII 级别标签:`[INFO]`、`[WARN]`、`[ERR]`。
- 单文件有明确大小上限并只保留有限轮转文件。
- 记录会话阶段、Job ID、capability、Adapter 版本、Worker PID、传输阶段和 HRESULT/异常类型。
- 不记录 Node Token、注册码、Authorization Header、完整签名 URL 或用户输入文件内容。
- “复制诊断信息”继续输出可粘贴摘要,不默认复制完整日志。
`NodeHostSnapshot` 至少包含:
- 当前节点状态和状态变化时间。
- 当前会话 ID 或本地连接代次,不包含凭据。
- 活动 Job、当前阶段和开始时间。
- 待恢复/待上传数量。
- Adapter 数量、能力摘要和最近 probe 时间。
- data root 和版本信息。
## 10. 测试策略
### 10.1 Characterization 测试
重构前固定当前行为:
- CLI 参数、stdout/stderr 标签和退出码。
- 未注册、在线、身份失效和普通断线状态差异。
- offer 先落盘后 accept。
- 重复 Job digest 校验。
- 终态与上传重试幂等。
- Workspace promotion 与 rollback。
- 活动任务退出的等待和取消路径。
- 数据目录迁移的停止、复制、校验和切换顺序。
### 10.2 .NET 单元测试
新增 `Zcbot.WindowsNode.Tests`,优先测试:
- `JobStateMachine` 合法和非法转换。
- `NodeHost` 启停、重复停止和重连屏障。
- `NodeProtocolCodec` 消息分类及错误处理。
- `JobRecoveryService` 对各持久化阶段的恢复决策。
- `TransferRetryPolicy` 对认证、网络、服务端和取消错误的分类。
- `AdapterContractLoader` 的路径、manifest、版本与合同校验。
- ViewModel Snapshot 映射、Command 可用性和异步失败恢复。
测试使用临时目录和可控的 HTTP handler/协议替身;不连接 `.env` 中的数据库或生产服务。
### 10.3 集成测试
- 本地假 WebSocket 服务完成 hello、心跳、offer、cancel 和重连。
- 固定测试 Worker 验证启动、超时、取消和进程树回收。
- 临时 data root 验证重启恢复、原子写和 Workspace promotion。
- framework-dependent 与 self-contained 两种发布包完成启动烟测。
- 发布包验证 Adapter、合同、验收资产和 requirements 完整。
### 10.4 UI 验收
- 未注册首次启动。
- 已注册后台启动和托盘显示。
- 托盘双击、窗口隐藏和恢复。
- 注册、重连、清身份和复制诊断。
- 软件自动检测、手选、清除和 runtime 安装。
- ANSYS 固定验收、取消、通过报告和执行门。
- 本机任务刷新、选中详情和重启后恢复。
- 活动任务等待退出、取消退出和返回。
- 100%、125%、150%、200% DPI。
### 10.5 现有 Python 源码测试调整
保留适合静态审计的测试:
- 项目目标框架和发布参数。
- 不暴露任意执行原语。
- Adapter 与合同是否完整打包。
- Token 不以明文模型字段或日志字段出现。
- 独立 Adapter 包不依赖重建 Node。
将控件名称、WinForms 布局字符串、按钮宽度等断言替换为 .NET 行为测试或 WPF 结构测试,避免测试阻止内部文件拆分。
## 11. 实施阶段
### 阶段 0基线与保护网
- 固化功能、协议、持久化和发布包基线。
- 增加关键生命周期 Characterization 测试。
- 记录真机 DPI 与托盘行为。
- 不改变生产行为。
完成门槛:现有 build、专项测试、打包和启动烟测全部通过。
### 阶段 1Composition Root 与应用控制器
- 引入唯一 `NodeCompositionRoot`
- 从托盘类提取注册、连接、重连、迁移和退出编排。
- GUI 与 headless 共用 Host 构造路径。
- WinForms 界面保持不变。
完成门槛:现有界面和 CLI 行为完全等价。
### 阶段 2Job 持久化与状态机
- 提取 `JobRepository`、`JobStateMachine` 和 `JobRecoveryService`
- 集中原子文件和路径约束。
- 保持现有落盘格式,验证新版能恢复旧 Job。
- 增加状态机和恢复测试。
完成门槛:旧 Job 样本可恢复,终态与 Workspace 不回退。
### 阶段 3会话、协议与传输
- 拆分 `NodeSession`、`NodeProtocolCodec` 和 `NodeProtocolClient`
- 引入共享 `NodeHttpClient`
- 统一错误分类、认证注入和安全重试。
-`JobCoordinator` 接管消息业务编排。
完成门槛假服务端重连、offer、cancel、上传重试和身份拒绝测试通过。
### 阶段 4Adapter、进程与 runtime
- 提取 `AdapterCatalog`、`AdapterExecutionService` 和 `ProcessSupervisor`
- 拆分软件位置、probe 和 runtime 安装。
- 统一活动任务执行门和取消收尾。
- 不改 Adapter 文件或合同。
完成门槛Origin、ANSYS、Blender 现有 probe、执行、取消和打包测试通过。
### 阶段 5WPF Shell 与托盘
- 启用 WPF建立 App、MainWindow、资源字典和 ViewModel 基础设施。
- 保留 WinForms NotifyIcon。
- 完成无参数 GUI、CLI 和 headless 启动分流。
- 先建立空页面与完整生命周期,不删除旧窗口。
完成门槛:单实例、后台启动、显示/隐藏和安全退出通过。
### 阶段 6逐页迁移
按以下顺序迁移:
1. 节点概览与首次注册。
2. 运行设置与数据目录迁移。
3. 专业软件卡和 runtime 管理。
4. ANSYS 固定验收。
5. 本机任务列表和详情。
完成门槛:每一页迁移后均完成行为等价检查,不等待最后一次性验收。
### 阶段 7移除 WinForms 主窗口与加固
- 删除 `ConfigurationForm` 和旧 `TrayApplicationContext`
- 保留 NotifyIcon 所需的最小 WinForms 依赖。
- 清理重复状态、静态 Service Locator 和临时兼容代码。
- 完成本机结构化日志与诊断摘要。
- 完成全量专项测试和发布包真机烟测。
完成门槛:源码中没有第二套可达 UI 或重复 Host 生命周期。
### 阶段 8文档与发布准备
- 架构落地后把稳定决策同步到根 `DESIGN.md`
- 如运行方式、诊断路径或部署步骤有变化,同步 `RUN.md``windows-node/README.md`
- 阶段性成果更新 `PROGRESS.md`
- WPF 稳定前不提升版本号,不创建已发布 Changelog 版本。
- 准备上线时再通过独立 release commit 更新版本和用户版 Changelog。
## 12. 建议提交拆分
每个提交只包含一个可验证的逻辑变化:
1. `test(windows-node): characterize host lifecycle`
2. `refactor(windows-node): add shared composition root`
3. `refactor(windows-node): extract job state and persistence`
4. `refactor(windows-node): separate session and protocol handling`
5. `refactor(windows-node): unify job transfers`
6. `refactor(windows-node): extract adapter process supervision`
7. `refactor(windows-node): split software runtime services`
8. `feat(windows-node): add wpf shell and tray host`
9. `feat(windows-node): migrate overview and settings views`
10. `feat(windows-node): migrate software and acceptance views`
11. `feat(windows-node): migrate local job monitor`
12. `refactor(windows-node): remove legacy winforms window`
13. `test(windows-node): cover wpf and host integration`
14. `docs(windows-node): document host and wpf architecture`
实施中可根据实际依赖合并相邻提交,但不得将 Host 重构、UI 视觉调整和发布版本提升混入同一提交。
## 13. 风险与控制
| 风险 | 控制措施 |
|---|---|
| 重连时重复启动任务 | Host 统一拥有会话代次和单槽执行门 |
| 退出后残留 Worker | 所有进程归 `ProcessSupervisor`,停止时等待完整树退出 |
| 上传重试导致重复执行 | 执行终态与传输状态分离,传输只重放幂等上传 |
| 后台线程更新 WPF 集合 | Snapshot 在 Dispatcher 上映射到 ObservableCollection |
| 新版无法恢复旧 Job | 不改落盘字段,使用历史样本做恢复测试 |
| 数据迁移期间重新接单 | Controller 先停调度并等待安全屏障,再执行迁移 |
| runtime 安装覆盖活动环境 | 本机执行门检查活动 Job临时安装验证后原子替换 |
| Token 泄露到日志或 UI | 集中认证客户端、脱敏器和诊断字段 allowlist |
| WPF 与 WinForms 类型冲突 | WinForms 仅限 Tray 命名空间并使用完整类型名 |
| 重构范围失控 | 非目标列表和阶段完成门槛作为 review gate |
## 14. 回退策略
- 整个改动在独立功能分支完成WPF 稳定前不让线上用户接触。
- 每个阶段保持可构建Host 抽取阶段继续由旧 WinForms UI 驱动。
- WPF 迁移期间旧窗口可作为开发对照,但最终合并前必须只有一套可达 UI。
- 发布前保留上一版完整 Node 程序包;回退只替换程序目录,不修改 data root。
- 因持久化格式保持兼容,回退旧 EXE 后仍能读取原有身份、Job 和 Workspace。
- 若实施中确需新增持久化字段,必须做到旧版本忽略、新版本可缺省,且在本方案中补充独立兼容说明。
## 15. 完成定义
满足以下条件才视为重构完成:
- WinForms `ConfigurationForm` 已移除,主界面全部使用 WPF。
- WinForms 仅用于 NotifyIcon 或已明确记录的必要互操作。
- GUI 和 headless 从同一个 Composition Root 获得 Node Host。
- `NodeConnectionLoop` 的会话、协议和 Job 编排职责已拆分。
- `JobInboxStore` 的持久化、恢复、状态机和 UI 展示职责已拆分。
- `SoftwareRuntimeManager` 的定义、位置、probe 和安装职责已拆分。
- 关键 Host 生命周期和 Job 状态转换有真实 .NET 行为测试。
- 旧节点身份、历史 Job 和 Workspace 在新版测试样本中可继续使用。
- Origin、ANSYS、Blender Adapter 无需修改即可运行。
- framework-dependent 与 self-contained 发布包均通过构建和启动烟测。
- 真机托盘、DPI、runtime 安装、任务执行、取消、上传和安全退出验收通过。
- `DESIGN.md`、`RUN.md`、`PROGRESS.md` 和 Windows Node README 已按实际落地范围同步。
## 16. 首个实施切片
建议第一个开发切片只做以下内容:
1. 增加 Host 生命周期 Characterization 测试。
2. 引入 `NodeCompositionRoot`,消除 GUI 与 headless 的重复构造路径。
3. 提取 `NodeApplicationController`,从托盘类移出注册、重连、迁移和退出编排。
4. 让现有 WinForms UI 继续驱动新的 Controller。
5. 完成 build、专项测试、打包和启动烟测。
该切片不启用 WPF、不移动 Job 格式、不改 Adapter也不改变用户可见界面。它先建立后续 Host 重构和 WPF 迁移共同依赖的稳定边界,风险最低、验证价值最高。
### 16.1 实施记录2026-08-31
首个切片已完成:新增 `Zcbot.WindowsNode.Tests` 与 Host 生命周期行为测试;引入唯一 `NodeCompositionRoot` 和 UI 无关的 `NodeApplicationController`GUI 与 `run --headless` 已共用组装入口;注册、重连、清除身份、迁移和安全退出编排已从托盘类移入 Controller。现有 WinForms 主窗口、协议、Job/Workspace 落盘格式、Adapter 和发布目录均保持不变。
验证已覆盖未注册启动、重连收尾屏障、幂等停止、取消退出、迁移失败重启和清身份收尾;.NET 测试、既有 Windows Node 源码专项测试、solution build、framework-dependent 打包及 GUI/headless 启动烟测通过。下一切片进入阶段 2提取 Job 持久化、状态机与恢复决策。
### 16.2 阶段 2 实施记录2026-08-31
Job 持久化、状态和恢复边界已提取:`JobRepository` 直接负责既有 request/state/terminal/marker 目录的读取与原子写,`JobStateMachine` 约束正常单向转换并以显式 `Recovery` 模式兼容重启后的输入复验和历史成功任务续传,`JobRecoveryService` 将落盘事实分类为续执行、续上传、重放终态和无需动作。`AtomicFile` 与 `PathGuard` 集中保持同目录临时文件、flush 后替换和 Job 派生路径不越界;`JobInboxStore` 暂时保留 offer 校验、旧调用薄入口和 UI 展示读取,后续由协议与 `JobMonitorService` 切片继续收窄。
新增测试覆盖完整状态图、非法回退、终态不可重开、恢复例外、旧 request/state 样本无迁移读取、终态幂等、offer 先持久化及 digest/lease 重放、Workspace promotion/rollback、原子替换和路径穿越拒绝。40 项 .NET 行为测试、25 项既有源码专项、格式检查、Release 发布包及 headless 启动烟测通过;下一切片进入阶段 3会话、协议与传输拆分。
### 16.3 阶段 3 实施记录2026-08-31
会话、协议和 Job 编排已从原连接循环分离:`NodeSession` 负责连接、hello、心跳、单接收循环与退避重连`NodeProtocolCodec` 保持既有消息信封并允许未知消息按原策略忽略,`NodeProtocolClient` 集中处理文本分片、1 MiB 上限、身份关闭语义和发送锁;`JobCoordinator` 接管 offer、cancel、export、恢复回报及本地执行流水线新的 `NodeConnectionLoop` 只组合并收尾两者。
输入下载和输出上传现共用每份已加载配置唯一的 `NodeHttpClient`,统一 BaseAddress、Bearer、Node ID 与 User-Agentlease/digest 保持逐请求 Header避免共享 Header 竞争。`TransferRetryPolicy` 将认证、瞬时、永久和取消错误分开,只有无请求体的 GET 下载执行有界自动重试,输出上传继续依赖既有服务端幂等确认与落盘恢复,不会因传输重试重新执行专业软件。新增假 WebSocket/HTTP handler 测试覆盖信封兼容、未知消息、分片接收、并发发送、4003 身份拒绝、断线重连、认证不重试和瞬时下载重试57 项 .NET 行为测试、25 项源码专项、格式与 diff 检查、Release 发布包及隔离数据目录 headless 烟测通过。下一切片进入阶段 4拆分 Adapter、进程监督与 runtime。
### 16.4 阶段 4 实施记录2026-08-31
Adapter 发现、合同、执行和进程边界已拆分:`AdapterCatalog` 以线程安全 Lazy 在启动期一次发现并缓存已安装描述符,`AdapterContractLoader` 负责 manifest 白名单、版本/runtime、相对路径边界、合同 capability、Workspace 输出、JSON Schema、入口扩展名和 SHA-256`AdapterExecutionService` 保留既有 Job 目录协议、终态读取、Workspace promotion/restore 和错误码,所有 capability 共享 `JobExecutionGate`,不再只依赖云端 slot。`ProcessSupervisor` 统一固定进程启动、受限标准流采集、取消、完整进程树终止和 30 秒退出确认;对不存在 Job 的取消不再创建遗留 CTS。
软件与 runtime 已拆为 `SoftwareCatalog`、`SoftwareLocationService`、`SoftwareProbeService` 和 `ManagedRuntimeInstaller`。Origin/ANSYS/Blender 固定定义、兼容环境变量、注册表与标准目录检测顺序保持不变probe 保持 30 秒缓存并可在 runtime 更新后显式失效;安装器仍只接受内置 requirements 和固定 Python 3.12 发现方式,在临时目录验证后原子替换并保留失败 rollback同时在入口机械拒绝活动执行。新增行为测试覆盖已打包 Adapter 合同加载、跨 capability 单槽、等待取消、软件目录定义和 probe 缓存失效62 项 .NET 行为测试、122 项 Origin/ANSYS/Blender/合同/Node 专项、格式与 diff 检查、Release 发布包及隔离数据目录 headless 烟测通过。下一切片进入阶段 5建立 WPF Shell、ViewModel 基础设施及 NotifyIcon 互操作。
### 16.5 阶段 5 实施记录2026-08-31
GUI 入口已切换为 WPF `Application` 消息循环,新增 `MainWindow`、Light 资源字典、`ObservableObject`、同步/异步 Command 和 `MainWindowViewModel`。主窗口提供节点状态总览、隐藏到托盘、安全退出及经典配置入口;关闭窗口只隐藏,不终止后台节点。新的 `TrayHost` 通过显式 WinForms 类型别名管理 `NotifyIcon`,把 Controller 状态切回 WPF Dispatcher延续托盘双击、重连、三选一活动任务退出和取消二次确认。尚未迁移的注册、专业软件、验收、本机任务和数据目录功能仍由按需创建的 `ConfigurationForm` 承担,阶段 8 完成前保持可达。
CLI 参数分支仍在创建任何 WPF 对象前执行,`run --headless`、单实例互斥体、Composition Root 和 Controller 生命周期保持原语义。新增 ViewModel 状态映射、命令转发、异步命令防重入与异常报告测试66 项 .NET 行为测试、188 项 Origin/ANSYS/Blender/合同/Node 专项、格式与 diff 检查、framework-dependent Release 包、隔离 GUI 消息循环及 headless 返回码烟测通过。下一切片进入阶段 6迁移节点概览、注册与运行设置页面。
### 16.6 阶段 6 第一切片实施记录2026-08-31
节点概览、首次注册和运行设置基础能力已迁入 WPF。`MainWindowViewModel` 现在持有页面导航、身份摘要、注册字段、操作反馈及 Command 可用状态,通过事件边界复用 Controller 的注册、重连和清身份流程;一次性注册码由 `PasswordBox` 同步到 ViewModel注册成功或身份状态刷新后立即清空。清身份继续二次确认注册继续使用 `EnrollOptions` 的 URL、名称和注册码校验。登录自启动经 `IStartupRegistration` 接口调用当前账号 `Run` 项,权限或 IO 失败会保留原开关状态并展示错误。运行设置展示实际 data root迁移按钮暂时进入经典配置以继续使用已有活动任务检查、固定磁盘校验、复制摘要和重启流程。
专业软件、runtime、ANSYS 验收、本机任务和数据目录迁移尚未迁入 WPF因此本记录不把阶段 6 标为整体完成。新增测试覆盖 WPF 页面导航、身份映射、注册参数转发、自启动成功与失败回滚,源码契约确认 WPF 不直接依赖 Controller69 项 .NET 行为测试、189 项 Origin/ANSYS/Blender/合同/Node 专项、格式与 diff 检查、framework-dependent Release 包及隔离 GUI/headless 烟测通过。下一切片迁移专业软件卡与 runtime 管理。
### 16.7 阶段 6 第二切片实施记录2026-08-31
专业软件卡和 runtime 管理已迁入 WPF。新的 `SoftwareManagementService` 组合 `SoftwareCatalog`、`SoftwareLocationService`、Adapter probe 和 `ManagedRuntimeInstaller`,向 Presentation 只暴露不可变快照与受控操作;`SoftwarePageViewModel` 在用户进入页面后异步刷新 Origin、ANSYS、Blender 卡片,支持选择严格匹配的 EXE/安装根目录、清除账号级配置后恢复自动检测、安装或更新隔离 runtime以及显式取消安装。文件和目录对话框、安装确认仍封装在 Tray WinForms 互操作层WPF ViewModel 不读取注册表、不创建进程,也不接触任意命令。
软件快照直接读取 `AdapterCatalog.HasActiveExecution`,全机任一 Job 执行时均禁用 runtime 安装;即使 UI 快照过期,安装器入口仍会二次拒绝活动执行。安装继续固定 Python 3.12、内置 requirements 和镜像配置,在临时目录验证后原子切换,取消时终止完整安装进程树,失败保持原 runtime。新增测试覆盖软件卡刷新、位置保存与清除、安装确认/结果、活动任务禁用和取消收尾73 项 .NET 行为测试、190 项 Origin/ANSYS/Blender/合同/Node 专项、格式与 diff 检查、framework-dependent Release 包及隔离 GUI/headless 烟测通过,未执行真实安装或启动专业软件。下一切片迁移 ANSYS 固定验收。
### 16.8 WPF UI 打磨记录2026-08-31
在不改变页面命令和 Host 生命周期的前提下Light 主题补齐集中颜色、表面、边框、状态和间距 token并用 WPF ControlTemplate 统一按钮与侧栏导航的圆角、悬停、按下、禁用和键盘焦点状态。侧栏使用单选视觉的 ToggleButton 显示当前页,页头增加实时节点状态徽标,概览卡增加同源状态色带,软件卡增加位置可用性徽标和安装忙碌进度;操作区改用 WrapPanel在 760 px 最小窗口下可自动换行。操作结果改为独立提示条,仅在有消息时占位,减少与底部常驻说明的竞争。
新增 UI 结构契约覆盖导航选中绑定、状态 tone、响应式操作区、忙碌反馈和主题资源既有 ViewModel 测试补充状态色与提示可见性73 项 .NET 行为测试、191 项 Origin/ANSYS/Blender/合同/Node/UI 专项、格式与 diff 检查、framework-dependent Release 包及隔离 GUI/headless 烟测通过。纯视觉改动未改变 CLI、托盘、注册、runtime 安装或持久化契约。
### 16.9 阶段 6 第三切片实施记录2026-08-31
按主导航可用性优先,将本机任务迁移提前到 ANSYS 固定验收之前。`JobMonitorService` 从 `JobInboxStore` 接管面向 UI 的只读快照生成,继续合并 request、state、terminal、cloud-terminal 和 upload-complete 落盘事实,不改变既有 Job 目录或恢复协议。新的 `JobPageViewModel` 负责状态中文映射、执行时长、摘要、选中详情与按 Job ID 保持选择WPF 页面以只读表格展示最近 50 条记录,进入页面立即刷新,并在窗口可见且任务页激活时每秒刷新。侧栏“本机任务”不再打开经典配置。
新增测试覆盖快照刷新、状态格式、读取失败、刷新后选择保持,以及真实 WPF 资源加载后的四页互斥可见性和标题绑定94 项 .NET 行为测试、28 项 Windows Node 源码专项、格式与 diff 检查通过。阶段 6 尚余 ANSYS 固定验收和数据目录完整迁移,完成后再删除经典任务页及其 WinForms 定时器,避免迁移期间影响现有回退入口。
### 16.10 阶段 6 完成记录2026-08-31
ANSYS 固定验收和数据目录完整迁移已进入 WPF。`AnsysAcceptanceService` 固定绑定已打包的 `ansys.mechanical.static_structural@v2` 验收入口ViewModel 提供报告目录选择、长时进度、显式停止、通过报告校验和机器级执行门;执行门仍要求本次报告 `passed=true`、用户确认许可证已释放,并在权限不足时保持关闭。`DataRootPageViewModel` 通过 `DataRootManagementService` 检查环境变量托管与未完成任务,目标规范化和二次确认后只调用 Controller 的迁移编排,继续保持停连、复制、逐文件 SHA-256 校验、成功后保存新根目录与失败重连回滚。
### 16.11 阶段 7 完成记录2026-08-31
诊断摘要提取为 `DiagnosticService` 并由 WPF 概览页复制,内容继续排除 Node Token。删除 `ConfigurationForm` 和旧 `TrayApplicationContext`托盘菜单不再暴露经典配置入口WinForms 仅保留 `NotifyIcon`、固定文件/目录选择、确认框和剪贴板互操作。Presentation 不直接持有 Controller不解析 Job JSON不访问注册表也不创建 Worker117 项 .NET 行为测试、28 项 Windows Node 源码专项、格式与 diff 检查通过。下一步进入阶段 8 发布准备与目标机 DPI/GUI 烟测。