# 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)。