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

25 KiB
Raw Blame History

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.0Dart ^3.5.0(来自 client/pubspec.yaml)。
  • 目标平台 Windows 10/11app requireAdministratormanifest 已设,勿降权)。
  • 颜色/间距等设计 token 来自 design/colors_and_type.css 单源,禁止硬编码 hex;字体 family 名沿用 PangolinFontsSora / 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.cmakeclient/pubspec.yamlclient/pubspec.lock(运行 flutter pub get 后生成)
  • Modify(视结论): client/pubspec.yaml

Interfaces:

  • Consumes: 无

  • Produces: 一份明确结论——jni 是否需要、是否影响 Windows 构建;若需移除则给出 pubspec.yaml 的依赖处置。

  • Step 1: 装依赖并定位 jni 来源

Run(在 client/):

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

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/):

flutter analyze
flutter test

Expected: analyze 无 error;测试全绿(确认动依赖没破坏现有功能)。

  • Step 4: 提交(仅当本任务确有改动)
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.dartmain()isDesktop 分支,TrayService.instance.init() 之后)

Interfaces:

  • Consumes: windowManager(来自 package:window_manager,已在 system_tray.dart 用,ensureInitialized() 已在 TrayService.init() 调过)

  • Produces: 启动后窗口为 920×640、最小 720×560;低于 720 宽不会掉进 MobileShellform_factor.dart 桌面降级阈值 680)。

  • Step 1: 在 main.dart 顶部引入 window_manager

client/lib/main.dart 增加 import(与现有 import 同区):

import 'package:window_manager/window_manager.dart';
  • Step 2: 在 TrayService.init() 之后设置窗口尺寸

client/lib/main.dartmain(),把:

    await TrayService.instance.init();

改为:

    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/):

flutter analyze
flutter test

Expected: analyze 无 error;测试全绿。

  • Step 4: macOS 可选实跑确认)

Run(在 client/,仅 macOS 上确认无回归,非 Windows 验证):

flutter run -d macos

Expected: 窗口约 920×640 居中;拖窄到极限不小于 720 宽。确认后关闭。

  • Step 5: 提交
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.shtools/fonts/common-hanzi.txt
  • Modify: client/pubspec.yaml(恢复 fonts: 段)、client/lib/pangolin_theme.dart(生产 useBundled=true + CJK fallback

Interfaces:

  • Consumes: PangolinFontsfamily 常量 display/sans/cjk/mono)、PangolinFonts.useBundled(现默认 false,测试置 true)

  • Produces: 生产构建用本地 ttf 渲染;中文经 fontFamilyFallback: [PangolinFonts.cjk] 用打包的 Noto Sans SC 子集;总字体增量 ≈ 2–3MB。

  • Step 1: 准备拉丁 ttf 到生产目录

Run(仓库根):

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 中文:

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

#!/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

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

du -ch client/fonts/*.ttf | tail -1

Expected: 合计 ≤ 3MB。若超,回 Step 3 收紧字表或仅保留必要字重。

  • Step 5: 恢复 pubspec 的 fonts 段

client/pubspec.yaml:把第 49–50 行的注释字体段替换为实际声明(保留 assets 段不动):

  # ── 字体(本地打包,全平台离线;拉丁全量 + 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,把:

  static bool useBundled = false;

改为:

  // 生产默认 true:用本地打包 ttf,不走 google_fonts 运行时拉取(离线可用)。
  static bool useBundled = true;

并在每个返回 TextStyle 的字体函数里加 CJK fallback。把 sora/manrope/jetBrainsMono 三个 useBundled ? TextStyle(...) 分支统一加 fontFamilyFallback: const [cjk]。例如 sora

  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);

manropeTextStyle(...) 分支同样加 fontFamilyFallback: const [cjk],jetBrainsMonoTextStyle(...) 分支同样加。manropeTextTheme 的 bundled 分支改为:

  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/):

flutter analyze
flutter test

Expected: analyze 无 error;测试全绿(含 golden)。若 golden 因字体度量变化失败,确认变化合理后 flutter test --update-goldens 并人工 review diff。

  • Step 8: macOS 实跑确认中文渲染

Runclient/macOS):

flutter run -d macos

Expected: 断网或正常网络下,界面中文均正常显示(不再依赖 google_fonts 联网),拉丁字体为 Sora/Manrope。确认后关闭。

  • Step 9: 提交
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 或仓库根 .gitignoretools/fonts/_*)。


Task 4: 真机首次构建(Windows,迭代到 green build

行者:用户在 Windows 10/11 机器。Claude 提供指令、据回贴日志改代码。 不走 TDD 单测循环;本任务的「测试」= flutter build windows 成功。

Files:

  • 视报错可能 Modify: client/windows/CMakeLists.txtclient/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。验证:

flutter doctor -v

Expected: "Visual Studio - develop Windows apps" 一项为 ✓,无致命缺失。

  • Step 2: 拉取仓库的 feature/windows 分支 + 拉内核二进制
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.dllSHA256 校验通过)。

国内网络若下载失败:开代理后重试,或手动放置二进制到该目录。

  • Step 3: pub get + 首次 build(管理员终端)

管理员 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 排除。

  • /WXwarning as error)阻断某插件 → 在 client/windows/CMakeLists.txt 对该插件 target 放宽(不要全局关 /WX)。

  • 插件缺 Windows 实现 → 评估替换或条件引入。

  • 重复 Step 3 直到 BUILD SUCCEEDED。

  • Step 5: 确认产物完整

dir build\windows\x64\runner\Release\

Expected: 含 pangolin_vpn.exeflutter_windows.dlldata\、以及 sing-box.exe + wintun.dllCMake install 段自动拷入;若缺,确认 Task 2/§CMake 的 install 触发)。

  • Step 6: 提交(若有修复改动)
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
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

新开普通终端:

curl https://api.ipify.org

记下当前(未连)出口 IP。

  • Step 3: 登录 → 选节点 → connect

UI 内完成登录、在「节点」选一个节点、回「连接」页点连接。Expected: 状态变为已连接,无报错。

  • Step 4: 验证全局代理生效
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

; 穿山甲 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"

MessagesFileChineseSimplified.isl 需在 Inno Setup 安装目录的 Languages\ 存在(官方语言包;缺则去掉中文那行或单独下载放入)。

  • Step 2: 编译安装包(Windows

装 Inno Setup 6(含 ChineseSimplified 语言包)。先确保已 flutter build windowsRelease),然后:

& "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: 提交

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: 提交

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 windowsRelease)真机成功(Task 4)。
  • 单次 UAC,主界面 DesktopShell 原生标题栏,无溢出(Task 5 Step 1)。
  • 登录→选节点→connectcurl https://api.ipify.org = 节点公网 IPTask 5 Step 4)。
  • 停止/重连/kill-switch/托盘/开机自启正常,退出无残留 TUN 与孤儿进程(Task 6)。
  • Inno Setup 安装包可装/运行/卸载干净(Task 7)。
  • 字体全平台本地打包,离线中文正常,增量 ≤ 3MB(Task 3)。