From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理
---
docs/superpowers/plans/2026-11-03-local-broker-mvp.md | 1328 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,328 insertions(+), 0 deletions(-)
diff --git a/docs/superpowers/plans/2026-11-03-local-broker-mvp.md b/docs/superpowers/plans/2026-11-03-local-broker-mvp.md
new file mode 100644
index 0000000..ba4e671
--- /dev/null
+++ b/docs/superpowers/plans/2026-11-03-local-broker-mvp.md
@@ -0,0 +1,1328 @@
+# Local + Broker 浠茶鏈哄埗 MVP 瀹炴柦璁″垝
+
+> **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:** 鍦ㄧ幇鏈� TapProject 涓互鏈�灏忎唬浠疯惤鍦� V3.0 鏋舵瀯鐨勪袱澶ф牳蹇冩満鍒垛�斺��**Local 鐩磋繛瀹夸富**涓� **DeviceBroker 浠g悊瀹夸富**鈥斺�斿苟浣胯法杩涚▼绉熺害浠茶璐┛"浠櫒娉ㄥ唽锛堟墽琛屼綅閫夋嫨/鍙戝竷闂ㄧ锛�"涓�"鎵嬪姩璋冭瘯锛堢淮鎶ゅ啓鏉冪敵璇�/鍗犵敤鍙/閲婃斁锛�"涓ゆ潯涓昏矾寰勩��
+
+**Architecture:** 涓夊眰閫掕繘锛氣憼 `Aging.ResourceContracts`锛堥浂渚濊禆濂戠害搴擄細LeaseGrant / DeviceOperation / IPC 灏佸锛夛紱鈶� `DbLeaseStore`锛圫QLite `arb.db` 涓婄殑璺ㄨ繘绋嬬绾︼細BEGIN IMMEDIATE 浜嬪姟 CAS + generation 鏍呮爮 + epoch + TTL/蹇冭烦 + 杩囨湡杩� RecoveryRequired 浜哄伐鍥炴敹锛夛紱鈶� 鎵ц鍙岄�氶亾鈥斺�擫ocal 璁惧鐢卞綋鍓嶈繘绋嬬洿椹憋紙淇濇寔鐜扮姸璺緞锛夛紝Broker 璁惧鐢辩嫭绔� `DeviceBroker.exe` 鍞竴鎸佹湁鐗╃悊浼氳瘽锛屽鎴风缁� `BrokeredCanDriver` 浠g悊椹卞姩 + 甯у崗璁� RPC 杞彂锛孊roker 鍑洪槦鏃跺啀鍋氭爡鏍忎笌閰嶇疆鍝堝笇鍙岄噸鏍¢獙锛圛NV-01/05 闂幆锛夈�傜幇鏈� `IResourceManager/IAutoDeviceFacade/DebugSessionManager` 閫氳繃缁勫悎锛堜笉鐮村潖绛惧悕锛夊垏鎹负璺ㄨ繘绋嬪疄鐜般��
+
+**Tech Stack:** .NET Framework 4.7.2銆丆# 10銆乄PF锛堢幇鏈夋彃浠� UI锛夈�丼ystem.Data.SQLite锛坅rb.db + WAL锛夈�丯amed Pipes锛堝抚鍗忚锛�4 瀛楄妭闀垮害鍓嶇紑 + UTF8 JSON锛夈�亁Unit锛堟柊 SDK 椋庢牸娴嬭瘯宸ョ▼锛夈�丮SBuild锛堢紪璇戞棫寮� WPF 宸ョ▼锛岃璁板繂瑙勫垯锛夈��
+
+---
+
+## MVP 鑼冨洿瑁佸壀锛堟槑纭繚鐣欎笌鎺ㄨ繜锛�
+
+| V3.0 鑳藉姏 | MVP 澶勭疆 | 璇存槑 |
+|-----------|---------|------|
+| 璺ㄨ繘绋嬬绾� + generation 鏍呮爮 + epoch | 鉁� 瀹炵幇 | 鏍稿績涓殑鏍稿績 |
+| INV-03锛氳繃鏈熶笉鑷姩绉讳氦 鈫� RecoveryRequired | 鉁� 瀹炵幇锛堜汉宸ュ洖鏀舵寜閽増锛� | 鍥炴敹=瀹夸富璇诲洖鎺㈡祴+鐧昏璇佹嵁+缃� Free |
+| Local 妯″紡锛堣繘绋嬪唴鐩撮┍ + 杩炴帴瀹堝崼 Mutex锛� | 鉁� 瀹炵幇 | 娌跨敤鐜版湁椹卞姩 |
+| Broker 妯″紡锛圫essionGate 鍘熷瓙鐗囨 + 鏍呮爮鍐嶆牎楠岋級 | 鉁� 瀹炵幇锛圤neShot 鍛戒护鍚屾妯″瀷锛� | 鍛ㄦ湡浠诲姟/璁㈤槄/Outbox 鎺ㄨ繜 |
+| 娉ㄥ唽椤� executionMode/authorityId/sessionKey/accessPolicy + 鍙戝竷闂ㄧ + 涓嶅彲鍙樺揩鐓у搱甯� | 鉁� 瀹炵幇锛堟渶灏忕増锛� | Broker 杩炴帴鎼� configHash 瀵硅处锛圛NV-04 闆忓舰锛� |
+| 璋冭瘯椤电淮鎶ゅ啓鏉冭法杩涚▼鐢宠銆佸崰鐢ㄨ�呭彲瑙併�佹嫆缁濊涔� | 鉁� 瀹炵幇 | 澶嶇敤 DebugSessionManager 鐜版湁璋冪敤閾� |
+| Draining 鑷姩璁╂浮 / 缁存姢鎶㈠崰 grace 娴佺▼ | 鈴� 鎺ㄨ繜锛圡VP 鐢�"鍗犵敤鍗虫嫆缁�"鐭╅樀鍚堟硶鍒嗘敮锛� | 鏂囨。 搂6.1 鍏佽 |
+| Task Journal/Outbox銆丳eriodicTx/Acquisition銆佺埗 Permit 澶氶�氶亾銆丳repare/Activate 涓ゆ寮忚法鍩熼鐣欍��120 绌轰綅鍏紡銆乤rb 鏈嶅姟绔� DB 鍖� | 鈴� 鎺ㄨ繜 | P2+ |
+
+**鍐崇瓥瀹屽鎬ц鏄庯紙瀹炴柦鑰呬笉闇�鍐嶅仛鍐冲畾锛夛細**
+- 浠茶搴撹矾寰勶細`%LOCALAPPDATA%\PluginIntegrationTest\arb.db`锛堜笌 data.db 鍚岀洰褰曪級
+- lease_id 鐩存帴澶嶇敤鐜版湁 `ResourceOwnerToken.TokenId`锛屼袱濂椾綋绯讳竴涓韩浠�
+- MVP 鏉冮檺浼樺厛绾х畝鍖栦负锛�**浠讳綍 Active 绉熺害 鈫� 浠栦汉涓�寰嬫嫆缁濓紙ConflictHeld + 鍗犵敤鑰呬俊鎭級**锛涗笉鍖哄垎 Maintenance 鎶㈠崰
+- Broker 鍩熸紨绀鸿澶囷細`CAN-002`锛坋xecutionMode=Broker锛宎uthorityId=broker-b01锛宻essionKey=canbus1锛夛紱Local 婕旂ず璁惧锛歚CAN-001`
+- 娑堟伅妯″瀷涓�**鍗曡繛鎺ュ悓姝ヨ姹�-鍝嶅簲**锛坃sendLock 涓茶锛夛紝澶氳矾澶嶇敤鎺ㄨ繜
+- 蹇冭烦 8s / TTL 30s / 鎺㈡祴瓒呮椂 3s锛堝彲鍚庣画鍏ラ厤缃級
+
+---
+
+## Task 0: 鍩虹嚎纭涓庡伐绋嬮鏋�
+
+**Files:**
+- Create: `Aging.ResourceContracts/Aging.ResourceContracts.csproj`
+- Create: `tests/Aging.MvpTests/Aging.MvpTests.csproj`
+- Modify: `WPFProject/TapProject.sln`锛堝姞鍏ヤ袱涓柊宸ョ▼锛�
+
+- [ ] **Step 1: 楠岃瘉褰撳墠鍩虹嚎鍙紪璇�**
+
+```powershell
+cd d:\TestPlatform\TapProject
+& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" Plugin.IntegrationTest\Plugin.IntegrationTest.csproj /p:Configuration=Debug /p:Platform=AnyCPU /t:Build /v:minimal
+```
+棰勬湡锛氭渶鍚庝竴琛� `Plugin.IntegrationTest -> ...exe`锛屾棤 error銆傦紙娉ㄦ剰锛氭湰浠撳簱鏃у紡宸ョ▼蹇呴』鐢� MSBuild锛屼絾鏈伐绋嬮摼涓� SDK-style 宸ョ▼涔熷彲鐢� MSBuild 缂栬瘧锛岀粺涓�鐢� MSBuild銆傦級
+
+- [ ] **Step 2: 鍒涘缓濂戠害绫诲簱宸ョ▼锛堥浂渚濊禆锛�**
+
+鍒涘缓 `Aging.ResourceContracts/Aging.ResourceContracts.csproj`锛�
+
+```xml
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>disable</ImplicitUsings>
+ </PropertyGroup>
+</Project>
+```
+
+- [ ] **Step 3: 鍒涘缓 xUnit 娴嬭瘯宸ョ▼**
+
+```powershell
+cd d:\TestPlatform\TapProject\tests
+dotnet new xunit -n Aging.MvpTests -f net472
+```
+缂栬緫 `Aging.MvpTests/Aging.MvpTests.csproj` 杩藉姞瀵瑰绾﹀簱涓庡悗缁鏂界殑寮曠敤涓� SQLite 澶嶅埗锛�
+
+```xml
+<ItemGroup>
+ <ProjectReference Include="..\..\Aging.ResourceContracts\Aging.ResourceContracts.csproj" />
+</ItemGroup>
+<ItemGroup>
+ <Content Include="..\..\Libs\SQLite.Interop.dll" Link="x64\SQLite.Interop.dll">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+</ItemGroup>
+```
+
+- [ ] **Step 4: 鍔犲叆瑙e喅鏂规骞堕獙璇�**
+
+```powershell
+cd d:\TestPlatform\TapProject
+dotnet sln WPFProject\TapProject.sln add Aging.ResourceContracts\Aging.ResourceContracts.csproj tests\Aging.MvpTests\Aging.MvpTests.csproj
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj
+```
+棰勬湡锛歚Passed! - 1 failed: 0, passed: 1`锛堟ā鏉胯嚜甯� UnitTest1锛夈�傝嫢 `dotnet sln add` 鍥犳棫寮忓伐绋嬫牸寮忔姤閿欙紝璁板綍鍚庡湪 VS 涓墜宸ユ坊鍔狅紝涓嶉樆濉炲悗缁换鍔°��
+
+- [ ] **Step 5: Commit**
+
+```powershell
+git add Aging.ResourceContracts tests WPFProject/TapProject.sln
+git commit -m "chore(mvp): 濂戠害搴撲笌娴嬭瘯宸ョ▼楠ㄦ灦"
+```
+
+---
+
+## Task 1: 濂戠害绫诲瀷锛圠easeGrant / AcquireRequest / DeviceOperation / IPC 灏佸锛�
+
+**Files:**
+- Create: `Aging.ResourceContracts/LeaseTypes.cs`
+- Create: `Aging.ResourceContracts/DeviceOperationTypes.cs`
+- Create: `Aging.ResourceContracts/IpcEnvelope.cs`
+- Test: `tests/Aging.MvpTests/ContractSerializeTests.cs`
+
+- [ ] **Step 1: 鍐欏け璐ユ祴璇曪紙JSON 寰�杩斾笉涓㈠瓧娈碉級**
+
+`tests/Aging.MvpTests/ContractSerializeTests.cs`锛�
+
+```csharp
+using System.Text.Json;
+using Aging.ResourceContracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+ public class ContractSerializeTests
+ {
+ [Fact]
+ public void LeaseGrant_RoundTrips_Json()
+ {
+ var g = new LeaseGrant("broker-b01", "can/can02", "lease_abc",
+ "W01.RunA", OwnerTypeV3.Maintenance, epoch: 7, generation: 42,
+ expiresUtcMs: 1793673600000);
+ var json = JsonSerializer.Serialize(g);
+ var back = JsonSerializer.Deserialize<LeaseGrant>(json)!;
+ Assert.Equal(g.Generation, back.Generation);
+ Assert.Equal(g.Epoch, back.Epoch);
+ Assert.Equal(g.LeaseId, back.LeaseId);
+ Assert.Equal(g.OwnerType, back.OwnerType);
+ }
+
+ [Fact]
+ public void IpcEnvelope_Carries_BodyAndIdentity()
+ {
+ var e = new IpcEnvelope
+ {
+ ProtocolVersion = 1, MessageType = "SubmitTask",
+ RequestId = "req-1", ClientInstanceId = "hmi/bootA/123",
+ AuthorityId = "broker-b01", ConfigHash = "sha256:x",
+ BodyJson = "{\"operationCode\":\"SEND\"}"
+ };
+ var json = JsonSerializer.Serialize(e);
+ var back = JsonSerializer.Deserialize<IpcEnvelope>(json)!;
+ Assert.Equal("SubmitTask", back.MessageType);
+ Assert.Equal("sha256:x", back.ConfigHash);
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 杩愯纭澶辫触**
+
+```powershell
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj --filter ContractSerializeTests
+```
+棰勬湡锛氱紪璇戦敊璇� `The type LeaseGrant does not exist`锛堝伐绋嬫湭寮曠敤瀹炵幇鍓嶇殑鑷劧澶辫触锛夈��
+
+- [ ] **Step 3: 瀹炵幇濂戠害绫诲瀷**
+
+`Aging.ResourceContracts/LeaseTypes.cs`锛�
+
+```csharp
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Aging.ResourceContracts
+{
+ /// <summary>涓庣幇鏈� OwnerType 鏁板�煎榻愶細Production=0, Maintenance=1銆�</summary>
+ public enum OwnerTypeV3 { Production = 0, Maintenance = 1 }
+
+ /// <summary>MVP 鐩镐綅锛欶ree 鍙敤 / Active 鎸佹湁 / RecoveryRequired 杩囨湡寰呭洖鏀讹紙INV-03锛夈��</summary>
+ public enum LeasePhase { Free = 0, Active = 1, RecoveryRequired = 2 }
+
+ /// <summary>姣忚祫婧愪竴寮犵绾﹀嚟璇侊紱鏍呮爮=(AuthorityId, Epoch, ResourceKey, LeaseId, Generation)銆�</summary>
+ public sealed class LeaseGrant
+ {
+ public string AuthorityId { get; }
+ public string ResourceKey { get; }
+ public string LeaseId { get; }
+ public string OwnerId { get; }
+ public OwnerTypeV3 OwnerType { get; }
+ public long Epoch { get; }
+ public long Generation { get; }
+ public long ExpiresUtcMs { get; }
+
+ public LeaseGrant(string authorityId, string resourceKey, string leaseId,
+ string ownerId, OwnerTypeV3 ownerType, long epoch, long generation, long expiresUtcMs)
+ {
+ AuthorityId = authorityId; ResourceKey = resourceKey; LeaseId = leaseId;
+ OwnerId = ownerId; OwnerType = ownerType; Epoch = epoch;
+ Generation = generation; ExpiresUtcMs = expiresUtcMs;
+ }
+ }
+
+ public sealed class AcquireRequest
+ {
+ public string OwnerId { get; set; } = "";
+ public OwnerTypeV3 OwnerType { get; set; }
+ /// <summary>boot_id + pid锛屽悓鏈鸿繘绋嬪尯鍒嗐��</summary>
+ public string ProcessInstanceId { get; set; } = "";
+ public string MachineName { get; set; } = Environment.MachineName;
+ public int HostProcessId { get; set; } = System.Diagnostics.Process.GetCurrentProcess().Id;
+ /// <summary>鐢宠鐞嗙敱锛堢淮鎶や細璇濆~鎿嶄綔鍛樹笌鍘熷洜锛岃繘 lease_history锛夈��</summary>
+ public string Reason { get; set; } = "";
+ /// <summary>Ordinals 鍗囧簭銆佸幓閲嶅悗鐢宠锛堢粍鐢宠 = 鍗曚簨鍔″叏鏈夊叏鏃狅級銆�</summary>
+ public string[] ResourceKeys { get; set; } = new string[0];
+ public int WaitTimeoutMs { get; set; } = 1000;
+ public int TtlSeconds { get; set; } = 30;
+ }
+
+ public enum AcquireResultCode { Acquired, Reused, ConflictHeld, RecoveryBlocked, Timeout }
+
+ public sealed class ConflictInfo
+ {
+ public string OwnerId { get; set; } = "";
+ public OwnerTypeV3 OwnerType { get; set; }
+ public string MachineName { get; set; } = "";
+ public int HostProcessId { get; set; }
+ public string Reason { get; set; } = "";
+ public long ExpiresUtcMs { get; set; }
+ }
+
+ public sealed class AcquireOutcome
+ {
+ public AcquireResultCode Code { get; set; }
+ public LeaseGrant[] Grants { get; set; } = new LeaseGrant[0];
+ public ConflictInfo? Holder { get; set; }
+ /// <summary>RecoveryBlocked 鏃舵寚鍚� recovery_checkpoint 鐨勯敭銆�</summary>
+ public string? RecoveryCheckpointKey { get; set; }
+ }
+
+ /// <summary>绉熺害鍗忚皟鍣細浠茶鍩熷敮涓�鍏ュ彛銆傚疄鐜版柟锛欴bLeaseStore銆�</summary>
+ public interface ILeaseCoordinator
+ {
+ /// <summary>鏉冨▉鍩熷綋鍓� epoch锛堥噸鍚悗閫掑锛屾棫 Grant 鑷姩澶辨晥锛夈��</summary>
+ long CurrentEpoch { get; }
+
+ AcquireOutcome Acquire(AcquireRequest request);
+ /// <summary>鍚� LeaseId+Generation 缁湡锛涜繑鍥� false=宸茶鍥炴敹/瓒呰秺锛堟湰鍦伴』鐔旀柇锛夈��</summary>
+ bool Renew(LeaseGrant grant, int extendSeconds);
+ bool Release(LeaseGrant grant, string note);
+ ConflictInfo? ProbeConflict(string resourceKey);
+ int SweepExpired();
+ /// <summary>RecoveryRequired 鈫� Free锛氬涓诲畬鎴愯鍥炴帰娴嬪悗鐧昏璇佹嵁骞舵斁琛屻��</summary>
+ bool ApproveRecovery(string resourceKey, string readbackJson, string approvedBy);
+ }
+}
+```
+
+`Aging.ResourceContracts/DeviceOperationTypes.cs`锛�
+
+```csharp
+namespace Aging.ResourceContracts
+{
+ public enum OperationState
+ {
+ Succeeded, Failed,
+ /// <summary>鏍呮爮鏍¢獙澶辫触锛欸rant 鐨� epoch/generation 宸插け鏁堬紙鍚� Broker 閲嶅惎锛夈��</summary>
+ RejectedStaleLease,
+ /// <summary>INV-04 闆忓舰锛氬鎴风 configHash 涓� Broker 鐢熸晥閰嶇疆涓嶄竴鑷淬��</summary>
+ RejectedConfigMismatch,
+ /// <summary>浼氳瘽闃熷垪婊°��</summary>
+ RejectedQueueFull,
+ /// <summary>宸插彂閫佷絾缁撴灉鏈煡锛圛NV-06锛氱姝㈠綋鏈墽琛岋級銆�</summary>
+ Unknown
+ }
+
+ /// <summary>涓�娆℃湁鐣岃澶囧姩浣滐紙MVP=OneShot锛夈�侴rants 蹇呴』瑕嗙洊 resources銆�</summary>
+ public sealed class DeviceOperation
+ {
+ /// <summary>鐧藉悕鍗曟搷浣滅爜锛屽 "CAN_SEND"/"CAN_RECV"/"CAN_AUTO_RESPONSE"/"CAN_STATUS"銆�</summary>
+ public string OperationCode { get; set; } = "";
+ public string ParametersJson { get; set; } = "{}";
+ public string IdempotencyKey { get; set; } = "";
+ public string ConfigHash { get; set; } = "";
+ public LeaseGrant[] Grants { get; set; } = new LeaseGrant[0];
+ public long DeadlineUtcMs { get; set; }
+ }
+
+ public sealed class OperationResult
+ {
+ public string RequestId { get; set; } = "";
+ public OperationState State { get; set; }
+ public string ErrorCode { get; set; } = "";
+ public string ReadbackJson { get; set; } = "{}";
+ public long ServerEpoch { get; set; }
+ }
+}
+```
+
+`Aging.ResourceContracts/IpcEnvelope.cs`锛�
+
+```csharp
+namespace Aging.ResourceContracts
+{
+ /// <summary>IPC 娑堟伅绫诲瀷甯搁噺锛圡VP 浠呭洓绉嶏級銆�</summary>
+ public static class IpcMessageTypes
+ {
+ public const string Hello = "Hello"; // 鎻℃墜锛氬洖 epoch/configHash
+ public const string SubmitTask = "SubmitTask"; // body=DeviceOperation
+ public const string CancelTask = "CancelTask";
+ public const string ProbeReadback = "ProbeReadback"; // 鎭㈠鍥炴敹鍓嶇殑璇诲洖鎺㈡祴
+ }
+
+ /// <summary>璇锋眰/鍝嶅簲鍏卞悓灏佸銆傚崗璁増鏈笉鍖归厤鐩存帴鎷掔粷杩炴帴銆�</summary>
+ public sealed class IpcEnvelope
+ {
+ public int ProtocolVersion { get; set; } = 1;
+ public string MessageType { get; set; } = "";
+ public string RequestId { get; set; } = "";
+ public string CorrelationId { get; set; } = "";
+ public string ClientInstanceId { get; set; } = "";
+ public string AuthorityId { get; set; } = "";
+ public string ConfigHash { get; set; } = "";
+ public long DeadlineUtcMs { get; set; }
+ public string BodyJson { get; set; } = "{}";
+ }
+
+ public sealed class HelloResult
+ {
+ public long ServerEpoch { get; set; }
+ public string ConfigHash { get; set; } = "";
+ public int ProtocolVersion { get; set; } = 1;
+ public string BrokerId { get; set; } = "";
+ }
+}
+```
+
+娴嬭瘯宸ョ▼ csproj 杩藉姞 `<PackageReference Include="System.Text.Json" Version="6.0.5" />`锛坣et472 鍙敤锛涘绾﹀簱鍚屾牱鍔犳鍖咃級銆�
+
+- [ ] **Step 4: 杩愯纭閫氳繃**
+
+```powershell
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj --filter ContractSerializeTests
+```
+棰勬湡锛歚Passed! - failed: 0, passed: 2`
+
+- [ ] **Step 5: Commit**
+
+```powershell
+git add Aging.ResourceContracts tests
+git commit -m "feat(contracts): LeaseGrant/ILeaseCoordinator/DeviceOperation/IPC 灏佸"
+```
+
+---
+
+## Task 2: DbLeaseStore 鈥斺�� 璺ㄨ繘绋嬬绾︽牳蹇冿紙arb.db锛�
+
+**Files:**
+- Create: `Aging.Infrastructure/Core/DbLeaseStore.cs`
+- Modify: `Aging.Infrastructure/Aging.Infrastructure.csproj`锛堝姞 PackageReference System.Data.SQLite + 濂戠害宸ョ▼寮曠敤锛�
+- Test: `tests/Aging.MvpTests/DbLeaseStoreTests.cs`
+
+- [ ] **Step 1: 鍐欏け璐ユ祴璇曪紙骞跺彂鐢宠浜掓枼 + 鏍呮爮 + 杩囨湡鍥炴敹 + epoch 澶辨晥锛�**
+
+`tests/Aging.MvpTests/DbLeaseStoreTests.cs`锛�
+
+```csharp
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Core;
+using Aging.ResourceContracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+ public class DbLeaseStoreTests : IDisposable
+ {
+ private readonly string _dbPath;
+ private readonly DbLeaseStore _store;
+
+ public DbLeaseStoreTests()
+ {
+ _dbPath = Path.Combine(Path.GetTempPath(),
+ "arb_test_" + Guid.NewGuid().ToString("N") + ".db");
+ _store = new DbLeaseStore(_dbPath, "local-main");
+ }
+ public void Dispose() { try { File.Delete(_dbPath); } catch { } }
+
+ private AcquireRequest Req(string key, OwnerTypeV3 type = OwnerTypeV3.Maintenance,
+ int ttl = 30) => new AcquireRequest
+ {
+ OwnerId = "test-owner", OwnerType = type,
+ ProcessInstanceId = Guid.NewGuid().ToString("N"),
+ ResourceKeys = new[] { key }, TtlSeconds = ttl
+ };
+
+ [Fact]
+ public void Acquire_FirstOwner_Succeeds()
+ {
+ var r = _store.Acquire(Req("can/can02"));
+ Assert.Equal(AcquireResultCode.Acquired, r.Code);
+ Assert.Equal(1, r.Grants[0].Generation);
+ }
+
+ [Fact]
+ public void Acquire_WhileActiveByOther_ReturnsConflictWithHolder()
+ {
+ _store.Acquire(Req("can/can02"));
+ var second = new AcquireRequest
+ {
+ OwnerId = "other-owner", OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "x", ResourceKeys = new[] { "can/can02" }
+ };
+ var r = _store.Acquire(second);
+ Assert.Equal(AcquireResultCode.ConflictHeld, r.Code);
+ Assert.Equal("test-owner", r.Holder!.OwnerId);
+ }
+
+ [Fact]
+ public void TwoIndependentStores_SameFile_SimulatesTwoProcesses()
+ {
+ // 涓や釜瀹炰緥鎸囧悜鍚屼竴 arb.db = 绛変环浜庝袱涓繘绋嬶紙SQLite 浜嬪姟涓轰徊瑁佸眰锛�
+ var storeB = new DbLeaseStore(_dbPath, "local-main");
+ var a = _store.Acquire(Req("psu/psu01/ch/5"));
+ var b = storeB.Acquire(new AcquireRequest
+ {
+ OwnerId = "W02", OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "p2", ResourceKeys = new[] { "psu/psu01/ch/5" }
+ });
+ Assert.Equal(AcquireResultCode.Acquired, a.Code);
+ Assert.Equal(AcquireResultCode.ConflictHeld, b.Code);
+ }
+
+ [Fact]
+ public void Concurrent_50Threads_OnlyOneWins()
+ {
+ int winners = 0;
+ Parallel.For(0, 50, _ =>
+ {
+ var s = new DbLeaseStore(_dbPath, "local-main");
+ var r = s.Acquire(new AcquireRequest
+ {
+ OwnerId = Guid.NewGuid().ToString("N"), OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "p", ResourceKeys = new[] { "can/bus" }, WaitTimeoutMs = 3000
+ });
+ if (r.Code == AcquireResultCode.Acquired) Interlocked.Increment(ref winners);
+ });
+ Assert.Equal(1, winners);
+ }
+
+ [Fact]
+ public void ExpiredLease_EntersRecoveryRequired_NotAutoReclaim()
+ {
+ _store.Acquire(Req("can/can02", ttl: -1)); // 绔嬪嵆杩囨湡锛堟祴璇曢挬瀛愶細璐� TTL锛�
+ _store.SweepExpired();
+ var r = _store.Acquire(Req("can/can02", OwnerTypeV3.Production));
+ Assert.Equal(AcquireResultCode.RecoveryBlocked, r.Code);
+
+ // 浜哄伐鍥炴敹锛氳鍥炶瘉鎹悎鏍煎悗鏀捐
+ Assert.True(_store.ApproveRecovery("can/can02", "{\"output\":\"off\"}", "admin"));
+ var again = _store.Acquire(Req("can/can02", OwnerTypeV3.Production));
+ Assert.Equal(AcquireResultCode.Acquired, again.Code);
+ Assert.Equal(2, again.Grants[0].Generation); // generation 鍗曡皟锛屾案涓嶅洖閫�
+ }
+
+ [Fact]
+ public void Renew_WithStaleGeneration_Fails()
+ {
+ var g = _store.Acquire(Req("can/can02")).Grants[0];
+ var stale = new LeaseGrant(g.AuthorityId, g.ResourceKey, g.LeaseId,
+ g.OwnerId, g.OwnerType, g.Epoch, generation: 999, g.ExpiresUtcMs);
+ Assert.False(_store.Renew(stale, 30));
+ Assert.True(_store.Renew(g, 30));
+ }
+
+ [Fact]
+ public void BumpEpoch_InvalidatesAllActiveGrants()
+ {
+ var g = _store.Acquire(Req("can/can02")).Grants[0];
+ _store.BumpEpochForTest("arb.db 閲嶅惎妯℃嫙");
+ Assert.False(_store.Renew(g, 30)); // 鏃� epoch Grant 鍏ㄩ儴澶辨晥
+ }
+
+ [Fact]
+ public void GroupAcquire_PartialConflict_RollsBackAll()
+ {
+ _store.Acquire(Req("psu/b")); // 鍏堝崰 b
+ var r = _store.Acquire(new AcquireRequest
+ {
+ OwnerId = "W09", OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "p",
+ ResourceKeys = new[] { "psu/a", "psu/b", "psu/c" } // 瀹炵幇鍐呴儴浼� Ordinal 鎺掑簭
+ });
+ Assert.Equal(AcquireResultCode.ConflictHeld, r.Code);
+ Assert.Null(_store.ProbeConflict("psu/a")); // a 鏈鍗婂崰鐢�
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 杩愯纭澶辫触**
+
+```powershell
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj --filter DbLeaseStoreTests
+```
+棰勬湡锛氱紪璇戝け璐ワ紙`DbLeaseStore` 涓嶅瓨鍦級銆傝嫢濂戠害娴嬭瘯宸ョ▼缂栬瘧鎶� `System.Data.SQLite` 缂哄け锛屽厛鍦ㄦ祴璇� csproj 鍔� `<PackageReference Include="System.Data.SQLite" Version="1.0.118" />`锛圛nterop 鐢� Step 3 鐨� Content 澶嶅埗瑙e喅锛夈��
+
+- [ ] **Step 3: 瀹炵幇 DbLeaseStore**
+
+鍏堜慨鏀� `Aging.Infrastructure/Aging.Infrastructure.csproj`锛孖temGroup 杩藉姞锛�
+
+```xml
+<ProjectReference Include="..\Aging.ResourceContracts\Aging.ResourceContracts.csproj" />
+<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
+```
+
+`Aging.Infrastructure/Core/DbLeaseStore.cs`锛�
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Aging.ResourceContracts;
+using log4net;
+using SQLite;
+
+namespace Aging.Infrastructure.Core
+{
+ /// <summary>
+ /// 璺ㄨ繘绋嬬绾﹀瓨鍌紙V3.0 搂8 MVP锛夈�俛rb.db 鐙珛浜� data.db銆�
+ /// 鏃堕棿涓�寰� DB 渚� UTC 姣锛圖ateTime.UtcNow 瀛� INTEGER锛夛紝
+ /// 鐢宠/缁湡/閲婃斁鍏ㄩ儴璧� BEGIN IMMEDIATE 鍗曞啓浜嬪姟 鈥斺�� SQLite 浜嬪姟鍗宠繘绋嬮棿浠茶灞傘��
+ /// INV-03锛氳繃鏈� 鈫� RecoveryRequired锛岀姝㈣嚜鍔ㄧЩ浜わ紱ApproveRecovery 鐧昏璇佹嵁鍚庣疆 Free銆�
+ /// </summary>
+ public sealed class DbLeaseStore : ILeaseCoordinator
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(DbLeaseStore));
+
+ private readonly string _dbPath;
+ private readonly string _authorityId;
+ private readonly long _bootNowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
+ public string AuthorityId => _authorityId;
+ public long CurrentEpoch { get; private set; }
+
+ public DbLeaseStore(string dbPath, string authorityId)
+ {
+ _dbPath = dbPath;
+ _authorityId = authorityId;
+ EnsureDirectory();
+ InitializeSchema();
+ CurrentEpoch = ReadOrInitEpoch();
+ }
+
+ private void EnsureDirectory()
+ {
+ var dir = Path.GetDirectoryName(_dbPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
+ }
+
+ private SQLiteConnection Open()
+ {
+ var conn = new SQLiteConnection(_dbPath);
+ conn.Execute("PRAGMA journal_mode=WAL;");
+ conn.Execute("PRAGMA busy_timeout=3000;");
+ conn.Execute("PRAGMA synchronous=FULL;");
+ return conn;
+ }
+
+ private void InitializeSchema()
+ {
+ using var conn = Open();
+ conn.Execute(@"
+ CREATE TABLE IF NOT EXISTS authority_meta(
+ authority_id TEXT PRIMARY KEY, epoch INTEGER NOT NULL,
+ boot_id TEXT NOT NULL, schema_version INTEGER NOT NULL);
+ CREATE TABLE IF NOT EXISTS resource_state(
+ resource_key TEXT PRIMARY KEY,
+ generation INTEGER NOT NULL DEFAULT 0,
+ phase INTEGER NOT NULL DEFAULT 0, -- LeasePhase
+ lease_id TEXT, owner_id TEXT, owner_type INTEGER,
+ process_instance_id TEXT, machine_name TEXT, host_process_id INTEGER,
+ epoch INTEGER NOT NULL DEFAULT 0, reason TEXT,
+ acquired_utc_ms INTEGER, expires_utc_ms INTEGER, updated_utc_ms INTEGER);
+ CREATE TABLE IF NOT EXISTS lease_history(
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ resource_key TEXT NOT NULL, generation INTEGER NOT NULL,
+ epoch INTEGER NOT NULL, lease_id TEXT, owner_id TEXT,
+ event_type TEXT NOT NULL, reason TEXT, utc_ms INTEGER NOT NULL);
+ CREATE TABLE IF NOT EXISTS recovery_checkpoint(
+ resource_key TEXT PRIMARY KEY, epoch INTEGER NOT NULL,
+ status INTEGER NOT NULL, -- 0=Pending 1=Approved
+ physical_readback_json TEXT NOT NULL, approved_by TEXT,
+ evidence_utc_ms INTEGER NOT NULL);");
+ }
+
+ private long ReadOrInitEpoch()
+ {
+ using var conn = Open();
+ var row = conn.Query<AuthorityMetaRow>(
+ "SELECT epoch, boot_id FROM authority_meta WHERE authority_id=?", _authorityId)
+ .FirstOrDefault();
+ long nowMs = NowMs();
+ if (row == null)
+ {
+ conn.Execute("INSERT INTO authority_meta VALUES(?,?,?,1)",
+ _authorityId, 1L, Guid.NewGuid().ToString("N"));
+ return 1;
+ }
+ // 姣忔杩涚▼鎵撳紑鍚屼竴鏉冨▉鍩� 鈫� boot_id 鍙樺寲 鈫� epoch+1锛堥噸鍚娇鏃� Grant 澶辨晥锛�
+ conn.Execute("UPDATE authority_meta SET epoch=epoch+1, boot_id=? WHERE authority_id=?",
+ Guid.NewGuid().ToString("N"), _authorityId);
+ CurrentEpoch = row.epoch + 1;
+ conn.Execute("INSERT INTO lease_history(resource_key,generation,epoch,lease_id,owner_id,event_type,reason,utc_ms)" +
+ " VALUES('*',{0},{1},'','system','EpochBump','open-authority',{2})",
+ CurrentEpoch, "", nowMs);
+ return CurrentEpoch;
+ }
+
+ // ================= ILeaseCoordinator =================
+
+ public AcquireOutcome Acquire(AcquireRequest request)
+ {
+ if (request.ResourceKeys.Length == 0)
+ throw new ArgumentException("ResourceKeys 涓嶈兘涓虹┖");
+ var keys = request.ResourceKeys.Select(NormKey).OrderBy(k => k, StringComparer.Ordinal)
+ .Distinct().ToArray();
+
+ var deadline = NowMs() + Math.Max(0, request.WaitTimeoutMs);
+ while (true)
+ {
+ var outcome = TryAcquireOnce(keys, request);
+ if (outcome.Code != AcquireResultCode.Timeout) return outcome;
+ if (NowMs() >= deadline) { outcome.Code = AcquireResultCode.Timeout; return outcome; }
+ System.Threading.Thread.Sleep(50);
+ }
+ }
+
+ private AcquireOutcome TryAcquireOnce(string[] keys, AcquireRequest req)
+ {
+ long now = NowMs();
+ using var conn = Open();
+ using var tx = conn.BeginTransaction(); // 榛樿鍗� IMMEDIATE
+ try
+ {
+ var grants = new List<LeaseGrant>(keys.Length);
+ foreach (var key in keys)
+ {
+ var st = conn.Query<ResourceStateRow>(
+ "SELECT * FROM resource_state WHERE resource_key=?", key).FirstOrDefault();
+ if (st != null && st.Phase == (int)LeasePhase.RecoveryRequired)
+ {
+ tx.Rollback();
+ return new AcquireOutcome
+ {
+ Code = AcquireResultCode.RecoveryBlocked,
+ RecoveryCheckpointKey = key
+ };
+ }
+ bool activeHeld = st != null && st.Phase == (int)LeasePhase.Active
+ && st.ExpiresUtcMs > now;
+ if (activeHeld)
+ {
+ // 鍚屼竴 LeaseId 閲嶅叆 = 缁湡璇箟锛圡VP 涓嶅仛璺ㄨ繘绋� Reused锛屽啿绐佸嵆杩斿洖锛�
+ tx.Rollback();
+ return new AcquireOutcome
+ {
+ Code = AcquireResultCode.ConflictHeld,
+ Holder = new ConflictInfo
+ {
+ OwnerId = st!.OwnerId, OwnerType = (OwnerTypeV3)st.OwnerType,
+ MachineName = st.MachineName, HostProcessId = st.HostProcessId,
+ Reason = st.Reason, ExpiresUtcMs = st.ExpiresUtcMs
+ }
+ };
+ }
+
+ long gen = (st?.Generation ?? 0) + 1;
+ string leaseId = "lease_" + Guid.NewGuid().ToString("N").Substring(0, 12);
+ var exp = now + req.TtlSeconds * 1000L;
+
+ if (st == null)
+ conn.Execute(
+ @"INSERT INTO resource_state(resource_key,generation,phase,lease_id,owner_id,
+ owner_type,process_instance_id,machine_name,host_process_id,epoch,reason,
+ acquired_utc_ms,expires_utc_ms,updated_utc_ms)
+ VALUES(?,?,1,?,?,?,?,?,?,?,?,?,?,?,?)",
+ key, gen, leaseId, req.OwnerId, (int)req.OwnerType,
+ req.ProcessInstanceId, req.MachineName, req.HostProcessId,
+ CurrentEpoch, req.Reason, now, exp, now);
+ else
+ conn.Execute(
+ @"UPDATE resource_state SET generation=?, phase=1, lease_id=?, owner_id=?,
+ owner_type=?, process_instance_id=?, machine_name=?, host_process_id=?,
+ epoch=?, reason=?, acquired_utc_ms=?, expires_utc_ms=?, updated_utc_ms=?
+ WHERE resource_key=?",
+ gen, leaseId, req.OwnerId, (int)req.OwnerType, req.ProcessInstanceId,
+ req.MachineName, req.HostProcessId, CurrentEpoch, req.Reason,
+ now, exp, now, key);
+
+ conn.Execute(
+ @"INSERT INTO lease_history(resource_key,generation,epoch,lease_id,owner_id,event_type,reason,utc_ms)
+ VALUES(?,?,?,?,?,'Acquire',?,?)",
+ key, gen, CurrentEpoch, leaseId, req.OwnerId, req.Reason, now);
+ grants.Add(new LeaseGrant(_authorityId, key, leaseId, req.OwnerId,
+ req.OwnerType, CurrentEpoch, gen, exp));
+ }
+ tx.Commit();
+ return new AcquireOutcome { Code = AcquireResultCode.Acquired, Grants = grants.ToArray() };
+ }
+ catch
+ {
+ try { tx.Rollback(); } catch { }
+ throw;
+ }
+ }
+
+ public bool Renew(LeaseGrant grant, int extendSeconds)
+ {
+ long now = NowMs();
+ using var conn = Open();
+ using var tx = conn.BeginTransaction();
+ int n = conn.Execute(
+ @"UPDATE resource_state SET expires_utc_ms=?, updated_utc_ms=?
+ WHERE resource_key=? AND lease_id=? AND generation=? AND epoch=?
+ AND phase=1 AND expires_utc_ms>?",
+ now + extendSeconds * 1000L, now,
+ NormKey(grant.ResourceKey), grant.LeaseId, grant.Generation, grant.Epoch, now);
+ if (n == 1) tx.Commit(); else { tx.Rollback(); Log.Warn($"[Lease] 缁湡澶辫触(鏍呮爮涓嶇): {grant.ResourceKey} gen={grant.Generation}"); }
+ return n == 1;
+ }
+
+ public bool Release(LeaseGrant grant, string note)
+ {
+ long now = NowMs();
+ using var conn = Open();
+ using var tx = conn.BeginTransaction();
+ int n = conn.Execute(
+ @"UPDATE resource_state SET phase=0, lease_id=NULL, owner_id=NULL,
+ expires_utc_ms=0, updated_utc_ms=?
+ WHERE resource_key=? AND lease_id=? AND generation=?",
+ now, NormKey(grant.ResourceKey), grant.LeaseId, grant.Generation);
+ conn.Execute(
+ @"INSERT INTO lease_history(resource_key,generation,epoch,lease_id,owner_id,event_type,reason,utc_ms)
+ SELECT ?,?,?,?,?, 'Release', ?, ? WHERE ?=1",
+ NormKey(grant.ResourceKey), grant.Generation, grant.Epoch, grant.LeaseId,
+ grant.OwnerId, note, now, n);
+ if (n == 1) tx.Commit(); else tx.Rollback();
+ return n == 1;
+ }
+
+ public ConflictInfo? ProbeConflict(string resourceKey)
+ {
+ long now = NowMs();
+ using var conn = Open();
+ var st = conn.Query<ResourceStateRow>(
+ "SELECT * FROM resource_state WHERE resource_key=? AND phase=1 AND expires_utc_ms>?",
+ NormKey(resourceKey), now).FirstOrDefault();
+ if (st == null) return null;
+ return new ConflictInfo
+ {
+ OwnerId = st.OwnerId, OwnerType = (OwnerTypeV3)st.OwnerType,
+ MachineName = st.MachineName, HostProcessId = st.HostProcessId,
+ Reason = st.Reason, ExpiresUtcMs = st.ExpiresUtcMs
+ };
+ }
+
+ public LeasePhase PhaseOf(string resourceKey)
+ {
+ using var conn = Open();
+ var st = connQuery(conn, resourceKey);
+ if (st == null) return LeasePhase.Free;
+ if (st.Phase == (int)LeasePhase.RecoveryRequired) return LeasePhase.RecoveryRequired;
+ if (st.Phase == (int)LeasePhase.Active && st.ExpiresUtcMs <= NowMs())
+ return LeasePhase.RecoveryRequired; // 璇讳晶鎯版�у垽瀹氾紝鍐欎晶鐢� SweepExpired 钀藉簱
+ return (LeasePhase)st.Phase;
+ }
+
+ public int SweepExpired()
+ {
+ long now = NowMs();
+ using var conn = Open();
+ using var tx = conn.BeginTransaction();
+ var expired = conn.Query<ResourceStateRow>(
+ "SELECT * FROM resource_state WHERE phase=1 AND expires_utc_ms<=?", now);
+ foreach (var st in expired)
+ {
+ conn.Execute("UPDATE resource_state SET phase=2, updated_utc_ms=? WHERE resource_key=?",
+ now, st.ResourceKey);
+ conn.Execute(
+ @"INSERT INTO lease_history(resource_key,generation,epoch,lease_id,owner_id,event_type,reason,utc_ms)
+ VALUES(?,?,?,?,?,'ExpiredToRecovery','ttl-missed',?)",
+ st.ResourceKey, st.Generation, st.Epoch, st.LeaseId, st.OwnerId, now);
+ conn.Execute(
+ @"INSERT OR REPLACE INTO recovery_checkpoint(resource_key,epoch,status,physical_readback_json,approved_by,evidence_utc_ms)
+ VALUES(?,?,0,'{}',NULL,?)", st.ResourceKey, st.Epoch, now);
+ }
+ tx.Commit();
+ return expired.Count;
+ }
+
+ public bool ApproveRecovery(string resourceKey, string readbackJson, string approvedBy)
+ {
+ long now = NowMs();
+ using var conn = Open();
+ using var tx = conn.BeginTransaction();
+ conn.Execute(
+ @"INSERT OR REPLACE INTO recovery_checkpoint(resource_key,epoch,status,physical_readback_json,approved_by,evidence_utc_ms)
+ VALUES(?,?,1,?,?,?)", NormKey(resourceKey), CurrentEpoch, readbackJson, approvedBy, now);
+ int n = conn.Execute(
+ @"UPDATE resource_state SET phase=0, lease_id=NULL, owner_id=NULL, updated_utc_ms=?
+ WHERE resource_key=? AND phase=2", now, NormKey(resourceKey));
+ conn.Execute(
+ @"INSERT INTO lease_history(resource_key,generation,epoch,lease_id,owner_id,event_type,reason,utc_ms)
+ SELECT resource_key,generation,epoch,'','system','RecoveryApproved',?,? FROM resource_state WHERE resource_key=?",
+ approvedBy, now, NormKey(resourceKey));
+ tx.Commit();
+ return n == 1;
+ }
+
+ /// <summary>娴嬭瘯涓� Supervisor 鐢細澶栭儴寮哄埗鎻愬崌 epoch锛堢瓑鏁� Broker/瀹夸富閲嶅惎锛夈��</summary>
+ public void BumpEpochForTest(string reason)
+ {
+ using var conn = Open();
+ conn.Execute("UPDATE authority_meta SET epoch=epoch+1, boot_id=? WHERE authority_id=?",
+ Guid.NewGuid().ToString("N"), _authorityId);
+ CurrentEpoch = conn.Query<AuthorityMetaRow>(
+ "SELECT epoch, boot_id FROM authority_meta WHERE authority_id=?", _authorityId)
+ .First().epoch;
+ Log.Info($"[Lease] epoch 鎻愬崌鑷� {CurrentEpoch}: {reason}");
+ }
+
+ // ================= 杈呭姪 =================
+
+ /// <summary>V3 瑙勮寖锛氱粺涓�灏忓啓銆佸浐瀹氬垎闅旂銆丱rdinal 姣旇緝锛埪�4.1锛夈��</summary>
+ public static string NormKey(string key) => key.Trim().ToLowerInvariant().Replace(' ', '-');
+
+ private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ private ResourceStateRow? connQuery(SQLiteConnection c, string k) =>
+ c.Query<ResourceStateRow>("SELECT * FROM resource_state WHERE resource_key=?", NormKey(k)).FirstOrDefault();
+
+ private class AuthorityMetaRow
+ {
+ public long epoch { get; set; }
+ public string boot_id { get; set; } = "";
+ }
+ private class ResourceStateRow
+ {
+ public string ResourceKey { get; set; } = "";
+ public long Generation { get; set; }
+ public int Phase { get; set; }
+ public string? LeaseId { get; set; }
+ public string? OwnerId { get; set; }
+ public int OwnerType { get; set; }
+ public string? MachineName { get; set; }
+ public int HostProcessId { get; set; }
+ public long Epoch { get; set; }
+ public string? Reason { get; set; }
+ public long ExpiresUtcMs { get; set; }
+ public static implicit operator bool(ResourceStateRow? r) => r is not null;
+ }
+ }
+}
+```
+
+> 娉ㄦ剰锛氬垪鍚嶄笌琛屽睘鎬ф槧灏勪緷璧� `SQLite.NET` 鐨勫垪鍚嶅尮閰嶏紙涓嬪垝绾垮垪闇�鏄惧紡 `[Column]`锛夈�備笂琛� DDL 鐢� `resource_key` 绛� snake_case锛岃�� `ResourceStateRow` 灞炴�ф槸 PascalCase鈥斺�斿疄鐜版椂鍦ㄧ被涓婂惎鐢� `SQLite.NET` 鏄犲皠淇锛�**缁熶竴鏀� DDL 鍒楀悕涓轰笌灞炴�т竴鑷寸殑 PascalCase**锛圫QLite 鍒楀悕澶у皬鍐欎笉鏁忔劅浣嗘煡璇覆瑕佷竴鑷达級銆傝惤鍦版墽琛岃�呬互姝や负鍑嗚皟鏁� DDL锛屼繚璇佷簩鑰呬竴鑷达紙杩欐槸鏈换鍔″敮涓�鐨勨�滃榻愨�濆伐浣滐紝娴嬭瘯浼氱珛鍒绘毚闇蹭笉鍖归厤锛夈��
+
+- [ ] **Step 4: Aging.Infrastructure 缂栬瘧楠岃瘉**
+
+```powershell
+cd d:\TestPlatform\TapProject
+& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" Aging.Infrastructure\Aging.Infrastructure.csproj /p:Configuration=Debug /p:Platform=AnyCPU /t:Build /v:minimal
+```
+棰勬湡锛氭棤 error锛涘嚭鐜� `Aging.Infrastructure -> ...dll`銆�
+
+- [ ] **Step 5: 杩愯绉熺害娴嬭瘯鍏ㄧ豢**
+
+```powershell
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj --filter DbLeaseStoreTests
+```
+棰勬湡锛歚failed: 0, passed: 8`
+
+- [ ] **Step 6: Commit**
+
+```powershell
+git add Aging.Infrastructure tests
+git commit -m "feat(lease): DbLeaseStore 璺ㄨ繘绋嬬绾︼紙TTL/鏍呮爮/epoch/RecoveryRequired锛�"
+```
+
+---
+
+## Task 3: LayeredResourceManager + Bootstrapper 鎺ョ嚎锛堜笂灞傞浂鏀瑰姩鎵撻�氳法杩涚▼锛�
+
+**Files:**
+- Create: `Aging.Infrastructure/Core/LayeredResourceManager.cs`
+- Modify: `Aging.Infrastructure/InfrastructureBootstrapper.cs`锛堟敞鍐屽锛�
+- Test: `tests/Aging.MvpTests/LayeredResourceManagerTests.cs`
+
+- [ ] **Step 1: 鍐欏け璐ユ祴璇�**
+
+`tests/Aging.MvpTests/LayeredResourceManagerTests.cs`锛�
+
+```csharp
+using System.IO;
+using System.Threading;
+using Aging.Infrastructure.Core;
+using Aging.Infrastructure.Contracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+ public class LayeredResourceManagerTests : System.IDisposable
+ {
+ private readonly string _dbPath;
+ private readonly LayeredResourceManager _rm;
+
+ public LayeredResourceManagerTests()
+ {
+ _dbPath = Path.Combine(Path.GetTempPath(), "arb_" + System.Guid.NewGuid().ToString("N") + ".db");
+ _rm = new LayeredResourceManager(new DbLeaseStore(_dbPath, "local-main"));
+ }
+ public void Dispose() { try { File.Delete(_dbPath); } catch { } }
+
+ [Fact]
+ public void ReserveAsync_Then_ValidateOwner_True()
+ {
+ var owner = new ResourceOwnerToken(OwnerType.Maintenance, "W01");
+ var key = new ResourceKey("CAN-002", "CAN");
+ var res = _rm.ReserveAsync(owner, key, CancellationToken.None).GetAwaiter().GetResult();
+ Assert.Equal(ReservationState.Active, res.State);
+ Assert.True(_rm.ValidateOwner(owner, key));
+ }
+
+ [Fact]
+ public void ReserveAsync_Conflict_ExceptionCarriesHolder()
+ {
+ var a = new ResourceOwnerToken(OwnerType.Maintenance, "holder-A");
+ var b = new ResourceOwnerToken(OwnerType.Production, "requester-B");
+ var key = new ResourceKey("CAN-002", "CAN");
+ _rm.ReserveAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+ var ex = Assert.Throws<ResourceConflictException>(() =>
+ _rm.ReserveAsync(b, key, CancellationToken.None).GetAwaiter().GetResult());
+ Assert.Contains("holder-A", ex.Message); // UI 鐩存帴寮瑰崰鐢ㄨ��
+ }
+
+ [Fact]
+ public void ReleaseReservationAsync_Then_OtherCanReserve()
+ {
+ var a = new ResourceOwnerToken(OwnerType.Maintenance, "A");
+ var b = new ResourceOwnerToken(OwnerType.Production, "B");
+ var key = new ResourceKey("CAN-002", "CAN");
+ _rm.ReserveAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+ _rm.ReleaseReservationAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+ var ok = _rm.ReserveAsync(b, key, CancellationToken.None).GetAwaiter().GetResult();
+ Assert.Equal(ReservationState.Active, ok.State);
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 杩愯纭澶辫触** 鈥� `dotnet test ... --filter LayeredResourceManagerTests`锛岄鏈熺紪璇戝け璐ワ紙绫诲瀷涓嶅瓨鍦級銆�
+
+- [ ] **Step 3: 瀹炵幇 LayeredResourceManager**
+
+`Aging.Infrastructure/Core/LayeredResourceManager.cs`锛堝唴瀛樼粏閿� + DbLeaseStore 缁勫悎锛屽涓婂眰 `IResourceManager` 绛惧悕闆舵敼鍔級锛�
+
+```csharp
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using Aging.ResourceContracts;
+using log4net;
+
+namespace Aging.Infrastructure.Core
+{
+ /// <summary>V3.0 搂9 杩囨浮 Adapter 璺嚎锛歊eserve/Validate/Release 杞彂 arb.db 绉熺害琛紝
+ /// grant 鍥炲瓨 TokenId鈫扡easeGrant[]锛屼緵闂ㄩ潰/璋冭瘯浼氳瘽鍙栫敤銆�</summary>
+ public sealed class LayeredResourceManager : IResourceManager
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(LayeredResourceManager));
+ private readonly ILeaseCoordinator _lease;
+ private readonly ConcurrentDictionary<string, SemaphoreSlim> _localLocks = new();
+ private readonly ConcurrentDictionary<string, LeaseGrant[]> _grantsByToken = new();
+
+ public LayeredResourceManager(ILeaseCoordinator lease) => _lease = lease;
+ public ILeaseCoordinator Lease => _lease;
+ public bool TryGetGrants(string tokenId, out LeaseGrant[] grants) => _grantsByToken.TryGetValue(tokenId, out grants!);
+
+ public async Task<ResourceReservation> ReserveAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ var sem = _localLocks.GetOrAdd(resource.ToString(), _ => new SemaphoreSlim(1, 1));
+ await sem.WaitAsync(ct).ConfigureAwait(false);
+ try
+ {
+ var req = new AcquireRequest
+ {
+ OwnerId = owner.OwnerId,
+ OwnerType = owner.OwnerType == OwnerType.Maintenance
+ ? OwnerTypeV3.Maintenance : OwnerTypeV3.Production,
+ Reason = "ReserveAsync",
+ ResourceKeys = new[] { resource.ToString() },
+ WaitTimeoutMs = 0 // MVP锛氬崰鐢ㄥ嵆鎷掞紝涓嶆帓闃燂紙V3 鐭╅樀鍚堟硶鍒嗘敮锛�
+ };
+ var outcome = _lease.Acquire(req);
+ switch (outcome.Code)
+ {
+ case AcquireResultCode.Acquired:
+ _grantsByToken[owner.TokenId] = outcome.Grants;
+ owner.Generation = outcome.Grants[0].Generation;
+ return new ResourceReservation(resource, owner) { State = ReservationState.Active };
+ case AcquireResultCode.ConflictHeld:
+ var h = outcome.Holder!;
+ long leftSec = Math.Max(0, (h.ExpiresUtcMs - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) / 1000);
+ throw new ResourceConflictException(
+ $"璧勬簮 {resource} 姝h鍗犵敤锛歿h.OwnerId}锛坽(h.OwnerType == OwnerTypeV3.Maintenance ? "缁存姢" : "鐢熶骇")}锛屾満鍣� {h.MachineName} PID {h.HostProcessId}锛屽墿浣欑绾� {leftSec}s锛�");
+ case AcquireResultCode.RecoveryBlocked:
+ throw new ResourceConflictException(
+ $"璧勬簮 {resource} 涓婃鎸佹湁鑰呭穿婧冿紝寰呮仮澶嶅洖鏀讹紙闇�璇诲洖鎺㈡祴璇佹嵁鍚庡湪鐩戞帶椤垫壒鍑嗭級");
+ default:
+ throw new TimeoutException($"璧勬簮 {resource} 鐢宠瓒呮椂");
+ }
+ }
+ finally { sem.Release(); }
+ }
+
+ public Task<IOperationLease> AcquireOperationAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ // MVP锛氬懡浠ょ骇涓茶鐢� SessionGate锛圔roker 闃熷垪锛�/Local 鍐� SemaphoreSlim 鎻愪緵锛�
+ // 鏈眰鍙牎楠屾爡鏍忋�侼oopLease 闇�鎸� Contracts/IOperationLease.cs 瀹為檯鎴愬憳琛ラ綈绌哄疄鐜般��
+ if (!ValidateOwner(owner, resource))
+ throw new InvalidOperationException($"璧勬簮 {resource} 鏍呮爮鏍¢獙澶辫触锛坥wner 宸插け鏁堬級");
+ return Task.FromResult<IOperationLease>(new NoopLease());
+ }
+
+ public async Task ReleaseReservationAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ var sem = _localLocks.GetOrAdd(resource.ToString(), _ => new SemaphoreSlim(1, 1));
+ await sem.WaitAsync(ct).ConfigureAwait(false);
+ try
+ {
+ if (_grantsByToken.TryGetValue(owner.TokenId, out var grants))
+ {
+ foreach (var g in grants) _lease.Release(g, "ReleaseReservationAsync");
+ _grantsByToken.TryRemove(owner.TokenId, out _);
+ }
+ }
+ finally { sem.Release(); }
+ }
+
+ public bool IsReserved(ResourceKey resource, out ResourceReservation? reservation)
+ {
+ var holder = _lease.ProbeConflict(resource.ToString());
+ reservation = holder == null ? null : new ResourceReservation(resource,
+ new ResourceOwnerToken(holder.OwnerType == OwnerTypeV3.Maintenance
+ ? OwnerType.Maintenance : OwnerType.Production, holder.OwnerId))
+ { State = ReservationState.Active };
+ return holder != null;
+ }
+
+ public bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource)
+ {
+ if (!_grantsByToken.TryGetValue(owner.TokenId, out var grants)) return false;
+ string key = resource.ToString();
+ foreach (var g in grants)
+ {
+ if (!string.Equals(g.ResourceKey, key, StringComparison.OrdinalIgnoreCase)) continue;
+ // 鏍呮爮锛氱敤 extendSeconds=1 鐨勭画鏈熶綔瀛樺湪鎬ф鏌ワ紙epoch/generation/phase 蹇呴』鍏ㄥ懡涓級
+ try { return _lease.Renew(g, 1); } catch { return false; }
+ }
+ return false;
+ }
+
+ private sealed class NoopLease : IOperationLease
+ {
+ public void Dispose() { }
+ }
+ }
+}
+```
+
+> 娉細鑻� `IOperationLease` 鍚� `ReleaseAsync` 绛夐澶栨垚鍛橈紝NoopLease 鎸夊叾瀹為檯绛惧悕琛ラ綈锛涘厛鐪� `Contracts/IOperationLease.cs`銆�
+
+- [ ] **Step 4: Bootstrapper 鎺ョ嚎**
+
+`InfrastructureBootstrapper.Initialize` 涓紝灏� `var resourceManager = new ResourceManager();` 鏇挎崲涓猴細
+
+```csharp
+// V3.0 MVP锛氫徊瑁佸簱 arb.db锛堜笌 data.db 鍚岀洰褰曪級
+string arbPath = Path.Combine(Path.GetDirectoryName(deviceConfigDbPath)!, "arb.db");
+var leaseStore = new DbLeaseStore(arbPath, "local-main");
+var resourceManager = new LayeredResourceManager(leaseStore);
+UtilContainer.RegisterUtil("LeaseCoordinator", leaseStore);
+```
+
+鍏朵綑娉ㄥ唽涓嶅彉锛圖eviceService/AutoDeviceFacade 鎸佹湁鐨� resourceManager 宸叉槸璺ㄨ繘绋嬬増锛夈�傛棫 `ResourceManager` 绫讳繚鐣欎笉鍒狅紙浣滀负鏃� DB 闄嶇骇澶囬�夛級銆�
+
+- [ ] **Step 5: 缂栬瘧 + 娴嬭瘯鍏ㄧ豢**
+
+```powershell
+& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" Aging.Infrastructure\Aging.Infrastructure.csproj /p:Configuration=Debug /p:Platform=AnyCPU /t:Build /v:minimal
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj
+```
+棰勬湡锛歁SBuild 鏃� error锛涘叏閮ㄦ祴璇� Pass銆�
+
+- [ ] **Step 6: Commit**
+
+```powershell
+git add Aging.Infrastructure
+git commit -m "feat(lease): LayeredResourceManager 缁勫悎浠茶锛屼笂灞傞浂鏀瑰姩璺ㄨ繘绋嬬敓鏁�"
+```
+
+---
+
+## Task 4: AutoDeviceFacade 鍗囩骇 鈥斺�� 蹇冭烦缁湡 + LeaseLost + ReleaseAsync
+
+**Files:**
+- Modify: `Aging.Infrastructure/Contracts/IAutoDeviceFacade.cs`
+- Modify: `Aging.Infrastructure/Services/AutoDeviceFacade.cs`
+- Test: `tests/Aging.MvpTests/FacadeLeaseLifecycleTests.cs`
+
+- [ ] **Step 1: 鍐欐祴璇曪紙楠岃瘉闂ㄩ潰渚濊禆鐨勭绾﹁涔夛級**
+
+```csharp
+using System;
+using System.IO;
+using System.Threading;
+using Aging.Infrastructure.Core;
+using Aging.ResourceContracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+ public class FacadeLeaseLifecycleTests : IDisposable
+ {
+ private readonly string _dbPath;
+ private readonly DbLeaseStore _store;
+ public FacadeLeaseLifecycleTests()
+ {
+ _dbPath = Path.Combine(Path.GetTempPath(), "arb_" + Guid.NewGuid().ToString("N") + ".db");
+ _store = new DbLeaseStore(_dbPath, "local-main");
+ }
+ public void Dispose() { try { File.Delete(_dbPath); } catch { } }
+
+ private LeaseGrant Grab(string key, int ttl)
+ => _store.Acquire(new AcquireRequest
+ {
+ OwnerId = "f", OwnerType = OwnerTypeV3.Production,
+ ResourceKeys = new[] { key }, TtlSeconds = ttl
+ }).Grants[0];
+
+ [Fact]
+ public void Heartbeat_Renew_Extends_Expiry()
+ {
+ var g = Grab("can/x", 2);
+ long before = g.ExpiresUtcMs;
+ Thread.Sleep(1200);
+ Assert.True(_store.Renew(g, 2));
+ Assert.True(_store.ProbeConflict("can/x").ExpiresUtcMs > before);
+ }
+
+ [Fact]
+ public void Expired_Sweep_StaleRenew_False_And_RecoveryBlocked()
+ {
+ var g = Grab("can/y", 1);
+ Thread.Sleep(1500);
+ Assert.Equal(1, _store.SweepExpired()); // 杩囨湡鈫扲ecoveryRequired
+ Assert.False(_store.Renew(g, 5)); // 鐔旀柇淇″彿锛氱画鏈熷繀璐�
+ Assert.Null(_store.ProbeConflict("can/y")); // 涓嶅啀鏄剧ず Active
+ Assert.Equal(LeasePhase.RecoveryRequired, _store.PhaseOf("can/y"));
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 杩愯娴嬭瘯** 鈥� `dotnet test ... --filter FacadeLeaseLifecycleTests`銆備袱娴嬭瘯鐩存帴楠岃瘉 Task 2 璇箟锛屽簲 Pass锛涜嫢 Fail 鍏堜慨 Task 2 鍐嶇户缁��
+
+- [ ] **Step 3: 鎵╁睍 IAutoDeviceLease 鎺ュ彛锛堝彔鍔犳垚鍛橈紝鏃㈡湁涓嶅姩锛�**
+
+```csharp
+ /// <summary>V3.0锛氱绾﹀け鏁堬紙缁湡杩炵画澶辫触/琚洖鏀讹級鏃惰Е鍙戯紱璁㈤槄鑰呭簲鍋滄涓嬪彂銆�</summary>
+ event EventHandler LeaseLost;
+ /// <summary>V3.0锛氭樉寮忓紓姝ラ噴鏀俱�侱ispose() 浠嶅悓姝ュ彲鐢ㄣ��</summary>
+ System.Threading.Tasks.Task ReleaseAsync(System.Threading.CancellationToken ct = default);
+ /// <summary>V3.0锛氱淮鎶よ娓¤姹傦紙Draining 棰勭暀锛孧VP 鎭� false锛夈��</summary>
+ bool HandoverRequested { get; }
+```
+
+- [ ] **Step 4: AutoDeviceLease 瀹炵幇蹇冭烦**
+
+`AutoDeviceFacade.cs` 鍏抽敭鐐癸細
+1. 鏋勯�犳柊澧炲彲閫夊弬鏁� `ILeaseCoordinator leaseCoordinator = null`锛圔ootstrapper 浼犲叆锛沶ull 淇濇寔鏃ц涓猴級锛�
+2. `AcquireAsync` 淇濈暀鏃㈡湁 Reserve锛岃嫢鎸佹湁 LayeredResourceManager 鍒� `TryGetGrants(owner.TokenId, out grants)` 鍚� `lease.StartHeartbeat(coordinator, grants)`锛�
+3. Lease 鍐呮柊澧烇細
+
+```csharp
+ private LeaseGrant[] _grants = new LeaseGrant[0];
+ private Timer _beacon;
+ private int _renewFailures;
+ private ILeaseCoordinator _coordinator;
+ public event EventHandler LeaseLost;
+ public bool HandoverRequested => false;
+
+ internal void StartHeartbeat(ILeaseCoordinator coordinator, LeaseGrant[] grants)
+ {
+ _coordinator = coordinator; _grants = grants;
+ _beacon = new Timer(_ =>
+ {
+ if (_disposed) return;
+ try
+ {
+ bool allOk = true;
+ foreach (var g in _grants)
+ if (!_coordinator.Renew(g, 30)) { allOk = false; break; }
+ _renewFailures = allOk ? 0 : _renewFailures + 1;
+ if (_renewFailures >= 2) LeaseLost?.Invoke(this, EventArgs.Empty); // 杩�2澶辫触鈫掔啍鏂�
+ }
+ catch { if (++_renewFailures >= 2) LeaseLost?.Invoke(this, EventArgs.Empty); }
+ }, null, 8000, 8000);
+ }
+
+ public Task ReleaseAsync(CancellationToken ct = default)
+ {
+ _beacon?.Dispose(); _beacon = null; // 鍏堝仠蹇冭烦锛屽啀璧版棦鏈夐噴鏀鹃摼
+ Dispose();
+ return Task.CompletedTask;
+ }
+```
+
+`Dispose()` 寮�澶村姞 `_beacon?.Dispose()`锛沗ExecuteAsync/QueryAsync` 寮�澶村姞 `if (_renewFailures >= 2) throw new InvalidOperationException("LEASE_STALE: 绉熺害宸插け鏁�");`銆�
+
+- [ ] **Step 5: 鍏ㄩ噺娴嬭瘯 + 缂栬瘧 + Commit**
+
+```powershell
+dotnet test tests\Aging.MvpTests\Aging.MvpTests.csproj
+& "...MSBuild.exe" Aging.Infrastructure\Aging.Infrastructure.csproj /p:Configuration=Debug /p:Platform=AnyCPU /t:Build /v:minimal
+git add Aging.Infrastructure tests
+git commit -m "feat(facade): 蹇冭烦缁湡+LeaseLost鐔旀柇+ReleaseAsync"
+```
+
+---
+
+## Task 5: 娉ㄥ唽椤�"鎵ц浣嶇疆"UI + 鍙戝竷闂ㄧ锛堟渶灏忕増锛岃ˉ榻愯寖鍥磋〃宸叉壙璇洪」锛�
+
+> 鑳屾櫙锛氳寖鍥磋〃涓�滄敞鍐岄〉 executionMode/authorityId/sessionKey/accessPolicy + 鍙戝竷闂ㄧ + 涓嶅彲鍙樺揩鐓у搱甯屸�濇鍓嶆棤瀵瑰簲瀹炴柦浠诲姟锛屾湰鑺備负琛ラ綈銆�
+
+**Files:**
+- Modify: `Plugin.DeviceConfig/Entity/DeviceDefinition.cs`锛堟柊澧� ExecutionModeKind 鏋氫妇 + 5 瀛楁 + 鍙娲剧敓 AccessPolicy锛�
+- Create: `Plugin.DeviceConfig/V3AuthorityCatalog.cs`锛堜徊瑁佸煙/Broker 鍊欓�夌敱瀹夸富 appSettings 鏋氫妇锛屾湭閰嶇疆鍥為��榛樿 local-main;broker-b01锛�
+- Modify: `Plugin.DeviceConfig/Plugin.DeviceConfig.csproj`锛堝紩鐢� System.Configuration锛�
+- Modify: `Plugin.DeviceConfig/DeviceEditModal.xaml(.cs)`锛堚�滄墽琛屼綅缃�濆尯鍧楋細Local/Broker 鍗曢�夈�丄uthorityId 蹇呭~涓嬫媺銆丅roker 鏃� BrokerId/OwnerProcessId/PhysicalSessionKey 鍐欐鍊欓�夈�丄ccessPolicy 鍙锛�
+- Modify: `Plugin.DeviceConfig/Model/DeviceConfigModel.cs`锛堝缓琛ㄦ柊鍒� + PRAGMA 杩佺Щ銆丼aveDevice/Map 鍚柊鍒椼�丆omputeConfigHash 绾冲叆鎵ц浣嶇疆銆佹柊澧� ValidatePublishGate锛�
+- Modify: `Plugin.DeviceConfig/DeviceConfig.xaml(.cs)`锛堣鎯呮爮灞曠ず鎵ц浣嶇疆/浠茶鍩燂紱PublishVersion 鍏堣窇闂ㄧ锛屾湭閫氳繃鎷︽埅锛�
+- Create: `Plugin.IntegrationTest/App.config`锛圴3.AuthorityIds / V3.BrokerIds 鍊欓�夋灇涓撅級
+
+- [ ] **Step 1: 瀛楁瀹氫箟鎸� V3.0 搂05鈥滄墽琛屼綅缃�濊锛�**
+
+| 鎺т欢 | 瀛楁 | 瑙勫垯 |
+|---|---|---|
+| RadioButton 脳2 | ExecutionMode=Local/Broker | 榛樿 Local锛涗簰鏂ヨ仈鍔ㄤ徊瑁佸煙 |
+| ComboBox | AuthorityId | 蹇呭~锛屽�欓�夋潵鑷� V3AuthorityCatalog.AuthorityIds锛堥厤缃灇涓撅級 |
+| ComboBox | BrokerId | 浠� Broker锛涘�欓�� BrokerIds锛涢棬绂佽姹� BrokerId==AuthorityId锛堣澶囧煙涓�鑷达級 |
+| 鍙 TextBox | OwnerProcessId | MVP 鍗犱綅褰撳墠杩涚▼ PID锛涗笉鍏ラ厤缃搱甯岋紙杩愯鏈熶俊鎭級 |
+| 鍙紪杈� ComboBox | PhysicalSessionKey | 浠� Broker锛涘�欓�変负绔偣瑙勮寖鍖栫殑鍐欐瀛楃涓诧紙can/{bus}/main銆乼cp/{ip:port}/main銆乻erial/{COM}/main銆亄deviceId}/main 绛夛級 |
+| 鍙 TextBox | AccessPolicy | 娲剧敓锛歀ocal鈫掔嫭鍗犵洿杩烇紱Broker鈫掑叡浜徊瑁� SessionGate 浠g悊锛涗笉钀藉簱 |
+
+- [ ] **Step 2: 鍙戝竷闂ㄧ ValidatePublishGate锛圴3.0 搂05 鏍¢獙鍒楋級锛�** AuthorityId 蹇呭~涓斿湪鍊欓�夋灇涓惧唴锛汱ocal 璁惧鍩熷繀椤� local- 鍓嶇紑锛汢roker 璁惧 BrokerId 蹇呭~銆佸湪鏋氫妇鍐呬笖涓� AuthorityId 涓�鑷淬�丳hysicalSessionKey 蹇呭~锛涘悓涓� PhysicalSessionKey 鍙厑璁镐竴涓涓诲煙锛堚�滃悓涓�浼氳瘽鍙兘鏈変竴涓涓烩�濓級銆備换涓�澶辫触闃绘鍙戝竷骞跺垪鍑洪敊璇��
+
+- [ ] **Step 3: 鍏煎鎬э細** 鏃у簱缁� PRAGMA+ALTER 琛ュ垪锛涘瓨閲忚澶� ExecutionMode 榛樿 Local銆丄uthorityId 绌衡�斺�旀敞鍐岄〉鎵撳紑鏃堕閫夋湰鏈� Local 鍩燂紝鍙戝竷鏃惰姹傞噸鏂颁繚瀛橈紙闂ㄧ鎻愮ず瀛橀噺鏁版嵁锛夛紱闆嗘垚娴嬭瘯棰勭疆璁惧锛堜笉缁忓彂甯冮摼璺級涓嶅彈褰卞搷锛涢厤缃搱甯屾柊澧炴墽琛屼綅缃锛屾棫鑽夌鍝堝笇涓庢柊璁$畻涓嶄竴鑷村睘棰勬湡锛堥厤缃‘瀹炲彉鏇达級銆�
+
+- [ ] **Step 4: 鎵嬪伐楠岃瘉 + 缂栬瘧**
+
+楠岃瘉璺緞锛氭柊澧炶澶囬�� Broker 鈫� 纭 BrokerId/浼氳瘽閿�欓�夎嚜鍔ㄧ敓鎴愪笖璁块棶绛栫暐鍙锛涗繚瀛樺悗缂栬緫鍥炴樉锛涘甫绌� AuthorityId 瀛橀噺璁惧鐐光�滃彂甯冣�� 鈫� 闂ㄧ鎷︽埅骞舵彁绀猴紱閲嶆柊淇濆瓨鍚庡彂甯冩垚鍔熶笖 Hash 鍙樺寲銆傜紪璇戯紙鐢� VS MSBuild锛屼笉鍔� Platform锛夛細
+
+```powershell
+& "...MSBuild.exe" Plugin.IntegrationTest\Plugin.IntegrationTest.csproj /t:Build /p:Configuration=Debug /v:minimal
+```
+
+- [ ] **Step 5: Commit**
+
+```powershell
+git add Plugin.DeviceConfig Plugin.IntegrationTest\App.config docs
+git commit -m "feat(deviceconfig): 娉ㄥ唽椤垫墽琛屼綅缃尯鍧�+鍙戝竷闂ㄧ锛圴3.0 搂05 鏈�灏忕増锛�"
+```
+
+鎺ㄨ繜椤癸紙P2+锛夛細澶� Authority 璺ㄦ満鍊欓�夎嚜鍔ㄥ彂鐜般�佸彂甯冮瑙堝樊寮傚睍绀恒�丼lotDeviceMapping 鈥滃叡浜槧灏勬樉寮忔爣娉ㄢ�濄��
+
+---
+
+## Task 6: Broker IPC 甯у崗璁� + 浠g悊椹卞姩锛堝鎴风渚э級
+
+**Files:**
+- Create: `Aging.ResourceContracts/BrokerIpc.cs`锛�4 瀛楄妭澶х闀垮害鍓嶇紑 + UTF8 JSON 灏佸缂栬В鐮侊紱SubmitBody/ResultBody 绾挎牸寮� DTO锛岃閬� LeaseGrant 鍙鏋勯�犲櫒鍙嶅簭鍒楀寲闄愬埗锛�
+- Create: `Aging.Infrastructure/Broker/BrokerClientChannel.cs`锛堝懡鍚嶇閬撳鎴风锛欻ello 鎻℃墜楠屽崗璁富鐗堟湰锛汻equestId/CorrelationId 鍝嶅簲鍏宠仈锛沝eadline/瓒呮椂锛涜寰幆鏂嚎鎰熺煡 + 鎯版�ч噸杩烇紱淇鏃� PipeClient 鈥淲riteMessage 鍚庣珛鍗� return鈥濅笉鍙揪鍝嶅簲闂锛�
+- Create: `Aging.Infrastructure/Broker/BrokerPipeName.cs`锛堟湇鍔″悕 `aging.{brokerId}.v{鍗忚涓荤増鏈瑌`锛孷3.0 搂13锛�
+- Create: `Aging.Infrastructure/Broker/DeviceConfigHash.cs`锛堜粠 data.db 璁$畻涓� DeviceConfigModel.ComputeConfigHash 鍚岃鍒欑殑蹇収鍝堝笇锛岃〃缂哄け鏃惰烦杩囩┖娈碉紱涓や晶鍚屽嚱鏁颁繚璇佸彲瀵硅处锛�
+- Create: `Aging.Infrastructure/Drivers/BrokeredCanDriver.cs`锛堝疄鐜� IDeviceDriver锛屽懡浠よ涔変笌 CanDriverAdapter 瀵归綈锛汢roker 杩斿洖鎬佲啋鏈湴寮傚父锛歊ejectedStaleLease鈫扡EASE_STALE銆丷ejectedConfigMismatch鈫扖ONFIG_MISMATCH銆乁nknown鈫扵imeoutException 绂佹鍋囧畾鏈墽琛岋級
+- Modify: `Aging.Infrastructure/InfrastructureBootstrapper.cs`锛堟柊澧� RegisterBrokeredDrivers锛氭壂 data.db 涓� ExecutionMode=Broker 璁惧鈫掓敞鍐屼唬鐞嗛┍鍔� + 鎼哄惎鍔ㄦ椂 configHash锛涘け璐ラ檷绾т笉闃诲 Local 閾捐矾锛�
+
+- [x] **Step 1: 鍗忚/閫氶亾/椹卞姩瀹炵幇**锛堝抚=4瀛楄妭澶х闀垮害+JSON锛汷neShot 鍚屾妯″瀷锛欳ONNECT/DISCONNECT/SEND/QUERY/STATUS 鐧藉悕鍗曪級
+- [x] **Step 2: 鎺ョ嚎**锛堜笂灞� IDeviceService/IAutoDeviceFacade/璋冭瘯椤甸浂鏀瑰姩锛氫唬鐞嗙偣閫夊湪椹卞姩灞傦級
+- [x] **Step 3: 缂栬瘧閫氳繃 + 鍚庣画 Task 7 e2e 楠岃瘉**
+
+---
+
+## Task 7: DeviceBroker.exe 瀹夸富 + 鍏ㄩ摼璺� e2e + 涓荤▼搴忚嚜鍔ㄦ媺璧�
+
+**Files:**
+- Create: `DeviceBroker/DeviceBroker.csproj`锛坣et472 Exe锛寈64锛涘凡鍔犲叆 `WPFProject\TapProject.sln`锛�
+- Create: `DeviceBroker/BrokerServer.cs`锛堢閬撳瀹炰緥鎺ュ彈寰幆锛涙瘡璇锋眰涓夐噸鍏冲崱锛氣憼OpCode 鐧藉悕鍗�+鍩熷唴璁惧杩囨护 鈶NV-04 configHash 瀵硅处锛�2s 鎯版�ч噸绠楋級鈶NV-01/03 鏍呮爮鍐嶆牎楠岋紙PhaseOf/ProbeConflict锛屾棤娲昏穬绉熺害鎷掔粷鐩存帴 I/O锛屼粎 DISCONNECT 璞佸厤锛夛紱SessionGate 鎸� PhysicalSessionKey 涓茶鍘熷瓙鐗囨锛涢槦鍒楄秴鏃垛啋RESOURCE_BUSY锛堢‘瀹氭湭鎵ц鍙畨閲嶈瘯锛夛紱鐗囨涓紓甯糕啋SEND/QUERY 淇濆畧鍒� Unknown锛�
+- Create: `DeviceBroker/Program.cs`锛�--db/--arb/--broker 鍙傛暟锛岄粯璁ゆ寚鍚戦泦鎴愭祴璇曠幆澧冪洰褰曪紱Ctrl+C 浼橀泤閫�鍑猴紝绉熺害闈� TTL 杩囨湡+浜哄伐鍥炴敹锛�
+- Create: `tests/Aging.MvpTests/BrokerE2ETests.cs`锛�6 鐢ㄤ緥锛氭湁绉熺害姝e悜閾捐矾 / 鏃犵绾︽嫆缁� / 鍝堝笇涓嶅尮閰嶆嫆缁� / 褰掕繕鍚庢嫆缁� / 鍚屼細璇濅覆琛� / 缁存姢鍐欐潈鏀捐锛涙瘡鐢ㄤ緥鍞竴 brokerId 鈫� 鐙珛绠¢亾鍚嶉殧绂诲苟琛岋級
+- Modify: `Plugin.IntegrationTest/MainWindow.xaml.cs`锛圛nitializeAutoServices 妫�娴� Broker 璁惧鈫掕嚜鍔ㄦ媺璧� DeviceBroker.exe锛沇indow_Closing 鍋滄満娓呯悊锛�
+- Modify: `tests/Aging.MvpTests/Aging.MvpTests.csproj`锛堝紩鐢� DeviceBroker锛�
+
+- [x] **Step 1: 瀹夸富瀹炵幇 + 宸ョ▼鍏� sln**
+- [x] **Step 2: e2e 娴嬭瘯锛圱DD锛�** 鈥� 棣栬疆绾細DeviceConfigHash 瀵圭己琛ㄥ穿婧冿紙no such table: DeviceChannel锛夛紱淇锛氱己琛ㄨ烦杩囩┖娈碉紙鍝堝笇瀵光�滀笉瀛樺湪鈥濅笌鈥滈浂琛屸�濅竴鑷达級锛涗簩杞叏缁�
+- [x] **Step 3: 璋冭瘯椤电淮鎶ゅ啓鏉冮摼璺‘璁�** 鈥� DeviceDebug.InitFinish 宸蹭粠 UtilContainer 鍙� LayeredResourceManager锛汻equestMaintenance 鐨勭淮鎶ょ绾︾粡 arb.db 璺ㄨ繘绋嬩簰鏂ワ紙鍗犵敤鍗虫嫆缁濓級锛汢roker 璁惧缁存姢鍐� = 缁存姢绉熺害鍏� arb.db 鈫� broker 鏍呮爮妫�鍑� Active 鏀捐锛屾棤闇�鏂颁唬鐮侊紱e2e 浠� MaintenanceLease_BrokeredWrite_Succeeds 瑕嗙洊
+- [x] **Step 4: 鍏ㄩ噺楠岃瘉** 鈥� MvpTests 22/22锛涗富绋嬪簭/瀹夸富 MSBuild exit 0
+
+宸茬煡绠�鍖栵紙P2+锛夛細owner鈫攃lientInstanceId 绮剧‘瀵归綈锛堝綋鍓嶅悓鏈� arb.db 鍙仛瀛樺湪鎬�/鐩镐綅妫�鏌ワ級銆佸懆鏈熶换鍔�/璁㈤槄/Outbox銆佽法鏈洪儴缃层��
+
+---
--
Gitblit v1.9.1