docs: Windows 客户端实现计划(plan)
8 任务分解 + 完成定义,配套 spec(5c9731f)。subagent-driven 执行依据。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <noreply@anthropic.com>`。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构(本计划新建/修改)
|
||||
|
||||
- `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 <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+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 <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 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 <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 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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完成定义(对应 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)。
|
||||
Reference in New Issue
Block a user