- stats-overhaul 实现计划(md 真相源 + html 阅读版) - 功能×测试覆盖清单(feature-test-coverage-checklist.html) - docs/index.html 登记上述文档 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
Pangolin 统计体系整改 Implementation Plan(todo #5)
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: 四端实时统计真生效(上传/下载/延迟/累计流量)+ 服务端按设备归因并按 GB 账户综合卡控 + 统计页按新设计(上聚合·下分设备)像素级还原。
Architecture: Flutter UI 全平台共享。实时指标经冻结的 pangolin/vpn/stats channel 由各原生侧(libbox CommandClient / Clash API)生产;累计用量由服务端 /v1/usage、/v1/me 提供。服务端控制面(Go,mysql/sqlite 双驱动)经 agent gRPC ReportUsage 按 dp_uuid 记账;本计划把记账粒度从「账户」细化到「设备」(每设备独立 dp_uuid),并把配额单位从分钟改 GB、按账户综合卡控。
Tech Stack: Flutter/Dart、Swift(iOS/macOS NEPacketTunnelProvider + libbox)、Kotlin(Android VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQL(dialect 层)。
设计依据: design/CONTRACT.md、design/ui_kits/{mobile,tablet,desktop}、docs/ios-ipad-support-design.html;探查结论见审批计划 ~/.claude/plans/crystalline-exploring-lerdorf.md。
Global Constraints
- Channel 契约冻结:
pangolin/vpn/stats字段 = uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults(client/lib/bridge/vpn_bridge.dart:27-37),不得私改。 - 配置由服务端渲染下发,客户端绝不拼配置。
- 服务端可移植铁律:走
dialect.Upsert/LockForUpdate(server/internal/db/dialect.go),时间 Go 端算,禁UTC_TIMESTAMP()/NOW()/FIELD()等 MySQL 专属;迁移server/migrations/{mysql,sqlite}双份。 - token 真相源单源:只改
design/colors_and_type.css+ 跑node design/codegen/gen_flutter_tokens.mjs;client/lib/pangolin_tokens.gen.dart勿手改。 - 统一元素:复用/提升
client/lib/widgets/,禁就地造元素;颜色禁硬编码 hex,走语义 token。 - 文案禁红线词(VPN/翻墙/科学上网…),用「加速」口径,过
ci/scan-redline.sh;双语单显不并排。 - iOS App Group
group.com.pangolin.pangolinVpn;macOS App GroupBYL4KQHMTN.com.pangolin.pangolin。
Phase 1 — 四端实时统计修复(无 schema 改动)
让上传/下载/延迟/累计流量在四端全部可见。优先级最高、风险最低、见效最快。
Task 1.1: 服务端记账核实(先确认数据源)
Files: server/internal/httpapi/clientconfig.go、server/internal/agentd/usage*.go、server/internal/nodes/handler_grpc.go。
- Step 1: 已核实——节点配置
render.go启用experimental.v2ray_api.stats(enabled:true+users:statsUsers(creds)),inbound 用户name=dp_uuid;agentusage_v2ray.go按user>>>{dp_uuid}>>>traffic精确读取。链路代码完整正确。 - Step 2: 已核实——
handler_grpc.go:282-313dp_uuid→user_id→store.go AccumulateUsage累加usage_daily(user_id,date),全程 dialect 可移植。 - Step 3(运行时): 「累计流量没数据」非代码缺陷,是运行时产物(需真实连接产生被记账的流量 / 确认节点 build 含
with_v2ray_api)——真机连一次验证。利好:dp_uuid 改每设备后这套命名天然按设备拆分,Phase 2 节点+agent 侧近零改动。
Task 1.2: iOS 实时 stats 生产者(核心)
Files: client/ios/PacketTunnel/PacketTunnelProvider.swift、client/ios/Runner/VpnManager.swift、client/ios/Runner/AppDelegate.swift。
Interfaces: Produces → pangolin/vpn/stats EventChannel 真实帧。
- Step 1: 新增
client/ios/Runner/StatsClient.swift——LibboxNewCommandClient(CommandStatus+CommandGroup,statusInterval 1s)连 App Group 容器(LibboxSetup同扩展路径),含 10 次重试。 - Step 2:
writeStatus映射 uplinkTotal/downlinkTotal→uploadBytes/downloadBytes、uplink/downlink→uploadSpeed/downloadSpeed。 - Step 3:
writeGroups遍历组成员urlTestDelay→契约 urltestResults。 - Step 4: 经
VpnStatsStreamHandler.shared.push推 EventChannel;VpnManager状态观察 connected→start / disconnected→stop;断开推零帧。pbxproj 四处登记(plutil OK)。 - Step 5(真机): Release build 装真机,连接后验证上传/下载/延迟实时跳动。
Task 1.3: macOS 实时 stats 生产者
Files: client/macos/Runner/VpnChannel.swift、client/macos/PacketTunnel/PacketTunnelProvider.swift。
- Step 1: 新增
client/macos/Runner/StatsClient.swift(镜像 iOS,app groupBYL4KQHMTN.com.pangolin.pangolin);替换VpnChannel.swift旧占位(旧帧 schema 也错,推 up/down 非契约字段)。 - Step 2:
VpnChannel状态观察驱动 start/stop,onStats回调推 statsSink;onStatsListen去掉占位 Timer。pbxproj 四处登记(plutil OK)。 - Step 2 验证(真机): 递增 CFBundleVersion + 公证流程(CLAUDE.md),装机验证。
Task 1.4: Android 延迟修复
Files: client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt。
- Step 1(代码核查):
PangolinVpnService.kt已addCommand(CommandGroup)、writeGroups正确解析getURLTestDelay()——读取链路无缺陷。 - Step 2(主动探测修复): 补偿 sing-box urltest 惰性探测——
writeGroups捕获groupTags,新增startUrlTestTimer()(java.util.Timer每 12s 对各组client.urlTest(tag)),doStop取消。让延迟稳定及时出现,不等 3min 默认 interval。同样的主动触发也加到了 iOS/macOSStatsClient(urlTestTimer)。 - Step 3(真机): 装机验证延迟出现。
Task 1.5: Windows 延迟修复
Files: client/lib/bridge/kernel_process.dart、(必要时)server/internal/httpapi/clientconfig.go。
- Step 1(根因定位+修复): 真正 bug——
getGroupDelay()早已实现却从未被调用,轮询只读/proxies缓存,urltest 惰性探测下history长期为空 → 延迟空白。修复:_startStatsPoll每_urlTestEvery=12拍对提取出的_urltestGroupsfire-and-forgetgetGroupDelay()主动触发探测;新增extractUrltestGroups()提取 URLTest/Fallback 组名。 - Step 2(单测):
kernel_process_test.dart新增「URLTest extraction」组 3 用例(组名提取/最新 history delay/空组)——flutter test26 全过。 - Step 3(真机): Windows 装机验证延迟出现。
Task 1.6: Phase 1 验收
flutter analyze+flutter test(含现有 golden)通过。- 四端连接后:上传/下载/延迟实时跳动;累计流量 + 周柱图有值。
- 提交,合并验收后再进 Phase 2。
Phase 2 — 每设备归因 + GB 综合配额(后端重型)
Task 2.1: Schema 迁移(mysql + sqlite 双份)
Files: server/migrations/{mysql,sqlite}/0000X_*.up/down.sql。
- Step 1:
devices.dp_uuid(sqlite TEXT / mysql CHAR(36),NULL)+ 独立CREATE UNIQUE INDEX idx_devices_dp_uuid(SQLite ADD COLUMN 不支持内联 UNIQUE,两库对齐)。 - Step 2: 新表
usage_device_daily,PK(device_id, date),索引(user_id, date)。 - Step 3:
plans.daily_mb(MB 整数,支持免费 500MB;与daily_minutes并存);seed free 500 / pro 102400(100GB) / team 204800(200GB),按用户确认数值。 - Step 4:
000015_per_device_usage.{up,down}.sql双驱动;sqliteTestSQLiteMigrateUpDownup+down 干净(版本→15、新表/列断言已加),run_sqlite_test.sh全过、go build ./...OK。mysql 侧待run_mysql_test.sh(需 docker)。
Task 2.2: 每设备 dp_uuid 链路
Files: server/internal/devices/service.go、server/internal/httpapi/nodes.go、server/internal/nodes/store.go、server/proto/agent/v1/agent.proto、server/internal/nodes/handler_grpc.go。
- Step 1:
store.EnsureDeviceDpUUID(userID, deviceUUID)——connect 时按设备懒 mintdevices.dp_uuid(idgen.NewString(),guarded UPDATE + re-read 处理并发竞态)。 - Step 2:
ConnectNode用设备 dp_uuid 建 cred + 渲染 config(EnsureDeviceDpUUID失败则回退账户 dp_uuid,不破坏未注册设备的旧客户端)。 - Step 3:
store.UserDeviceByDpUUID(dpUUID) → (user, device)——优先查devices.dp_uuid,回退 legacyusers.dp_uuid(device=0)。 - Step 4:
ReportUsage双写——账户AccumulateUsage+ 设备AccumulateDeviceUsage(仅 device>0)。 - Step 5: 兼容——懒 mint 使存量设备下次 connect 自动补发;legacy 账户 dp_uuid 仍解析(device=0,不归设备)。节点侧零改动:每设备 dp_uuid 作独立 connect_credential →
render.go statsUsers/inbound 天然按设备拆。 - 验证:
sqlite_per_device_test.go3 用例(mint 幂等/区分/未知设备报错、dp_uuid→(user,device) 解析+账户回退、设备用量累加分离)+grpc_test.go TestReportUsage_PerDevice(双写)/TestReportUsage_Accumulates(账户级 device=0 不写设备)——go test ./...全过。
Task 2.3: GB 综合配额卡控
Files: server/internal/usage/quota.go、server/internal/httpapi/nodes.go。
- 决策(用户): 免费=看广告解锁+分钟与 GB 双卡;免费 500MB/天;付费高 GB 上限(pro 100GB/team 200GB)。
- Step 1: GB 综合卡控落在真实 connect 路径
ConnectNode(CheckFreeConnect实为未接线死代码)——ent.DailyMB.Valid时取store.AccountDayBytes(账户当日 up+down),≥ daily_mb*1MB即拒 403QUOTA_EXHAUSTED;免费/付费统一。EntitlementForUser加载daily_mb(free 回退 500)。 - 验证:
TestSQLite_AccountDayBytes(求和/隔离日期)+TestSQLite_EntitlementForUser_DailyMB(free 回退 500 / pro 订阅 102400)——全过。 - Step 2(后续): 复用
ReportUsage周期路径跨阈值 revoke 凭证,实现近持续卡控(当前仅 connect 时卡)。 - Step 3(后续): 完整 ad-unlock 门控接线(ConnectNode 现为 MVP「flat 10min」,未强制看广告);
/v1/mequota 语义改 GB(并入 Task 2.4)。
Task 2.4: 按设备用量 API ✅
Files: server/internal/usage/{store,service,handler}.go、server/cmd/server/main.go、client/lib/models/device_usage.dart、client/lib/services/account_api.dart、client/lib/state/account_providers.dart。
- Step 1: 新增
GET /v1/usage/devices?days=N(独立端点,复用auth.RequireAuthgroup)。后端:usage.Store.DeviceUsageRange(JOIN devices + GROUP BY 设备,窗口求和,busiest-first,可移植 SQL)→Service.DeviceUsage→DeviceUsageHandler,返回{devices:[{uuid,name,platform,bytes_up,bytes_down,minutes_used}]}。 - Step 2: 客户端 model
DeviceUsage+AccountApi.deviceUsage({days})+deviceUsageProvider(family days)。UI 渲染留到 Phase 3(设计先行)。 - 验证: sqlite 实库测试
TestSQLite_DeviceUsageRange(分组求和/窗口隔离/账户隔离/busiest-first/JOIN 元数据);handler guard 测试TestDeviceUsageHandler_Guards(401/405/400);Dartdevice_usage_test.dart(fromJson + 默认值)。全量go test ./...绿、flutter test146 过。
Task 2.5: Phase 2 验收
- sqlite 实库测试(
go test ./...+./server/run_sqlite_test.sh)覆盖每设备累加、综合卡控、dp_uuid 解析。 - mysql 集成测试(
./server/run_mysql_test.sh)通过。 - 端到端两设备分别连接、用量各归各账;GB 超额被拒。
Phase 3 — 统计页重设计(design-first)+ 四端实现
Task 3.1: 分设备明细设计(design-source 同步)✅
Files: design/ui_kits/{mobile,tablet,desktop}/*、design/CONTRACT.md。
- Step 1: 「设备明细 / By device」section 加入三端原型(mobile
screens.jsx StatsScreen、tablettabapp.jsx TabStats、desktopdapp.jsx DStats):上聚合沿用现 3 指标卡 + 周柱;下分设备列表 = 平台 Lucide 图标盒 38(accent-subtle) + 名称 + 占比迷你条(height 4, accent .85) + 流量(mono)/时长。字串入各端 dict(byDevice/noDeviceUsage,双语)。 - Step 2:
design/CONTRACT.md§3.3 增分设备明细像素规格 + 数据源标注。
Task 3.2: 统一组件提升 ✅
Files: client/lib/widgets/metric_card.dart(新)、client/lib/widgets/device_stat_row.dart(新)。
- Step 1:
stats_page.dart私有_MetricCard→ 公共widgets/metric_card.dart(MetricCard)。 - Step 2: 新增
widgets/device_stat_row.dart(DeviceStatRow+ 静态iconFor平台图标,与 account_screens 同一视觉语言)。
Task 3.3: 四端 stats_page 接线 ✅
Files: client/lib/screens/stats_page.dart、client/lib/l10n/*。
- Step 1: stats_page(mobile/tablet 窄宽分支 + desktop isWide)接
deviceUsageProvider(30);上聚合下分设备(_DeviceDetail);占比条按最忙设备归一;全程语义 token(占比条 accent/border,无硬编码 hex)。 - Step 2: 文案入
l10n/app_text.dart+strings_{zh,en}.dart(byDevice/noDeviceUsage),双语单显,无红线词(「设备明细」中性)。
Task 3.4: Phase 3 验收 ✅
- golden 回归闸(CONTRACT §4 既定 pixel gate):tablet 注入 demo 设备用量 →
tablet_stats_{light_zh,light_en,dark_zh}重生;desktop 零数据态 →desktop_stats重生(空态 section)。 flutter analyze无 error/warning;flutter test146 过;go test ./...绿。- 真机/模拟器多端目测(待用户验证)。
不在本次范围
- 组织级多租户层级(父子账户)——user==account 已满足。
- 协议选择(todo #8)、KillSwitch(#1/#2/#3)。