wangjia
|
16b8823f1f
|
feat(server): pay webhook 接收器(验签/时间窗/nonce + out_trade_no 幂等开通,回 SUCCESS)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 22:38:01 +08:00 |
|
wangjia
|
932953bafe
|
feat(server): /v1/pay 下单代理端点 + 购买台账(JWT 鉴权,user→biz_ref 映射)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 22:32:43 +08:00 |
|
wangjia
|
db2461ed1d
|
feat(server): pay v2 出站客户端(HMAC 签名下单/查单/retry/cancel)
|
2026-07-10 20:52:33 +08:00 |
|
wangjia
|
7370b17bd9
|
feat(server): codes.GrantPaidSubscriptionTx 提炼订阅叠加语义供 pay 复用
CreateSubscription/applySubscription 加 source 参数(兑换路径传 "code"
零行为变化);新导出 GrantPaidSubscriptionTx(ctx, tx, ...) 供 pay 侧在
调用方事务内以 source='pay' 复用同一段叠加语义(同 plan 活跃订阅原地
延长,否则新建行),写 pay_grant 审计。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 20:49:25 +08:00 |
|
wangjia
|
85140edf1c
|
feat(server): 迁移 000021 pay_purchases 台账 + subscriptions.source 扩 pay
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 20:46:13 +08:00 |
|
wangjia
|
16855a533a
|
chore(server): 清理 stale code_batches 注释
|
2026-07-10 15:03:45 +08:00 |
|
wangjia
|
db3428f622
|
test(server): mysql 集成测试对齐 codes 库新表(断言不变,环境搭建换 ApplyMigrations)(#codes-lib)
|
2026-07-10 15:03:43 +08:00 |
|
wangjia
|
ef4ed47759
|
feat(server): admin 批次列表/作废改指 codes 库新表,清理被取代的旧 store 方法(#codes-lib)
|
2026-07-10 14:53:29 +08:00 |
|
wangjia
|
d1e2fed563
|
feat(server): webhook 薄壳化——保留 X-Pangolin HMAC 协议,铸码走库单事务原语(#codes-lib)
|
2026-07-10 14:45:08 +08:00 |
|
wangjia
|
0822d22e2c
|
feat(server): Redeem 换芯 GuardedRedeem——grant 回调同事务复刻订阅叠加+双审计(#codes-lib)
|
2026-07-10 14:34:30 +08:00 |
|
wangjia
|
e128c96d22
|
feat(server): CreateBatch/生成器切换到 codes 库(Mint 单事务全成或全无)(#codes-lib)
Store 挂库 store(NewStore 内部构造 libcodes.Store,签名不变);
generator.go 三函数委托 libcodes,ErrDuplicate 别名同一哨兵;
Service.CreateBatch 走 libcodes.Mint(签名不变)。openMigratedSQLite
挪到共享 sqlite_helper_test.go,backfill_test.go/service_sqlite_test.go
共用。Store.CreateBatch/CreateCode/CodeExistsByHash 原样保留供
webhook.go(Task 6 改用新原语,Task 7 删除)。
|
2026-07-10 14:24:18 +08:00 |
|
wangjia
|
15f0d78a91
|
feat(server): legacy 码表 → codes 库新表的幂等回填(Go 双方言)(#codes-lib)
|
2026-07-10 14:13:21 +08:00 |
|
wangjia
|
7a4c9b97d0
|
fix(server): migrate down 兼容 codes 库自建表(先 DROP 再改名回)+ 真实接线回环测试(#codes-lib)
Reviewer 复现:cmd/migrate up 接线 ApplyCodesLibMigrations 后,codes 库自建的
codes/codes_batches/codes_audit_log/codes_schema_migrations 四张表不受
golang-migrate 追踪;000020 的 down 脚本做 legacy_codes -> codes 改名回时,
撞上库自建的同名 codes 表,报 "table already exists" 硬失败。
000020 的 sqlite/mysql down 脚本改名前先 DROP TABLE IF EXISTS 掉库自建四张表,
并注明:down = 完整回滚「rename + 库建表」组合,库表数据随 down 销毁属预期
(权威数据仍在 legacy_* 表,回填是后续任务,回填落地后 down 语义需重新审视)。
新增 TestCodesLibMigrateRoundTrip 走真实接线路径(MigrateUp ->
ApplyCodesLibMigrations -> 单步撤销 000020 断言 legacy 复原/库表清空 ->
撤销回去 -> 再跑 ApplyCodesLibMigrations 验幂等 -> 全量 store.MigrateDown 即
cmd/migrate down 的真实调用路径,确认不再硬失败)。先用 git stash 掉修复验证
测试能精确复现 reviewer 报的 "table already exists" 报错(RED),再恢复修复
转绿(GREEN)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 14:06:01 +08:00 |
|
wangjia
|
5a9b5f1d8e
|
feat(server): cmd/migrate up 接入 codes 库自建迁移(ApplyCodesLibMigrations)(#codes-lib)
顺带修 000020 sqlite 索引名冲突:ALTER TABLE RENAME 不会带着重命名索引,
legacy_codes 上遗留的 idx_codes_status 与 codes 库自己迁移里同名索引撞名,
一并改成 idx_legacy_codes_status(up/down 对称)。
|
2026-07-10 13:55:14 +08:00 |
|
wangjia
|
665749a90b
|
feat(server): 迁移000020 旧码表更名封存 legacy_*(为 codes 库表让位)(#codes-lib)
RENAME codes/code_batches -> legacy_codes/legacy_code_batches(双方言),
为共享库 github.com/wangjia/codes 的同名 codes 表让位。sqlite_migrate_test.go
同步到 version=20 + legacy_* 表清单。
顺带 skip TestSQLite_CodesRedeemFlow(internal/store/sqlite_stores_test.go):
该测试直调 internal/codes.Store 的低层方法(CreateBatch/CreateCode/
FindCodeByHashForUpdate/MarkRedeemed),现在指向被更名的旧表。计划本身在
Task 5(Store 换芯 + Redeem 走库)显式删除此测试、由 Service 级 sqlite 用例
接管等价覆盖 —— 这里先 skip 而非改写/删除,保持 go test ./... 全绿,避免
在 Task 1 提前动到 Task 4/5 范围内的 internal/codes/store.go。
|
2026-07-10 13:46:17 +08:00 |
|
wangjia
|
e44e4616cd
|
feat(server): 接入 github.com/wangjia/codes 共享库依赖(pin c772d52)(#codes-lib)
Task 0 preflight: 验证锚点(表结构/service.go 行为/main.go 装配点)无漂移,
建立改动前基线(25 包全绿)。
Controller resolution 覆盖计划原文 Step 2:验证分支不改机器级 git config /
GOPRIVATE,改用 server/go.mod 内 replace 本地路径(见 go.mod 注释),合并前
需切换为 git insteadOf + 已发布版本(登记为协调项)。go get + go mod tidy
后 codes 因尚无代码 import 而未在 go.mod 落 require(预期行为,后续任务
接线后会重新出现);replace 指令本身及其带出的依赖抬升(modernc.org/sqlite
1.18.1→1.38.2, redis/go-redis/v9 9.20.1→9.21.0)已保留并通过改动前/改动后
两轮全量测试(均 25 ok / 0 FAIL)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
|
2026-07-10 13:39:08 +08:00 |
|
wangjia
|
0bedd1c4d1
|
fix(server): 连接删掉账户级 dp_uuid 回退,未注册设备直接拒(#16)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Has been cancelled
漏洞:设备被移除后行已删,但靠残留 access token + 连接时回退「账户级 dp_uuid」
还能连上(EnsureDeviceDpUUID 找不到设备→回退),且不计入活跃设备数,绕过设备上限。
修:ConnectNode 去掉账户级回退。设备未注册/已被移除(EnsureDeviceDpUUID 返回
ErrDeviceNotFound)→ 403 {code:DEVICE_NOT_REGISTERED} + 提示「请重新登录以重新
注册设备」。客户端现有 ConnectApiException 处理会把该 message_zh 显示在连接页,
用户重新登录即重新注册(并命中登录挡板/超限流程)。
- nodes: 新增哨兵 ErrDeviceNotFound;EnsureDeviceDpUUID 包装它
- httpapi ConnectNode: errors.Is(ErrDeviceNotFound) → 拒连;其余错误 500
go build/vet 干净;nodes/httpapi 测试全过。客户端不用改。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-07-01 22:51:12 +08:00 |
|
wangjia
|
0a70a24cdb
|
feat: 连接设备上限 backstop — 兜住"已登录的超限设备"(#16)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Has been cancelled
登录挡板只拦新登录,已登录的超限会话(如 pro 已连 5 台)永远不被提示 → 限制形同虚设。
补服务端连接卡点:超限账户连接直接拒,兜住已登录设备(下次一连即被拦,无需重登)。
服务端:
- Entitlement 加 MaxDevices(EntitlementForUser 查 p.max_devices;free 默认 1)
- NodeStore.CountActiveDevices(近 30d 活跃)+ SQLNodeStore/mock 实现
- ConnectNode:activeCount > MaxDevices → 403 {code:DEVICE_LIMIT_EXCEEDED, max_devices}
客户端:
- ConnectApiException 解析 code/max_devices(结构化错误体)
- _connect 遇 DEVICE_LIMIT_EXCEEDED → 置 deviceLimitProvider → 顶层路由弹「移除设备」页
(设备列表走 devicesProvider),移除到上限内自动放行
后端 go test(store/nodes/httpapi/devices)全过;前端 analyze 干净 + 66 测试过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-07-01 22:11:13 +08:00 |
|
wangjia
|
6bac7fd2f0
|
feat(server): 设备上限登录闸(超限非硬拒登,返回 device_limit 信号)#16
启用设备数量限制的服务端部分。登录照常成功签发 token(非硬拒登),但若账户活跃
设备数超套餐上限,登录响应带 device_limit 信号,客户端据此弹「移除设备」页。
- devices/store.go:CountActiveDevices(last_seen 近 staleWindow)+ PruneStaleDevices
(删超期僵尸行,免费版重装 churn 自愈)
- devices/service.go:staleWindow=30d;DeviceLimitStatus + CheckDeviceLimit
(best-effort prune → ResolvePlan → 活跃 count > cap 即 Over,附活跃设备列表)
- auth:DeviceRegistrar 加 CheckDeviceLimit;recordLogin 回传 *DeviceLimit;
LoginOutcome.DeviceLimit;Login 透传;handler tokenPairResponse.device_limit(omitempty)
- main.go:authDeviceRegistrar 适配 devices.CheckDeviceLimit → auth.DeviceLimit
- 测试:auth 登录透传超限信号(仍签发 token);devices within/over/prune-stale
连接侧 backstop(服务端硬拦)本轮从简未做,作为后续硬化(登录闸为客户端可信信号)。
DB 无 schema 变更。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-07-01 19:16:10 +08:00 |
|
wangjia
|
27dc59ed63
|
feat(server): pro 套餐设备上限 5 → 3
- seed(sqlite/mysql 000007)+ 001_init:pro max_devices 5→3(全新库直接对)
- 新增迁移 000019(sqlite/mysql,up/down):UPDATE plans SET max_devices=3
WHERE code='pro' —— 已迁移的线上库靠它更新;down 回退 5
- 000002/001 注释同步(free 1 / pro 3 / team 10)
- 测试:sqlite_migrate 期望版本 18→19;devices 集成测试 pro 断言 5→3
- free(1)/team(10)不变
注:设备上限当前仍未强制执行(登录注册 MaxDevices=0),仅数据/展示口径;
真正启用+超限 UX 见 todo #16。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-07-01 18:07:03 +08:00 |
|
wangjia
|
f0af3bcc94
|
fix(server): 强退设备立即离线 — online 须含活跃会话 + 撤销会话不刷 last_seen
trick bug:强退后被踢设备的下一次会话轮询会先刷 last_seen 再发现 active=false,那一刷
把它顶成「在线」直到 90s 窗口过期。修:① online = last_seen 新 且 有未撤销会话
(ActiveSessionDeviceIDs);被强退设备无活跃会话 → 立即离线。② SessionActive 仅在 active
时才 TouchLastSeen,撤销会话的轮询不再刷新。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-30 08:51:10 +08:00 |
|
wangjia
|
3167ca8a74
|
fix(server): 设备「在线」准确反映 app 运行中 — 会话轮询刷 last_seen + 窗口缩到 90s
原 online=last_seen 在 3min 内,而 last_seen 只在连接/用量上报时刷 → app 运行≠刷新,
显示成'最近连过'而非'正在运行';关 app 后还'在线'到窗口过期。修:SessionActive(每 15s
轮询)顺便 TouchLastSeen → 运行中的 app 持续刷新;onlineWindow 3min→90s,关 app ~90s 转离线。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-30 07:55:17 +08:00 |
|
wangjia
|
0b33f12400
|
feat: 近实时远程下线 — 客户端轮询会话有效性(~15s),服务端 GET /v1/me/session
控制面无推送通道,access token 是无状态 JWT(15min),强制退出后被踢设备要等 token 过期
(≤15min)才登出。改成客户端每 15s 轮询会话是否仍有效,被强制退出即登出 → 延迟压到 ~15s。
- 服务端:sessions.HasActiveSession(user,device) + devices.SessionActive(按 UUID,fail-open)
+ GET /v1/me/session?device_id= 返回 {active}(恒 200,判据在 body)。无新迁移。
- 客户端:account_api.sessionActive + main.dart _RootFlowState 15s 轮询,active=false 即 logout
(网络/鉴权异常不据此登出,fail-safe)。
- 测试:TestSQLite_SessionHasActiveSession(建会话=活跃→RevokeByDevice→非活跃)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-30 07:25:16 +08:00 |
|
wangjia
|
2080062d8c
|
feat: 数据面故障韧性 — 节点健康上报(控制面) + 客户端连通看门狗
盲区:节点 status 只看 agent 的 gRPC 在线,sing-box 数据面坏了(崩/配置坏/数据口不通)
而 agent 仍在线时,节点仍显示 up、connect 放行、客户端「已连接」但流量全失败,且不自愈。
控制面:
- agent 每心跳探 sing-box clash_api /version(2s 超时,连续 2 次失败才报不健康,防抖),
经新增 HeartbeatRequest.data_plane_healthy 上报(手写 agentv1 契约 + proto 同步;JSON codec)。
- NodeLoad 加 DataPlaneHealthy(随 load 写 Redis;字段缺失默认 healthy 防滚动期误杀)。
- effectiveNodeStatus 扩为 (dbStatus, agentOnline, dataPlaneHealthy);ListNodes 与 ConnectNode
统一改用它 → 数据面坏的节点列表置灰 + connect 返回 404 拦截。
客户端(connection_provider 连通看门狗):
- 连上后每 15s 经隧道 HTTP 探海外 generate_204(可注入),连续 3 次失败判当前节点不可用。
- 智能选择 → 自动切到其他最优可用节点重连;手动选定 → 断开并提示「节点异常」(尊重用户选择)。
- 新增 nodeUnhealthySwitched/nodeUnhealthyError 文案。
测试:agent 健康探测防抖、effectiveNodeStatus 真值表、dataPlaneHealthy 助手、看门狗
智能切/手动断/健康不触发;go test ./... 与 flutter test 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 13:19:22 +08:00 |
|
wangjia
|
315c343ed5
|
feat(devices): 设备默认名「平台简写·主机名」+ 支持重命名(1.0.13)
ci-pangolin / Lint — shellcheck (push) Successful in 7s
ci-pangolin / OpenAPI Sync Check (push) Successful in 17s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Failing after 14s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 6s
ci-pangolin / Go — build + test (push) Successful in 11s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m3s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
① 默认名:DeviceIdentity 改「平台简写(Win/Mac/Linux/iOS/Andr)·主机名」,主机名截
到 8、总长≈12(替代裸主机名,更短)。
② 重命名:新端点 POST /v1/me/devices/{uuid}/rename(Service.RenameDevice 校验归属+
截 64);store.UpdateName;客户端 account_api.renameDevice + provider.rename +
「我的设备」⋯ 菜单加「重命名」+ 输入弹窗;l10n(zh/en)。
③ 版本 1.0.13。
测试:server RenameDevice(归属/空名/404)+ 客户端 rename widget;全量 go/flutter 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 09:12:36 +08:00 |
|
wangjia
|
8a1db07d19
|
fix(devices+stats): /v1/me/devices 401 修复 + 设备下拉接已登录设备 + 版本号
ci-pangolin / Lint — shellcheck (push) Successful in 9s
ci-pangolin / OpenAPI Sync Check (push) Successful in 19s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Failing after 15s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Successful in 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m2s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 15s
① 「我的设备打不开」根因:devices handler 读 devices.CtxKeyUserID,而鉴权中间件
把 user id 写在 auth 的 key 下(类型不同→取不到)→ /v1/me/devices 一直 401。
改 handler 用 auth.UserIDFromContext(与 accountAPI 等一致)。
② 统计页设备下拉改读 devicesProvider(已登录设备),不再只列有用量的设备 →
windows 设备现在会出现。
③ pubspec version 1.0.2+3 → 1.0.11+12,client_version 不再显示陈旧的 v1.0.2。
测试:devices_screen + stats_device_filter widget(下拉读 /v1/me/devices)+全量绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 08:37:11 +08:00 |
|
wangjia
|
e333f59cb3
|
feat(stats): #10 第②层 —— 统计页按设备过滤(per-device 本地日曲线)
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 19s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Failing after 14s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 6s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 4s
ci-pangolin / Go — build + test (push) Successful in 11s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m8s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 15s
镜像账户级时区曲线到「按设备」:
- migration 000018 `usage_device_hourly`(每设备 UTC 小时桶,稀疏)。
- ReportUsage deviceID>0 时加 AccumulateDeviceHourly;NodeStore 接口+impl+mock。
- usage.Store.DeviceHourlyRange(JOIN devices 校验归属+解析 uuid→id);UsageCurve
增 deviceUUID 形参:空=账户级,非空=该设备本地日曲线(分桶逻辑复用)。
- /v1/usage?device=<uuid>;客户端 account_api.usage(device)、usageProvider key 改
记录 (days,device)、stats_page 接 statsDeviceProvider → 选设备即重取该设备曲线。
测试:per-device 曲线隔离+归属校验(A的设备不进B)、UsageCurve(deviceUUID)、
migration v18、客户端 widget(选设备→/v1/usage 带 device=)。全量 go/flutter 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 08:04:33 +08:00 |
|
wangjia
|
f2499a64e1
|
fix(usage): 统计按客户端本地日聚合 —— 修「29号却显示28号数据」时区 bug
ci-pangolin / Lint — shellcheck (push) Successful in 7s
ci-pangolin / OpenAPI Sync Check (push) Successful in 16s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Successful in 24s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 4s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Successful in 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m8s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
根因:服务端用量全按 UTC 预切天,UTC+8 用户在中国 0-8 点时 UTC 还是前一天,
「今天」就显示前一天数据。
改法(用户拍板:小时桶+查询带时区偏移):
- migration 000017 `usage_hourly`(UTC 纪元小时整数桶,稀疏存储,空小时不落行)。
- ReportUsage 同时累加 usage_hourly(按 windowEnd 的 UTC 小时);usage_daily 保留作配额/今日。
- `UsageCurve(days, offsetMin)`:读 UTC 小时桶,按客户端时区偏移**重新聚合成本地日**曲线。
- `/v1/usage?tz_offset=分钟`;客户端 `usage()` 带 `DateTime.now().timeZoneOffset.inMinutes`。
- 客户端早已「取曲线最后一点当今日」,故只改服务端聚合 + 带偏移即可。
测试:store 时区分桶证明(UTC 18:00 在 UTC vs UTC+8 落不同本地日,锁住 bug 修复)+
migration v17 + usage_hourly 表;集成测试改喂 hourly;全量 go/flutter test 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 07:02:04 +08:00 |
|
wangjia
|
bcc114088c
|
feat(devices): P3 强制退出 + 清除增强(per-device 凭证吊销)
ci-pangolin / Lint — shellcheck (push) Successful in 9s
ci-pangolin / OpenAPI Sync Check (push) Successful in 17s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 5s
ci-pangolin / Flutter — analyze + test (push) Successful in 26s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Successful in 12s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 15s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m4s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 15s
后端:新端点 POST /v1/me/devices/{uuid}/logout(ForceLogout:吊销该设备会话+
丢 Redis JTI,设备留列表)。DeleteDevice 增强:先吊销会话再删设备(FK ON DELETE
CASCADE 清理会话行)+ 按 dp_uuid 吊销数据面凭证。CredentialRevoker 接口改
per-device RevokeDevice(dpUUID),由 nodes.Service 实现(查 connect_credentials
持有节点→推 CommandTypeRevoke + 删凭证行),main 注入替 NoopRevoker;devices 注入
SessionPort/JTIRevoker。修 SQLite 跨连接死锁(会话吊销移到 delete tx 之前)。
migration 000016 sessions FK 加 ON DELETE CASCADE。
客户端:account_api.forceLogout + devicesProvider.forceLogout(UI 留 P6)。
测试:ForceLogout(吊销会话+JTI+设备保留+403/404)+ DeleteDevice(级联+按 dp_uuid
吊销);NoopRevoker 改 dp_uuid;全量 server/flutter 测试绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 06:01:41 +08:00 |
|
wangjia
|
2f298f0a0a
|
feat(devices): P2 sessions 表 + 在线/最后登录/客户端版本
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 18s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Successful in 24s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 4s
ci-pangolin / Go — build + test (push) Successful in 11s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m13s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
migration 000016(mysql+sqlite,含 down):新增 sessions 表(绑 device+refresh JTI)
+ devices 加 client_version/totp_trusted_until。devices 唯一键改 + platform CHECK
加 linux(需 SQLite 表重建)拆出后续迁移,降风险。
后端:新 internal/sessions Store(Create/Rotate/Revoke/RevokeByDevice/
LastLoginByDevice);TokenManager 外露 refresh JTI(IssueWithJTI/RefreshWithJTI/
ParseRefreshJTI);auth.Service 注入 SessionStore——登录建会话、刷新轮换、登出吊销;
DeviceRegistrar 返回 deviceID;ReportUsage 心跳 touch devices.last_seen(在线判定);
devices.ListDevices 经 LastLoginSource 注入返回 online(last_seen<3min)/client_version/
last_login;RegisterIfAbsent 存 client_version。
客户端:Device model 加 online/clientVersion/lastLogin(fromJson 自动解析)。
测试:sessions store 3 例 + ListDevices 在线/最后登录 + device model 2 例 +
migration v16;全量 go test/flutter test 绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 00:50:24 +08:00 |
|
wangjia
|
8370ee1eb7
|
chore: 删除误入库的一次性临时文件(tmphash / zz_tmpverify_test)
ci-pangolin / Lint — shellcheck (push) Successful in 5s
ci-pangolin / OpenAPI Sync Check (push) Successful in 17s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 5s
ci-pangolin / Flutter — analyze + test (push) Successful in 23s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Successful in 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m9s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 15s
P1 提交时 git add -A 误带入早期改密码调试残留(注释自标「临时,不提交」)。
|
2026-06-29 00:28:54 +08:00 |
|
wangjia
|
c0c4b94e29
|
feat(devices): P1 设备注册打通 —— 登录/注册即写 devices 表
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 16s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 7s
ci-pangolin / Flutter — analyze + test (push) Successful in 26s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 15s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m10s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
后端:auth.Service 加 DeviceMeta + DeviceRegistrar 接口(consumer-side 解耦),
Login/Register 成功签发后 best-effort 注册设备(不强制设备上限,避免免费档重装
churn 锁死用户);handler 加 device 请求体;main 用 authDeviceRegistrar 适配
devices.Service 注入;normalizePlatform 加 linux。
客户端:新 device_identity.dart(SecureKV 接缝 + 稳定 UUIDv4 device_id 持久化 +
名称/平台/版本);弃用硬编码 'mac-001';auth_api login/register + connect 携带
device 元数据。加 uuid + device_info_plus 依赖。
测试:auth 设备注册(触发/best-effort/空 meta) + device_identity(生成/持久/
读失败不重生成/UUIDv4 形态);normalizePlatform linux=true。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-29 00:28:13 +08:00 |
|
wangjia
|
636a3bbf2f
|
feat(server/stats): stats-overhaul Phase2 — 每设备归因 + GB 综合配额
服务端记账从「账户」细到「每设备」(每设备独立 dp_uuid),配额单位分钟→GB
按账户综合卡控。000015_per_device_usage 迁移(mysql+sqlite 双份):devices.dp_uuid
+ usage_device_daily 表 + plans.daily_mb。handler_grpc 按 dp_uuid 回映射
(user_id,device_id) 双写账户+每设备;usage 服务/handler 暴露 /v1/usage(/devices)。
含 sqlite_per_device / usage handler 测试。
注:此迁移 prod 已 migrate up 运行、部署二进制已内嵌;本次补提交使 git 与线上一致。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-28 18:12:00 +08:00 |
|
wangjia
|
4c1a289633
|
fix(nodes): 节点健康/connect 绑定 agent 在线状态,不再伪装连上
本次 google 打不开暴露:agent 离线 6 天,节点却一直显示健康、还让客户端
"连上"(凭证推送失败被 _ = 吞掉),实际节点不认 UUID。三处收口:
- Hub.IsOnline(nodeUUID):基于本地 agent 流注册的存活信号(单实例权威;
多实例需 Redis presence,另议)。
- ListNodes:effectiveNodeStatus 把 DB='up' 但 agent 离线的节点降级为
"down",不再永远显示健康(status 列只反映供给/调度,不反映 agent 死活)。
- ConnectNode:agent 离线即 503 ErrNodeUnavailable(凭证持久化留待 resync),
在线时 push 失败也 503——不再吞 pushErr、不再下发节点无法兑现的配置。
测试:TestHub_IsOnline(注册/结束/未注册)、TestEffectiveNodeStatus
(up+离线→down 等)。新增 apierr.ErrNodeUnavailable(503)。
掉线告警 + 静默吞错全量审计另起 TODO。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-28 08:36:31 +08:00 |
|
wangjia
|
3527537c96
|
fix(connect): 4 处 500 记 slog.Error;加连接路径 schema 绑定测试
ConnectNode 四条 ErrInternal 路径(entitlement/配额/节点/配置渲染)此前只写
500 不记 err,导致「no such column: p.daily_mb」这种 SQL 错全静默 → 难排查。
改为各记 slog.Error(含 user/node/err)。
新增 store 层测试 TestSQLite_ConnectPathSchema:在跑过完整迁移(含 000015)的
真 SQLite 库上,跑连接路径依赖 015 schema 的三条查询——EntitlementForUser
(断言 DailyMB 从 plans.daily_mb 取到 102400)、EnsureDeviceDpUUID(devices.dp_uuid)、
AccountDayBytes——任一引用了没有迁移建的列即 SQL 报错、测试变红,守住本次
「查询引用了未迁移列」一类回归。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-28 08:11:32 +08:00 |
|
wangjia
|
77ce809d48
|
fix(auth): SMTP 信封发件人用裸地址,修 501;发信失败记日志
两个真问题,前者被后者掩盖:
1. mailer.go 把带显示名的 SMTP_FROM(「穿山甲 <noreply@x>」)直接当 SMTP
信封发件人(MAIL FROM)传给 smtp.SendMail,Resend 报 501 Bad sender address
syntax → 验证码发不出。改用 net/mail 解析出裸地址作信封发件人,From: 头
仍保留完整显示名。
2. service.go 两处异步发信把错误静默吞了(_ = SendCode/SendAlreadyRegistered),
导致 SMTP 故障日志无痕、难排查(本次端口被封+501 都被吞)。改为失败时
slog.Error 记日志,邮箱经 maskEmail 打码,绝不记验证码(守 no-secret-in-logs)。
测试:TestEnvelopeFrom(显示名/裸址/unicode → 裸址)守 501 回归;TestMaskEmail
守日志脱敏。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-28 07:50:37 +08:00 |
|
wangjia
|
75cdbf583f
|
test(server): 刀2 — v2ray 真实采集后端测试(缺口②)
V2RayUsageSource 是「线上按租户记账」链路的源头(agent 读 sing-box v2ray_api
StatsService per-user 计数器 → 聚合 → ReportUsage),此前零测试覆盖
(复查发现之前以为有单测是错的)。本刀按用户定的范围只补 CI 可跑的后端采集:
- TestParseUserStat:表驱动,正常 uplink/downlink + 非 user/段数异常应 ok=false。
- TestV2RayCollect:起假 StatsService(loopback gRPC,ServiceName 与 client.go 的
规范全名一致),让真 Collect() 跑真链路,断言:按 dp_uuid 聚合 up/down 正确、
方向不串、全 0 用户与非 user 计数器被丢弃、SessionMinutes=1、且请求以
Reset_=true + pattern "user>>>" 发出(窗口取值清零语义)。
纯 go test、无 docker / 无真 sing-box;自动进 go-server job + 计入覆盖率闸。
真 sing-box 真出网 e2e 仍按计划留文档手测。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 23:01:27 +08:00 |
|
wangjia
|
0fd3bce7a5
|
fix(server): 修复腐烂的集成测试套件 + devices 注册回填 last_seen
这批 -tags integration 测试(usage/auth/codes/devices/store)因依赖冲突
长期编译不过、从未进 CI 跑过,代码静默腐烂。本次逐层修复:
- 依赖:testcontainers-go v0.34→v0.43(原 v0.34 配 docker v28.3.3 编译失败:
archive.Compression/sockets.DialPipe undefined)。
- auth: LoginOutcome 重构成 Tokens 嵌套后,测试仍引用扁平 RefreshToken;
users 测试 schema 缺 totp_enabled 列 → GetUserByEmail 报错 → SendCode 500。
- usage/auth/codes/devices: DSN 里 time_zone='+00:00' 作 URL query 透传时 '+'
被解码成空格 → MySQL Error 1298 ' 00:00';改百分号编码。
- store: 测试 DSN 缺 multiStatements=true → 多语句迁移 Error 1064;
WithConfigFile("") 在 v0.43 被拒,改写真 my.cnf 设 +08:00 真正考验 UTC 覆盖。
- devices(产品 bug):insertDeviceTx 把 last_seen 写进库却没回填返回值,
刚注册的设备 API 响应 last_seen=null 与库不一致;TestFullChain 据此把关。
修复后完整 integration 套件 -p 1 串行全绿(25 包 + 5 testcontainers 包)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 21:19:10 +08:00 |
|
wangjia
|
65bd7c381b
|
fix(test): e2e 去掉 /v1/usage/devices 断言(stats-overhaul WIP)
ci-pangolin / Lint — shellcheck (pull_request) Successful in 8s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 20s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 6s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 43s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 5s
ci-pangolin / Go — build + test (pull_request) Successful in 2m5s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 1m58s
ci-pangolin / Golden — 视觉回归 (components + auth) (pull_request) Successful in 39s
CI 暴露:e2e smoke 断言 /v1/usage/devices 返 200,但该路由(deviceUsageHandler)是
未提交的 stats-overhaul WIP,committed server 没注册它 → 404 → assertStatus200
(t.Errorf 非致命)记错 → TestE2ESmoke FAIL(尽管 HTTP/enroll/ReportUsage 全 ✓)。
本地复现不了(工作区有 WIP、路由在)。去掉该断言,/v1/usage 保留。
这是第 4 个'测试依赖未提交 WIP'实例,与契约测试同源。待 stats-overhaul 合并后补。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 18:38:15 +08:00 |
|
wangjia
|
eb1bd11439
|
fix(test): 契约测试解耦 stats-overhaul WIP(CI 抓到的真问题)
ci-pangolin / Lint — shellcheck (pull_request) Failing after 9s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 18s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 6s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 39s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Successful in 2m2s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 2m14s
ci-pangolin / Golden — 视觉回归 (components + auth) (pull_request) Successful in 31s
CI 暴露:我的契约测试引用了未提交的 stats-overhaul 文件,本地能过(文件在工作区)、
CI checkout 已提交代码就崩:
- api_contract_test.dart import device_usage.dart(?? 未提交)→ flutter analyze 报
'Target of URI doesn't exist'
- usage/contract_test.go 引用 DeviceUsagePoint/deviceUsageResponse(HEAD service.go/
handler.go 均无)→ go-server 编译失败
摘掉 /v1/usage/devices(DeviceUsage)契约,保留 /v1/me + /v1/usage(已提交)。
待 stats-overhaul 合并后再补 DeviceUsage 契约。
验证:flutter analyze test/contract 'No issues';go vet usage 无错。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 18:13:27 +08:00 |
|
wangjia
|
64f96fe018
|
refactor(api): /v1/me 收敛 expire_at → 只发 expires_at(无 web 用户中心)
之前后端同时发 expire_at + expires_at(分别给 app + web 用户中心)。现已无 web 端,
双发是纯冗余,统一只用 expires_at:
- account.go:meResponse 删 ExpireAt 字段 + GetMe handler 不再赋值
- client me.dart:fromJson 删 (expires_at ?? expire_at) 别名回退
- 两端契约快照同步:Go meResponse 冻结集去 expire_at(12→11 key)、删 Dart 别名测试
- 验证:go test httpapi 绿、flutter test contract(14)绿、analyze EXIT 0、e2e 全链路绿
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 10:08:50 +08:00 |
|
wangjia
|
fd82d82fe3
|
test(e2e): 第5刀b L4 gRPC 全链路(enroll→ReportUsage→统计真入库真读出)
在 HTTP 段基础上接 gRPC mTLS 全链路,真验证「统计准不准」:
- 脚本:生成 Node CA + gRPC server 证书(镜像 deploy.sh)、起 server 带 gRPC、
nodectl 签 bootstrap token,传 E2E_GRPC_ADDR/CA/token/node 给 driver
- driver:复用 agentd.EnsureEnrolled 真 TCP enroll → enroll 到的 client cert
建 mTLS → ReportUsage 注入 1GiB/100MiB(dp_uuid=用户) → 轮询 /v1/usage
断言 bytes_up=1GiB·bytes_down=100MiB 真入库真读回
- 本地全链路绿;go build ./... 不受影响(tag e2e)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 07:04:26 +08:00 |
|
wangjia
|
b3cbeef7cf
|
test(e2e): 第5刀a L4 进程级冒烟编排 + HTTP 段
真起 server 二进制(sqlite + Redis)跑 HTTP 用户旅程,验证 in-process 测试给不了的
信心(二进制起/迁移/路由/鉴权/DB 端到端):
- scripts/e2e-smoke.sh:起栈编排(Redis 三选一:已有 addr / docker / 内嵌 miniredis
兜底)+ 随机端口 + trap 清理;调 Go driver
- server/test/e2e/smoke_test.go (tag e2e):造用户(复用 argon2 HashPassword)
→ 登录 → /v1/me(dp_uuid/plan/email) → /v1/usage 鉴权 + 无 token 401
- server/test/e2e/miniredis (tag e2e):无 docker 时 Redis 兜底 launcher
- 本地绿;go build/test ./...(无 tag)仍 25 包绿,tag e2e 文件被静默跳过
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-25 06:59:39 +08:00 |
|
wangjia
|
747ceb06f0
|
test(contract): 第2刀c Go 契约快照(/v1/me·usage·UsageEntry)+ Go CI job
支柱 2(契约单源)后端守门 + 两端对账:
- httpapi/contract_test.go:冻结 /v1/me 字段面(12 key,与 Dart _frozenMeKeys 对账,
注明后端多发 uuid/dp_uuid + expire_at 别名)
- usage/contract_test.go:冻结 /v1/usage(UsagePoint)、/v1/usage/devices
(DeviceUsagePoint + envelope)字段面
- pb/agentv1/contract_test.go:冻结 agent↔控制面 UsageEntry 字段面
- ci.yml:新增 go-server job(golang:1.25 build+test)——此前 server 测试未进 CI
- 本地:5 契约用例 + 全量 go test ./...(25 包)绿
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-24 23:35:23 +08:00 |
|
wangjia
|
7a4d0cf7cd
|
fix(diag): host 参数走 base64 避免 GFW 拦明文敏感词诊断请求
控制面跑明文 HTTP 在国外 IP 上,白盒诊断请求 URL 里出现 google/youtube 等 GFW
敏感词时被墙重置(返回空)→ 出海段拿不到、报告显示「—」。改:host 用 base64url
传(host_b64),响应也回 b64、不回明文,明文里无敏感词(不影响真实加密隧道流量)。
- diag.go: EgressTiming 支持 ?host_b64=,解码后查白名单,响应 echo b64
- vpn_whitebox.py: node_egress 用 base64url 编码 host
验证:cara(墙内)google/youtube 出海段从 None → 8-9ms,与 github/cloudflare 一致。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-22 14:34:05 +08:00 |
|
wangjia
|
bc890974c0
|
fix(server): 补 #5 国内分流的 DNS 面 + 链路诊断端点(#12)
#5 只分了数据面(geoip/geosite-cn 路由 direct),DNS 仍 final:remote 全量经隧道
解析 → 国内域名解析到非 CN IP、漏过 geoip-cn 又走隧道(白盒实测国内 TLS
1000-1660ms;修后 ~50ms)。
- clientconfig.go: 开分流时 dns.rules 加 {rule_set:[geosite-cn]→local},国内域名
用 local(223.5.5.5)直连解析 → 拿到真 CN IP → geoip-cn 命中直连
- main.go: PANGOLIN_PUBLIC_URL 缺失时启动告警(空则分流静默跳过,是隐蔽坑)
- nodes.go: connect 渲染加可观测日志(split_cn/rules_base/split_active/bytes)
- diag.go: 新增只读端点 GET /v1/diag/egress?host=X,节点侧量出海段耗时(白名单
防 SSRF、只回耗时数字),供白盒拆「接入段 vs 出海段」
验证:go test(splitCN 开渲染 dns.rules→local、关无 dns.rules)+ go vet;cara
实测国内 TLS 1000ms+→~50ms、接入段占 TLS 握手 ~98%。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-22 14:12:31 +08:00 |
|
wangjia
|
43b25c8aa0
|
feat: 国内流量直连分流(geoip-cn / geosite-cn)— #5
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
国内 IP/域名直连(不走隧道)→ 省流量 + 国内访问快;非国内走代理。
- clientconfig.go: BuildClientConfig 加 ClientConfigOpts{SplitCN,RulesBaseURL};
开启时 route 加 {rule_set:[geoip-cn,geosite-cn]→direct} + 定义 rule_set
(remote .srs,download_detour:direct 直连下载)
- rules.go: 控制面静态服务 /v1/rules/{name}.srs(白名单防穿越)——自托管避免
GitHub 在国内被墙的鸡生蛋;客户端反正连控制面,可达性有保证
- nodes.go ConnectNode: 读 ?split_cn → opts;NodeAPI 加 rulesBaseURL
(PANGOLIN_PUBLIC_URL);main.go 挂 /v1/rules 路由 + RulesHandler
- 客户端: connect_api splitCN→?split_cn=1;connection_provider 传 smartRoute 偏好
- deploy/single-node: 拉 geoip-cn/geosite-cn.srs 到 $DATA_DIR/rules +
设 PANGOLIN_PUBLIC_URL/PANGOLIN_RULES_DIR
验证:go test(splitCN 开/关渲染 + RulesHandler 白名单/404)+ flutter analyze +
shellcheck;不需要节点。订阅链接暂用默认 opts、DNS 分流为后续增强。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-19 20:34:00 +08:00 |
|
wangjia
|
cf2bd93c93
|
feat(server): agent 热重载 sing-box(SIGHUP)替代全量重启(#3)
sing-box 支持 SIGHUP 热重载(cmd_run.go:进程内 check()验证→close 旧实例→
start 新实例;坏配置保留旧实例继续跑、不断网)。原 agent 用 systemctl restart
(全量重启:断所有连接 + 坏配置让 sing-box 起不来全断)。
- Restarter 接口加 Reload;SystemdRestarter.Reload 取 MainPID(systemctl show
-p MainPID)发 SIGHUP——agent 与 sing-box 同 pangolin 用户,免 polkit/不改 unit;
PID 取不到或发信号失败回退 Restart
- SingBox.started:首次/进程未起走 Restart(冷启动),之后配置变更走 Reload
- noopRestarter/fakeRestarter 补 Reload;加 TestWriteAndRestartFirstColdThenReload
价值:切节点/续期/加删用户不整进程重启、不全断流;坏配置不至于打死节点。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-19 19:36:46 +08:00 |
|
wangjia
|
da8f3a4fef
|
feat(server): Hy2 自签 TLS 证书(方案①)— 补全 hysteria2 入站可用
Hy2 跑在 QUIC 上、强制 TLS,需服务端证书(不像 REALITY 借大站证书)。此前
handler_grpc 只发 ListenPort、cert 路径为空 → hysteria2 入站起不来。改为节点自签:
- agentd/hy2cert.go: ensureSelfSignedCert 生成 ECDSA P-256 自签证书到
/etc/sing-box/hy2.{crt,key}(幂等,缺失才生成;key 0600;SAN=reality SNI)
- singbox.go: writeAndRestart 渲染前对启用 hy2 的节点 ensure 证书并把
CertPath/KeyPath 写回 hy2 配置
- httpapi/clientconfig.go: hy2 出站 TLS 加 insecure:true + server_name,收自签
(两端自有,服务端鉴权靠 per-user 派生的 hy2 密码)
验证:单元测试(证书可被 crypto/tls 加载/幂等/0600)+ sing-box check 接受自签 hy2 入站。
注:节点实际启用 hy2 还需配 Hy2Port + 放行 UDP(单独步骤)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-19 12:12:35 +08:00 |
|
wangjia
|
64a1a64c3f
|
feat(server): 按用户精确统计流量 — agent 改读 v2ray_api StatsService
节点 sing-box 编入 with_v2ray_api 后,每个 dp_uuid 有独立计数器
user>>>{dp_uuid}>>>traffic>>>uplink|downlink。agent 用 QueryStats(reset=true)
取窗口 delta,按用户精确上报(替代旧 clash 节点总量分摊:单用户准、多用户近似)。
- render.go: experimental.v2ray_api(loopback :19091)+ stats.users 列全部 dp_uuid
- v2rayapi/: vendor sing-box stats.pb.go(消息)+ 手写 client(用 v2ray 规范
ServiceName 路径,绕开生成代码的 experimental.v2rayapi.* 误名)
- usage_v2ray.go: V2RayUsageSource,uplink/downlink 天然用户视角(无需 swap)
- agent.UseV2RayUsage() 取代 UseClashUsage();clash_api 保留作本地调试
节点需部署带 with_v2ray_api 的 sing-box(已编好 linux/amd64 二进制)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2026-06-19 10:18:33 +08:00 |
|