# Windows Node Host 与 WPF 重构方案 > 状态:阶段 0–7 已落地,进入阶段 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 Root,GUI 与 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 StopAsync(NodeShutdownMode mode, CancellationToken cancellationToken); NodeHostSnapshot Current { get; } event EventHandler? 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、专项测试、打包和启动烟测全部通过。 ### 阶段 1:Composition Root 与应用控制器 - 引入唯一 `NodeCompositionRoot`。 - 从托盘类提取注册、连接、重连、迁移和退出编排。 - GUI 与 headless 共用 Host 构造路径。 - WinForms 界面保持不变。 完成门槛:现有界面和 CLI 行为完全等价。 ### 阶段 2:Job 持久化与状态机 - 提取 `JobRepository`、`JobStateMachine` 和 `JobRecoveryService`。 - 集中原子文件和路径约束。 - 保持现有落盘格式,验证新版能恢复旧 Job。 - 增加状态机和恢复测试。 完成门槛:旧 Job 样本可恢复,终态与 Workspace 不回退。 ### 阶段 3:会话、协议与传输 - 拆分 `NodeSession`、`NodeProtocolCodec` 和 `NodeProtocolClient`。 - 引入共享 `NodeHttpClient`。 - 统一错误分类、认证注入和安全重试。 - 由 `JobCoordinator` 接管消息业务编排。 完成门槛:假服务端重连、offer、cancel、上传重试和身份拒绝测试通过。 ### 阶段 4:Adapter、进程与 runtime - 提取 `AdapterCatalog`、`AdapterExecutionService` 和 `ProcessSupervisor`。 - 拆分软件位置、probe 和 runtime 安装。 - 统一活动任务执行门和取消收尾。 - 不改 Adapter 文件或合同。 完成门槛:Origin、ANSYS、Blender 现有 probe、执行、取消和打包测试通过。 ### 阶段 5:WPF 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-Agent;lease/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 不直接依赖 Controller;69 项 .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,不访问注册表,也不创建 Worker;117 项 .NET 行为测试、28 项 Windows Node 源码专项、格式与 diff 检查通过。下一步进入阶段 8 发布准备与目标机 DPI/GUI 烟测。