2026-06-23-配置收敛-实现计划.md 28 KB

配置收敛(operate↔control 连接组单一数据源 + 死键清理)实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让 operate 与 control 共享的 7 个连接键收敛为唯一数据源 tl-shared.config(换机只改一处),并清理 operate 端 12 个换气/CCD 死副本键。

Architecture: 方案一——新建共享 appSettings 片段 tl-shared.config 放 operate 输出根;operate/control 各自 App.config 用 <appSettings file="…tl-shared.config"> 只读合并(两进程读取 C# 零改动);写入由 operate 侧 AppConfigHelper 经新纯逻辑单元 SharedConfigStore(XDocument 直写)收口。control 仅加一行 XML、C# 与 csproj 均不改。凭据组/front/Java/oplog-config 本轮不动。

Tech Stack: C# / .NET 6 (net6.0-windows) / WPF / xUnit / System.Configuration / System.Xml.Linq。

对应设计: 项目文档/需求文档/specs/2026-06-23-配置收敛-operate-control连接组单一数据源-design.md

分支: feature/config-consolidation(已建,spec 已提交 f547ac9)。

真机口径: UAC 静默可提权、当前无 control 在跑/无活体培养,可自由启停;EEPROM/连接验证无电机风险。验证用部署布局(control 在 operate 根的 control\ 子目录)。


关键文件一览

  • 新建 ivf_tl_operate_2.0/ivf_tl_Operate/tl-shared.config —— 唯一数据源(7 连接键)。
  • 新建 ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/SharedConfigStore.cs —— 纯 XDocument 读写 appSettings 片段。
  • 新建 ivf_tl_operate_2.0/ivf_tl_Operate.Tests/(xUnit)—— operate 首个单测工程,测 SharedConfigStore。
  • ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/AppConfigHelper.cs —— Save 按 key 归属分流。
  • ivf_tl_operate_2.0/ivf_tl_Operate/ivf_tl_Operate.csproj —— 拷贝 tl-shared.config 到输出根。
  • ivf_tl_operate_2.0/ivf_tl_Operate/App.config —— 加 file= + 删 7 共享键 + 删 12 死键。
  • ivf_tl_operate_2.0/control/ivf_tl_ControlHost/App.config —— 加 file="..\tl-shared.config" + 删 7 共享键。
  • 改文档:开发环境/双进程部署指南.md开发环境/连接配置清单-换服务器必读.md、续接三件套。

Task 1: 硬验证 <appSettings file="..\x.config"> 子目录可读父目录(破假设)

方案一命脉。过不了当场退方案二。临时 harness 放 gitignore 的 临时文件/,不入库。

Files:

  • Create: 临时文件/ConfigFileProbe/ConfigFileProbe.csproj
  • Create: 临时文件/ConfigFileProbe/Program.cs
  • Create: 临时文件/ConfigFileProbe/App.config

  • [ ] Step 1: 写 harness 工程

临时文件/ConfigFileProbe/ConfigFileProbe.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <Nullable>disable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
  </ItemGroup>
</Project>

临时文件/ConfigFileProbe/App.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings file="..\probe-shared.config">
    <add key="inlineKey" value="INLINE"/>
  </appSettings>
</configuration>

临时文件/ConfigFileProbe/Program.cs:

using System;
using System.Configuration;

class Program
{
    static int Main()
    {
        var shared = ConfigurationManager.AppSettings["probeKey"];
        var inline = ConfigurationManager.AppSettings["inlineKey"];
        Console.WriteLine($"probeKey={shared}|inlineKey={inline}");
        return shared == "HELLO" ? 0 : 1;
    }
}
  • Step 2: 构建并按"子目录布局"部署

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/临时文件/ConfigFileProbe"
dotnet build -c Release
# 模拟部署布局:把 exe 放进 sub/,共享文件放 sub 的父目录
mkdir -p deploy/sub
cp bin/Release/net6.0-windows/ConfigFileProbe.exe deploy/sub/
cp bin/Release/net6.0-windows/ConfigFileProbe.dll deploy/sub/
cp bin/Release/net6.0-windows/ConfigFileProbe.runtimeconfig.json deploy/sub/
cp bin/Release/net6.0-windows/ConfigFileProbe.dll.config deploy/sub/
cp bin/Release/net6.0-windows/System.Configuration.ConfigurationManager.dll deploy/sub/ 2>/dev/null || true
printf '<?xml version="1.0" encoding="utf-8"?>\n<appSettings>\n  <add key="probeKey" value="HELLO"/>\n</appSettings>\n' > deploy/probe-shared.config
  • Step 3: 运行,确认子目录进程读到父目录共享键

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/临时文件/ConfigFileProbe/deploy/sub"
./ConfigFileProbe.exe; echo "exit=$?"

Expected: 输出 probeKey=HELLO|inlineKey=INLINE,exit=0

✅ 过 → file=+.. 子目录可读父目录成立,继续 Task 2。 ❌ 不过(probeKey 空/exit=1)→ 停下,退回方案二(ProgramData+自写加载器),回写 spec 与本计划,不要继续。

  • Step 4: 不提交(临时文件已 gitignore)

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0" && git status --porcelain 临时文件/ | head

Expected: 无输出(临时文件/ 已被 gitignore)。无需提交。


Task 2: SharedConfigStore 纯逻辑单元 + operate 单测工程(TDD)

Files:

  • Create: ivf_tl_operate_2.0/ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj
  • Create: ivf_tl_operate_2.0/ivf_tl_Operate.Tests/SharedConfigStoreTests.cs
  • Create: ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/SharedConfigStore.cs

  • [ ] Step 1: 建 operate 单测工程并加入 sln

ivf_tl_operate_2.0/ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0-windows</TargetFramework>
    <Nullable>disable</Nullable>
    <IsPackable>false</IsPackable>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.6.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\ivf_tl_Operate\ivf_tl_Operate.csproj" />
  </ItemGroup>
</Project>

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet sln ivf_tl_Operate.sln add ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj

Expected: Project ... added

  • Step 2: 写失败测试

ivf_tl_operate_2.0/ivf_tl_Operate.Tests/SharedConfigStoreTests.cs:

using System.IO;
using ivf_tl_Operate.Helpers;
using Xunit;

namespace ivf_tl_Operate.Tests
{
    public class SharedConfigStoreTests
    {
        private static string TempFile() =>
            Path.Combine(Path.GetTempPath(), "tl-shared-test-" + Path.GetRandomFileName() + ".config");

        [Fact]
        public void Read_missing_file_returns_null()
        {
            var path = TempFile();
            Assert.Null(SharedConfigStore.Read(path, "urlIp"));
        }

        [Fact]
        public void Write_then_Read_roundtrips()
        {
            var path = TempFile();
            try
            {
                SharedConfigStore.Write(path, "urlIp", "http://10.0.0.5");
                SharedConfigStore.Write(path, "urlPort", "10010");
                Assert.Equal("http://10.0.0.5", SharedConfigStore.Read(path, "urlIp"));
                Assert.Equal("10010", SharedConfigStore.Read(path, "urlPort"));
            }
            finally { File.Delete(path); }
        }

        [Fact]
        public void Write_existing_key_updates_value()
        {
            var path = TempFile();
            try
            {
                SharedConfigStore.Write(path, "mqttIp", "192.168.0.108");
                SharedConfigStore.Write(path, "mqttIp", "192.168.0.200");
                Assert.Equal("192.168.0.200", SharedConfigStore.Read(path, "mqttIp"));
            }
            finally { File.Delete(path); }
        }

        [Fact]
        public void Written_file_is_appSettings_fragment()
        {
            var path = TempFile();
            try
            {
                SharedConfigStore.Write(path, "kfkaIP", "192.168.0.108");
                var text = File.ReadAllText(path);
                Assert.Contains("<appSettings>", text);
                Assert.Contains("key=\"kfkaIP\"", text);
            }
            finally { File.Delete(path); }
        }
    }
}
  • Step 3: 运行,确认编译失败(类型不存在)

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet test ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj

Expected: 编译失败 CS0103/CS0246 SharedConfigStore 不存在 = RED。

  • Step 4: 实现 SharedConfigStore

ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/SharedConfigStore.cs:

using System.IO;
using System.Xml.Linq;

namespace ivf_tl_Operate.Helpers
{
    /// <summary>
    /// 配置收敛:operate↔control 共享连接键的唯一数据源文件(tl-shared.config)读写。
    /// 文件格式 = 独立 appSettings 片段(&lt;appSettings&gt;&lt;add key.. value../&gt;&lt;/appSettings&gt;),
    /// 正是 &lt;appSettings file="…"&gt; 期望的外部文件格式 → 两进程经 file= 只读合并即读到。
    /// 用 XDocument 直写,绕开 ConfigurationManager 写回 file= 的不确定行为。
    /// </summary>
    public static class SharedConfigStore
    {
        /// <summary>读片段文件里某键值;文件不存在或键缺失返回 null。</summary>
        public static string Read(string path, string key)
        {
            if (!File.Exists(path)) return null;
            try
            {
                var doc = XDocument.Load(path);
                foreach (var add in doc.Descendants("add"))
                {
                    if ((string)add.Attribute("key") == key)
                        return (string)add.Attribute("value");
                }
                return null;
            }
            catch
            {
                return null;
            }
        }

        /// <summary>写某键值(存在则更新,不存在则新增);文件不存在则创建片段骨架。</summary>
        public static void Write(string path, string key, string value)
        {
            XDocument doc;
            if (File.Exists(path))
            {
                try { doc = XDocument.Load(path); }
                catch { doc = NewDoc(); }
            }
            else
            {
                doc = NewDoc();
            }

            var root = doc.Root ?? new XElement("appSettings");
            if (doc.Root == null) doc.Add(root);

            XElement target = null;
            foreach (var add in root.Elements("add"))
            {
                if ((string)add.Attribute("key") == key) { target = add; break; }
            }
            if (target == null)
            {
                target = new XElement("add", new XAttribute("key", key), new XAttribute("value", value ?? ""));
                root.Add(target);
            }
            else
            {
                target.SetAttributeValue("value", value ?? "");
            }
            doc.Save(path);
        }

        private static XDocument NewDoc() =>
            new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement("appSettings"));
    }
}
  • Step 5: 运行,确认 4 测试全绿

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet test ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj

Expected: Passed! - Failed: 0, Passed: 4

  • Step 6: 提交

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0"
git add ivf_tl_operate_2.0/ivf_tl_Operate.Tests ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/SharedConfigStore.cs ivf_tl_operate_2.0/ivf_tl_Operate.sln
git commit -m "feat(config): SharedConfigStore 共享配置片段读写 + operate 单测工程(TDD 4 绿)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 3: AppConfigHelper 写入按 key 归属分流到共享文件

读取保持不变(经 ConfigurationManager 的 file= 合并);只改 Save 的落点。

Files:

  • Modify: ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/AppConfigHelper.cs

  • [ ] Step 1: 加共享键集合 + 共享文件路径 + Save 分流

AppConfigHelper 类内,把现有 Save 方法(约 :58-74)替换为下面这段(新增 SharedKeys/SharedConfigPath/SaveShared,改 Save):

        /// <summary>配置收敛:operate↔control 共享的连接键(唯一数据源 tl-shared.config)。</summary>
        private static readonly System.Collections.Generic.HashSet<string> SharedKeys =
            new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
            { "urlIp", "urlPort", "mqttIp", "mqttPort", "kfkaIP", "kfkaPort", "outInter" };

        /// <summary>共享文件路径 = operate 输出根目录\tl-shared.config(control 经 ..\ 指此)。</summary>
        public static string SharedConfigPath =>
            System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "tl-shared.config");

        /// <summary>
        /// 写回普通键。配置收敛:共享键落唯一数据源 tl-shared.config(SharedConfigStore),
        /// 非共享键沿用 OpenExeConfiguration 落 operate 自己的 config。写后刷新本进程缓存。
        /// </summary>
        public static void Save(string key, string value)
        {
            try
            {
                if (SharedKeys.Contains(key))
                {
                    SharedConfigStore.Write(SharedConfigPath, key, value);
                }
                else
                {
                    Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                    if (config.AppSettings.Settings[key] == null)
                        config.AppSettings.Settings.Add(key, value);
                    else
                        config.AppSettings.Settings[key].Value = value;
                    config.Save(ConfigurationSaveMode.Modified);
                }
                ConfigurationManager.RefreshSection("appSettings");
            }
            catch
            {
                // 写失败不抛出(避免阻断 UI/启动);[M7] 运行环境核查落盘。
            }
        }
  • Step 2: 编译 operate(Release)确认 0 错

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet build ivf_tl_Operate/ivf_tl_Operate.csproj -c Release

Expected: Build succeeded. 0 Error(s)。(若报 MSB3021 DLL 锁,先停掉在跑的 operate/control 再编。)

  • Step 3: 复跑 operate 单测确认未回归

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet test ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj

Expected: Passed: 4

  • Step 4: 提交

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0"
git add ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/AppConfigHelper.cs
git commit -m "feat(config): AppConfigHelper.Save 共享键落 tl-shared.config、非共享键不变

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 4: 新建 tl-shared.config + operate csproj 拷贝 + operate App.config 接 file= 并删共享键/死键

Files:

  • Create: ivf_tl_operate_2.0/ivf_tl_Operate/tl-shared.config
  • Modify: ivf_tl_operate_2.0/ivf_tl_Operate/ivf_tl_Operate.csproj(在已有 oplog-config.json 的 ItemGroup 旁加一项)
  • Modify: ivf_tl_operate_2.0/ivf_tl_Operate/App.config

  • [ ] Step 1: 创建唯一数据源 tl-shared.config(当前 108 值)

ivf_tl_operate_2.0/ivf_tl_Operate/tl-shared.config:

<?xml version="1.0" encoding="utf-8"?>
<!-- 配置收敛:operate↔control 共享连接键的【唯一数据源】。
     换中间件服务器/改连接参数只改本文件一处。
     operate 经 App.config <appSettings file="tl-shared.config"> 读;
     control 经 ..\tl-shared.config 读。两进程读取代码零改动。 -->
<appSettings>
  <add key="urlIp" value="http://127.0.0.1"/>
  <add key="urlPort" value="10010"/>
  <add key="mqttIp" value="192.168.0.108"/>
  <add key="mqttPort" value="1883"/>
  <add key="kfkaIP" value="192.168.0.108"/>
  <add key="kfkaPort" value="9092"/>
  <add key="outInter" value="0"/>
</appSettings>
  • Step 2: operate csproj 拷贝到输出根(仿 oplog-config.json)

ivf_tl_operate_2.0/ivf_tl_Operate/ivf_tl_Operate.csproj 已有的 oplog-config.json 那段(:171-174 附近)下方,同一 <ItemGroup> 内追加:

    <None Remove="tl-shared.config" />
    <Content Include="tl-shared.config">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  • Step 3: operate App.config 接 file= + 删 7 共享键 + 删 12 死键

ivf_tl_operate_2.0/ivf_tl_Operate/App.config<appSettings> 开标签改为:

	<appSettings file="tl-shared.config">

然后删除这 7 个共享键行(连同其上的注释):outInterurlIpurlPortmqttIpmqttPortkfkaIPkfkaPort。 再删除这 12 个换气/CCD 死键行(连同注释):CCDErrorcsTimegbTimeVentNumVentPreVentWaitTimeBVentWaitTimeDAutoWaitTimeCCDAutoWaitTimeCCDFailedWaitTimeCCDFailedNumberQueuAir

保留(operate 独有):autoFocususerNamepassWordengineerPwdtlNumhouseEnabledLanguageStopProcacheDiskcontrolPortcontrolExePath

  • Step 4: 删死键前逐键 grep 坐实 operate 树无消费点

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0/ivf_tl_Operate"
for k in CCDError csTime gbTime VentNum VentPre VentWaitTimeB VentWaitTimeD AutoWaitTime CCDAutoWaitTime CCDFailedWaitTime CCDFailedNumber QueuAir; do
  echo "== $k =="; grep -rn "AppSettings\[\"$k\"\]\|GetString(\"$k\"\|GetInt(\"$k\"" . --include=*.cs;
done

Expected: 每个键无输出(operate 端不消费)。若某键有输出 → 该键不删、保留并在交接卡记录例外。

  • Step 5: 编译 operate(Release)0 错

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet build ivf_tl_Operate/ivf_tl_Operate.csproj -c Release

Expected: Build succeeded. 0 Error(s);输出目录 bin/Release/net6.0-windows/tl-shared.config 存在。

Run(确认落盘):

ls -l "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0/ivf_tl_Operate/bin/Release/net6.0-windows/tl-shared.config"

Expected: 文件存在。

  • Step 6: 提交

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0"
git add ivf_tl_operate_2.0/ivf_tl_Operate/tl-shared.config ivf_tl_operate_2.0/ivf_tl_Operate/ivf_tl_Operate.csproj ivf_tl_operate_2.0/ivf_tl_Operate/App.config
git commit -m "feat(config): operate 接 tl-shared.config(file=)+删7共享键+删12换气CCD死键

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 5: control App.config 接 ..\tl-shared.config 并删 7 共享键

control 只动 App.config 一处:加 file= 指父目录 + 删共享键。C#/csproj 不动。换气/CCD 业务键 control 仍读 → 保留。

Files:

  • Modify: ivf_tl_operate_2.0/control/ivf_tl_ControlHost/App.config

  • [ ] Step 1: control App.config 接 file= + 删 7 共享键

ivf_tl_operate_2.0/control/ivf_tl_ControlHost/App.config<appSettings> 开标签改为:

	<appSettings file="..\tl-shared.config">

然后删除这 7 个共享键行(连同注释):urlIpurlPortmqttIpmqttPortkfkaIPkfkaPortoutInter

保留(control 独有,缺则 NPE):userNamepassWordengineerPwdtlNumhouseEnabledLanguageautoFocusCCDErrorcsTimegbTimeVentNumVentPreVentWaitTimeBVentWaitTimeDAutoWaitTimeCCDAutoWaitTimeCCDFailedWaitTimeCCDFailedNumberQueuAirStopProcacheDiskcontrolPort

  • Step 2: 编译 control 全 sln 0 错

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0/control"
dotnet build ivf_tl_Control.sln -c Release

Expected: Build succeeded. 0 Error(s)

  • Step 3: 提交

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0"
git add ivf_tl_operate_2.0/control/ivf_tl_ControlHost/App.config
git commit -m "feat(config): control 接 ..\\tl-shared.config(file=)+删7共享键,C#零改动

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 6: 真机端到端验证 + 回归 + 文档同步 + 收尾提交

验证用部署布局:operate 输出根 + 其下 control\(ControlHost)+ 根的 tl-shared.config。UAC 静默提权,无活体培养可自由启停。

  • Step 1: 按部署布局部署 + 改共享文件为可辨识值

Run(部署 control 到 operate 根的 control\ 子目录、确认共享文件就位):

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
OPDIR="ivf_tl_Operate/bin/Release/net6.0-windows"
mkdir -p "$OPDIR/control"
cp -r control/ivf_tl_ControlHost/bin/Release/net6.0-windows/* "$OPDIR/control/"
ls -l "$OPDIR/tl-shared.config" "$OPDIR/control/ivf_tl_ControlHost.dll.config"

Expected: 两文件都在。control/没有 tl-shared.config(靠 ..\ 指父目录)。

  • Step 2: 单进程读取验证(operate 读 file= 合并)

用 Task1 的方式直接验 control 端读取:提权静默启动 control.exe,查 /status 连接来自共享文件的值。先把共享文件 mqttIp 改个可辨识值再启。

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
OPDIR="ivf_tl_Operate/bin/Release/net6.0-windows"
# 提权启动 control(独立进程,读 ..\tl-shared.config)
powershell -Command "Start-Process -Verb RunAs -FilePath '$PWD/$OPDIR/control/ivf_tl_ControlHost.exe'"
sleep 12
curl -s http://127.0.0.1:38080/status; echo

Expected: /status 返回 {"ok":true,"pid":…,"tlSn":"NEO-1-20230411","started":…},证明 control 读到了 ..\tl-shared.config 的 url/gateway(否则连不上、started 不会 true / 进程起不来)。

  • Step 3: 改共享文件单点 → 两进程同时生效(核心验证)

Run(停 control → 改共享文件 kfkaIP 为占位 → 重启 → 看日志/连接反映新值):

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
OPDIR="ivf_tl_Operate/bin/Release/net6.0-windows"
curl -s -X POST http://127.0.0.1:38080/shutdown -d 'token=tl13579'; echo   # 受护栏停 control
sleep 3
# 仅改共享文件一处(模拟换机)
sed -i 's#key="mqttIp" value="192.168.0.108"#key="mqttIp" value="192.168.0.108"#' "$OPDIR/tl-shared.config"
grep mqttIp "$OPDIR/tl-shared.config"
powershell -Command "Start-Process -Verb RunAs -FilePath '$PWD/$OPDIR/control/ivf_tl_ControlHost.exe'"
sleep 12
curl -s http://127.0.0.1:38080/status; echo

Expected: control 正常起、/status started,证明改一处 tl-shared.config 即对 control 生效(operate 同机制读同文件 → 同样生效)。验证后把 mqttIp 改回 108。

  • Step 4: operate Release 真外壳 E2E(读 file= 合并 + 登录)

沿用既往 E2E 脚手架口径(OPERATE_E2E=1 绕僵尸 Mutex、用 App.config 凭据走真实 AppDataInit)。确认 operate 经 file= 读到 urlIp 后真服务器登录成功 + 拉起 control。

Run(若僵尸 operate 仍占主 Mutex,用 E2E 包装;否则直接起):

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
OPDIR="ivf_tl_Operate/bin/Release/net6.0-windows"
# 提权启动真 operate.exe(它经 file= 读 tl-shared.config 的 urlIp → 登录 → 拉起 control)
powershell -Command "Start-Process -Verb RunAs -FilePath '$PWD/$OPDIR/ivf_tl_Operate.exe'"
sleep 20
curl -s http://127.0.0.1:38080/status; echo

Expected: operate 登录成功(不闪退)+ 10s 后拉起 control、/status started:true。证明 operate 端 file= 合并读取通。

若僵尸 PID 20268 占 operate 主 Mutex 致真 operate 起不来:记录"operate 外壳受僵尸门控、需重启清",但 Step2/3 已用真 control 进程坐实 file= 读取与单点生效;operate 与 control 读取机制同源(均 ConfigurationManager+file=),可据此判定。

  • Step 5: 回归——operate 单测 + control 40 单测 + HIL 套件 + 双编译

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0/ivf_tl_operate_2.0"
dotnet test ivf_tl_Operate.Tests/ivf_tl_Operate.Tests.csproj
dotnet test control/ivf_tl_SerialHelper.Tests/ivf_tl_SerialHelper.Tests.csproj
dotnet test control/IvfTl.Hardware.HilTests/IvfTl.Hardware.HilTests.csproj
dotnet build ivf_tl_Operate/ivf_tl_Operate.csproj -c Release
dotnet build control/ivf_tl_Control.sln -c Release

Expected: operate 4 绿;SerialHelper 40 绿;HIL 零写入 2 过 2 跳(无写开关);双编译 0 错。

  • Step 6: 停掉测试用 control + 清残留

Run:

curl -s -X POST http://127.0.0.1:38080/shutdown -d 'token=tl13579'; echo
sleep 3
curl -s http://127.0.0.1:38080/ping; echo "  <- 应连不上=已退出"

Expected: /ping 连不上(control 干净退出、7 COM 释放)。

  • Step 7: codegraph sync

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0" && codegraph sync

Expected: 增量同步完成。

  • Step 8: 文档同步(回写协议 §3.3)

更新下列文档,与代码对齐后再提交:

  • 项目文档/开发环境/连接配置清单-换服务器必读.md 第三节:operate/control 的 C# 换机由"改 operate+control 两份 App.config"改为"只改 operate 根的 tl-shared.config 一份";标注共享键来源。
  • 项目文档/开发环境/双进程部署指南.md:产物清单加 tl-shared.config(operate 根,control 经 ..\ 读);自检清单加"确认 tl-shared.config 在 operate 根、control\ 下无此文件";升级注意 PreserveNewest 保站点值。
  • 项目文档/需求文档/操作端逻辑与配置全景.md + control-逻辑与配置全景.md:连接键章节标注"共享键移入 tl-shared.config、operate 删 12 换气/CCD 死键"。
  • 续接三件套:进度状态.yaml(覆盖断点)、工作计划表.md(配置收敛状态)、交接卡.md(追加本次)、进度数据.js

  • [ ] Step 9: 收尾提交

Run:

cd "C:/Users/AIVFO/Documents/trae_projects/TLProject/aivfo-tl-3.0"
git add 项目文档/
git commit -m "docs(config): 配置收敛文档同步——换机清单/部署指南/两全景/续接三件套

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

自查(spec 覆盖核对)

  • spec §四 文件布局 → Task 4(tl-shared.config + csproj)、Task 5(control file=)。✓
  • spec §五 键归属(7 共享 / operate 死键 12 / 各独有保留)→ Task 4 Step3-4、Task 5 Step1。✓
  • spec §六 读零改动 / 写经 SharedConfigStore 分流 → Task 2(SharedConfigStore)、Task 3(AppConfigHelper.Save 分流)。✓
  • spec §七 构建拷贝 / PreserveNewest → Task 4 Step2;部署/升级 → Task 6 Step1、Step8。✓
  • spec §八 边界(缺文件可见失败 / 写失败兜底 / RefreshSection)→ AppConfigHelper try 兜底(Task3)、Task6 Step3 RefreshSection 即时生效验证。✓
  • spec §九 测试(file= 硬验证 / SharedConfigStore 单测 / 真机 E2E / 回归)→ Task1 / Task2 / Task6 Step2-4 / Step5。✓
  • spec §十 风险(file=+.. / 死键误删 / 写回即时生效)→ Task1 拦截 / Task4 Step4 grep / Task6 Step3。✓

类型一致性核对: SharedConfigStore.Read(path,key) / Write(path,key,value)(Task2 定义)= AppConfigHelper 调用(Task3)= 测试调用(Task2)签名一致。✓