Files
pangolin/docs/superpowers/plans/2026-06-24-stats-overhaul.md
T
wangjia 1a6eb7ac9a docs(stats): stats-overhaul 计划/测试清单 + index 登记
- stats-overhaul 实现计划(md 真相源 + html 阅读版)
- 功能×测试覆盖清单(feature-test-coverage-checklist.html)
- docs/index.html 登记上述文档

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:13:49 +08:00

14 KiB
Raw Blame History

Pangolin 统计体系整改 Implementation Plantodo #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 ReportUsagedp_uuid 记账;本计划把记账粒度从「账户」细化到「设备」(每设备独立 dp_uuid),并把配额单位从分钟改 GB、按账户综合卡控。

Tech Stack: Flutter/Dart、SwiftiOS/macOS NEPacketTunnelProvider + libbox)、KotlinAndroid VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQLdialect 层)。

设计依据: design/CONTRACT.mddesign/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/urltestResultsclient/lib/bridge/vpn_bridge.dart:27-37),不得私改
  • 配置由服务端渲染下发,客户端绝不拼配置。
  • 服务端可移植铁律:走 dialect.Upsert/LockForUpdateserver/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.mjsclient/lib/pangolin_tokens.gen.dart 勿手改。
  • 统一元素:复用/提升 client/lib/widgets/,禁就地造元素;颜色禁硬编码 hex,走语义 token。
  • 文案禁红线词(VPN/翻墙/科学上网…),用「加速」口径,过 ci/scan-redline.sh;双语单显不并排。
  • iOS App Group group.com.pangolin.pangolinVpnmacOS App Group BYL4KQHMTN.com.pangolin.pangolin

Phase 1 — 四端实时统计修复(无 schema 改动)

让上传/下载/延迟/累计流量在四端全部可见。优先级最高、风险最低、见效最快。

Task 1.1: 服务端记账核实(先确认数据源)

Files: server/internal/httpapi/clientconfig.goserver/internal/agentd/usage*.goserver/internal/nodes/handler_grpc.go

  • Step 1: 已核实——节点配置 render.go 启用 experimental.v2ray_api.statsenabled:true+users:statsUsers(creds)),inbound 用户 name=dp_uuidagent usage_v2ray.gouser>>>{dp_uuid}>>>traffic 精确读取。链路代码完整正确。
  • Step 2: 已核实——handler_grpc.go:282-313 dp_uuid→user_idstore.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.swiftclient/ios/Runner/VpnManager.swiftclient/ios/Runner/AppDelegate.swift

Interfaces: Produces → pangolin/vpn/stats EventChannel 真实帧。

  • Step 1: 新增 client/ios/Runner/StatsClient.swift——LibboxNewCommandClientCommandStatus+CommandGroupstatusInterval 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 推 EventChannelVpnManager 状态观察 connected→start / disconnected→stop;断开推零帧。pbxproj 四处登记(plutil OK)。
  • Step 5(真机): Release build 装真机,连接后验证上传/下载/延迟实时跳动。

Task 1.3: macOS 实时 stats 生产者

Files: client/macos/Runner/VpnChannel.swiftclient/macos/PacketTunnel/PacketTunnelProvider.swift

  • Step 1: 新增 client/macos/Runner/StatsClient.swift(镜像 iOSapp group BYL4KQHMTN.com.pangolin.pangolin);替换 VpnChannel.swift 旧占位(旧帧 schema 也错,推 up/down 非契约字段)。
  • Step 2: VpnChannel 状态观察驱动 start/stoponStats 回调推 statsSinkonStatsListen 去掉占位 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.ktaddCommand(CommandGroup)writeGroups 正确解析 getURLTestDelay()——读取链路无缺陷。
  • Step 2(主动探测修复): 补偿 sing-box urltest 惰性探测——writeGroups 捕获 groupTags,新增 startUrlTestTimer()java.util.Timer 每 12s 对各组 client.urlTest(tag)),doStop 取消。让延迟稳定及时出现,不等 3min 默认 interval。同样的主动触发也加到了 iOS/macOS StatsClienturlTestTimer)。
  • 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 拍对提取出的 _urltestGroups fire-and-forget getGroupDelay() 主动触发探测;新增 extractUrltestGroups() 提取 URLTest/Fallback 组名。
  • Step 2(单测): kernel_process_test.dart 新增「URLTest extraction」组 3 用例(组名提取/最新 history delay/空组)——flutter test 26 全过。
  • 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_uuidsqlite TEXT / mysql CHAR(36)NULL+ 独立 CREATE UNIQUE INDEX idx_devices_dp_uuidSQLite ADD COLUMN 不支持内联 UNIQUE,两库对齐)。
  • Step 2: 新表 usage_device_dailyPK (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 双驱动;sqlite TestSQLiteMigrateUpDown up+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.goserver/internal/httpapi/nodes.goserver/internal/nodes/store.goserver/proto/agent/v1/agent.protoserver/internal/nodes/handler_grpc.go

  • Step 1: store.EnsureDeviceDpUUID(userID, deviceUUID)——connect 时按设备懒 mint devices.dp_uuididgen.NewString()guarded UPDATE + re-read 处理并发竞态)。
  • Step 2: ConnectNode 用设备 dp_uuid 建 cred + 渲染 configEnsureDeviceDpUUID 失败则回退账户 dp_uuid,不破坏未注册设备的旧客户端)。
  • Step 3: store.UserDeviceByDpUUID(dpUUID) → (user, device)——优先查 devices.dp_uuid,回退 legacy users.dp_uuiddevice=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.go 3 用例(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.goserver/internal/httpapi/nodes.go

  • 决策(用户): 免费=看广告解锁+分钟与 GB 双卡;免费 500MB/天;付费高 GB 上限(pro 100GB/team 200GB)。
  • Step 1: GB 综合卡控落在真实 connect 路径 ConnectNodeCheckFreeConnect 实为未接线死代码)——ent.DailyMB.Valid 时取 store.AccountDayBytes(账户当日 up+down),≥ daily_mb*1MB 即拒 403 QUOTA_EXHAUSTED;免费/付费统一。EntitlementForUser 加载 daily_mbfree 回退 500)。
  • 验证: TestSQLite_AccountDayBytes(求和/隔离日期)+ TestSQLite_EntitlementForUser_DailyMBfree 回退 500 / pro 订阅 102400)——全过。
  • Step 2(后续): 复用 ReportUsage 周期路径跨阈值 revoke 凭证,实现近持续卡控(当前仅 connect 时卡)。
  • Step 3(后续): 完整 ad-unlock 门控接线(ConnectNode 现为 MVP「flat 10min」,未强制看广告);/v1/me quota 语义改 GB(并入 Task 2.4)。

Task 2.4: 按设备用量 API

Files: server/internal/usage/{store,service,handler}.goserver/cmd/server/main.goclient/lib/models/device_usage.dartclient/lib/services/account_api.dartclient/lib/state/account_providers.dart

  • Step 1: 新增 GET /v1/usage/devices?days=N(独立端点,复用 auth.RequireAuth group)。后端:usage.Store.DeviceUsageRangeJOIN devices + GROUP BY 设备,窗口求和,busiest-first,可移植 SQL)→ Service.DeviceUsageDeviceUsageHandler,返回 {devices:[{uuid,name,platform,bytes_up,bytes_down,minutes_used}]}
  • Step 2: 客户端 model DeviceUsage + AccountApi.deviceUsage({days}) + deviceUsageProviderfamily days)。UI 渲染留到 Phase 3(设计先行)。
  • 验证: sqlite 实库测试 TestSQLite_DeviceUsageRange(分组求和/窗口隔离/账户隔离/busiest-first/JOIN 元数据);handler guard 测试 TestDeviceUsageHandler_Guards401/405/400);Dart device_usage_test.dartfromJson + 默认值)。全量 go test ./... 绿、flutter test 146 过。

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、tablet tabapp.jsx TabStats、desktop dapp.jsx DStats):上聚合沿用现 3 指标卡 + 周柱;下分设备列表 = 平台 Lucide 图标盒 38(accent-subtle) + 名称 + 占比迷你条(height 4, accent .85) + 流量(mono)/时长。字串入各端 dictbyDevice/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.dartMetricCard)。
  • Step 2: 新增 widgets/device_stat_row.dartDeviceStatRow + 静态 iconFor 平台图标,与 account_screens 同一视觉语言)。

Task 3.3: 四端 stats_page 接线

Files: client/lib/screens/stats_page.dartclient/lib/l10n/*

  • Step 1: stats_pagemobile/tablet 窄宽分支 + desktop isWide)接 deviceUsageProvider(30);上聚合下分设备(_DeviceDetail);占比条按最忙设备归一;全程语义 token(占比条 accent/border,无硬编码 hex)。
  • Step 2: 文案入 l10n/app_text.dart + strings_{zh,en}.dartbyDevice/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/warningflutter test 146 过go test ./... 绿。
  • 真机/模拟器多端目测(待用户验证)。

不在本次范围

  • 组织级多租户层级(父子账户)——user==account 已满足。
  • 协议选择(todo #8)、KillSwitch#1/#2/#3)。