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