Files
pangolin/docs/superpowers/plans/2026-06-21-windows-client.md
T
wangjia d7c2e22e6f docs: Windows 客户端实现计划(plan)
8 任务分解 + 完成定义,配套 spec(5c9731f)。subagent-driven 执行依据。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:18:04 +08:00

594 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/11app `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 <noreply@anthropic.com>`
---
## 文件结构(本计划新建/修改)
- `client/lib/pangolin_theme.dart`(改)— `PangolinFonts.useBundled` 生产默认 trueCJK 经 `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 <noreply@anthropic.com>"
```
> 若结论是「无需改动,留待 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 <noreply@anthropic.com>"
```
---
## 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+4E009FFF` 兜底,见 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.52.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 <noreply@anthropic.com>"
```
> 把 `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 13 的全部改动(已在 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 <noreply@anthropic.com>"
```
---
## 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-<ver>.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 <noreply@anthropic.com>"
```
---
## Task 8: 文档收尾
**Files:**
- Modify: `client/windows/README.md`
**Interfaces:**
- Consumes: Task 17 的实际结论(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 <noreply@anthropic.com>"
```
---
## 完成定义(对应 spec §8 验收标准)
- [ ] `flutter build windows`Release)真机成功(Task 4)。
- [ ] 单次 UAC,主界面 `DesktopShell` 原生标题栏,无溢出(Task 5 Step 1)。
- [ ] 登录→选节点→connect`curl https://api.ipify.org` = 节点公网 IPTask 5 Step 4)。
- [ ] 停止/重连/kill-switch/托盘/开机自启正常,退出无残留 TUN 与孤儿进程(Task 6)。
- [ ] Inno Setup 安装包可装/运行/卸载干净(Task 7)。
- [ ] 字体全平台本地打包,离线中文正常,增量 ≤ 3MB(Task 3)。