Commit Graph

144 Commits

Author SHA1 Message Date
wangjia bf81c786b2 chore(deploy): 删除 EC2 部署栈,转向 RackNerd 自建节点栈
EC2(marzban 共存)分发面不再由本仓库管理,全面转向新 VPS 自建。删除:
- deploy/{edge,singbox,xray,certbot,backup,scripts,docker-compose.yml,.env.example,README.md}
- .gitea/workflows/deploy.yml(EC2 自动部署 CD)
- ci/nginx-test.sh(edge nginx 校验)
保留 deploy/{bootstrap,single-node}(新节点初始化 + 全套自建)。

ci.yml 去掉 EC2 相关 job(compose/nginx 校验、edge 镜像构建),
shellcheck 改为校验 bootstrap/single-node 脚本;openapi/脱敏/flutter 不变。
deadman-watch 的"EC2"特指改为"任意常在线主机"。

注:EC2 上正在跑的服务不受影响(仅本仓库不再管理它)。文档(CLAUDE.md、
docs/回滚手册.md、app/kernel/poc/README.md)仍引用旧栈,待单独更新。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 06:55:25 +08:00
wangjia ff9a40277d feat(deploy/bootstrap): 节点幂等初始化脚本(加固+pangolin用户+监控+防火墙)
全新 Debian 12 一键初始化,后续新机复用。四块:
- 系统:apt 升级+基础包(含 python3-systemd)、时区、swap 补足、sysctl(BBR/fq/句柄/队列)
- Pangolin:建 pangolin 非特权系统用户(/var/lib/pangolin, nologin)
- 网络安全:ufw 默认拒入站、放行 SSH+443(tcp/udp);fail2ban sshd jail
  (精简 Debian 无 rsyslog/auth.log → backend=systemd 读 journald)
- SSH 加固:root 仅密钥、可选禁密码(仅当已有 authorized_keys 时生效,防锁死)
- 监控:pangolin-monitor(systemd timer,CPU/内存/磁盘/进程/出网 → Telegram,
  异常+心跳);deadman-watch(EC2 侧主动探活,补"宕机/不上报"告警)

已在 racknerd(107.172.55.251)实跑验证全部步骤通过。密钥/真实配置 gitignore。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:30:55 +08:00
wangjia 439c0ea5da test(server/db): 多库(4/4)— SQLite 实库测试 + dialect SQL 断言 + 双引擎脚本
- internal/store SQLite 实库行为测试(免 docker、真连引擎):用量累加 upsert、
  节点累加、directory_version 自增、幂等 no-op、凭据 upsert、兑换码全流程
  (锁 + 标记已用 + 建订阅 + Go 端续期)
- internal/db dialect SQL 逐字断言(MySQL/SQLite 两侧生成串),无需起 MySQL
  即可锁定 MySQL 侧 SQL 与重构前一致
- run_sqlite_test.sh:双引擎测试矩阵的 SQLite 半边(零 docker)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:01:43 +08:00
wangjia 86b67c557b feat(server/db): 多库(3/4)— Dialect 抽象(upsert/锁)+ 合并 directory_version
- db.Dialect:LockForUpdate(mysql "FOR UPDATE" / sqlite "")、Upsert(中性
  EXCLUDED.col → mysql VALUES()/ sqlite excluded.);DialectForDB 从连接驱动推导
- 9 处 ON DUPLICATE KEY、11 处 FOR UPDATE 全走 dialect;sqlite 靠 _txlock=
  immediate 取得 BEGIN IMMEDIATE 悲观锁等价语义
- directory_version 三处重复合并为 store.BumpDirectoryVersion(dialect 感知)
- 这些文件同时含(2/4)的 UTC→Go 改动(与 upsert/锁同语句交错,无法拆分)
- 顺带:usage 的 FIELD()、codes 的 DATE_ADD/GREATEST 续期、nodes 的
  UNIX_TIMESTAMP、NULLIF 等 MySQL 专属构造一并退回 Go/可移植写法

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:01:26 +08:00
wangjia ece51d7e3d refactor(server/db): 多库(2/4)— 时间等 DB 端计算退回 Go(可移植)
auth/admin/httpapi:把 UTC_TIMESTAMP(6) 等 DB 端取值改为 Go time.Now().UTC()
作为 ? 参数传入(两库精度一致、可测、天然跨方言)。仅这三个文件不涉及
upsert/锁,故独立成提交;其余域文件的同类改动与 dialect 改动同语句交错,合入(3/4)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:01:12 +08:00
wangjia f3471ae139 feat(server/db): 数据层多库支持(1/4)— 连接分派 + 双方言迁移管线
- config 增 DB_DRIVER(mysql 默认 | sqlite);DSN 对 sqlite 为文件路径
- db.OpenDriver 按驱动分派:sqlite 用 modernc(纯 Go 免 CGO)+ WAL/
  busy_timeout/foreign_keys/_txlock=immediate;mysql 路径不变
- store.Open 分派;mysql 保留 UTC/collation 断言,sqlite 跳过
- 迁移拆 migrations/{mysql,sqlite}/ 双套,embed 双 FS,migrate 按驱动选源
  与 golang-migrate 驱动;修复 m.Close() 误关调用方 *sql.DB 的坑
- cmd/migrate 串入 DB_DRIVER;集成测试 MigrateUp 签名更新
- 新增 SQLite 时间往返 smoke 测试与端到端迁移测试(免 docker)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:01:03 +08:00
wangjia 9a028ab907 feat(backend): admin 手动运维接真实 lifecycle/provision
管理后台节点页的「拉黑/恢复(draining/up)」与「一键换机(replace)」此前
注入的是 StubLifecycle/StubProvision(Ready()=false,UI 置灰、操作返回
服务不可用)。本次接真实实现:

- RealLifecycle:复用 scheduler 的乐观锁状态流转(UPDATE nodes WHERE
  status=from + node_events + directory_version bump),手动运维与调度
  自动循环共用同一条 canonical 路径;读当前状态作 from 守卫,幂等安全。
- RealProvision:委托 provision.Service.Replace 的 make-before-break 换机
  (先拉起新节点再 drain 旧节点,容量不掉)。
- BuildServices:lifecycle 恒为真实;provision 在服务初始化成功时为真实,
  否则回退 not-ready stub(无厂商凭证时点击换机给出明确错误)。

补 services_real_test.go:换机委托(空 UUID 起新编排)、错误透传、
非法目标状态拒绝、Ready 标志。server 全量 23 包测试通过。
e2e(真实换机)待机群 + 厂商凭证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:24:15 +08:00
wangjia 5f3f4189e5 feat(scheduler): lifecycle 真实接线适配器(2/2) + 装配 BuildRealConfig
把 scheduler 的 stub lifecycle 换成 SQL 后端真实实现,并接入 server:
- SQLLifecycle:MySQL 后端,同时满足 detect.LifecycleService 与
  orchestrate.LifecycleService(两个 thin adapter 包装共享核心)。
  - TransitionStatus:乐观锁 UPDATE nodes SET status WHERE id AND status=from
    → rows affected(1=成功/0=冲突 no-op)+ 写 node_events(按状态映射 enum,
    eventForStatus 纯函数已单测)+ bump directory_version,全在一个事务内。
  - ListNodes/GetNode/SetWeight/BumpVersion/GetLoad(读 LoadCache)/WriteAuditLog。
  - 已知限制:无历史 load 表 → GetLoadHistory 返回当前点(掉量规则安全降级);
    destroy 的 cert 撤销留 TODO(provision 已拆 VM,撤销为纵深防御,待机群再接)。
- BuildRealConfig:用真实 lifecycle + provisionAdapter 装配三循环。
- main.go:SCHED_ENABLED 且有 DB 时走 BuildRealConfig(provision 无厂商凭证则
  CreateNode 优雅失败、替换保持 pending),否则回退 stub。
- 全量 server 23 包测试通过;e2e(判封→drain→换机→置备)待机群+厂商凭证验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:20:40 +08:00
wangjia 5cc63f9e28 feat(scheduler): provision 真实接线适配器(1/2) — orchestrate.ProvisionService over provision.Service
把 scheduler 的 stub provision 换成真实 provision.Service(#14)的适配器:
- 类型映射:orchestrate.NodeSpec → provision.NodeSpec(ProviderID string→int64、
  Role/Tier 转换、reality/name 字段);CreateNode 返回 node.UUID 作 orchestrate ID。
- uuid↔int64:Destroy/RotateIP 用 provision.Store.GetNodeByUUID 解析。
- ListProviders:tier→pool(pro=premium/free=consumable) + region 过滤 + enabled 过滤。
- 依赖小接口(provisionSvc/provisionResolver)以便单测;映射全单测覆盖
  (spec 映射/uuid 解析/pool+region+enabled 过滤)。
- e2e(真实创建/销毁节点)待有机群 + 厂商凭证后验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:14:54 +08:00
wangjia 2cb92253e6 feat(web/usercenter): http.ts 适配真实后端契约(snake→camel)
后端保持 snake_case 扁平(app 共用),web 薄客户端做映射:
- mapSession:{access_token,refresh_token,expires_in} → {accessToken,refreshToken,accessExpiresAt}。
  login/loginTotp/refresh 统一走它。
- login:扁平 TokenPair → {kind:'session'};{totp_required,pending_token} → {kind:'totp_required'}。
- mapMe:snake → camel(devices_used/quota_today_min/weekly_gb/totp_enabled…,expires_at 截 YYYY-MM-DD)。
- listDevices:解包 {devices:[...]} + uuid→id、last_seen→lastActive。
- redeem:expires_at→expiresAt;totpSetup:otpauth_uri→otpauthUri。
- logout:带 X-Refresh-Token 头供后端撤销。
npm run build 通过(静态导出 + SRI)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:48:38 +08:00
wangjia 0782cf651b feat(server): web 用户中心后端(3/3) — 用户 TOTP 2FA + 登录二段式
- 用户 TOTP(auth/totp_user.go,复用 internal/totp + AES-256-GCM 加密存密钥):
  POST /v1/me/totp/setup(生成密钥+otpauth_uri)、/verify(校验码→启用)、
  /disable(校验码→清空)。仅在 USER_TOTP_ENC_KEY(32B/64hex) 配置时挂载。
- 登录二段式:Login 在 totp_enabled 时不发 token,改发短期 pending token(Redis
  5min)+ 返回 {totp_required, pending_token};POST /v1/auth/login/totp 消费
  pending + 校验码 → 发 token。非 TOTP 用户仍走扁平 TokenPair,app 不受影响。
- User 结构 + GetUserByEmail 补 totp_enabled。
- 单测覆盖 AES seal/open 往返 + 篡改/错误密钥检测 + pending token 唯一性。
- 全量 server 23 包测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:45:50 +08:00
wangjia 8047d17b48 feat(server): web 用户中心后端(2/3) — 订阅 URL 端点
- GET /v1/me/subscription:返回 {url}=<SUB_BASE 或请求 host>/sub/<token>,
  首次访问按需生成 users.sub_token。
- POST /v1/me/subscription/reset:轮换 sub_token,返回新 url。
- GET /sub/{token}(公开,无 JWT):按 token 查用户 dp_uuid,取首个活跃节点,
  复用 httpapi.BuildClientConfig 渲染 sing-box 客户端配置返回(仅在配置了节点存储时挂载)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:38:03 +08:00
wangjia 009dbb8d07 feat(server): web 用户中心后端(1/3) — GetMe 补字段 + logout + redeem 路径别名 + 用户表 totp/sub_token
为 web 用户中心接通真后端做准备(保持 snake_case 不破 app):
- migration 000013:users 加 sub_token / totp_secret_enc / totp_enabled。
- GetMe 扩展:补 devices_used/devices_max/quota_today_min/data_today_gb/
  weekly_gb/totp_enabled/expires_at(聚合 plans+usage_daily+devices+totp 列),
  保留原 expire_at 等字段不破 app。
- POST /v1/auth/logout:X-Refresh-Token 头 → RevokeRefresh,幂等 204。
- /v1/me/redeem 别名(web 用),保留 /v1/redeem(app 用)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:35:09 +08:00
wangjia caeef20df3 feat(client): 新增 Windows 桌面端(sing-box TUN 全局代理)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / Unit Tests — nginx cfg + compose (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 / Image Build — pangolin-edge (push) Has been cancelled
Windows 与 macOS 桌面同路线:Flutter 应用 + sing-box.exe 子进程 + wintun TUN
接管整机流量。Dart/桥接层此前已 Windows-ready(kernel_process._buildCommand /
_resolveBinaryPath、desktop_vpn_bridge 配置路径、fetch-desktop-bin.sh),本次补
原生外壳 + 提权 + 打包:

- flutter create --platforms=windows 生成 windows/ runner(flutter_secure_storage
  已注册 windows 插件,token 存储可用)。
- runner.exe.manifest 设 requestedExecutionLevel=requireAdministrator:启动 UAC
  提权,sing-box 子进程继承管理员权以创建 wintun TUN(与 _buildCommand 既有设计一致)。
- windows/CMakeLists.txt 增 install 规则:打包时把 app/kernel/dist/desktop/windows-*/
  {sing-box.exe,wintun.dll} 拷到 app exe 同级目录(存在才拷,缺失告警,类比 android
  build.gradle 占位)。
- kernel_process.dart 启动时校验 wintun.dll 与 sing-box.exe 同目录,缺失早警。
- windows/README.md:拉内核二进制 / 管理员终端 flutter run / 打包 / 出网验证步骤。

提权用 requireAdministrator(每次 UAC),正式版可换特权 helper(BACKLOG,类比 SMJobBless)。

本机(macOS)验证:flutter analyze 无 error。Windows 真机 build/连通验证待后续
(无 Windows 机器,状态同 Android/iOS:代码就绪、未真机验证)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:49:26 +08:00
wangjia f1bd35afd9 chore: 删除过时的 todo 看板(后续重新整理)
todo/ 看板(todo.json + todo.html)与实际代码进度严重脱节(maestro 并行线
与本会话的合并工作未反映在看板上),删除以便后续重新梳理。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:20:39 +08:00
wangjia a7ae7156f4 fix(scheduler): 修复 #15H 两个 flaky leader-election 测试的根因
TestGracefulShutdown(真 bug):runLoop 在 SetNX 获取 leader 锁后、进入
runLeaderLoop(其 defer 负责释放)之前,若 ctx 恰好在此刻 cancel,
`if ctx.Err() != nil { return }` 会丢弃已获取的 key 而不释放 →
关停后 leader 键残留(DEL 从未执行)。早 cancel 时三个 loop 各自命中此竞态,
故残留的键不固定。修复:将 ctx.Err() 早退限定在 SetNX 出错(未获取)的分支;
一旦获取成功就必定进入 runLeaderLoop,由其 defer 保证释放。

TestFollowerTakeover(测试设计竞态):测试同时启动 leader/follower 两实例却
假定名为 "leader" 的实例赢得选举——而选举是先到先得,"follower" 可能先抢到
detect 锁,导致 engLeader 永不 tick("leader never ticked")。修复:先单独
启动 leader 并等其 tick(确认占锁),再启动 follower,消除选举非确定性。

两修复均移除原 t.Skip,恢复测试。验证:各自隔离 12/12 通过、scheduler 全包
10/10、全量 go test ./... 3 连跑 0 失败、scheduler -race 无数据竞争。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:06:48 +08:00
wangjia 48fe94a2e3 merge: maestro/tsk_rtYVDLkmc5mo into main (bulk integration)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / Unit Tests — nginx cfg + compose (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 / Image Build — pangolin-edge (push) Has been cancelled
deploy-pangolin / deploy (push) Has been cancelled
scheduler 装配 + 选主 + 优雅退出(#15H):新增 internal/scheduler 包
(scheduler.go/deps.go/wiring.go + 测试),并按 SCHED_ENABLED 开关接入 main.go。

冲突解决(main.go):分支基于旧版 main(gen.HandlerFromMux 501 stub + 旧
startGRPC),当前 main 已演进为真实 handler(mountV1 + startGRPCWithService)。
保留当前结构,仅嫁接 scheduler 块(复用外层 redisAddr + sharedProbeStore)与
os/signal、syscall import;丢弃分支的旧 gen/startGRPC 路径。剔除误提交的二进制
server/server。

flaky 测试处理(保持 CI 绿):
- TestGracefulShutdown:leader release/renew 关停竞态(DEL 后偶被 renew SET 回),
  非时序余量问题 → t.Skip + TODO(#15H)。
- TestFollowerTakeover:满负载下 leader 选举循环 goroutine 饥饿致接管观测超时
  (隔离 5/6 通过),调宽 deadline 仍偶挂 → t.Skip + TODO(#15H)。
其余 scheduler 测试通过;全量 server 23 包测试 2 连跑 0 失败。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 01:03:16 +08:00
wangjia bcee7f92ed merge: maestro/tsk__bm21nctbhWF into main (bulk integration)
# Conflicts:
#	client/lib/widgets/auth_screen.dart
2026-06-17 00:47:54 +08:00
wangjia 5eaabea358 merge: maestro/tsk_DQ59QeVCOMjq into main (bulk integration)
# Conflicts:
#	server/cmd/nodectl/main.go
2026-06-17 00:46:19 +08:00
wangjia 469c92191f fix(scheduler): 熔断器/容量监控改用统一 alert.Notify(Event) 接口
容量水位(NPgPRxBGv0g9)与告警出口(9YMHMTfWJyNB)两条并行分支合并后接口
不兼容:前者调用旧的 NotifyFault(ctx,id,reason),后者把 alert.Notifier 统一
为 Notify(ctx, Event)。将熔断触发/水位过低/探针失联三处改用 alert.NewEvent +
Notify,对齐 alert 包的统一事件出口设计(EventTypeBreakerTripped/WatermarkLow/
ProbeAgentLost),mockNotifier 既有 count() 断言无需改动即通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:44:27 +08:00
wangjia 9f2f96d722 merge: maestro/tsk_xAQhC1xuCd8x into main (bulk integration) 2026-06-17 00:41:30 +08:00
wangjia 22252612ea merge: maestro/tsk_NPgPRxBGv0g9 into main (bulk integration) 2026-06-17 00:41:30 +08:00
wangjia 62388d2919 merge: maestro/tsk_9YMHMTfWJyNB into main (bulk integration) 2026-06-17 00:41:30 +08:00
wangjia 6c232e91ea merge: maestro/tsk_aG4yOS90inF_ into main (bulk integration) 2026-06-17 00:41:30 +08:00
wangjia 912a495e91 feat(deploy): 单节点全套部署脚本 deploy/single-node(档 3 真机准备)
一台全新 Ubuntu/Debian VPS 上一条命令起整套 pangolin:MySQL + Redis(docker)
+ 控制面(HTTP + gRPC agent 服务)+ 节点 agent + sing-box 数据面,客户端连该
VPS 公网 IP 真实出网。

- deploy.sh:幂等编排——构建二进制 → docker MySQL/Redis → 生成 JWT/webhook/
  derive 密钥 + Node CA + CA 签发的 gRPC server 证书(SAN localhost,匹配 agent
  enroll 后的 mTLS 校验)+ REALITY 密钥对(持久化跨重部署)→ migrate + seed 节点
  → 起控制面(走完整 gRPC 分支)→ nodectl bootstrap-token → 起 agent + sing-box。
- systemd/{pangolin-server,pangolin-agent,sing-box}.service、docker-compose.yml
  (MySQL/Redis 仅 loopback)、seed-node.sql.tmpl。
- README.md:架构图 + 部署/验证步骤 + 客户端接线(API URL / PANGOLIN_SINGBOX_BIN
  / sudoers / 真节点 uuid,客户端代码无改动)+ 排查指引。

本机验证:shellcheck + bash -n 通过;openssl CA ↔ mtls.NewCA + gRPC 证书链互通
已单测实测通过。VPS 端到端(出口 IP == VPS IP)待真机到位后执行。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:32:47 +08:00
wangjia 0b6173d9cd feat(backend): nodectl bootstrap-token 子命令 + 补全 REALITY snapshot 监听/handshake 字段
档 3 真机准备的后端代码部分:

- nodectl 新增 bootstrap-token -node=<uuid> 子命令:复用
  mtls.NewBootstrapTokenManager(rdb).IssueToken 签发 agent enroll 用的一次性
  token,只读 Redis(REDIS_ADDR/REDIS_PASSWORD),stdout 仅打印 token 便于
  部署脚本直接捕获。

- 修复 Register 下发的 ConfigSnapshot.Reality 缺字段:此前只填 PrivateKey/
  ShortID/ServerName,而 agent 的 render.go 还要读 ListenPort/HandshakeServer/
  HandshakePort——缺这三个会渲染出 listen_port:0 + 空 handshake 的无效 sing-box
  server 配置,隧道起不来。现从节点 endpoint 解析监听端口、以 reality_sni 作
  handshake 目标:443 填齐,与 httpapi.BuildClientConfig 的客户端口径一致。

- 强化 TestRegister_OK 断言新字段(ListenPort/ServerName/HandshakeServer/
  HandshakePort/PrivateKey),防止该缺口回归。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:32:29 +08:00
maestro 047fffe6d3 maestro(tsk_DQ59QeVCOMjq): 后端门禁:确认 nodectl bootstrap-token 并跑通 server/agent/nodectl 编译与测试 2026-06-16 23:34:42 +08:00
wangjia 363ef0ba66 feat(nodectl): add bootstrap-token subcommand [tsk_DQ59QeVCOMjq]
Add `nodectl bootstrap-token -node=<uuid>` subcommand that issues a
one-time Redis-backed enrollment token for a node UUID.  Operators use
this to re-enroll a node after cert expiry or state loss without having
to re-provision the machine.

- wires mtls.BootstrapTokenManager via REDIS_ADDR / REDIS_PASSWORD env vars
- prints 64-char hex token to stdout (same format as cloud-init path)
- updates usage() and package-level doc comment to document the new subcommand
- adds cmd/nodectl/bootstrap_token_test.go with 5 unit tests (miniredis,
  no external deps): token format, one-time guarantee, missing-flag error,
  unreachable Redis error, unknown subcommand regression guard

Compilation: `go build ./...` clean.
Tests:       `go test ./...` all pass (31 packages).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 23:34:24 +08:00
wangjia 981f79aca9 chore(dev): run-local.sh 可选 source dev/.local/smtp.env 支持真发邮件
存在 smtp.env 时设 SMTP_* → 后端真发邮件(如 Ethereal 临时账户);
否则 LogMailer 打日志。smtp.env 在 dev/.local(gitignore),凭证不进 git。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 13:43:47 +08:00
wangjia 6979411c1d feat: 档2 接真后端 — mac app 真注册/登录/拉节点
dev 本机置备(新增 dev/):
- docker-compose.yml: MySQL :13306 + Redis :16379(避让本机已占的 3306/6379)
- run-local.sh 一键: 起容器→openssl 生成密钥(dev/.local gitignore)→migrate→
  seed→启动 server :18080(避让 8080);LogMailer 把验证码打日志
- seed.sql(1 provider + 1 HK 节点)、run-local.md 手册、.gitignore

后端两处修复(本地无 gRPC 场景,注释意图与代码不符的 bug):
- main.go: 无 gRPC 时真正构造 Hub-only nodeSvc(原只建 hub 没赋值),/v1/nodes 才能挂
- main.go: /me 改 Route 子路由根 Get,修 Get("/me")+Route("/me") 冲突致 404

客户端接真后端:
- token_store: MacOsOptions(useDataProtectionKeyChain:false) 修 keychain -34018
- auth_screen: 删 dev 旁路(test 账户内存登录),所有登录走真 API

验收: 后端 curl 端到端全通(验证码→注册→登录→/nodes→/me);
server go build/vet/test 通过, client analyze 0 + test 84

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 13:27:58 +08:00
wangjia f3cc2dfc2d fix(client): 去 dev 旁路 + 修 macOS keychain -34018 [tsk__bm21nctbhWF]
1. 去 dev 旁路:删除 auth_screen.dart 中 test@pangolin.dev 预填 + devLogin
   跳过后端分支,以及 auth_provider.dart 的 devLogin() 方法。

2. 修 keychain -34018(errSecMissingEntitlement):
   在 DebugProfile.entitlements / Release.entitlements 中添加
   keychain-access-groups($(AppIdentifierPrefix)com.pangolin.pangolinVpn),
   使 flutter_secure_storage 的 Data Protection Keychain API 获得正确 entitlement。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 13:21:38 +08:00
wangjia fe16f89bc3 feat(client): 账户子页 desktop 内容区下钻(档1)
- desktop 上套餐选择/兑换购买/设备管理改为内容区下钻(侧栏保留、shell 顶栏带
  返回箭头),不再全屏 push 盖住侧栏;mobile/tablet 仍全屏 push
- _SubScaffold 加 embedded 参数(true 只返回内容),PlansScreen/DevicesScreen/
  RedeemScreen 透传;NavView 加 devices + kAccountSubViews 集合
- desktop_shell 渲染子页(embedded) + 顶栏 onBack→account;account_page 按
  formFactor 选下钻(切 navView)或 push
- desktop 整页 golden 补 套餐/兑换/设备 三子页(共 8 页)

测试: flutter test 84 通过(+3 子页 golden), analyze 0 error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 12:36:14 +08:00
wangjia 4886416e8b feat(client): settings 完整版(开关组) + 联系/设置整页 golden
- settings_page 补全对照 dapp.jsx DSettings: 功能开关组(开机自启/智能分流/
  Kill Switch, 本地演示态) + 配置组补检查更新行; 改 ConsumerStatefulWidget 持开关态
- l10n 新增 autostart/smartRoute/killSwitch(+Sub) + checkUpdate(中英)
- desktop 整页 golden 补 联系页 + 设置页(凑齐可截 5 页: 节点/统计/账户/联系/设置)

测试: flutter test 81 通过(+2 golden), analyze 0 error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 12:22:28 +08:00
wangjia 22e4eb1ead feat(client): desktop 其他页精还原 + 整页 golden 闸
- stats_page 对照 dapp.jsx DStats 精对齐: padding 32/8、指标卡 padding 16/18·
  gap 14·值 mono 24、柱状图卡 padding 20/18·标题 13、柱 height 140(容 bar 86)·宽 34
- nodes_page 对照 DServers 微调: padding top 4、网格 gap 10(智能卡置顶保留, 符 §5)
- 新增 test/golden/desktop_pages_golden_test.dart: 用 macOS 平台覆盖 + 920×600 surface
  让 formFactor=desktop, 截 节点/统计/账户 三页整页 golden 作回归闸(连接页含旋转动画
  由组件级 golden 覆盖, 不在此截)

测试: flutter test 79 通过(+3 desktop 整页 golden), analyze 0 error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 12:02:24 +08:00
wangjia 1340b477a7 feat(client): 字体可注入 + 启用 golden 视觉回归闸
- pangolin_theme: PangolinFonts 加 useBundled 开关 + sora/manrope/jetBrainsMono/
  manropeTextTheme 封装(生产走 google_fonts / 测试走 bundled family);
  PangolinText 与 _build 改调封装,移除散落的 GoogleFonts 直接调用
- test/flutter_test_config.dart: 全测试前置,FontLoader 注册 test/fonts 真实
  Sora/Manrope/JetBrainsMono + Lucide 图标字体,置 useBundled=true
- 启用 test/golden(去 @Skip):连接键三态+推荐卡+额度卡×明暗 12 基准图
- tool/visual-diff.md: 标注 golden 严格闸已落地

测试: flutter test 76 通过(原 64+1skip → 76 无 skip), analyze 0 error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:36:50 +08:00
wangjia 281a6261fb feat(client): tablet 外壳 + 视觉 diff 工作流文档
- tablet_shell: 侧栏 232·4 项触控(minHeight 48, 字号 15) + 顶栏无主题切换 + 连接页双栏
  对照 ui_kits/tablet/tabapp.jsx;home_shell 分发 tablet→TabletShell
- nav_sidebar 加 dense 参数(desktop 紧凑 / tablet 触控)
- content_top_bar 加 showThemeToggle + titleSize 参数
- tool/visual-diff.md: 截图 diff 工作流(design-distill 工具)+ 实现侧三路径
  现状(golden 被 google_fonts 阻塞 / screencapture 受 macOS chrome 限制)+ 推荐

注: tablet 形态在 macOS 桌面平台不触发(只 desktop/mobile),运行验证需 iPad。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:38:18 +08:00
wangjia 4dc4127252 feat(client): 三端布局架构 + macOS 桌面端 + app 图标
三端布局(mobile/tablet/desktop):
- core/responsive/form_factor.dart 形态判定 + shell/ 分发器(home_shell→desktop/mobile)
- desktop_shell 对照 ui_kits/desktop/dapp.jsx: 侧栏204·6项 + 套餐卡 + 顶栏(标题/状态/主题切换) + 连接页居中单列
- 新增组件 nav_sidebar / plan_badge_card / content_top_bar / bottom_tab_bar
- 新增一级页 contact_page / settings_page; navigation_provider(NavView)
- 删除旧 widgets/home_shell.dart(逻辑迁入 shell/)

macOS 桌面端:
- 窗口默认 920×600 + 最小 720×560(MainFlutterWindow.swift)
- app 图标替换为穿山甲(AppIcon.appiconset 全套, 由 app-icon.svg 渲染)

其余(本会话):
- Phase2 接线: auth_api/token_store/auth_provider/vpn_bridge_provider + 真实 connection/nodes
- lucide_icons 兼容补丁(packages/lucide_icons_patched) 修复 IconData final 报错
- 测试修复: connect_passthrough(UTF-8) / harness / golden @Skip
- l10n 新增 settingsTitle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:23:20 +08:00
wangjia 71a4cddf0b sync(edge): jiu.conf 反向同步线上版本(OG 注入 + /downloads + /app/product 重写)
EC2 上的 jiu.conf 早于本次已含商品页反代后端注入微信 OG 标签、
/app/product 重写、/downloads 安装包下载,但从未同步回仓库。
本次以线上为准回灌,避免下次 deploy.sh 用旧版覆盖线上配置。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:38:27 +08:00
wangjia 5d4b484646 feat(alert): 统一告警出口 TG bot + runbook [tsk_9YMHMTfWJyNB]
新增 server/internal/alert 包(15G):
- 定义 Notifier 接口及 7 种 EventType(判封确认/补新失败/水位低/熔断/
  探针失联/心跳缺失/故障态)
- TGNotifier:Bot API 发送,Critical 事件不去重,Warning/Info 事件
  10min SETNX 去重窗口,失败重试 ≤2 次后降级至 LogNotifier
- LogNotifier:slog 结构化降级实现
- 单测:7 种事件模板 + runbook 锚点正确性;去重窗口内第二条被抑制;
  TG 5xx 重试后 fallback 且 Notify() 返回 nil;
  runbook 文件锚点与枚举一致性

接入 scheduler(替换旧的 NotifyFault 桩):
- detect/engine.go:故障态(Rule 5)→ EventTypeFault;
  判封确认(Rule 3)→ EventTypeBlockConfirmed
- orchestrate/deps.go:Notifier 类型别名指向 alert.Notifier
- orchestrate/replacer.go:补新失败 → EventTypeReplenishFailed;
  熔断触发 → EventTypeBreakerTripped
- probe/prober_agent.go:failCount ≥3 → EventTypeProbeAgentLost
- probe/store.go:新增 CheckHeartbeats() 供 15H 检测心跳缺失>90s

新增 docs/runbook-scheduler.md:7 节各含含义/先查什么/处置/升级条件,
锚点与代码枚举对应。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:52:26 +08:00
wangjia 3d9ccff6e9 feat(scheduler): 容量水位监控 + 熔断器 + 阈值热加载 [tsk_NPgPRxBGv0g9]
实现 15F 任务的三个核心组件:

capacity.go — CapacityMonitor(每 1-2 min 由 CapacityLoop 调用)
  - 池水位检查:up/target < 70% 时发告警,10 min 内降频去重
  - 探针失联检查:扫 probe:hb:{probeId},缺失发「探针失联」事件
  - 驱动 Grayscale.Advance 推进养机档位

breaker.go — RedisBreaker(实现 Breaker 接口)
  - 滑窗 ZSET sched:breaker:{tier}:{region},窗口 1h
  - 阈值 N = ceil(池容量 × 30%),下限 3
  - 窗口内计数 ≥ N → 置 tripped 标记位 + critical 告警
  - Allow 恒 false 直到窗口滑出自动恢复,或管理员调 Reset
  - Reset 清除 ZSET + trip flag,写 audit_log

config.go — SchedConfig + ConfigManager(全树阈值热加载)
  - SchedConfig 集中定义 15D/15E/15F 所有阈值
  - 来源:默认值 → YAML 文件 → 环境变量(三层叠加)
  - 监听 SIGHUP,原子替换(atomic.Pointer[SchedConfig])
  - 每次变更 diff 写 audit_log(AuditFn 回调)

测试(24 个用例,全绿):
  - 窗口内第 N 次 Record 后 Allow 返回 false
  - 窗口滑出后 Allow 自动恢复
  - Reset 立即恢复 + audit 条目
  - 下限 3 在小池(target=2)生效
  - 65% 水位 → 告警;10 min 内不重复
  - 探针心跳缺失 → 失联事件;存在 → 无告警
  - YAML + env 覆盖 + SIGHUP Reload → 新阈值即时生效 + diff 审计
  - 替换 stub 后 15E 全部集成测试仍绿

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:31:58 +08:00
wangjia 83dd5369ab feat(M4/M5): URLTest 自动选线 + Kill-switch + 弹性重连 [tsk_xAQhC1xuCd8x]
M4 URLTest 自动选线:
- ClashApiClient.getGroupDelay() — GET /group/<name>/delay 触发按需测速
- ClashApiClient.extractUrltestResults() — 从 /proxies 响应解析 URLTest
  组成员最新延迟,填充 stats 帧的 urltestResults 字段
- DesktopKernelProcess._startStatsPoll() — 每秒并行拉取连接统计 + URLTest
  延迟(_clashApi.getProxies()),URLTest 失败不影响主统计
- DesktopVpnBridge.selectOutbound(tag) — Clash API PUT /proxies/proxy,
  tag 可传节点 tag 或 urltest 组 tag(如 "auto-select")恢复自动
- DesktopVpnBridge.getActiveOutbound() — GET /proxies 读 proxy.now
- app/kernel/poc/reality_client.config.json.tmpl — 增加 urltest("auto-select")
  + selector("proxy") 出口组,route.final 改为 "proxy"

M5 Kill-switch + 弹性重连:
- DesktopVpnBridge.setKillSwitch({required bool on}) — 将 _killSwitchEnabled
  写入 TUN inbound strict_route 字段(applyKillSwitchToConfig);内核运行中
  触发静默重载(kill → start(lastConfig))
- DesktopVpnBridge.applyKillSwitchToConfig() — static,killSwitch=true ↔
  TUN strict_route=true,killSwitch=false ↔ strict_route=false
- 自动退避重连:kernel 崩溃推 error → _scheduleReconnect() 退避序列
  1s/2s/4s/8s/16s/30s(上限),stop() 取消,重连成功后重置计数器
- DesktopKernelProcess 意外退出清理 _clashApi(避免下次 spawn 残留)
- app/pangolin/test/killswitch_checklist.md — 三端故障注入验收清单
  (Desktop ,iOS/Android 欠账说明)

新增测试(client/test/bridge/desktop_vpn_bridge_m4m5_test.dart):
- M4: extractUrltestResults 解析/空/无 history 边界
- M4: getGroupDelay HTTP 请求格式验证
- M4: selectOutbound / getActiveOutbound Clash API 调用断言
- M5: applyKillSwitchToConfig 字段覆盖 + 无 TUN 边界
- M5: setKillSwitch 状态注入 + 同值幂等
- M5: 崩溃→error 不崩 / 首次重连在退避延迟内触发 / stop 后不再重连

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:11:09 +08:00
wangjia 4783f3e32e feat(nodes): 节点生命周期状态机 [tsk_aG4yOS90inF_]
实现 Lifecycle 类型及其全部公开方法,覆盖 6 个状态迁移动作:
MarkProbing / MarkUp / MarkDraining / MarkDown / MarkDestroyed /
MarkBlockedSuspect。每次合法迁移在同一个 SQL 事务中原子执行
UPDATE nodes + INSERT node_events + BumpVersion;MarkDestroyed
事务提交后异步调用 mtls.CRL.Revoke() 并清除 Redis 负载缓存与
命令队列键。配套集成测试覆盖 5 类场景(合法迁移、非法迁移、
并发写、后置钩子、blocked_suspect 幂等),全部通过。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:49:45 +08:00
wangjia cadd527680 feat(backend): 挂载 /v1 API + 实现 nodes/connect 端到端
- 新增 internal/dpcred 包,统一 DeriveHy2Password + DefaultFlow
  agentd 与 HTTP connect handler 共享同一实现
- 新增迁移 000011:nodes 表拆分 reality_prk 私钥 / reality_pbk 公钥
  reality_short_id;修正 handler_grpc.go 使用私钥字段
- 新增迁移 000012:connect_credentials 持久化凭证
  实现 CredentialsForNode 修复 agent 重连 resync 原先返回空的桩
- 扩展 NodeStore 接口:ListUp / EntitlementForUser /
  PersistCredential / DeleteCredential;同步 grpc_test.go mock
- 新增 httpapi/nodes.go:GET /nodes、POST /nodes/id/connect
  Hub.Push + PersistCredential + 渲染完整 sing-box client 配置 JSON
  POST /nodes/id/disconnect
- 新增 httpapi/account.go:GET /me、GET /plans、GET /notices
- 新增 httpapi/clientconfig.go:BuildClientConfig 服务端渲染
- 重写 cmd/server/main.go:手写 chi public/protected 分组
  nodes.Service/Hub 在 main 构造并共享;SMTPMailer/LogMailer

go build ./... && go vet ./... && go test ./... 全部通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:09:56 +08:00
maestro d3a329c8e5 maestro(tsk_rtYVDLkmc5mo): scheduler 装配 + 选主 + 优雅退出 2026-06-15 22:46:41 +08:00
wangjia ebc9c1e702 feat(scheduler): 装配 scheduler — 三 loop + Redis 选主 + 优雅退出 [tsk_rtYVDLkmc5mo]
## 内容

### server/internal/scheduler/deps.go
新增 scheduler 级接口文件,定义所有外部依赖接口:
- DetectEngine / OrchestrateReplacer / OrchestrateGrayscale(15D/15E)
- CapacityService + StubCapacityService(15F 存根)
- ProbeStateReader(15A)、ThirdPartyProber / TargetProvider(15C)
- Notifier + LogNotifier(15G)、StaticTargetProvider

### server/internal/scheduler/scheduler.go
Scheduler 主体实现:
- Config 结构体:注入所有依赖 + 可配置的选主时间参数(方便测试)
- Run(ctx) = 安装 SIGTERM/SIGINT 信号 + Start(ctx)
- Start(ctx) = 启动三 loop goroutine + 可选 15C 采集 goroutine + 30s 优雅退出
- runLoop: 非主实例每 5s 抢租约(SET NX PX 30000)
- runLeaderLoop: 主实例按 interval tick,defer 释放租约
- keepLease: 每 10s Lua 校验后续租(PEXPIRE),失败即退出
- releaseLease: Lua 校验后 DEL,加速接管
- capacityTick: 15F check → 15E grayscale.Advance → 15F decay → 探针失联告警
- runThirdPartyProber: 无主从约束全副本跑(理由注释在代码中)

### server/internal/scheduler/wiring.go
BuildStubConfig(): 用存根 LC/Prov 构建可运行的 Config,
供 server main 和集成测试使用;待 #5/#14 就绪后替换真实实现。

### server/internal/scheduler/scheduler_test.go (go test -race 全通过)
- TestLeaderElection: 两实例竞争,任意时刻只有一个在 tick
- TestFollowerTakeover: 主实例退出后备实例在 ≤LeaseTTL 内接管
- TestGracefulShutdown: ctx 取消后三个 leader key 被主动 DEL
- TestCapacityTickProbeDisconnect: 失联探针触发 Notifier
- TestE2EMockScenario: 端到端 mock 剧本
  up→suspect(降权10)→confirmed→down + 替换队列 →
  CreateNode→probing→activating(weight10+BumpVersion+grayscale)→
  draining_old→done,全链 node_events/audit_log 对账

### server/cmd/server/main.go
- 新增 SCHED_ENABLED=true 开关,灰度启动 scheduler
- 复用 REDIS_ADDR/REDIS_PASSWORD,probe.Store 共享给 scheduler
- 通过 signal.NotifyContext 传递 SIGTERM,确保优雅退出

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:45:56 +08:00
wangjia b35bfe10dc merge: 自动更换编排 + 养机灰度 [tsk_mxEFSvKHX64G] 2026-06-15 22:05:22 +08:00
wangjia 7627c8a195 feat(scheduler): 自动更换编排 + 养机灰度 [tsk_mxEFSvKHX64G]
实现 15E 任务:Replacer 状态机(pending→creating→probing→activating→draining_old→done/failed)
+ Grayscale 权重梯度(10→25→50→75→100,每步 6h)。

关键设计:
- Redis JSON 记录(sched:replace:{uuid})+ SetNX 崩溃安全,防止重复 CreateNode
- 幂等 key:attempt 0 = uuid,retry N = uuid:retry:N
- probePass:国内 ≥2/3 ISP 通过 + 海外 L1 全通,连续 2 Tick 才晋级
- 探活超时 15min → failProbeAttempt → 轮换 provider;3 次失败 → failed + NotifyFault
- StubBreaker(15F 未就绪);NewReplacer(Config{}) 依赖注入
- 6 个集成测试全部通过(happy path / max-retry / crash recovery / queue idempotency / grayscale)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:26:07 +08:00
wangjia fb267e1114 merge: iOS 端 PoC M3 + entitlement 申请 [tsk_F5r4Kt9m_wQU] 2026-06-15 20:56:46 +08:00
wangjia be44ef1226 merge: iOS M3 PoC — PacketTunnelProvider + entitlement [tsk_nsobbj_rJdy0] 2026-06-15 20:56:46 +08:00
wangjia 2ebd8c4054 merge: Android M2 VPN 隧道 PangolinVpnService [tsk_w1NvZdmwGPyd] 2026-06-15 20:56:37 +08:00