A 补 libbox / B Xcode 接线 / C Provider 重写对齐 macOS / D 真机连通+内存双闸 / E iPad 横竖屏适配 / F TestFlight 分发。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 KiB
iOS + iPad 支持 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: 把 Pangolin Flutter 客户端的 iOS 隧道从「能编译的 M3 PoC 桩」推进到「iPhone/iPad 真机真正连通、扛得住 NE 内存红线、可推 TestFlight」,iPad 共用同一 iOS target 并完成布局+横屏适配。
Architecture: iOS PacketTunnel 扩展按 macOS 已验证的 libbox 集成方式重写(LibboxNewCommandServer + LibboxPlatformInterfaceProtocol + 真 utun fd),删除 PoC 的 socketpair 手工桥接;主 App 侧 VpnManager 已基本可用,仅需对齐配置 key。iPad 复用已有 form_factor.dart(宽≥600 自动走 TabletShell)+ tablet_shell.dart,补横竖屏 orientation 与大屏校验。分发走 App Store Connect / TestFlight。
Tech Stack: Swift / NetworkExtension / libbox(sing-box v1.13.13 via sagernet gomobile fork) / Flutter(Riverpod) / Xcode / App Store Connect。
Global Constraints
每个任务的要求都隐含包含本节。值均逐字取自设计稿与代码。
- sing-box 版本:
v1.13.13(与节点对齐,SINGBOX_VERSION默认值)。 - TeamID:
BYL4KQHMTN。 - Bundle ID:主 App
com.pangolin.pangolinVpn;扩展com.pangolin.pangolinVpn.PacketTunnel。 - App Group(iOS 格式):
group.com.pangolin.pangolinVpn(注意:非 macOS 的<TeamID>.前缀)。 - 扩展链接铁律:
Libbox.xcframework只 Link 不 Embed(静态库);PacketTunnel target 设OTHER_LDFLAGS = ""(切断 CocoaPods 继承标志,否则会把flutter_secure_storage链进扩展);otool -L扩展二进制应零外部@rpath依赖。 - libbox 集成铁律:
startTunnel在后台队列启 libbox(防三方死锁);startOrReloadService传非空LibboxOverrideOptions()(传 nil → SIGSEGV);startDefaultInterfaceMonitor阻塞到首个 path 更新再返回(否则no available network interface)。 - 配置来源:sing-box 配置由服务端
BuildClientConfig渲染、原样下发,客户端不拼;DNS 劫持首条规则随配置下发(服务端已处理,iOS 无需改)。 - 红线词:用户可见文案不得出现 VPN / 翻墙 / 科学上网 等词;用「加速」口径。
- 内存红线:iOS NE 进程 ~50MB(旧设备 ~15MB),jetsam 超限杀进程——头号风险。
- 设计 token 单源:颜色/间距走 token,禁硬编码 hex;
pangolin_tokens.gen.dart勿手改。 - 平台:iOS 12+,设备族
"1,2"(iPhone+iPad,已设)。 - Frameworks 不入 git:
Libbox.xcframework本地构建,加.gitignore排除。
File Structure
| 文件 | 责任 | 动作 |
|---|---|---|
client/ios/Frameworks/Libbox.xcframework |
iOS+sim libbox 静态框架 | 构建产物放入(不入 git) |
client/ios/.gitignore |
排除框架产物 | 追加一行 |
client/ios/Runner.xcodeproj/project.pbxproj |
扩展链接 Libbox+系统框架、Team、OTHER_LDFLAGS | Xcode 改 |
client/ios/PacketTunnel/PacketTunnelProvider.swift |
NE 扩展:libbox 集成(重写对齐 macOS) | 重写 |
client/ios/Runner/VpnManager.swift |
主 App 侧隧道控制 | 微调(配置 key 已是 config,核对即可) |
client/ios/Runner/Info.plist |
iPad orientation | 改 |
client/test/responsive/form_factor_test.dart |
断点判定测试 | 新建 |
client/lib/Runner/Info.plist(导出合规) |
ITSAppUsesNonExemptEncryption |
改 |
参照(只读,不改):client/macos/PacketTunnel/PacketTunnelProvider.swift(金标准)、client/lib/core/responsive/form_factor.dart、client/lib/shell/tablet_shell.dart。
Phase A — libbox iOS 框架(解阻塞)
Task A1: 构建含 iOS 切片的 Libbox.xcframework 并放入 iOS 工程
Files:
- Create:
client/ios/Frameworks/Libbox.xcframework(构建产物拷入) - Modify:
client/ios/.gitignore(追加排除)
Interfaces:
-
Produces:
client/ios/Frameworks/Libbox.xcframework,含ios-arm64(真机) 与ios-arm64_x86_64-simulator(模拟器) slice,供 Task B1 链接。 -
Step 1: 构建全 Apple xcframework
Run(需 macOS + Xcode CLT + Go;耗时数分钟到十几分钟):
bash /Users/wangjia/code/pangolin/scripts/build-libbox.sh apple
Expected: 末尾打印 ✅ 产物: /tmp/pangolin-libbox/sing-box/Libbox.xcframework
- Step 2: 验证含 iOS 切片
Run:
ls -1 /tmp/pangolin-libbox/sing-box/Libbox.xcframework/
Expected: 列出含 ios-arm64、ios-arm64_x86_64-simulator、macos-arm64_x86_64(可能还有 tvos)等目录。
- Step 3: 拷入 iOS 工程
Run:
rm -rf /Users/wangjia/code/pangolin/client/ios/Frameworks/Libbox.xcframework
mkdir -p /Users/wangjia/code/pangolin/client/ios/Frameworks
cp -R /tmp/pangolin-libbox/sing-box/Libbox.xcframework /Users/wangjia/code/pangolin/client/ios/Frameworks/
(保留 macOS 既有 client/macos/Frameworks/Libbox.xcframework 不动,避免扰动已验证的 macOS 构建。)
- Step 4: gitignore 排除框架
向 client/ios/.gitignore 追加一行(仿 macOS 的 client/macos/.gitignore:10):
**/Frameworks/Libbox.xcframework/
- Step 5: 验证已忽略
Run:
cd /Users/wangjia/code/pangolin && git status --porcelain client/ios/Frameworks/
Expected: 无输出(框架未被 git 跟踪)。
- Step 6: Commit
cd /Users/wangjia/code/pangolin
git add client/ios/.gitignore
git commit -m "build(client/ios): 引入 iOS libbox 框架 + gitignore 排除产物"
Phase B — Xcode 接线与签名
Task B1: 把 Libbox + 系统框架链入 PacketTunnel 扩展(Link-only),设 Team 与 OTHER_LDFLAGS
Files:
- Modify:
client/ios/Runner.xcodeproj/project.pbxproj(经 Xcode GUI 改,避免手改 pbxproj 出错)
Interfaces:
- Consumes:
client/ios/Frameworks/Libbox.xcframework(Task A1) - Produces: 可编译的 PacketTunnel 扩展 target,零外部
@rpath依赖。
照 macOS 扩展 target 的链接清单(macos/Runner.xcodeproj 的 PacketTunnel Frameworks build phase:Libbox + NetworkExtension + libresolv + SystemConfiguration),iOS 把 AppKit 去掉。
- Step 1: 打开工程
Run:
open /Users/wangjia/code/pangolin/client/ios/Runner.xcworkspace
- Step 2: 给 PacketTunnel target 链接框架(Link-only)
在 Xcode 选 PacketTunnel target → General → Frameworks and Libraries,添加并将 Embed 全设为 Do Not Embed:
-
Frameworks/Libbox.xcframework(Add Other… 选 Task A1 拷入的) -
NetworkExtension.framework -
SystemConfiguration.framework -
libresolv.tbd -
Step 3: 设 Build Settings
PacketTunnel target → Build Settings:
Other Linker Flags(OTHER_LDFLAGS)→ 置为空(删除任何继承值;防 CocoaPods 标志把flutter_secure_storage链进扩展)。Framework Search Paths含$(PROJECT_DIR)/Frameworks。Development Team(DEVELOPMENT_TEAM)→BYL4KQHMTN。
Runner(主 App)target → Build Settings:Development Team → BYL4KQHMTN。
- Step 4: 确认 Signing & Capabilities
Runner 与 PacketTunnel 两 target 的 Signing & Capabilities 均含:
-
App Groups →
group.com.pangolin.pangolinVpn -
PacketTunnel:Network Extensions → Packet Tunnel Provider(entitlements 文件已声明,确认未报红)。 -
Step 5: 编译验证
Run:
cd /Users/wangjia/code/pangolin/client && flutter build ios --debug --no-codesign
Expected: 构建成功(Swift 此时尚未引用 libbox,仅验证链接配置不破坏构建)。
- Step 6: 验证扩展零外部 @rpath 依赖
Run:
otool -L /Users/wangjia/code/pangolin/client/build/ios/iphoneos/Runner.app/PlugIns/PacketTunnel.appex/PacketTunnel
Expected: 仅 /usr/lib/* 与 /System/Library/Frameworks/*;无 @rpath/...(尤其无 flutter_secure_storage、无 @rpath/Libbox——静态库不应出现)。
- Step 7: 验证 Team 已写入
Run:
grep -c "DEVELOPMENT_TEAM = BYL4KQHMTN" /Users/wangjia/code/pangolin/client/ios/Runner.xcodeproj/project.pbxproj
Expected: ≥ 2(主 App + 扩展)。
- Step 8: Commit
cd /Users/wangjia/code/pangolin
git add client/ios/Runner.xcodeproj/project.pbxproj
git commit -m "build(client/ios): PacketTunnel 链接 libbox+系统框架(Link-only) + 设 Team/OTHER_LDFLAGS"
Phase C — iOS Provider 重写(对齐 macOS 验证过的 libbox API)
Task C1: 用 macOS CommandServer+PlatformInterface 方式重写 iOS PacketTunnelProvider
Files:
- Modify(整体重写):
client/ios/PacketTunnel/PacketTunnelProvider.swift - Reference(只读):
client/macos/PacketTunnel/PacketTunnelProvider.swift - Keep:
client/ios/PacketTunnel/MemoryMonitor.swift(复用)
Interfaces:
- Consumes:
LibboxNewCommandServer(_:_:_:)、LibboxPlatformInterfaceProtocol、LibboxOverrideOptions、LibboxSetup(真实框架 API,与 macOS 一致);VpnManager经options["config"]传配置、经 App Group keylastConfigJson缓存。 - Produces:
class PacketTunnelProvider、final class PangolinPlatformInterface、final class InterfaceIterator、func simpleError(_:)。
背景:现有 iOS 文件用的是不存在于真实框架的旧 API(LibboxNewService / LibboxPlatformInterface / openTun(_:)->Int32 + socketpair 桥接),链入真实 Libbox 后编不过。本任务用 macOS 金标准结构整体替换,只改三处:App Group 前缀、log subsystem、配置 key;并保留 MemoryMonitor 接入与精简 IPC。
- Step 1: 以 macOS 文件为基底重写
把 client/macos/PacketTunnel/PacketTunnelProvider.swift 全文作为基底写入 client/ios/PacketTunnel/PacketTunnelProvider.swift,并施加以下精确改动:
(a) 顶部常量:
// 改这两行(原 macOS 值 → iOS 值)
private let log = Logger(subsystem: "com.pangolin.pangolinVpn.PacketTunnel", category: "provider")
// iOS App Group 用 group. 前缀(非 macOS 的 <TeamID>. 前缀)。
private let appGroup = "group.com.pangolin.pangolinVpn"
(b) 在 final class PacketTunnelProvider 顶部加内存监控成员:
final class PacketTunnelProvider: NEPacketTunnelProvider {
private var commandServer: LibboxCommandServer?
private var platform: PangolinPlatformInterface?
private let memoryMonitor = MemoryMonitor() // ← 新增(iOS 内存红线打点)
(c) startTunnel 成功处(completionHandler(nil) 之前)启动监控——在 self.commandServer = server 之后加:
self.commandServer = server
self.memoryMonitor.start() // ← 新增
log.info("startTunnel: service started")
completionHandler(nil)
(d) stopTunnel 开头停监控并打峰值——在 try? commandServer?.closeService() 之前加:
log.info("stopTunnel reason=\(reason.rawValue)")
memoryMonitor.stop() // ← 新增
log.info("memory peak: \(self.memoryMonitor.summaryString(), privacy: .public)") // ← 新增
try? commandServer?.closeService()
(e) resolveConfig 的 options key 改为 config(匹配 VpnManager.start)并补 App Group 缓存回退:
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
if let inline = options?["config"] as? String, !inline.isEmpty {
return inline
}
// App Group UserDefaults 缓存(VpnManager 写入 key=lastConfigJson,崩溃恢复用)
if let shared = UserDefaults(suiteName: appGroup),
let cached = shared.string(forKey: "lastConfigJson"), !cached.isEmpty {
return cached
}
guard let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw simpleError("no app group container")
}
let content = (try? String(contentsOf: base.appendingPathComponent("config.json"),
encoding: .utf8)) ?? ""
guard !content.isEmpty else { throw simpleError("empty sing-box config") }
return content
}
(f) handleAppMessage 替换为支持 getStatus / getMemory 的 IPC(对齐 VpnManager.sendProviderMessage;删 PoC 的 socketpair/selectOutbound 等无对应内核 API 的分支):
override func handleAppMessage(_ messageData: Data,
completionHandler: ((Data?) -> Void)?) {
guard let req = try? JSONSerialization.jsonObject(with: messageData) as? [String: Any],
let action = req["action"] as? String else { completionHandler?(nil); return }
var resp: [String: Any] = [:]
switch action {
case "getStatus":
resp["status"] = (commandServer != nil) ? "on" : "error"
case "getMemory":
resp["memory"] = memoryMonitor.summaryString()
resp["peakRssBytes"] = memoryMonitor.peakResidentBytes
default:
resp["error"] = "unknown action: \(action)"
}
completionHandler?((try? JSONSerialization.data(withJSONObject: resp)) ?? nil)
}
(g) PangolinPlatformInterface、InterfaceIterator、simpleError 三部分原样照搬 macOS(iOS 全部 API 可用:tunFd() 的 utun 扫描、IP_BOUND_IF/IPV6_BOUND_IF、getifaddrs 在 iOS 同样有效,与 sing-box-for-apple 一致)。import 段照搬 macOS(Darwin/Foundation/Libbox/Network/NetworkExtension/os,无 AppKit)。
- Step 2: 编译验证(链入真实 libbox)
Run:
cd /Users/wangjia/code/pangolin/client && flutter build ios --debug --no-codesign
Expected: 构建成功;不再出现 LibboxNewService / LibboxPlatformInterface 等旧符号未定义错误。
- Step 3: 静态核对铁律
Run:
grep -nE "DispatchQueue.global|LibboxOverrideOptions\(\)|firstUpdate.wait|options\?\[\"config\"\]" \
/Users/wangjia/code/pangolin/client/ios/PacketTunnel/PacketTunnelProvider.swift
Expected: 四项均命中(后台队列、非空 OverrideOptions、阻塞 interface monitor、config key)。
- Step 4: Commit
cd /Users/wangjia/code/pangolin
git add client/ios/PacketTunnel/PacketTunnelProvider.swift
git commit -m "feat(client/ios): Provider 对齐 macOS libbox 集成(CommandServer+utun fd)+ 内存监控"
Phase D — 真机连通与内存验收
Task D1: iPhone 真机连通 + 内存基线
Files: 无代码改动(验证任务;如需签名修复改 project.pbxproj)
Interfaces:
-
Consumes: Task C1 的 Provider;
VpnManager(已实现 load/save/start/stop)。 -
Step 1: 真机安装运行
USB 接入开发者已登记的 iPhone(设备 UDID 已在 App ID 描述文件中):
cd /Users/wangjia/code/pangolin/client && flutter run --release -d <iphone-device-id>
Expected: app 在真机启动。
- Step 2: 触发连接并看日志
app 内点连接。同时在 macOS 终端开 device console 过滤扩展日志:
log stream --predicate 'subsystem == "com.pangolin.pangolinVpn.PacketTunnel"' --info
Expected: 依次出现 startTunnel → startTunnel: service started;无 no available network interface、无 SIGSEGV 崩溃。
- Step 3: 验证真出网
真机连接态下,Safari 打开一个境外站点(如 example.com 以外的被墙站点)。 Expected: 能打开(DNS 解析不失败 + 数据走节点)。
- Step 4: 读内存峰值
连接保持 10 分钟后,app 内触发 getMemory IPC(或读 device console 的 memory peak 行)。
Expected: peakRssBytes < 设备 jetsam 上限(新设备目标 < 50MB;旧设备 < 15MB);进程未被系统杀。
- Step 5: 记录结论
把内存实测峰值与设备型号记到 docs/ios-ipad-support-design.html 的「头号风险」卡片下(追加一行实测数据)。若超限 → 执行 Task D2;若达标 → 跳过 D2。
- Step 6: Commit(如有文档更新)
cd /Users/wangjia/code/pangolin
git add docs/ios-ipad-support-design.html
git commit -m "docs(client/ios): 记录 iPhone 真机连通 + NE 内存实测峰值"
Task D2:(条件触发)内存超限 → 本地 .srs 预取瘦身
仅当 Task D1 Step 4 内存超限时执行。 头号风险缓解的第二道闸:避免启动期把远程 geoip-cn.srs/geosite-cn.srs 下载进 NE 进程内存。
Files:
- Modify:
server/internal/httpapi/clientconfig.go(rule_set 由type:"remote"改为本地type:"local",path:...,仅对 iOS 客户端或全平台) - Create:
client/ios/Runner/Resources/geoip-cn.srs、geosite-cn.srs(随 app 打包) - Modify:
client/ios/Runner/AppDelegate.swift(启动时把 bundle 内 .srs 拷到 App Group 容器)
Interfaces:
-
Consumes: 控制面
/v1/rules/*.srs(构建期预取一次)。 -
Produces: App Group 容器内
geoip-cn.srs/geosite-cn.srs本地文件;下发配置引用其本地路径。 -
Step 1: 预取 .srs 打入 app bundle
Run(从节点拉一次,提交进仓库):
curl -fsSL http://<节点公网>:8080/v1/rules/geoip-cn.srs -o /Users/wangjia/code/pangolin/client/ios/Runner/Resources/geoip-cn.srs
curl -fsSL http://<节点公网>:8080/v1/rules/geosite-cn.srs -o /Users/wangjia/code/pangolin/client/ios/Runner/Resources/geosite-cn.srs
Expected: 两个 .srs 文件落地,非空。在 Xcode 把 Resources/ 加入 Runner target 的 Copy Bundle Resources。
- Step 2: 启动期拷到 App Group 容器
在 AppDelegate.swift 的 application(_:didFinishLaunchingWithOptions:) 内,加把 bundle 内 .srs 拷到 group.com.pangolin.pangolinVpn 容器的逻辑(若不存在再拷):
let group = "group.com.pangolin.pangolinVpn"
if let base = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group) {
for name in ["geoip-cn", "geosite-cn"] {
let dst = base.appendingPathComponent("\(name).srs")
if !FileManager.default.fileExists(atPath: dst.path),
let src = Bundle.main.url(forResource: name, withExtension: "srs") {
try? FileManager.default.copyItem(at: src, to: dst)
}
}
}
- Step 3: 服务端配置引用本地路径
在 server/internal/httpapi/clientconfig.go 的 split_cn 分支,把 geoip-cn/geosite-cn 两条 rule_set 由 type:"remote"(带 url/download_detour)改为 type:"local" + path 指向 App Group 容器内的 .srs(路径由客户端启动期已知,作为渲染参数传入或约定固定相对路径)。
Expected: 下发配置中两条 rule_set 为 "type":"local"。
- Step 4: Go 测试
Run:
cd /Users/wangjia/code/pangolin/server && go test ./internal/httpapi/...
Expected: PASS(含 BuildClientConfig 渲染测试,确认 local 分支正确)。
- Step 5: 真机复测内存
重复 Task D1 Step 1–4。
Expected: peakRssBytes 降到设备 jetsam 上限以下。
- Step 6: Commit
cd /Users/wangjia/code/pangolin
git add server/internal/httpapi/clientconfig.go client/ios/Runner/AppDelegate.swift
git commit -m "perf(client/ios): 本地 .srs 预取替代远程下载,压低 NE 内存峰值"
Phase E — iPad UI 适配(布局 + 横屏)
Task E1: form_factor 断点判定测试(锁定 iPad → tablet)
Files:
- Create:
client/test/responsive/form_factor_test.dart - Reference:
client/lib/core/responsive/form_factor.dart
Interfaces:
-
Consumes:
FormFactorContext.formFactor(已有:非桌面平台宽≥600 →.tablet)。 -
Step 1: 写失败测试
// client/test/responsive/form_factor_test.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pangolin_vpn/core/responsive/form_factor.dart';
Future<FormFactor> _ffAt(WidgetTester tester, Size size) async {
late FormFactor ff;
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(MediaQuery(
data: MediaQueryData.fromView(tester.view),
child: Builder(builder: (c) { ff = c.formFactor; return const SizedBox(); }),
));
return ff;
}
void main() {
testWidgets('iPad 竖屏(768) → tablet', (t) async {
expect(await _ffAt(t, const Size(768, 1024)), FormFactor.tablet);
});
testWidgets('iPad 横屏(1024) → tablet', (t) async {
expect(await _ffAt(t, const Size(1024, 768)), FormFactor.tablet);
});
testWidgets('iPhone(390) → mobile', (t) async {
expect(await _ffAt(t, const Size(390, 844)), FormFactor.mobile);
});
}
- Step 2: 跑测试看通过/失败
Run:
cd /Users/wangjia/code/pangolin/client && flutter test test/responsive/form_factor_test.dart
Expected: 3 测试全 PASS(现有 form_factor.dart 逻辑已满足;本测试锁定行为防回归)。若 iPhone case 因默认平台非 iOS 而判成 tablet,则说明测试环境 defaultTargetPlatform 默认 android(非桌面),390<600 仍 → mobile,应 PASS。
- Step 3: Commit
cd /Users/wangjia/code/pangolin
git add client/test/responsive/form_factor_test.dart
git commit -m "test(client): 锁定 form_factor 断点(iPad→tablet, iPhone→mobile)"
Task E2: 开 iPad 横竖屏 orientation + 大屏校验
Files:
- Modify:
client/ios/Runner/Info.plist - Reference:
client/lib/shell/tablet_shell.dart、design/ui_kits/tablet/
Interfaces:
-
Consumes:
TabletShell(已实现侧栏+内容区双栏)。 -
Step 1: 开 iPad 全向
在 client/ios/Runner/Info.plist 设 iPad 支持全部方向(UISupportedInterfaceOrientations~ipad):
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
- Step 2: 模拟器多尺寸目测
Run(iPad 模拟器跑起来):
cd /Users/wangjia/code/pangolin/client && flutter run -d "iPad (10th generation)"
逐屏(connect / nodes / stats / account)竖屏与横屏各看一遍。
Expected: 走 TabletShell(侧栏+双栏内容),无溢出(无黄黑条)、不空旷、不拉伸;对照 design/ui_kits/tablet/tabapp.jsx 原型布局一致。
- Step 3: analyze 通过
Run:
cd /Users/wangjia/code/pangolin/client && flutter analyze
Expected: No issues。
- Step 4: Commit
cd /Users/wangjia/code/pangolin
git add client/ios/Runner/Info.plist client/lib/shell/tablet_shell.dart
git commit -m "feat(client/ios): 开 iPad 全向 orientation + 平板布局校验"
Task E3: iPad 真机连通验收
- Step 1: iPad 真机跑连接
Run:
cd /Users/wangjia/code/pangolin/client && flutter run --release -d <ipad-device-id>
连接 → 验出网(同 Task D1 Step 2–3);竖横屏切换布局正常。 Expected: iPad 连通且布局达标;内存峰值同样 < 上限。
Phase F — 分发到 TestFlight
Task F1:(用户操作)开发者后台注册 — App ID / NE 能力 / App Group / 描述文件
这是用户在 developer.apple.com / App Store Connect 手点的清单,Claude 无法代点。
-
Step 1: 注册 App ID(两个)
com.pangolin.pangolinVpn(主 App)— 勾 App Groups、Network Extensions。com.pangolin.pangolinVpn.PacketTunnel(扩展)— 勾 App Groups、Network Extensions → Packet Tunnel Provider。
-
Step 2: 建 App Group
group.com.pangolin.pangolinVpn,关联上面两个 App ID。
-
Step 3: 描述文件
- 为两个 App ID 各建 App Store 分发描述文件(或用自动签名);登记测试设备 UDID(开发阶段)。
-
Step 4: App Store Connect 建 app 记录
- 新建 app,Bundle ID 选
com.pangolin.pangolinVpn,填基本信息。
- 新建 app,Bundle ID 选
Task F2: 导出合规 + Archive + 上传 TestFlight
Files:
-
Modify:
client/ios/Runner/Info.plist(导出合规键) -
Step 1: 标注导出合规
在 client/ios/Runner/Info.plist 加(避免每次上传被问加密合规;如使用标准加密走豁免,按实际填):
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
- Step 2: 递增构建号
确认 client/pubspec.yaml 的 build number(+N)已递增(驱动 FLUTTER_BUILD_NUMBER → 扩展与主 App 的 CFBundleVersion)。
- Step 3: Archive
Run:
cd /Users/wangjia/code/pangolin/client && flutter build ipa --release
Expected: 产出 build/ios/ipa/*.ipa,签名成功。
- Step 4: 上传 TestFlight
Run(或用 Xcode Organizer / Transporter):
xcrun altool --upload-app -f /Users/wangjia/code/pangolin/client/build/ios/ipa/*.ipa \
-t ios --apiKey <KEY_ID> --apiIssuer <ISSUER_ID>
Expected: 上传成功;App Store Connect → TestFlight 出现构建(处理后可加外部测试者)。
- Step 5: Commit
cd /Users/wangjia/code/pangolin
git add client/ios/Runner/Info.plist client/pubspec.yaml
git commit -m "release(client/ios): 导出合规标注 + 构建号递增,准备 TestFlight"
验收标准(Definition of Done)
- ✅
Libbox.xcframework含 iOS 切片;扩展otool -L零外部@rpath(A1 Step2 / B1 Step6)。 - ✅ iPhone 真机:连接 → sing-box 起来 → 打开境外站点(D1 Step2–3)。
- ✅ iPad 真机:连通 + 竖/横屏布局对照原型无拉伸/空旷(E2、E3)。
- ✅
MemoryMonitor真机峰值 < 设备 jetsam 上限,10 分钟不被杀(D1 Step4,必要时 D2)。 - ✅ 成功 Archive 并上传 TestFlight,外部测试者可装可连(F2)。
Self-Review 记录
- Spec 覆盖:设计稿 5 工作线 → Phase A(①libbox) / B+C(②Provider) / C(③App Group IPC,并入 VpnManager 既有实现) / E(④iPad) / F(⑤分发);头号风险内存闸 → D1+D2 双闸。全覆盖。
- 占位符:D2 的
<节点公网>、F 的<KEY_ID>/<device-id>为运行期真实值占位(环境相关,不可硬编码),非逻辑占位;其余步骤均含可执行命令与期望输出。 - 类型一致:
PangolinPlatformInterface/InterfaceIterator/simpleError/commandServer/memoryMonitor命名跨任务一致;IPC keygetStatus/getMemory与VpnManager.sendProviderMessage一致;配置 key 统一为config。