diff --git a/docs/superpowers/plans/2026-06-21-windows-client.md b/docs/superpowers/plans/2026-06-21-windows-client.md new file mode 100644 index 0000000..7b90772 --- /dev/null +++ b/docs/superpowers/plans/2026-06-21-windows-client.md @@ -0,0 +1,593 @@ +# Windows 客户端 bring-up + 安装包 Implementation Plan + +> **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:** 让已写好但从未在真 Windows 上构建/运行的 Windows 客户端跑通端到端(登录→选节点→connect→全局代理出网),并产出 Inno Setup 安装包;顺带把字体改为全平台本地打包。 + +**Architecture:** 沿用既有「整 app UAC 提权 + `sing-box.exe` 子进程继承管理员权 + wintun TUN 全局代理」子进程方案(`DesktopVpnBridge`),不引入 native helper。UI 复用 `DesktopShell`(原生 Windows 标题栏)。字体从 `google_fonts` 运行时拉取改为本地 ttf 打包(拉丁全量 + Noto Sans SC 7000 字子集 + `fontFamilyFallback`),生产 `PangolinFonts.useBundled=true`。 + +**Tech Stack:** Flutter (desktop/Windows)、Dart、Riverpod、sing-box + wintun、`window_manager`/`tray_manager`、CMake/MSVC、Inno Setup、fonttools(`pyftsubset`)。 + +## Global Constraints + +- Flutter SDK `>=3.24.0`,Dart `^3.5.0`(来自 `client/pubspec.yaml`)。 +- 目标平台 Windows 10/11;app `requireAdministrator`(manifest 已设,勿降权)。 +- 颜色/间距等设计 token 来自 `design/colors_and_type.css` 单源,**禁止硬编码 hex**;字体 family 名沿用 `PangolinFonts`(Sora / Manrope / Noto Sans SC / JetBrains Mono)。 +- 字体改动对**全平台生效**(macOS/Windows/Linux),不得只在 Windows 分支改。 +- 真机相关任务(Task 4–6, 7 的打包验证)由**用户在 Windows 机器执行**,Claude 提供逐条 PowerShell 指令并据回贴日志迭代;Claude 在 macOS 无法构建/运行 Windows。 +- 提交信息结尾附 `Co-Authored-By: Claude Opus 4.8 `。 + +--- + +## 文件结构(本计划新建/修改) + +- `client/lib/pangolin_theme.dart`(改)— `PangolinFonts.useBundled` 生产默认 true;CJK 经 `fontFamilyFallback` 接入。 +- `client/lib/main.dart`(改)— 桌面窗口默认/最小尺寸。 +- `client/pubspec.yaml`(改)— 恢复 `fonts:` 段,声明 4 个 family。 +- `client/fonts/`(新建)— 生产 ttf:拉丁 8 个静态权重(复用 `test/fonts/`)+ `NotoSansSC-subset.ttf`。 +- `tools/fonts/make-cjk-subset.sh`(新建)— 生成 Noto Sans SC 子集的可复现脚本。 +- `tools/fonts/common-hanzi.txt`(新建)— 子集字表(通用规范汉字 + l10n 实际用字)。 +- `client/windows/flutter/generated_plugins.cmake`(视 Task 1 结论可能改,但属生成文件,优先改 pubspec 依赖根因)。 +- `client/windows/installer/pangolin.iss`(新建)— Inno Setup 脚本。 +- `client/windows/README.md`(改)— 打包/出安装包步骤。 + +--- + +## Task 1: 排查并处理 Windows 端 `jni` FFI 插件 + +**Files:** +- Inspect: `client/windows/flutter/generated_plugins.cmake`、`client/pubspec.yaml`、`client/pubspec.lock`(运行 `flutter pub get` 后生成) +- Modify(视结论): `client/pubspec.yaml` + +**Interfaces:** +- Consumes: 无 +- Produces: 一份明确结论——`jni` 是否需要、是否影响 Windows 构建;若需移除则给出 `pubspec.yaml` 的依赖处置。 + +- [ ] **Step 1: 装依赖并定位 `jni` 来源** + +Run(在 `client/`): +```bash +flutter pub get +flutter pub deps --style=compact | grep -B3 -i "jni" || echo "no jni in dep tree" +``` +Expected: 看到哪个直接依赖传递引入了 `jni`(常见于某些原生插件)。记下引入链。 + +- [ ] **Step 2: 判断 `jni` 是否支持/需要 Windows** + +Run: +```bash +cat client/windows/flutter/generated_plugins.cmake +ls client/.dart_tool/flutter_build/ 2>/dev/null; flutter pub deps --json | grep -i jni +``` +判定规则: +- 若 `jni` 仅被某 Android-only 功能使用、且该功能 Windows 不需要 → 该依赖应在 Windows 不生效;`generated_plugins.cmake` 把它列进 `FLUTTER_FFI_PLUGIN_LIST` 多半是误纳或上游缺 platform 限定。 +- 若属误纳:根因在引入它的依赖。能去掉该依赖(Windows 不需要)则在 `pubspec.yaml` 移除/用条件依赖;不能去掉则记录为「Task 4 构建时若报错再处理」的已知风险。 + +- [ ] **Step 3: 验证 macOS 侧不回归** + +Run(在 `client/`): +```bash +flutter analyze +flutter test +``` +Expected: analyze 无 error;测试全绿(确认动依赖没破坏现有功能)。 + +- [ ] **Step 4: 提交(仅当本任务确有改动)** + +```bash +git add client/pubspec.yaml client/pubspec.lock +git commit -m "build(client/windows): 处理 jni FFI 插件在 Windows 的纳入问题 + +Co-Authored-By: Claude Opus 4.8 " +``` +> 若结论是「无需改动,留待 Task 4 真机构建时按报错处理」,则不提交,仅在计划勾选并把结论记入 Task 4 备注。 + +--- + +## Task 2: 桌面窗口默认/最小尺寸 + +**Files:** +- Modify: `client/lib/main.dart`(`main()` 内 `isDesktop` 分支,`TrayService.instance.init()` 之后) + +**Interfaces:** +- Consumes: `windowManager`(来自 `package:window_manager`,已在 `system_tray.dart` 用,`ensureInitialized()` 已在 `TrayService.init()` 调过) +- Produces: 启动后窗口为 920×640、最小 720×560;低于 720 宽不会掉进 `MobileShell`(`form_factor.dart` 桌面降级阈值 680)。 + +- [ ] **Step 1: 在 main.dart 顶部引入 window_manager** + +`client/lib/main.dart` 增加 import(与现有 import 同区): +```dart +import 'package:window_manager/window_manager.dart'; +``` + +- [ ] **Step 2: 在 TrayService.init() 之后设置窗口尺寸** + +`client/lib/main.dart` 的 `main()`,把: +```dart + await TrayService.instance.init(); +``` +改为: +```dart + await TrayService.instance.init(); + // 桌面窗口尺寸:默认 920×640;最小宽 720(低于 form_factor 的 680 阈值会 + // 掉进 MobileShell,侧栏消失)。init() 内已 windowManager.ensureInitialized()。 + await windowManager.setMinimumSize(const Size(720, 560)); + await windowManager.setSize(const Size(920, 640)); + await windowManager.center(); +``` + +- [ ] **Step 3: 分析 + 测试** + +Run(在 `client/`): +```bash +flutter analyze +flutter test +``` +Expected: analyze 无 error;测试全绿。 + +- [ ] **Step 4: (macOS 可选实跑确认)** + +Run(在 `client/`,仅 macOS 上确认无回归,非 Windows 验证): +```bash +flutter run -d macos +``` +Expected: 窗口约 920×640 居中;拖窄到极限不小于 720 宽。确认后关闭。 + +- [ ] **Step 5: 提交** + +```bash +git add client/lib/main.dart +git commit -m "feat(client/desktop): 设定窗口默认 920x640 / 最小宽 720 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +## Task 3: 字体本地打包(全平台离线) + +**Files:** +- Create: `client/fonts/`(拉丁 ttf 复用 `client/test/fonts/` 的 8 个文件 + `NotoSansSC-subset.ttf`) +- Create: `tools/fonts/make-cjk-subset.sh`、`tools/fonts/common-hanzi.txt` +- Modify: `client/pubspec.yaml`(恢复 `fonts:` 段)、`client/lib/pangolin_theme.dart`(生产 `useBundled=true` + CJK fallback) + +**Interfaces:** +- Consumes: `PangolinFonts`(family 常量 `display`/`sans`/`cjk`/`mono`)、`PangolinFonts.useBundled`(现默认 false,测试置 true) +- Produces: 生产构建用本地 ttf 渲染;中文经 `fontFamilyFallback: [PangolinFonts.cjk]` 用打包的 Noto Sans SC 子集;总字体增量 ≈ 2–3MB。 + +- [ ] **Step 1: 准备拉丁 ttf 到生产目录** + +Run(仓库根): +```bash +mkdir -p client/fonts +cp client/test/fonts/Sora-Regular.ttf client/test/fonts/Sora-SemiBold.ttf client/test/fonts/Sora-Bold.ttf \ + client/test/fonts/Manrope-Regular.ttf client/test/fonts/Manrope-Medium.ttf client/test/fonts/Manrope-SemiBold.ttf client/test/fonts/Manrope-Bold.ttf \ + client/test/fonts/JetBrainsMono-Regular.ttf \ + client/fonts/ +ls -la client/fonts/ +``` +Expected: `client/fonts/` 出现 8 个拉丁 ttf(合计 ≈ 630KB)。 + +- [ ] **Step 2: 准备 CJK 子集字表** + +创建 `tools/fonts/common-hanzi.txt`:内容 = 「通用规范汉字表」常用字 + app 实际用字。先下载常用字表(pinned,若失效改用 `U+4E00–9FFF` 兜底,见 Step 3 备注),再并入 l10n 中文: +```bash +mkdir -p tools/fonts +# 通用规范汉字(一级+二级,~6500字) — 来源 pinned,若 404 见 Step 3 兜底 +curl -fsSL "https://raw.githubusercontent.com/kaienfr/Font/master/learnfiles/%E9%80%9A%E7%94%A8%E8%A7%84%E8%8C%83%E6%B1%89%E5%AD%97%E8%A1%A8.txt" -o tools/fonts/_common_raw.txt || echo "下载失败,改用兜底" +# 抽取仓库内所有中日韩统一表意文字(确保 UI/l10n 实际用字一定在子集里) +grep -rhoE "[\x{4e00}-\x{9fff}]" client/lib | sort -u | tr -d '\n' > tools/fonts/_app_cjk.txt +# 合并去重 → common-hanzi.txt +cat tools/fonts/_common_raw.txt tools/fonts/_app_cjk.txt 2>/dev/null | grep -oE "[\x{4e00}-\x{9fff}]" | sort -u | tr -d '\n' > tools/fonts/common-hanzi.txt +wc -c tools/fonts/common-hanzi.txt +``` +Expected: `common-hanzi.txt` 约 1.8万–2.1万字节(≈ 6000–7000 个三字节汉字)。 + +- [ ] **Step 3: 写子集生成脚本并生成 Noto Sans SC 子集** + +创建 `tools/fonts/make-cjk-subset.sh`: +```bash +#!/usr/bin/env bash +# 生成 Noto Sans SC 子集(变量字体,保留 wght 轴,单文件覆盖全字重)。 +# 依赖: python3 + fonttools(pip install fonttools brotli)。 +# 用法: bash tools/fonts/make-cjk-subset.sh +set -euo pipefail +cd "$(dirname "$0")/../.." # 仓库根 +SRC_DIR="tools/fonts/_src" +OUT="client/fonts/NotoSansSC-subset.ttf" +CHARS="tools/fonts/common-hanzi.txt" +mkdir -p "$SRC_DIR" +# Noto Sans SC 变量字体(pinned tag);若 404 换 google/fonts 最新 raw 路径。 +SRC_TTF="$SRC_DIR/NotoSansSC.ttf" +if [[ ! -f "$SRC_TTF" ]]; then + curl -fsSL "https://github.com/notofonts/noto-cjk/raw/main/Sans/Variable/TTF/NotoSansSC-VF.ttf" -o "$SRC_TTF" +fi +python3 -m fontTools.subset "$SRC_TTF" \ + --text-file="$CHARS" \ + --output-file="$OUT" \ + --layout-features='*' \ + --name-IDs='*' \ + --retain-gids +ls -la "$OUT" +``` +Run: +```bash +python3 -m pip install --quiet fonttools brotli +bash tools/fonts/make-cjk-subset.sh +``` +Expected: 生成 `client/fonts/NotoSansSC-subset.ttf`,体积 ≈ 1.5–2.5MB。 +> 兜底:若 Step 2 字表下载失败,把 `--text-file=...` 换成 `--unicodes="U+4E00-U+9FFF,U+3000-U+303F,U+FF00-U+FFEF"`(会偏大,约 4–5MB,可接受但非首选)。 + +- [ ] **Step 4: 校验总增量 ≤ 3MB** + +Run: +```bash +du -ch client/fonts/*.ttf | tail -1 +``` +Expected: 合计 ≤ 3MB。若超,回 Step 3 收紧字表或仅保留必要字重。 + +- [ ] **Step 5: 恢复 pubspec 的 fonts 段** + +`client/pubspec.yaml`:把第 49–50 行的注释字体段替换为实际声明(保留 assets 段不动): +```yaml + # ── 字体(本地打包,全平台离线;拉丁全量 + Noto Sans SC 7000字子集)── + fonts: + - family: Sora + fonts: + - asset: fonts/Sora-Regular.ttf + - asset: fonts/Sora-SemiBold.ttf + weight: 600 + - asset: fonts/Sora-Bold.ttf + weight: 700 + - family: Manrope + fonts: + - asset: fonts/Manrope-Regular.ttf + - asset: fonts/Manrope-Medium.ttf + weight: 500 + - asset: fonts/Manrope-SemiBold.ttf + weight: 600 + - asset: fonts/Manrope-Bold.ttf + weight: 700 + - family: JetBrains Mono + fonts: + - asset: fonts/JetBrainsMono-Regular.ttf + - family: Noto Sans SC + fonts: + - asset: fonts/NotoSansSC-subset.ttf +``` + +- [ ] **Step 6: 生产开启 useBundled + 接入 CJK fallback** + +`client/lib/pangolin_theme.dart`,把: +```dart + static bool useBundled = false; +``` +改为: +```dart + // 生产默认 true:用本地打包 ttf,不走 google_fonts 运行时拉取(离线可用)。 + static bool useBundled = true; +``` +并在每个返回 `TextStyle` 的字体函数里加 CJK fallback。把 `sora`/`manrope`/`jetBrainsMono` 三个 `useBundled ? TextStyle(...)` 分支统一加 `fontFamilyFallback: const [cjk]`。例如 `sora`: +```dart + static TextStyle sora({double? fontSize, FontWeight? fontWeight, double? height, double? letterSpacing}) => + useBundled + ? TextStyle(fontFamily: display, fontFamilyFallback: const [cjk], fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing) + : GoogleFonts.sora(fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing); +``` +`manrope` 的 `TextStyle(...)` 分支同样加 `fontFamilyFallback: const [cjk],`;`jetBrainsMono` 的 `TextStyle(...)` 分支同样加。`manropeTextTheme` 的 bundled 分支改为: +```dart + static TextTheme manropeTextTheme(TextTheme base) => + useBundled ? base.apply(fontFamily: sans, fontFamilyFallback: const [cjk]) : GoogleFonts.manropeTextTheme(base); +``` + +- [ ] **Step 7: 测试钩子兼容性(确保测试仍可覆盖 useBundled)** + +`client/test/flutter_test_config.dart` 已在测试启动设 `PangolinFonts.useBundled = true` 并注册 `test/fonts/`。生产默认改 true 后测试行为不变;但需确认测试不会因缺少 Noto Sans SC 注册而失败(fallback 字体未注册时 Flutter 用默认字体兜,不抛异常)。 + +Run(在 `client/`): +```bash +flutter analyze +flutter test +``` +Expected: analyze 无 error;测试全绿(含 golden)。若 golden 因字体度量变化失败,确认变化合理后 `flutter test --update-goldens` 并人工 review diff。 + +- [ ] **Step 8: macOS 实跑确认中文渲染** + +Run(`client/`,macOS): +```bash +flutter run -d macos +``` +Expected: 断网或正常网络下,界面中文均正常显示(不再依赖 google_fonts 联网),拉丁字体为 Sora/Manrope。确认后关闭。 + +- [ ] **Step 9: 提交** + +```bash +git add client/fonts/ client/pubspec.yaml client/lib/pangolin_theme.dart tools/fonts/make-cjk-subset.sh tools/fonts/common-hanzi.txt +# 注意:不提交 tools/fonts/_src、_common_raw.txt、_app_cjk.txt 等中间产物 +git status # 确认无中间产物被加入;如有,git restore --staged 之 +git commit -m "feat(client): 字体改本地打包(拉丁全量 + Noto Sans SC 7000字子集) + +生产 useBundled=true,离线可用;中文经 fontFamilyFallback 用打包子集。 +增量约 2-3MB。新增 tools/fonts/make-cjk-subset.sh 可复现子集生成。 + +Co-Authored-By: Claude Opus 4.8 " +``` +> 把 `tools/fonts/_src/`、`tools/fonts/_*.txt` 中间产物加入 `client/.gitignore` 或仓库根 `.gitignore`(`tools/fonts/_*`)。 + +--- + +## Task 4: 真机首次构建(Windows,迭代到 green build) + +> **执行者:用户在 Windows 10/11 机器。Claude 提供指令、据回贴日志改代码。** 不走 TDD 单测循环;本任务的「测试」= `flutter build windows` 成功。 + +**Files:** +- 视报错可能 Modify: `client/windows/CMakeLists.txt`、`client/pubspec.yaml`、插件相关 +- 备注:携带 Task 1 关于 `jni` 的结论 + +**Interfaces:** +- Consumes: Task 1–3 的全部改动(已在 macOS 验证) +- Produces: `client\build\windows\x64\runner\Release\pangolin_vpn.exe` + 同级 `sing-box.exe` + `wintun.dll` + +- [ ] **Step 1: 装 Windows 工具链** + +在 Windows 机器:装 Flutter(含 desktop 支持,`flutter config --enable-windows-desktop`)、Visual Studio 2022(勾「使用 C++ 的桌面开发」负载)、Git。验证: +```powershell +flutter doctor -v +``` +Expected: "Visual Studio - develop Windows apps" 一项为 ✓,无致命缺失。 + +- [ ] **Step 2: 拉取仓库的 feature/windows 分支 + 拉内核二进制** + +```powershell +git fetch; git checkout feature/windows +# Git Bash 或 WSL 跑(脚本是 bash): +bash app/kernel/fetch-desktop-bin.sh windows amd64 +dir app\kernel\dist\desktop\windows-amd64 +``` +Expected: 出现 `sing-box.exe` + `wintun.dll`(SHA256 校验通过)。 +> 国内网络若下载失败:开代理后重试,或手动放置二进制到该目录。 + +- [ ] **Step 3: pub get + 首次 build(管理员终端)** + +用**管理员 PowerShell**: +```powershell +cd client +flutter pub get +flutter build windows +``` +Expected(理想):BUILD SUCCEEDED,产物在 `build\windows\x64\runner\Release\`。 +若失败:把完整报错(尤其 MSVC / CMake / 插件名 / `jni` 相关行)回贴给 Claude。 + +- [ ] **Step 4: 迭代修复(Claude ↔ 用户循环)** + +按报错类别处理(Claude 据实际日志给具体 patch): +- `jni` 相关链接/编译错误 → 按 Task 1 结论移除依赖或在 Windows 排除。 +- `/WX`(warning as error)阻断某插件 → 在 `client/windows/CMakeLists.txt` 对该插件 target 放宽(不要全局关 `/WX`)。 +- 插件缺 Windows 实现 → 评估替换或条件引入。 +- 重复 Step 3 直到 BUILD SUCCEEDED。 + +- [ ] **Step 5: 确认产物完整** + +```powershell +dir build\windows\x64\runner\Release\ +``` +Expected: 含 `pangolin_vpn.exe`、`flutter_windows.dll`、`data\`、以及 `sing-box.exe` + `wintun.dll`(CMake install 段自动拷入;若缺,确认 Task 2/§CMake 的 install 触发)。 + +- [ ] **Step 6: 提交(若有修复改动)** + +```powershell +git add -A +git commit -m "fix(client/windows): 真机构建修复(<具体内容>)`n`nCo-Authored-By: Claude Opus 4.8 " +``` + +--- + +## Task 5: 端到端连通验证(Windows) + +> **执行者:用户在 Windows 机器。** 「测试」= 出口 IP 变为节点公网 IP。 + +**Files:** 无(验证为主,按需回到 Task 4 修复) + +**Interfaces:** +- Consumes: Task 4 的 Release 产物 或 `flutter run -d windows` +- Produces: 确认全局代理生效的证据 + +- [ ] **Step 1: 启动 app(管理员,单次 UAC)** + +```powershell +# 开发态:管理员 PowerShell +cd client +flutter run -d windows --dart-define=PANGOLIN_API_URL=http://<后端IP>:8080 +``` +或直接双击 Release 的 `pangolin_vpn.exe`(会弹一次 UAC)。 +Expected: 弹一次 UAC 后进主界面(`DesktopShell`,原生标题栏 + 侧栏 + 内容区),无布局溢出。 + +- [ ] **Step 2: 记录基线出口 IP** + +新开普通终端: +```powershell +curl https://api.ipify.org +``` +记下当前(未连)出口 IP。 + +- [ ] **Step 3: 登录 → 选节点 → connect** + +UI 内完成登录、在「节点」选一个节点、回「连接」页点连接。Expected: 状态变为已连接,无报错。 + +- [ ] **Step 4: 验证全局代理生效** + +```powershell +curl https://api.ipify.org +``` +Expected: 返回的 IP = 所选节点公网 IP(≠ Step 2 基线)。 +若仍为基线 IP 或超时:查 app 日志面板的 `[stderr]/[stdout]`(sing-box);常见为 `wintun.dll` 缺失(应与 `sing-box.exe` 同目录)、TUN 创建被拒(确认管理员权)。回贴日志给 Claude。 + +- [ ] **Step 5: 记录结果** + +把 Step 2/4 的 IP、连接耗时、任何告警回贴。无代码改动则本任务仅勾选;有修复则随 Task 4 提交。 + +--- + +## Task 6: 功能验证(Windows) + +> **执行者:用户在 Windows 机器。** 逐项验收,问题回贴 Claude 修复。 + +**Files:** 无(按需回修) + +- [ ] **Step 1: 停止 / 重连** + +UI 点断开 → `curl https://api.ipify.org` 应恢复基线 IP;再连接应恢复节点 IP。Expected: 切换干净,无残留。 + +- [ ] **Step 2: kill-switch** + +设置里开 kill-switch → 连接 → 手动结束 `sing-box.exe`(任务管理器)→ Expected: 网络应被切断(strict_route 生效),app 触发自动退避重连。 + +- [ ] **Step 3: 自动重连** + +连接态下杀 `sing-box.exe` → Expected: app 按 1/2/4/8/16/30s 退避自动重连成功(见日志面板)。 + +- [ ] **Step 4: 托盘 / 关闭即最小化 / 单实例** + +点窗口关闭 → Expected: 隐藏到托盘而非退出;托盘左键/「显示主界面」恢复窗口;再启一个实例 → Expected: 唤起已有窗口并自身退出(单实例锁 47654 端口)。 + +- [ ] **Step 5: 开机自启** + +设置里开开机自启 → 重启或登出登入 → Expected: 注册表 Run 项生效,app 随登录启动。 + +- [ ] **Step 6: 退出无残留** + +托盘「退出」 → Expected: 内核先拆隧道再退出;`tasklist | findstr sing-box` 无残留;网络适配器列表无残留 wintun TUN;`curl` 恢复基线 IP。 + +- [ ] **Step 7: 高分屏 DPI** + +在 125%/150% 缩放显示器上 Expected: 界面清晰不糊、不溢出(manifest 已 PerMonitorV2)。 + +- [ ] **Step 8: 汇总验收** + +把 1–7 结果回贴;任何失败项由 Claude 定位修复并回到对应任务提交。 + +--- + +## Task 7: Inno Setup 安装包 + +**Files:** +- Create: `client/windows/installer/pangolin.iss` +- 构建/出包步骤由用户在 Windows 执行 + +**Interfaces:** +- Consumes: Task 4 的 `build\windows\x64\runner\Release\` 全量 +- Produces: `client\windows\installer\Output\pangolin-setup-.exe` + +- [ ] **Step 1: 写 Inno Setup 脚本** + +创建 `client/windows/installer/pangolin.iss`: +```ini +; 穿山甲 Pangolin Windows 安装包 (Inno Setup 6) +; 编译: 用 Inno Setup Compiler 打开本文件或命令行 ISCC.exe pangolin.iss +; 前置: 先 flutter build windows(Release),产物在 ..\..\build\windows\x64\runner\Release +#define MyAppName "穿山甲 Pangolin" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "Pangolin" +#define MyAppExeName "pangolin_vpn.exe" +#define BuildDir "..\..\build\windows\x64\runner\Release" + +[Setup] +AppId={{B2F1B0A0-7A3C-4C2E-9E5A-PANGOLIN0001} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\Pangolin +DefaultGroupName=Pangolin +DisableProgramGroupPage=yes +OutputBaseFilename=pangolin-setup-{#MyAppVersion} +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +; 安装需管理员(写 Program Files + app 本身 requireAdministrator) +PrivilegesRequired=admin +ArchitecturesInstallIn64BitMode=x64compatible + +[Languages] +Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +; 整个 Release 目录(含 pangolin_vpn.exe, flutter_windows.dll, data\, sing-box.exe, wintun.dll, 插件 dll) +Source: "{#BuildDir}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent + +[UninstallDelete] +; 卸载清理运行期数据(kernel 配置等) +Type: filesandordirs; Name: "{localappdata}\Pangolin" +``` +> `MessagesFile` 的 `ChineseSimplified.isl` 需在 Inno Setup 安装目录的 `Languages\` 存在(官方语言包;缺则去掉中文那行或单独下载放入)。 + +- [ ] **Step 2: 编译安装包(Windows)** + +装 Inno Setup 6(含 ChineseSimplified 语言包)。先确保已 `flutter build windows`(Release),然后: +```powershell +& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" client\windows\installer\pangolin.iss +dir client\windows\installer\Output +``` +Expected: 生成 `pangolin-setup-1.0.0.exe`。 + +- [ ] **Step 3: 安装 → 运行 → 卸载验证** + +- 双击 setup → 中文向导 → 装到 `Program Files\Pangolin`,生成开始菜单 + 桌面快捷方式。 +- 从快捷方式启动 → 弹一次 UAC → 可登录/连接/出网(重复 Task 5 Step 4 的 `curl` 检查)。 +- 控制面板卸载 → Expected: app 移除,`%LOCALAPPDATA%\Pangolin` 被清理,无残留 TUN/进程。 + +- [ ] **Step 4: 提交** + +```powershell +git add client/windows/installer/pangolin.iss +git commit -m "build(client/windows): Inno Setup 安装包脚本`n`nCo-Authored-By: Claude Opus 4.8 " +``` + +--- + +## Task 8: 文档收尾 + +**Files:** +- Modify: `client/windows/README.md` + +**Interfaces:** +- Consumes: Task 1–7 的实际结论(jni 处置、字体打包、安装包步骤、已知坑) +- Produces: 可复现的 Windows 构建 + 出安装包文档 + +- [ ] **Step 1: 更新 README** + +`client/windows/README.md` 增补/修订: +- 把「未在真 Windows 验证过」状态更新为「已验证:<日期/版本>」。 +- 新增「字体本地打包」说明(`tools/fonts/make-cjk-subset.sh` 用法)。 +- 新增「出安装包」段(Task 7 的 ISCC 命令 + 前置 flutter build)。 +- 记录 Task 4 实际踩的坑与解法(jni / `/WX` 等)。 + +- [ ] **Step 2: 提交** + +```bash +git add client/windows/README.md +git commit -m "docs(client/windows): 更新构建/打包/已知坑文档 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +## 完成定义(对应 spec §8 验收标准) + +- [ ] `flutter build windows`(Release)真机成功(Task 4)。 +- [ ] 单次 UAC,主界面 `DesktopShell` 原生标题栏,无溢出(Task 5 Step 1)。 +- [ ] 登录→选节点→connect,`curl https://api.ipify.org` = 节点公网 IP(Task 5 Step 4)。 +- [ ] 停止/重连/kill-switch/托盘/开机自启正常,退出无残留 TUN 与孤儿进程(Task 6)。 +- [ ] Inno Setup 安装包可装/运行/卸载干净(Task 7)。 +- [ ] 字体全平台本地打包,离线中文正常,增量 ≤ 3MB(Task 3)。