From 292350c3eeefeed69e377ec8a94d9dac2ece800f Mon Sep 17 00:00:00 2001
From: missile <3397241346@qq.com>
Date: Sun, 6 Sep 2026 18:41:57 +0800
Subject: [PATCH 1/8] =?UTF-8?q?feat(wave-a):=20C#/Avalonia=20=E9=AA=A8?=
=?UTF-8?q?=E6=9E=B6=20+=20AppBar=20=E4=B8=80=E7=AD=89=E5=85=AC=E6=B0=91?=
=?UTF-8?q?=E5=8C=96=20(=C2=A75=20=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0)=20?=
=?UTF-8?q?+=2054=20=E9=A1=B9=E5=8D=95=E6=B5=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
解决方案 (SmartSideBAR.slnx, .NET 10 / Avalonia 11.3.20, 方案 §4.1):
- SmartSideBAR.Core (net10.0, 平台无关): SidekickConfig 逐字段同构 types.ts
(附录 B 互认) · ConfigService 三层合并+迁移+键白名单(B3 根修)+未知字段保留 ·
EventBus(IPC 事件替代) · SchedulerService(TimeProvider, C1 变更检测落盘,
§6.10 过期周期提醒启动不补触发) · LinksService(E1 全量回写) · PolicyService ·
FloatBallLayout(floatball-layout.ts 逐函数移植)
- SmartSideBAR.Windows (net10.0-windows): AppBarNative(LibraryImport) ·
AppBarApi(QUERYPOS 语义对齐 appbar.cc) · AppBarService 通知状态机 ·
WndProcHook(SetWindowLongPtr 子类化, Wave B 热键/USB 复用)
- SmartSideBAR.Avalonia: Program.cs DI 装配(§4.2) · WindowManager ·
SidebarWindow Demo rail · SSB_SMOKE_EXIT_MS 冒烟自动退出
- tests: Core 40 + Windows 14 = 54 用例全绿 (FakeAppBarApi 状态机序列断言)
AppBar 缺口 K1-K5/K7 闭环 (§5.1): ABN 通知 WndProc 闭环(K1) · ABM_ACTIVATE/
WINDOWPOSCHANGED 上报(K2) · 全屏降级与恢复(K3/V5) · 物理像素单轨(K4:
PhysicalDpiScope 强制 PMv2 线程上下文 + MoveWindow 直达) · node-gyp 退役(K5) ·
DISPLAYCHANGE/DPICHANGED 重排(K7) · 孤儿注册自愈(V6) · 退出 WorkArea 恢复(V8)
对方案勘误 (docs/mlp-wave-a-report.md §2.3):
1) Reposition 锚点 rcWork→rcMonitor: rcWork 已含自身占位, 注册期 ABN 风暴中
占位带逐次内漂一个带宽 (实测工作区被蚕食至 43px); 沿边裁剪交 QUERYPOS
2) Avalonia AddWndProcCallback 非公开 API → SetWindowLongPtr 子类化等效实现
真机冒烟 (2560x1600 @150%): ABM_NEW=True · 侧栏贴右缘 64px · 工作区稳定扣除 ·
退出后 WorkArea 完全恢复 · 旧目录配置自动迁移(.migrated.bak)
CI 同步: dotnet job 指向 slnx; testing.md 追加 V1-V8 验收清单
---
.github/workflows/ci.yml | 8 +-
Directory.Build.props | 11 +
SmartSideBAR.slnx | 11 +
docs/mlp-wave-a-report.md | 103 ++++++++
docs/testing.md | 19 ++
src/SmartSideBAR.Avalonia/App.axaml | 8 +
src/SmartSideBAR.Avalonia/App.axaml.cs | 56 ++++
.../Logging/FileLoggerProvider.cs | 62 +++++
src/SmartSideBAR.Avalonia/Program.cs | 65 +++++
.../SmartSideBAR.Avalonia.csproj | 25 ++
.../ViewModels/RelayCommand.cs | 18 ++
.../ViewModels/SidebarViewModel.cs | 68 +++++
.../Views/SidebarWindow.axaml | 67 +++++
.../Views/SidebarWindow.axaml.cs | 44 ++++
src/SmartSideBAR.Avalonia/WindowManager.cs | 64 +++++
src/SmartSideBAR.Avalonia/app.manifest | 20 ++
.../Configuration/AppearanceConfig.cs | 65 +++++
.../Configuration/ConfigJsonContext.cs | 19 ++
.../Configuration/ConfigService.cs | 249 ++++++++++++++++++
.../Configuration/FloatBallConfig.cs | 44 ++++
.../Configuration/SidekickConfig.cs | 145 ++++++++++
.../FloatBall/FloatBallLayout.cs | 130 +++++++++
src/SmartSideBAR.Core/Links/LinksService.cs | 74 ++++++
src/SmartSideBAR.Core/Messaging/EventBus.cs | 65 +++++
src/SmartSideBAR.Core/Policy/PolicyService.cs | 30 +++
.../Scheduling/SchedulerService.cs | 209 +++++++++++++++
.../SmartSideBAR.Core.csproj | 13 +
src/SmartSideBAR.Windows/AppBar/AppBarApi.cs | 153 +++++++++++
.../AppBar/AppBarNative.cs | 104 ++++++++
.../AppBar/AppBarService.cs | 159 +++++++++++
src/SmartSideBAR.Windows/AppBar/IAppBarApi.cs | 41 +++
.../Native/IWndProcHook.cs | 16 ++
.../Native/Win32Display.cs | 14 +
.../Native/WndProcHook.cs | 122 +++++++++
.../SmartSideBAR.Windows.csproj | 20 ++
.../ConfigServiceTests.cs | 197 ++++++++++++++
.../SmartSideBAR.Core.Tests/EventBusTests.cs | 65 +++++
.../FloatBallLayoutTests.cs | 94 +++++++
tests/SmartSideBAR.Core.Tests/GlobalUsings.cs | 1 +
.../LinksServiceTests.cs | 79 ++++++
.../SchedulerServiceTests.cs | 149 +++++++++++
.../SmartSideBAR.Core.Tests.csproj | 18 ++
.../TestDoubles/FakeTimeProvider.cs | 82 ++++++
.../AppBarServiceTests.cs | 213 +++++++++++++++
.../GlobalUsings.cs | 1 +
.../SmartSideBAR.Windows.Tests.csproj | 20 ++
.../TestDoubles/FakeAppBarApi.cs | 131 +++++++++
47 files changed, 3367 insertions(+), 4 deletions(-)
create mode 100644 Directory.Build.props
create mode 100644 SmartSideBAR.slnx
create mode 100644 docs/mlp-wave-a-report.md
create mode 100644 src/SmartSideBAR.Avalonia/App.axaml
create mode 100644 src/SmartSideBAR.Avalonia/App.axaml.cs
create mode 100644 src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs
create mode 100644 src/SmartSideBAR.Avalonia/Program.cs
create mode 100644 src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj
create mode 100644 src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs
create mode 100644 src/SmartSideBAR.Avalonia/ViewModels/SidebarViewModel.cs
create mode 100644 src/SmartSideBAR.Avalonia/Views/SidebarWindow.axaml
create mode 100644 src/SmartSideBAR.Avalonia/Views/SidebarWindow.axaml.cs
create mode 100644 src/SmartSideBAR.Avalonia/WindowManager.cs
create mode 100644 src/SmartSideBAR.Avalonia/app.manifest
create mode 100644 src/SmartSideBAR.Core/Configuration/AppearanceConfig.cs
create mode 100644 src/SmartSideBAR.Core/Configuration/ConfigJsonContext.cs
create mode 100644 src/SmartSideBAR.Core/Configuration/ConfigService.cs
create mode 100644 src/SmartSideBAR.Core/Configuration/FloatBallConfig.cs
create mode 100644 src/SmartSideBAR.Core/Configuration/SidekickConfig.cs
create mode 100644 src/SmartSideBAR.Core/FloatBall/FloatBallLayout.cs
create mode 100644 src/SmartSideBAR.Core/Links/LinksService.cs
create mode 100644 src/SmartSideBAR.Core/Messaging/EventBus.cs
create mode 100644 src/SmartSideBAR.Core/Policy/PolicyService.cs
create mode 100644 src/SmartSideBAR.Core/Scheduling/SchedulerService.cs
create mode 100644 src/SmartSideBAR.Core/SmartSideBAR.Core.csproj
create mode 100644 src/SmartSideBAR.Windows/AppBar/AppBarApi.cs
create mode 100644 src/SmartSideBAR.Windows/AppBar/AppBarNative.cs
create mode 100644 src/SmartSideBAR.Windows/AppBar/AppBarService.cs
create mode 100644 src/SmartSideBAR.Windows/AppBar/IAppBarApi.cs
create mode 100644 src/SmartSideBAR.Windows/Native/IWndProcHook.cs
create mode 100644 src/SmartSideBAR.Windows/Native/Win32Display.cs
create mode 100644 src/SmartSideBAR.Windows/Native/WndProcHook.cs
create mode 100644 src/SmartSideBAR.Windows/SmartSideBAR.Windows.csproj
create mode 100644 tests/SmartSideBAR.Core.Tests/ConfigServiceTests.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/EventBusTests.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/FloatBallLayoutTests.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/GlobalUsings.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/LinksServiceTests.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/SchedulerServiceTests.cs
create mode 100644 tests/SmartSideBAR.Core.Tests/SmartSideBAR.Core.Tests.csproj
create mode 100644 tests/SmartSideBAR.Core.Tests/TestDoubles/FakeTimeProvider.cs
create mode 100644 tests/SmartSideBAR.Windows.Tests/AppBarServiceTests.cs
create mode 100644 tests/SmartSideBAR.Windows.Tests/GlobalUsings.cs
create mode 100644 tests/SmartSideBAR.Windows.Tests/SmartSideBAR.Windows.Tests.csproj
create mode 100644 tests/SmartSideBAR.Windows.Tests/TestDoubles/FakeAppBarApi.cs
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b677951..cab0ff8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,18 +42,18 @@ jobs:
dotnet:
name: Avalonia (Core/Windows/Avalonia 三层)
runs-on: windows-latest
- if: hashFiles('SmartSideBAR.sln') != ''
+ if: hashFiles('SmartSideBAR.slnx') != ''
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
- run: dotnet restore SmartSideBAR.sln
+ run: dotnet restore SmartSideBAR.slnx
- name: Build
- run: dotnet build SmartSideBAR.sln -c Release --no-restore
+ run: dotnet build SmartSideBAR.slnx -c Release --no-restore
- name: Test (覆盖率采集)
- run: dotnet test SmartSideBAR.sln -c Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
+ run: dotnet test SmartSideBAR.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
# 覆盖率门禁:Wave A 先落机制、阈值 0;Wave D(§10.1)提升至 80
- name: Coverage gate
shell: pwsh
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..139aae8
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+ enable
+ enable
+ latest
+ nullable
+ true
+ 2.0.0-waveA
+
+
diff --git a/SmartSideBAR.slnx b/SmartSideBAR.slnx
new file mode 100644
index 0000000..ba19d73
--- /dev/null
+++ b/SmartSideBAR.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/mlp-wave-a-report.md b/docs/mlp-wave-a-report.md
new file mode 100644
index 0000000..4af85e4
--- /dev/null
+++ b/docs/mlp-wave-a-report.md
@@ -0,0 +1,103 @@
+# MLP Wave A 交付报告(骨架 + AppBar 一等公民化)
+
+> 依据《优化方案_Master_MLP_v1.md》(M1.0, 2026-09-06)。分支:`avalonia`(主线开发);
+> `main` 已完成 Wave 0 治理并冻结维护(仅安全补丁)。
+
+## 1. Wave 0 仓库治理(已交付,main @ 263db0a)
+
+| 项 | 结果 |
+|---|---|
+| G1 密钥治理 | **勘误**:`cert/` 私钥从未被 git 跟踪(`git log --all -- cert/` 为空),无需历史重写。处置:`.gitignore` 改为按文件类型精确忽略(pfx/p12/password/thumbprint),公钥 `SmartSideBAR-root.cer` 与信任脚本入库可审计;新增 `uninstall-trust.cmd`(安装包默认附带,卸载/轮换时移除旧信任根)。密钥轮换步骤见 `cert/README.md`(附录 C.1),留作用户手动动作(需交互式口令) |
+| G3 CI | `.github/workflows/ci.yml`:gitleaks 前置(全历史扫描)+ Electron 冻结门禁(typecheck/lint/test 全绿本地验证)+ Avalonia dotnet 门禁(构建/测试/覆盖率机制,阈值 Wave D 提升至 80)+ v2* tag release job(签名走 secrets) |
+| G6 | `aede938` 无关提交已在历史,按方案默认 3a 不重写(gitleaks 防再犯) |
+| 分支策略 | main 冻结 / avalonia 主开发线(ADR-M1 绞杀者迁移) |
+
+## 2. Wave A 解决方案(本报告范围)
+
+### 2.1 结构(方案 §4.1 落地)
+
+```
+SmartSideBAR.slnx
+├── src/SmartSideBAR.Core net10.0(平台无关,100% 单测)
+│ ├── Configuration/ SidekickConfig(逐字段同构 types.ts) · ConfigService(三层合并+迁移)
+│ │ ConfigMigrator(内嵌 Load) · ConfigJsonContext(源生成) · Appearance/FloatBall 钳制
+│ ├── Scheduling/ SchedulerService(TimeProvider 注入,C1 变更检测落盘)
+│ ├── Links/ LinksService(E1 全量回写)
+│ ├── Policy/ PolicyService + ModuleIds
+│ ├── FloatBall/ FloatBallLayout(floatball-layout.ts 逐函数移植)
+│ └── Messaging/ EventBus(替代 IPC 事件推送)
+├── src/SmartSideBAR.Windows net10.0-windows(互操作层,零 UI 依赖)
+│ ├── AppBar/ AppBarNative(LibraryImport) · IAppBarApi/AppBarApi · AppBarService(状态机)
+│ └── Native/ WndProcHook(SetWindowLongPtr 子类化) · Win32Display
+├── src/SmartSideBAR.Avalonia net10.0-windows(UI 层,Avalonia 11.3.20)
+│ ├── Program.cs(DI 装配 §4.2) · App.axaml · WindowManager · Views/SidebarWindow(Demo rail)
+│ └── Logging/FileLoggerProvider(Wave D 换 Serilog)
+└── tests/ Core.Tests(40) + Windows.Tests(14) = 54 用例全绿
+```
+
+### 2.2 AppBar 缺口对照(方案 §5.1 → 现状)
+
+| 缺口 | 状态 | 落点 |
+|---|---|---|
+| K1 ABN 通知无人接收 | ✅ 闭环 | `WndProcHook` 子类化 + `AppBarService.OnWndProc` 状态机(POSCHANGED/STATECHANGE/FULLSCREENAPP/WINDOWARRANGE 全处理) |
+| K2 ACTIVATE/WINDOWPOSCHANGED 未上报 | ✅ | WM_ACTIVATE → ABM_ACTIVATE;每次重排后 ABM_WINDOWPOSCHANGED |
+| K3 ABN_FULLSCREENAPP | ✅ | 全屏收起 rail / 退出按用户偏好恢复(V5),`FullscreenAppChanged` 事件 |
+| K4 坐标双轨 | ✅ 单轨 | 物理像素全链路:`PhysicalDpiScope` 强制 PMv2 线程上下文 + `MoveWindow` 直达,零 DIP 换算 |
+| K5 构建链脆弱 | ✅ 消灭 | node-gyp/N-API 全退役,P/Invoke 编译期绑定 |
+| K6 自动隐藏 | ⏳ P2 | 常量已备(ABM_SETAUTOHIDEBAR),按方案列 P2 |
+| K7 多显示器/热插拔 | ✅ | WM_DISPLAYCHANGE / WM_DPICHANGED → Reposition |
+| V6 孤儿注册自愈 | ✅ | Attach 前补发 ABM_REMOVE(对未注册 hwnd 无副作用) |
+| V8 退出恢复 WorkArea | ✅ 实测 | Detach → ABM_REMOVE;进程死亡后系统回收亦实测确认 |
+
+### 2.3 对方案的两处实现勘误(重要)
+
+1. **§5.3 `Reposition` 锚点:`rcWork` → `rcMonitor`**。方案蓝本以工作区为锚,但注册后 rcWork
+ 已扣除自身 AppBar 占位——注册期 ABN_POSCHANGED 风暴中占位带每次向屏幕内侧漂移一个带宽
+ (实测 150% 缩放下工作区被蚕食至 43px)。已改为全屏矩形锚定,沿边裁剪交给 ABM_QUERYPOS
+ (与 v1.2.0 appbar.cc 语义一致),并加了漂移回归测试 `Reposition_IsStable_UnderRepeatedPosChanged`。
+2. **§5.3 `WinProcHook`:Avalonia `AddWndProcCallback` 非公开 API**,改用 `SetWindowLongPtr(GWLP_WNDPROC)`
+ 经典子类化——等效且零 UI 框架耦合,Wave B 的全局热键(WM_HOTKEY)与 USB(WM_DEVICECHANGE)复用同一钩子。
+
+另:宿主(Avalonia)线程的 DPI 感知上下文不受我们控制,实测坐标被虚拟化(÷1.5)。
+`AppBarApi` 所有调用经 `SetThreadDpiAwarenessContext(PMv2)` 作用域包裹——物理像素单轨由此硬保证。
+
+### 2.4 开发期修复的 bug 清单
+
+| # | Bug | 修复 |
+|---|---|---|
+| 1 | EventBus 退订实现错误 | Unsubscription 携带事件类型 |
+| 2 | 调度器启动追赶循环遍历时修改集合 | 快照遍历 |
+| 3 | `IReadOnlyList` 缺 JSON 源生成元数据 | Context 补注册 |
+| 4 | `NormalizeAccent` 3 位色号未校验 hex 字符 | 对齐 appearance.ts 正则语义 |
+| 5 | `NextFullHour` 跨日对齐错误 | 按本地时间整点重构 |
+| 6 | rcWork 锚点反蚀漂移(见勘误 1) | rcMonitor 锚定 + 回归测试 |
+| 7 | 线程 DPI 上下文虚拟化(见勘误 3) | PhysicalDpiScope 强制 PMv2 |
+
+### 2.5 验证证据
+
+- **单测 54/54 全绿**(Core 40 + Windows 14,零警告零错误构建)。AppBarService 状态机经
+ FakeAppBarApi 断言操作序列:Attach→NEW/QUERYPOS/SETPOS/WINDOWPOSCHANGED/MOVE;
+ POSCHANGED→重协商;FULLSCREENAPP→rail/恢复;ACTIVATE 上报;DISPLAYCHANGE→重排;Detach→REMOVE。
+- **真机冒烟(2560×1600 @150%)**:
+ - `ABM_NEW → True`,侧栏窗口物理贴右缘(虚拟坐标 1664-1707 = 物理 2496-2560,64px 宽)
+ - 工作区稳定扣除恰好一个带宽(0,0)-(1664,1019),多次 ABN_POSCHANGED 无漂移
+ - 退出:ABM_REMOVE → 工作区完全恢复 (0,0)-(1707,1019)
+ - 冒烟自动化:`SSB_SMOKE_EXIT_MS` 环境变量 N 毫秒自动退出(CI 可用)
+- **配置互认**:`%APPDATA%\SmartSideBAR\config.json` camelCase 与 Electron 版逐字段互认;
+ 遗留目录(`%APPDATA%\smartsidebar`、`%ProgramData%\SeewoSidekick`)首启自动迁移(.migrated.bak)。
+
+### 2.6 已落点的 IPC 通道(附录 A 进度)
+
+| 区块 | 状态 |
+|---|---|
+| A.7 config:get/set/updated、power(部分) | ✅ ConfigService(B3 键白名单根修) |
+| A.8 reminder:add/remove/list/due | ✅ SchedulerService |
+| A.9 window:resize(rail/panel 切换) | ✅ Demo(SetExpanded→AppBar 重排) |
+| A.10 diag:update(配置变更广播) | ✅ ConfigChanged |
+| 其余(IME/USB/捕获族/悬浮球/OOBE…) | Wave B/C(接口桩已在 DI 预留) |
+
+### 2.7 Wave B 入口提示
+
+- `IWndProcHook` 支持同窗口多回调叠加——HotkeyService/UsbDeviceWatcher 直接 `Add`。
+- `ModuleIds` + `PolicyService` 就绪,服务启动跳过逻辑按 P2-1 语义接入。
+- 铃声播放 (NAudio)、IME (InputLanguage)、USB (WM_DEVICECHANGE) 按方案 §6 对应节实现。
diff --git a/docs/testing.md b/docs/testing.md
index e321580..655f511 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -126,3 +126,22 @@ npm run win && node scripts/bench-p28.mjs --exe dist\win-unpacked\SmartSideBAR.e
- 录屏产物为 WebM(非 MP4),README 不应宣称「转 MP4」。
- 自动更新、锁屏/电源已移除(见 `docs/project-audit-and-roadmap.md` P2-5/P2-6);快捷键槽位已实现(P2-2)。
+
+## Wave A 追加:AppBar 验收清单(方案 §5.6 V1-V8)
+
+> Avalonia 版基座已完成 AppBar 一等公民化(`docs/mlp-wave-a-report.md`)。
+> 下表为出口验收清单;自动化层见 `tests/SmartSideBAR.Windows.Tests`(状态机序列断言)。
+
+| # | 场景 | 预期 | 状态 |
+|---|---|---|---|
+| V1 | 任一应用最大化 | 工作区自动避让侧栏,侧栏完整可见 | ✅ 真机验证(ABM_NEW=True,WorkArea 扣除带宽) |
+| V2 | 任务栏移到左缘/上缘 | ABN_POSCHANGED → 自动重排(≤200ms 目标 Wave D 计时) | ✅ 单测(序列断言);真机人工项 |
+| V3 | 100%~250% 缩放像素级对齐 | 物理像素单轨,无 DIP 取整误差 | ✅ 结构性保证(PMv2 强制 + MoveWindow 直达);150% 真机验证 |
+| V4 | 双显示器 + 热插拔 | WM_DISPLAYCHANGE → 重注册 | ✅ 单测;多屏真机人工项 |
+| V5 | 全屏授课视频 | ABN_FULLSCREENAPP → 收起 rail,退出恢复 | ✅ 单测(含用户偏好恢复) |
+| V6 | 进程被强杀 | 启动自愈:Attach 前补发 ABM_REMOVE | ✅ 单测 + 实测(杀进程后 WorkArea 回收) |
+| V7 | 与第三方 AppBar 共存 | 系统自动分栏无死锁 | ⏳ 真机人工项(注册失败自动回退 Topmost 已实现) |
+| V8 | 退出应用 | WorkArea 完全恢复 | ✅ 实测(ABM_REMOVE → 对比退出前后工作区) |
+
+冒烟命令:`SSB_SMOKE_EXIT_MS=8000 src/SmartSideBAR.Avalonia/bin/Debug/net10.0-windows/SmartSideBAR.Avalonia.exe`
+(N 毫秒后自动退出;日志在 `%APPDATA%\SmartSideBAR\logs\`)
diff --git a/src/SmartSideBAR.Avalonia/App.axaml b/src/SmartSideBAR.Avalonia/App.axaml
new file mode 100644
index 0000000..1ac6f56
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/src/SmartSideBAR.Avalonia/App.axaml.cs b/src/SmartSideBAR.Avalonia/App.axaml.cs
new file mode 100644
index 0000000..a2a388a
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/App.axaml.cs
@@ -0,0 +1,56 @@
+using Microsoft.Extensions.DependencyInjection;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using SmartSideBAR.Core.Scheduling;
+using SmartSideBAR.Windows.AppBar;
+
+namespace SmartSideBAR.Avalonia;
+
+public partial class App : Application
+{
+ private readonly IServiceProvider? _sp;
+
+ public App(IServiceProvider sp) => _sp = sp;
+
+ // 设计器/XAML 预览用
+ public App() { }
+
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (_sp is not null && ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var scheduler = _sp.GetRequiredService();
+ var appBar = _sp.GetRequiredService();
+ var windowManager = _sp.GetRequiredService();
+
+ desktop.MainWindow = windowManager.CreateSidebar();
+
+ // V8: 退出前注销 AppBar / 停调度, 系统 WorkArea 完全恢复
+ desktop.ShutdownRequested += (_, _) =>
+ {
+ appBar.Detach();
+ scheduler.Stop();
+ };
+
+ ConfigureSmokeExit(desktop);
+ }
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ /// 冒烟验证: SSB_SMOKE_EXIT_MS>0 时 N 毫秒后自动退出 (自动化/CI 用)。
+ private void ConfigureSmokeExit(IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (!int.TryParse(Environment.GetEnvironmentVariable("SSB_SMOKE_EXIT_MS"), out var ms) || ms <= 0) return;
+ var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(ms) };
+ timer.Tick += (_, _) =>
+ {
+ timer.Stop();
+ desktop.Shutdown();
+ };
+ timer.Start();
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs b/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs
new file mode 100644
index 0000000..234888a
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs
@@ -0,0 +1,62 @@
+// Wave A 轻量文件日志 —— 按日滚动 (Wave D 按方案换 Serilog 滚动 7d 保留)。
+// 约束: 无第三方依赖, 同步写 + 锁 (Wave A 日志量极小, 不构成瓶颈)。
+using Microsoft.Extensions.Logging;
+
+namespace SmartSideBAR.Avalonia.Logging;
+
+public sealed class FileLoggerProvider : ILoggerProvider
+{
+ private readonly object _gate = new();
+ private readonly string _logDir;
+ private DateTime _currentDay;
+ private StreamWriter? _writer;
+
+ public FileLoggerProvider(string logDir)
+ {
+ _logDir = logDir;
+ Directory.CreateDirectory(logDir);
+ }
+
+ public ILogger CreateLogger(string categoryName) => new FileLogger(this, categoryName);
+
+ private StreamWriter AcquireWriter()
+ {
+ var today = DateTime.UtcNow.Date;
+ if (_writer is null || _currentDay != today)
+ {
+ _writer?.Dispose();
+ _currentDay = today;
+ _writer = new StreamWriter(
+ Path.Combine(_logDir, $"ssb-{today:yyyyMMdd}.log"), append: true)
+ {
+ AutoFlush = true,
+ };
+ }
+ return _writer;
+ }
+
+ public void Dispose()
+ {
+ lock (_gate) _writer?.Dispose();
+ }
+
+ private sealed class FileLogger(FileLoggerProvider owner, string category) : ILogger
+ {
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
+ Func formatter)
+ {
+ if (!IsEnabled(logLevel)) return;
+ var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{logLevel}] {category}: {formatter(state, exception)}";
+ if (exception is not null) line += Environment.NewLine + exception;
+ lock (owner._gate)
+ {
+ try { owner.AcquireWriter().WriteLine(line); }
+ catch { /* 日志失败不影响业务 */ }
+ }
+ }
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Program.cs b/src/SmartSideBAR.Avalonia/Program.cs
new file mode 100644
index 0000000..dbd9928
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Program.cs
@@ -0,0 +1,65 @@
+// 服务装配 (方案 §4.2): 无 IPC —— UI 与服务同进程直调类型安全接口。
+using Avalonia;
+using Avalonia.Win32;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using SmartSideBAR.Avalonia.Logging;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Core.Links;
+using SmartSideBAR.Core.Messaging;
+using SmartSideBAR.Core.Policy;
+using SmartSideBAR.Core.Scheduling;
+using SmartSideBAR.Windows.AppBar;
+using SmartSideBAR.Windows.Native;
+
+namespace SmartSideBAR.Avalonia;
+
+internal static class Program
+{
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ var services = new ServiceCollection();
+
+ // Core 层 (100% 可单测)
+ services.AddSingleton(ConfigPaths.CreateDefault());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(TimeProvider.System);
+ services.AddSingleton();
+
+ // Windows 互操作层 (接口 + 实现成对注册, 测试可替换)
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // UI 层
+ services.AddSingleton();
+
+ services.AddLogging(b =>
+ {
+ b.SetMinimumLevel(LogLevel.Information);
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ b.AddProvider(new FileLoggerProvider(Path.Combine(appData, "SmartSideBAR", "logs")));
+ });
+
+ using var sp = services.BuildServiceProvider();
+
+ // 与 v1.2.0 main.ts bootstrap 顺序同构: 配置迁移 → 日志 → 服务 → 窗口
+ var config = sp.GetRequiredService();
+ config.Load();
+ var log = sp.GetRequiredService().CreateLogger("Bootstrap");
+ log.LogInformation(
+ "SmartSideBAR (Avalonia Wave A) 启动 — config v{Version}, 侧栏 {Side} 侧, 链接 {Links} 条, 提醒 {Reminders} 条",
+ config.Current.Version, config.Current.Display.SidebarSide,
+ config.Current.Links.Count, config.Current.Reminders.Count);
+
+ return AppBuilder.Configure(() => new App(sp))
+ .UsePlatformDetect()
+ .With(new Win32PlatformOptions())
+ .LogToTrace()
+ .StartWithClassicDesktopLifetime(args);
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj b/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj
new file mode 100644
index 0000000..a8627d2
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+ WinExe
+ net10.0-windows
+ SmartSideBAR.Avalonia
+ true
+ app.manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs b/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs
new file mode 100644
index 0000000..9ff18e1
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs
@@ -0,0 +1,18 @@
+// 轻量命令 (无第三方依赖; Wave B 如引入 CommunityToolkit 可替换)
+using System.Collections;
+using System.Windows.Input;
+
+namespace SmartSideBAR.Avalonia.ViewModels;
+
+public sealed class RelayCommand(Action