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

164 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `ReportUsage``dp_uuid` 记账;本计划把记账粒度从「账户」细化到「设备」(每设备独立 dp_uuid),并把配额单位从分钟改 GB、按账户综合卡控。
**Tech Stack:** Flutter/Dart、SwiftiOS/macOS NEPacketTunnelProvider + libbox)、KotlinAndroid VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQLdialect 层)。
**设计依据:** `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 Group `BYL4KQHMTN.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`
- [x] **Step 1:** 已核实——节点配置 `render.go` 启用 `experimental.v2ray_api.stats``enabled:true`+`users:statsUsers(creds)`),inbound 用户 `name=dp_uuid`agent `usage_v2ray.go``user>>>{dp_uuid}>>>traffic` 精确读取。链路代码完整正确。
- [x] **Step 2:** 已核实——`handler_grpc.go:282-313` `dp_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 真实帧。
- [x] **Step 1:** 新增 `client/ios/Runner/StatsClient.swift`——`LibboxNewCommandClient`CommandStatus+CommandGroupstatusInterval 1s)连 App Group 容器(`LibboxSetup` 同扩展路径),含 10 次重试。
- [x] **Step 2:** `writeStatus` 映射 uplinkTotal/downlinkTotal→uploadBytes/downloadBytes、uplink/downlink→uploadSpeed/downloadSpeed。
- [x] **Step 3:** `writeGroups` 遍历组成员 `urlTestDelay`→契约 urltestResults。
- [x] **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`
- [x] **Step 1:** 新增 `client/macos/Runner/StatsClient.swift`(镜像 iOSapp group `BYL4KQHMTN.com.pangolin.pangolin`);替换 `VpnChannel.swift` 旧占位(旧帧 schema 也错,推 up/down 非契约字段)。
- [x] **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`
- [x] **Step 1(代码核查):** `PangolinVpnService.kt``addCommand(CommandGroup)``writeGroups` 正确解析 `getURLTestDelay()`——读取链路无缺陷。
- [x] **Step 2(主动探测修复):** 补偿 sing-box urltest 惰性探测——`writeGroups` 捕获 `groupTags`,新增 `startUrlTestTimer()``java.util.Timer` 每 12s 对各组 `client.urlTest(tag)`),`doStop` 取消。让延迟稳定及时出现,不等 3min 默认 interval。同样的主动触发也加到了 iOS/macOS `StatsClient``urlTestTimer`)。
- [ ] **Step 3(真机):** 装机验证延迟出现。
### Task 1.5: Windows 延迟修复
**Files:** `client/lib/bridge/kernel_process.dart`、(必要时)`server/internal/httpapi/clientconfig.go`
- [x] **Step 1(根因定位+修复):** 真正 bug——`getGroupDelay()` 早已实现却**从未被调用**,轮询只读 `/proxies` 缓存,urltest 惰性探测下 `history` 长期为空 → 延迟空白。修复:`_startStatsPoll``_urlTestEvery=12` 拍对提取出的 `_urltestGroups` fire-and-forget `getGroupDelay()` 主动触发探测;新增 `extractUrltestGroups()` 提取 URLTest/Fallback 组名。
- [x] **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`
- [x] **Step 1:** `devices.dp_uuid`sqlite TEXT / mysql CHAR(36)NULL+ 独立 `CREATE UNIQUE INDEX idx_devices_dp_uuid`SQLite ADD COLUMN 不支持内联 UNIQUE,两库对齐)。
- [x] **Step 2:** 新表 `usage_device_daily`PK `(device_id, date)`,索引 `(user_id, date)`
- [x] **Step 3:** `plans.daily_mb`(MB 整数,支持免费 500MB;与 `daily_minutes` 并存);seed free 500 / pro 102400(100GB) / team 204800(200GB),按用户确认数值。
- [x] **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.go``server/internal/httpapi/nodes.go``server/internal/nodes/store.go``server/proto/agent/v1/agent.proto``server/internal/nodes/handler_grpc.go`
- [x] **Step 1:** `store.EnsureDeviceDpUUID(userID, deviceUUID)`——connect 时按设备**懒 mint** `devices.dp_uuid``idgen.NewString()`guarded UPDATE + re-read 处理并发竞态)。
- [x] **Step 2:** `ConnectNode` 用设备 dp_uuid 建 cred + 渲染 config`EnsureDeviceDpUUID` 失败则**回退账户 dp_uuid**,不破坏未注册设备的旧客户端)。
- [x] **Step 3:** `store.UserDeviceByDpUUID(dpUUID) → (user, device)`——优先查 `devices.dp_uuid`,回退 legacy `users.dp_uuid`device=0)。
- [x] **Step 4:** `ReportUsage` 双写——账户 `AccumulateUsage` + 设备 `AccumulateDeviceUsage`(仅 device>0)。
- [x] **Step 5:** 兼容——懒 mint 使存量设备下次 connect 自动补发;legacy 账户 dp_uuid 仍解析(device=0,不归设备)。节点侧零改动:每设备 dp_uuid 作独立 connect_credential → `render.go statsUsers`/inbound 天然按设备拆。
- [x] **验证:** `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.go``server/internal/httpapi/nodes.go`
- [x] **决策(用户):** 免费=看广告解锁+分钟与 GB **双卡**;免费 500MB/天;付费高 GB 上限(pro 100GB/team 200GB)。
- [x] **Step 1:** GB 综合卡控落在**真实 connect 路径** `ConnectNode``CheckFreeConnect` 实为未接线死代码)——`ent.DailyMB.Valid` 时取 `store.AccountDayBytes`(账户当日 up+down),`≥ daily_mb*1MB` 即拒 403 `QUOTA_EXHAUSTED`;免费/付费统一。`EntitlementForUser` 加载 `daily_mb`free 回退 500)。
- [x] **验证:** `TestSQLite_AccountDayBytes`(求和/隔离日期)+ `TestSQLite_EntitlementForUser_DailyMB`free 回退 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}.go``server/cmd/server/main.go``client/lib/models/device_usage.dart``client/lib/services/account_api.dart``client/lib/state/account_providers.dart`
- [x] **Step 1:** 新增 `GET /v1/usage/devices?days=N`(独立端点,复用 `auth.RequireAuth` group)。后端:`usage.Store.DeviceUsageRange`JOIN devices + GROUP BY 设备,窗口求和,busiest-first,可移植 SQL)→ `Service.DeviceUsage``DeviceUsageHandler`,返回 `{devices:[{uuid,name,platform,bytes_up,bytes_down,minutes_used}]}`
- [x] **Step 2:** 客户端 model `DeviceUsage` + `AccountApi.deviceUsage({days})` + `deviceUsageProvider`family days)。UI 渲染留到 Phase 3(设计先行)。
- [x] **验证:** sqlite 实库测试 `TestSQLite_DeviceUsageRange`(分组求和/窗口隔离/账户隔离/busiest-first/JOIN 元数据);handler guard 测试 `TestDeviceUsageHandler_Guards`401/405/400);Dart `device_usage_test.dart`fromJson + 默认值)。全量 `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`
- [x] **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)/时长。字串入各端 dict`byDevice`/`noDeviceUsage`,双语)。
- [x] **Step 2:** `design/CONTRACT.md` §3.3 增分设备明细像素规格 + 数据源标注。
### Task 3.2: 统一组件提升 ✅
**Files:** `client/lib/widgets/metric_card.dart`(新)、`client/lib/widgets/device_stat_row.dart`(新)。
- [x] **Step 1:** `stats_page.dart` 私有 `_MetricCard` → 公共 `widgets/metric_card.dart``MetricCard`)。
- [x] **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/*`
- [x] **Step 1:** stats_pagemobile/tablet 窄宽分支 + desktop isWide)接 `deviceUsageProvider(30)`;上聚合下分设备(`_DeviceDetail`);占比条按最忙设备归一;全程语义 token(占比条 accent/border,无硬编码 hex)。
- [x] **Step 2:** 文案入 `l10n/app_text.dart` + `strings_{zh,en}.dart``byDevice`/`noDeviceUsage`),双语单显,无红线词(「设备明细」中性)。
### Task 3.4: Phase 3 验收 ✅
- [x] golden 回归闸(CONTRACT §4 既定 pixel gate):tablet 注入 demo 设备用量 → `tablet_stats_{light_zh,light_en,dark_zh}` 重生;desktop 零数据态 → `desktop_stats` 重生(空态 section)。
- [x] `flutter analyze` 无 error/warning`flutter test` **146 过**`go test ./...` 绿。
- [ ] 真机/模拟器多端目测(待用户验证)。
---
## 不在本次范围
- 组织级多租户层级(父子账户)——user==account 已满足。
- 协议选择(todo #8)、KillSwitch#1/#2/#3)。