跳到主要内容

全栈审计 — GUI 主窗体/导航/导出/授权层

审计范围: Darra_Profinet_Slave\Darra_Pnet_GUI\ (MainForm*.cs / 页面事件\ / 功能实现\ / Services\ / 导出\ / 授权\ / Localization.cs / Program.cs / 日志, 排除 Views\ 内容本身, Views 归 f6 agent)。 审计方式: 只读 (Read/Grep), 0 代码改动。分级: P1=阻断/数据风险, P2=契约错位/功能缺口, P3=低危/死代码/已知限制。 审计日期: 2026-08-14。

总览

清单项结论发现数
1. 启停链 / 状态契约基本对齐, ConfigError 新态契约 GUI 侧 2 处未跟进P2×2
2. 自动保存无死循环/无重复落盘, 1 处跨线程竞态窗口 + 1 处非原子写P3×2
3. 导出链后台化/文件名规范/异常路径良好, 校验门跨路径不一致 + 5 个孤儿校验 APIP2×1, P3×2
4. 授权激活窗新布局 + 调用链良好, 白字口径一致; 多语言预留未落地等P3×3
5. 多语言字典 136 键 × 9 列全满、下拉 9 项合格; 26 个调用键缺失 + LocalizeUI 覆盖大缺口P2×2, P3×1
6. 状态栏v6 宽度/对齐/7 项清单与更新代码完全一致, 无孤儿引用P3×1 (Id 缺口, 无功能影响)

合计: P1×0, P2×5, P3×7


1. 启停链 / 互锁矩阵 / 状态契约

结论: 结构正确, 但 2026-08-14 服务侧新增的 ConfigError 降级态 GUI 契约未跟进 (2 处 P2)。

证据与逐点核对

启停入口收敛 (✓)

  • 左栏 _btnStartService/_btnStopService页面事件/导航事件.cs 启动设备/停止设备调用启停接口 → 共享 PnetGuiClient.StartAsync/StopAsync (POST /api/start|/api/stop)。
  • 客户端超时: GuiClient.cs:377 PostStartStopAsync 用 15s CTS (与 MainForm 注释"客户端 15 秒上限, 启停是服务端慢端点"一致); 状态/IO 轮询用 2s (GuiClient.cs:62)。

互锁矩阵 (✓)同步启停按钮状态 (MainForm.cs:956-984):

  • 运行中 (_observedReachable && state ∉ {Unknown,Stopped,""}) → 启动禁用/停止可用;
  • 已停止 (state=="Stopped") → 启动可用/停止禁用;
  • 在途 (_startStopPending) / 未知 / 离线 → 双禁用。
  • 请求发出即置 _startStopPending, 完成后 启停请求完成 解除并按结果乐观更新 (成功 → Initialized/Stopped, 失败/超时/断连不动缓存, 以 3s 观测循环校正)。停止失败不解锁, 配置锁定由观测回落真实运行态自动解锁 — 与服务侧语义一致。

停止后无隐式再启动路径 (✓)

  • GUI 侧 start 触发源仅 3 处, 全部用户显式: ① 左栏启动按钮; ② PromptRestartForConfigApply 的 停止→启动 (仅在用户 Yes 确认后, MainForm.cs:1543-1614); ③ DataTestView 启动按钮 (Views 域, 同端点)。状态观测循环只读 (GET /api/status + /api/io), 无任何定时器/观测路径触发 start。语言切换 form.Close() 走 FormClosing 补存 + Program.Main 循环重建, 无启动动作。

配置锁定与状态契约 — 发现 P2-1: ConfigError 被当作"运行中"锁定, 阻断服务文档化的降级恢复路径

  • GUI 判定 (MainForm.cs:839-840): running = reachable && state != "Unknown" && state != "Stopped"ConfigError 落入 running 分支 → 设置配置锁定(true): 设备/槽位/IO 三页只读 + "应用配置"按钮禁用 + (同步启停按钮状态) 启动按钮禁用。
  • 服务侧契约 (PnetStateService.cs:24-33 ConfigError 注释): "用户在 GUI 改选有效网卡 → POST /api/config/reload → POST /api/start 即走全新启动恢复"; PnetApiServer.cs:743-782 BuildConfigReload 对运行态无门禁, 任何状态均接受 reload。
  • 冲突: ConfigError 态下 GUI 把含"网卡选择"的 DeviceView 锁死、应用按钮禁用、启动按钮禁用 → 文档化三步恢复路径 (改网卡→reload→start) 在 GUI 上走不通, 用户被迫先 停止 → Stopped 解锁 → 改网卡 → 应用 → 启动 (四步绕行)。
  • 放大项: 锁定提示文案固定为 "配置锁定 — 服务运行中不允许修改配置" (MainForm.cs:941-944), 在 ConfigError 态 (数据面并未运行) 显示"服务运行中", 与实际状态自相矛盾, 用户无法理解为何网卡改不了。
  • 建议: running 判定排除 ConfigError (或锁定提示按 state 区分文案); 与 q2-bind-missing-nic-graceful (服务侧) 交叉对齐。

发现 P2-2: statusMessage 契约字段 GUI 侧未消费 (孤儿契约)

  • 服务侧: PnetStateService.Message + PnetApiServer.cs:1596 "状态详情文案 … ConfigError 态携带缺失网卡名与失败原因, 经 GET /api/status 的 statusMessage 字段如实上报 GUI/现场"。
  • GUI 侧: Darra_Pnet_GuiClient/Models.cs:25-66 StatusResponse 无 StatusMessage 字段; MainForm 观测循环仅读 State/Connected/EverConnected/IoEpoch/ErrorCode/AlarmCount/CriticalAlarmCount/Version。结果: 网卡缺失时 GUI 左栏仅显示裸 "ConfigError", 缺失网卡名/失败原因看不到 (只在服务日志里), 与"如实上报 GUI/现场"的契约注释不符。
  • 证据: 全仓 grep StatusMessage 在 GuiClient/GUI 中 0 命中 (仅服务侧存在)。

状态观测循环 (✓): 3s 周期 + CancellationToken 关闭取消 (FormClosing → StopServiceStatusObserver) + BeginInvoke marshal + IsDisposed 守卫 + 状态翻转去抖记 WARN + 上线边沿刷新配置来源 (诚实口径: 本地配置 + 运行态, 不假"从服务拉配置")。


2. 自动保存

结论: 防抖/脏标记/关闭补存/启动补存齐备, 与应用/导出无死循环、无重复落盘; 2 处 P3 加固点。

证据

  • 防抖: _autoSaveTimer (1.5s 单次) — SetDirty(true) 重置计时 (MainForm.cs:1918-1928); 到点 自动保存配置()保存配置到默认路径() (ApplyViews + Normalize + Recalculate + ProjectStore.Save) → SetDirty(false) (定时器已 Stop, 无自触发循环)。
  • 无死循环验证 (✓): 三个视图的 Apply() 仅写 _project 字段, 不触发 内容变更; 内容变更 仅在用户编辑经 OnContentChanged_suppressChangeEvent=false 时触发 (DeviceView.cs:345 / IoView.cs:161 / SlotView.cs:505)。即 自动保存→ApplyViews 不会重新置脏 → 无 保存→置脏→再保存 环。
  • 与应用/导出的关系 (✓): 自动保存只落盘 device-config.dpnet, 不校验、不生成运行配置 XML、不下发服务 (注释明确: 校验门在应用/导出); DoApplyConfig = 校验+保存+生成 XML+reload (独立路径); 无"保存→触发应用"耦合。SetDirty(false) + 标记已保存配置() (去重) 防重复落盘/重复 UI 同步。
  • 关闭补存: FormClosing → 关闭时自动补存() — 有脏改才存, 不弹"是否保存"对话框, 失败弹真实错误但不阻断关闭; 语言切换重建窗体 (form.Close) 同路径补存, 语义一致。
  • 启动补存: 调用启停接口(start) 分支调 启动前自动补存() — 失败如实日志+状态栏, 不阻断启动 (服务启动读的是运行配置 XML, 本地 .dpnet 仅持久化)。
  • 锁定期间: 自动保存配置()_configLocked 时跳过; 设置配置锁定(false) 解锁且 _dirty 时重启防抖补存 (MainForm.cs:932-938)。

发现 P3-1: 模态导出窗体打开期间自动保存定时器仍可触发 → 与导出窗体后台生成并发改写 PnetProject

  • WinForms Timer 在模态 ShowDialog 的消息泵上仍会 Tick。用户编辑后 1.5s 防抖窗口内点"导出代码" (DoExportCode 不 SetDirty(false)) → 导出窗体打开后, UI 线程的 自动保存 (ApplyViews + NormalizeIdentNumbers + RecalculateLengths + 序列化读全模型) 与导出窗体 Task.Run 后台生成 (GsdmlWriter.Generate:307-311 / ConfigWriter.Generate:47-51 内部调 NormalizeIdentNumbers + RecalculateLengths, 改写同一 _project 长度缓存) 无跨线程锁。导出窗体自身的 _generationLock 只串行化窗体内部生成, 管不到 MainForm 侧。归一化/长度重算幂等性高, 实际损害概率低, 但属共享模型无锁并发改写。
  • 建议: DoExportCode/DoExportGsdml 打开模态窗体前 Stop 自动保存定时器, 窗体关闭后按 _dirty 重启; 或 PnetProject 归一化/重算加锁。

发现 P3-2: ProjectStore.Save 非原子写

  • File.WriteAllText(filePath, json, ...) 直接覆盖 (ProjectStore.cs:67), 无 temp+rename。自动保存每 1.5s 高频触发, 掉电/崩溃窗口内 device-config.dpnet 可能半写损坏。启动侧已有兜底 (构造加载 try-catch → 回退 CreateDefaultProject + ERROR 日志, MainForm.cs:361-370), 影响有限。

3. 导出链

结论: 后台化/文件名规范/异常路径设计良好; 校验门跨路径不一致 (P2×1), 死 API (P3×2)。

证据

  • DoExportGsdml (MainForm.cs:1077-1129): ApplyViews → Normalize → Recalculate → ValidateDevice (Error 阻止) → _gsdmlWriter.GenerateValidateGsdmlOutput (GSDML 输出轨: hex 格式/31.25µs 时基, Error 阻止) → SaveFileDialog → SaveToFile (try-catch + ERROR 日志 + MessageBox)。文件名 GsdmlWriter.BuildSuggestedFileName = GSDML-V2.32-<厂商>-<设备族>-<yyyyMMdd>.xml (GsdmlWriter.cs:361-369, TIA 硬规则: 设备族后仅允许 yyyymmdd 日期段, 规范合规)。
  • DoExportCode (MainForm.cs:1138-1167): ApplyViews → Normalize → Recalculate → ValidateDevice (Error 阻止) → 导出窗体.ShowExport(this, _project) (模态)。
  • 导出窗体后台化 (✓) (导出/导出窗体.cs): 生成走 BeginGenerate (Task.Run + _generationLock 串行 + _requestSeq 序号令牌陈旧回填作废 + 忙态占位 + 安全回填 防窗体已关闭); 保存走 RunSaveInBackground (写盘/重生成后台, UI 只负责对话框)。"可以有点慢, 但是可以用" 2026-08-14 卡死修复落实到位。
  • 运行配置文件名: ConfigWriter.BuildSuggestedFileName = pnet-<站点名>-config.xml (无日期要求, 合理); 代码导出文件名 = 站点名安全化 + 语言扩展名; SanitizeFileName 非法字符替换 (三处实现一致)。
  • 异常路径 (✓): 主窗体两条导出路径均 try-catch + AddLog(ERROR) + MessageBox 真错误; 导出窗体后台生成/保存失败 XtraMessageBox 如实呈现; 模板缺失抛 FileNotFoundException 带搜索路径 (代码导出.cs:206-210)。

发现 P2-3: 校验门跨路径不一致 + 5 个校验 API 孤儿 (0 调用方)

  • 顶导路径 DoExportGsdml 有 设备校验 + GSDML 输出轨校验 双门; 而导出窗体内 GSDML tab 的保存 (SaveXml → _gsdmlWriter.SaveToFile) 与 运行配置 tab 的保存 (_configWriter.SaveToFile) 不再经过任何校验 — 打开窗体的入场门只有 DoExportCode 的设备级校验。同一 GSDML 导出, 顶导路径被输出轨校验拦截的错误, 经导出窗体路径可直接落盘。
  • ValidationServiceValidateConfigOutput / ValidateConfigSemantics / ValidateGsdmlSemantics / ValidateTiaAcceptance / ValidateForExport 五个公开校验 API 在当前 GUI 0 调用方 (全仓 grep 仅命中 ValidationService.cs 自身与 _archive_GUI_bak_20260809 旧版 MainForm/XmlPreviewView)。即"运行配置 XML 输出轨校验/语义校验/TIA 验收校验"代码存在但从未接线; DoApplyConfig 生成运行配置 XML 也只有设备级校验 + ConfigWriter 自身往返校验 (往返校验查输出器缺陷, 不查语义)。
  • 建议: 导出窗体 GSDML/运行配置保存前复用同款输出轨校验; 或明确裁定哪条校验门是权威口径, 消除双路径差异。

发现 P3-3: 孤儿 API

  • TemplateEngine.RenderToFile (TemplateEngine.cs:426) 0 调用方。
  • 代码导出.单个导出对话框 + 导出对话框结果 (代码导出.cs:72-127) 0 调用方 (导出窗体用自带 SaveCode; 注释自述"主线程稍后接线", 属残留)。

4. 授权

结论: 激活窗新布局/调用链/白字口径合格; 3 处 P3 (多语言预留未落地、ETH 残留文件、Splash 孤儿属性)。

证据

  • 调用链 (✓): navLicense 点击 → 触发授权()授权管理器.显示激活对话框(this) → 模态 激活 窗体 → 关闭后 刷新授权状态标()。管理器: GUI 授权\授权管理器.cs (Darra.Pnet.GUI.授权) 只消费 SDK PnetAuthorization (LicenseStatus/Activate/CurrentCertificate/注册表路径), 不实现验签/落盘; 与 ETH 旧版 Authorization.cs (namespace Darra_EtherCAT_Master.授权) 无命名空间冲突。
  • 激活窗新布局 (✓) (激活.cs + 激活.Designer.cs): ClientSize 钉死 520×400 + Min=Max 封顶; TableLayoutPanel 规范排布 (激活码区/按钮区/状态区/底部关闭); 深色底 (30,30,30 / 45,45,48) + 白字; 激活码单行 TextEdit; 主蓝 #0078D4 激活按钮; 状态行/到期日/指纹短显 (ToolTip 全量) /结果行内联。激活流程: HKLM 可写预检 (非管理员给明确提示) → 禁用输入防重复 → SDK 同步 HTTP 放 Task.Run 后台 → 成功关闭窗体 (授权标由 MainForm 刷新) / 失败内联显示原因。体验计时器 30s, 过期提示一次 (防重复弹)。
  • 未激活白字一致性 (✓): ① navLicense 蓝底(#0078D4)白字恒定 (刷新授权状态标, "未激活"禁红字 — 激活与否看文案); ② 激活窗体未激活态 ForeColor=White (深底); ③ 左栏 labelEnvLicenseValue 默认皮肤深字浅底 — 三处口径一致无白字白底不可见问题。
  • 发现 P3-4: 授权文案多语言仅预留: 授权文案.取文本 现实现 = 中文默认直出 (授权管理器.cs:185-188 注释自认"当前仅中文, key 保留供资源表索引")。激活窗体动态文案 (结果行/体验剩余/过期提示) 在非中文语言下仍中文; 静态控件文案经 9 语 resx (激活.*.resx) 已多语。属已知设计限制, 落档。
  • 发现 P3-5: 授权\Authorization.cs ETH 残留: csproj 已 <Compile Remove> (注释完整落档 0 引用方), 文件仍在盘, 清理由主线程决策。
  • 发现 P3-6: SplashScreen 静态属性 0 消费方: 启动画面.授权状态文本 / 驱动可探测 / 网卡数量 后台计算后无任何读取方 (MainForm 经 RefreshEnvState 独立探测, 授权经 授权管理器.已激活)。孤儿计算 (SplashScreen.cs:30-36)。

5. 多语言

结论: 字典结构完整 (136 键 × 9 列全满) + 语言下拉 9 项合格; 但 26 个调用键缺失 (回退中文) + LocalizeUI 覆盖缺口大 (P2×2), 孤儿键 ~49 (P3×1)。

证据

  • 九语字典完整性 (✓): Localization.cs 136 条字典项, 每条 9 个管道分隔列 (zh|en|ja|de|es|pt|fr|ko|ru), 0 空单元格 (awk 校验: 136 行 NF=9, || 0 命中, 尾列空 0 命中)。语言顺序与 ETH 对齐。
  • 语言下拉 (✓): Designer 静态声明 9 个 tileNavItem (zh/en/ja/de/es/pt/fr/ko/ru), 各语言自名 Caption (中文/English/日本語/Deutsch/Español/Português/Français/한국어/Русский); 导航点击.cs 9 个 case 分发齐备; 语言切换 = 设置文化 + 持久化 lang.txt + form.Close() → Program.Main do-while 循环重建窗体 (不重启进程), 首次启动系统语言检测 (九语全识别, 其余默认英文)。
  • LocalizeUI 覆盖核对: 窗体标题/磁贴导航/5 个可见 tab 页头/状态栏 4 项/日志清空/左栏 3 按钮已接 Localization.L

发现 P2-4: 26 个 L()/LF() 调用键不在字典 (英文界面显示中文) 全仓 literal-key 比对 (callers vs dict keys), 缺失 24 个 L 键 + 2 个 LF 键:

  • 主窗体层: 应用配置 (左栏主操作按钮, LocalizeUI.cs:70 调用, 非中文界面显示中文按钮)。
  • 诊断页新标签 (DiagView 改版后未补字典): 帧统计 / 周期连接 / 最近错误码 / 驱动状态 / 链路物理状态 / 看门狗超时 / 输入区长度 / 输出区长度 / 数据量 / 使用中 / 已激活 IOCR 数 / 发送帧数 / 接收帧数 / 发送周期帧 / 接收周期帧 / 丢弃帧 / 非法帧 / 网卡名称 / 描述。
  • 数据测试页: 无已保存配置 — 输入区/输出区不可用 / 服务未启动 — 输入区/输出区不可用。
  • 键变体失配 (改文案未同步字典): 模块标识号 (0x… 8 位, 留空自动分配) / 子模块标识号 (0x… 1-8 位, 留空自动分配) (字典为无" 8 位/1-8 位"后缀旧版)。
  • LF 缺失: {0}='{1}' 不是合法 16 位十六进制 (格式: 0x0000-0xFFFF), 已恢复原值。 / 标识号 '{0}' 不是合法十六进制 … 或十进制数字。请留空自动分配, 或重填。

发现 P2-5: LocalizeUI 覆盖遗漏 — 左状态列/日志列头/状态栏状态字/导航运行期文案全硬编码中文

  • 左栏合并组 groupInfoMerged.Text = "设备信息" + 7 个标题标签 服务状态:/运行态:/控制器连接:/IO Epoch:/驱动:/授权:/版本: — Designer 硬编码中文, LocalizeUI 未设置, 字典也无对应键。
  • 日志列头 时间/级别/消息 (Designer colTime/colLevel/colMessage) — 未本地化 ("消息"键在字典但未接线; "时间/级别"无键)。
  • _lblState 已保存/未保存 (SetDirty + 消息定时器回填 + Designer 初始) — 硬编码中文; 字典无 未保存/已保存 键。
  • navState / navLicense / labelServiceStateValue 运行期文案 "服务状态: 在线/离线/未安装"、"授权: 已激活/未激活"、labelEnvLicenseValue "已激活/未激活" — 硬编码; 字典有 在线/授权/服务状态 键但未接线, 离线/未安装/已激活/未激活/探测中 无键。
  • 状态栏短消息 (ShowStatusMessage 全部中文) — 瞬态文案, 与 ETH 口径一致, 记为已知限制。
  • _lblWkc "IOPS BAD" 为技术术语, 不译 (合理)。

发现 P3-7: ~49 个孤儿字典键 (0 literal 调用方; 已排除 9 个语言切换 case 字符串)。来源: 报警页删除残留 (活动报警/确认选中/全部确认/严重度/发生时间/次数/来源/共 {0} 条…)、诊断页改版残留 (诊断计数器/添加诊断/更新附加值/移除诊断/设备诊断注入/诊断不可用/分区/计数器/值/说明/共 {0} 项…)、键变体残留 (模块标识号 (0x…, 留空自动分配)/子模块标识号 (0x…, 留空自动分配)/保存/保存配置/打开/新建/成功/错误/警告/提示/日志/启动/停止/启停/版本/信息/在线/服务离线/刷新/确认/消息/子槽/子模块名称/子模块长度/删除模块/删除子模块/输出/输入/模块名称/标识号 '{0}' …或十进制数字。/成功/次数…)。全仓仅 1 处变量键调用 (DataTestView.cs:638 三元两字面量, 均在字典内), 故上述基本可判定为死键。警示: 有"诊断计数器"等键在字典而"帧统计"等实际用键缺失, 说明 DiagView 改版只改了代码没同步字典。


6. 状态栏 (v6 整理)

结论: 宽度/对齐/项清单与更新代码完全一致, 无孤儿引用 (✓); 1 处 P3 (Id 缺口, 无功能影响)。

证据

  • 项清单 (Designer:492-500 顺序 = 更新代码顺序): _lblState(状态: 已保存/未保存 + 瞬态短消息) → _lblServiceState(固定宽 140) → _lblJitter(错误码) → _lblRtt(报警) → _lblWkc(IOPS BAD) → _lblLoss(严重报警) → _lblSpring(AutoSize=Spring 推右) → _lblVersion(版本)。
  • 宽度规格与 Designer 一致: _lblState AutoSize=Content + MinWidth 90 + MaxWidth 380; _lblServiceState Size/Width 固定 140; _lblJitter MinWidth 100; _lblRtt 90; _lblWkc 110; _lblLoss 110; _lblVersion MinWidth 70; 弹簧 Spring。
  • 深色文字 (2026-08-14 浅色底白字不可见修复): 应用状态栏深色文字() 覆盖 7 项 (弹簧无文字不需着色), ApplyStateLabel 同色口径 — 与 Designer 注释/更新代码三方一致。
  • 更新代码与项清单无孤儿: 观测循环在线/离线两分支均更新全部 7 项 (_lblWkc/_lblJitter/_lblRtt/_lblLoss/_lblVersion/_lblServiceState/_lblState); 原"在线(服务 connected)"项 (Id 6) 已删除且更新代码无残留引用 (grep _lblOnline 仅 LocalizeUI 注释 1 处, 无代码引用)。
  • 发现 P3-8: _lblState.Id=0, _lblSpring.Id=8, _lblVersion.Id=7 — Id 6 空缺 (被删"在线"项遗留), DevExpress 不要求连续, 无功能影响, 落档。

分级汇总

#级别条目发现
P2-1启停链ConfigError 态被当"运行中"→ 配置锁定+启动禁用, 阻断服务文档化恢复路径 (改网卡→reload→start); 锁定文案"服务运行中"与 ConfigError 实际态自相矛盾
P2-2启停链statusMessage 契约字段 GUI 未消费 (GuiClient StatusResponse 无该字段) — ConfigError 缺失网卡详情不上 GUI, 与服务注释"如实上报 GUI/现场"不符
P2-3导出链校验门跨路径不一致: 导出窗体 GSDML/运行配置保存绕过输出轨校验; ValidateConfigOutput/ConfigSemantics/GsdmlSemantics/TiaAcceptance/ForExport 5 个校验 API 0 调用方 (孤儿)
P2-4多语言26 个调用键不在字典 (含左栏主按钮"应用配置"), 非中文界面显示中文
P2-5多语言LocalizeUI 遗漏: 左栏组标题+7 标题标签+日志列头+_lblState 已保存/未保存+导航/授权运行期文案 全硬编码中文
P3-1自动保存模态导出窗体打开期间自动保存定时器仍触发 → 与窗体后台生成并发改写 PnetProject (无跨线程锁)
P3-2自动保存ProjectStore.Save 非原子写, 掉电窗口可损坏 device-config.dpnet (启动有回退兜底)
P3-3导出链孤儿 API: TemplateEngine.RenderToFile / 代码导出.单个导出对话框+导出对话框结果 0 调用方
P3-4授权授权文案.取文本 仅中文 (多语言预留未落地, 设计声明)
P3-5授权授权\Authorization.cs ETH 残留文件在盘 (已排除编译, 有落档)
P3-6授权SplashScreen 授权状态文本/驱动可探测/网卡数量 3 静态属性 0 消费方
P3-7多语言~49 个孤儿字典键 (报警页/诊断页改版残留 + 键变体)
P3-8状态栏Id 6 缺口 (被删"在线"项遗留, 无功能影响)

通过项 (无发现)

  • 启停互锁矩阵完整 (在途/未知/离线双禁用), 请求在途标记防重复点击, 停止失败不解锁以观测为准。
  • GUI 侧无隐式再启动触发源 (仅用户显式 3 路径)。
  • 自动保存无死循环 (视图 Apply 不触发内容变更事件, 有 _suppressChangeEvent 守卫), 无"保存→触发应用"耦合, 无重复落盘。
  • GSDML 文件名 yyyyMMdd 规范合规 (TIA 硬规则), 导出窗体后台化 + 序号令牌 + 串行锁 + 关窗守卫落实 (2026-08-14 卡死修复)。
  • 激活窗体 520×400 钉死 + TableLayoutPanel 布局 + 白字口径三处一致 + HKLM 预检 + 异步激活。
  • 字典 136 键 × 9 列全满 0 空单元格; 语言下拉 9 项静态声明, 切换重建窗体闭环。
  • 状态栏 7 项 + 弹簧宽度/对齐/深色文字与更新代码完全一致, 无孤儿引用。
  • GuiClient 超时契约 (启停 15s / 状态 2s) 与 MainForm 注释一致; 服务状态枚举序列化 (ToString) 与 GUI 字符串消费对齐 (Unknown/Stopped/Initialized/DataExchange/Aborted 五态 + 新增 ConfigError)。

本报告由 GUI 主窗体层审计 agent 只读生成, 未改动任何代码文件。