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
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
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
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
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
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
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
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
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
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
58e2bb0190
merge: gRPC server + hub + 跨实例 pub/sub [tsk_mF5RWPEwngai]
2026-06-15 20:56:28 +08:00
wangjia
5d54aa2fb2
merge: 判定引擎 signals+rules+streak [tsk_OYEiDCzM9_0Y]
2026-06-15 20:56:22 +08:00
wangjia
2c182fcdcf
merge: probe ingest endpoint + Redis store [tsk_rBPr0Xuy10bz]
2026-06-15 20:55:32 +08:00
wangjia
bfecb4314c
merge: gRPC server + hub + 跨实例 pub/sub [tsk__58l3wTLvaSn]
2026-06-15 20:53:25 +08:00
wangjia
bf3b6fd210
merge: apierr + idgen + CONVENTIONS.md [tsk_nI2T8qpcoier]
2026-06-15 20:51:08 +08:00
wangjia
88757b2ac4
feat(detect): 判定引擎 signals+rules+streak (tsk_OYEiDCzM9_0Y)
...
新建 server/internal/scheduler/detect/ 包,实现任务 15D:
## 核心模块
**lifecycle.go**
- 定义 LifecycleService Go interface(ListNodes / TransitionStatus / SetWeight /
BumpVersion / GetLoad / GetLoadHistory),供 #5 真实实现,内含乐观锁语义(0 行
受影响即本周期放弃,幂等可重入)
- 提供 MockLifecycle 内存 stub(SetConflict / Events 等测试辅助方法)
**signals.go**
- computeSignals:读 SnapshotsByNode,归一化为 NodeSignal
- domesticFailISPs:按 ISP 聚合(stripPrefix "3rd-" 合并 15B/15C 同 ISP 视角),
L3 失败权重最高(L3 挂即记该 ISP 失败,即使 L1/L2 通),无数据 vantage 不计分母
- overseasOK:境外对照点全部 L1 通为真
- trafficDropPct:15min 前后半段在线数降幅百分比
- ProbeSnapshotter interface(解耦 probe.Store,便于 mock)
**rules.go**
- DetectConfig:全量阈值常量(DomesticFailNumerator/Denominator=2/3、
SuspectStreakMin=2、TrafficDropThreshold=80%、TrafficBaselineMin=20、
ConfirmedStreakMin=6、RecoverStreakMin=2、SuspectWeight=10)
- isSuspectTriggered:整数算术避免浮点误差(fail×den >= total×num)
- isFault / effectiveSuspectStreakMin / isTrafficWarning 规则助手
**streak.go**
- StreakStore:Redis hash detect:streak:{node}(fail_streak / suspect_streak /
recover_streak),TTL 2h(宽于 6×5min=30min),进程重启后自动恢复
**engine.go**
- Engine.Tick(ctx):列举 up/blocked_suspect 节点,按节点依序运行五条规则
- Rule 1 suspect:≥2/3 ISP 失败 + 境外正常 → 连续 2 周期 → up→blocked_suspect,
weight→10,写 probe:freq:{node}=60(1min 提频标记),BumpVersion
- Rule 2 流量预警:trafficDrop≥80% + 基线≥20 → suspect 门槛放宽至 1 周期
- Rule 3 confirmed:blocked_suspect 连续 6 周期 → blocked_confirmed → down(跳过
draining),入队 detect:replace:queue(JSON: nodeId + replacementUuid)
- Rule 4 recover:suspect 期间境内连续 2 周期恢复 → up(weight 交 15E warmup)
- Rule 5 fault:境内+境外同时失败 → 不迁移,调 Notifier.NotifyFault(log stub)
- Notifier interface + LogNotifier stub(15G 就绪前输出 slog.Warn)
**engine_test.go**(19 个测试,全部通过)
- 表驱动覆盖:恰好 2/3 失败、1/3 失败不触发、9min vs 10min streak、
流量预警 1 周期即 suspect、suspect 第 6 周期 confirmed、境内外同挂 fault 不迁移、
恢复 2 周期回 up、3rd- 前缀合并、L3 覆盖 L1/L2
- 乐观锁冲突:SetConflict → 放弃本周期 → 无副作用 → 解冲突后正常重试
- 进程重启:同一 miniredis,新 Engine 读取已存在 streak 继续计数不误清零
- 产出验证:replace queue JSON 结构、probe:freq 标记、weight 变更、事件 detail 字段
**migrations/000010_node_blocked_statuses.{up,down}.sql**
- 扩展 nodes.status ENUM 加入 blocked_suspect / blocked_confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 20:15:13 +08:00
wangjia
8ed1cfcc75
merge: gRPC server + hub + 跨实例 pub/sub [tsk_mF5RWPEwngai]
...
Resolve merge conflict between maestro/tsk__58l3wTLvaSn (gRPC nodes feature)
and main (admin backend + JWT config + OpenAPI routes):
- server/cmd/server/main.go: keep admin backend (startAdminIfConfigured) AND
add gRPC agent server (startGRPC) — both coexist as independent optional
listeners governed by their respective env vars
- server/internal/config/config.go: keep JWT fields (JWTPrivateKeyPath /
JWTKeyID / JWTPublicKeys) AND add gRPC fields (GRPCAddr / CAKeyPath /
CACertPath / GRPCCertPath / GRPCKeyPath)
- server/internal/nodes/: add all new files from tsk__58l3wTLvaSn
(hub.go, hub_test.go, load.go, handler_grpc.go, grpc_test.go,
service.go, store.go) — 18 tests, all passing
Test: go test ./internal/nodes/... → PASS (18/18)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 20:10:01 +08:00
wangjia
628440b3f9
style(pangolin): gofmt store/mysql.go(import 排序)
...
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-13 18:52:30 +08:00
wangjia
20a1f8e2af
merge: 管理端最小后台 + TOTP 双因素 [tsk_SCMtcGF4F434]
...
以 main 现有 cmd/server 设计为准(保留 probe + /v1 路由),叠加分支新增:internal/admin/ 自包含后台(session/2FA/IP 白名单/审计/模板)、internal/totp/ + cmd/adminctl/(TOTP + 管理员创建 CLI)、cmd/server/main.go 的 startAdminIfConfigured()(opt-in:ADMIN_SECRET_KEY+DB_DSN 才起)。统一 getenvDefault 命名;go.mod 取 HEAD + go mod tidy(x/term 提为直接依赖);admins 迁移与 main 既有 000008_provision 撞号,重排为 000009_admins。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-13 18:47:38 +08:00
wangjia
03b00b9a0f
merge: config + store(UTC DSN) + 自动迁移 [tsk_zRA6fGU1JuHj]
...
以 main 现有 config 设计为准(FromEnv/*Config/单 DSN/RS256),移植分支新增的 store 层:mysql.go 的 Open(*config.Config) 改用 cfg.DSN、结构化强制 UTC + 会话时区断言;migrate.go + migrations/embed.go 提供 golang-migrate 嵌入式迁移;cmd/migrate/main.go 实现 migrate CLI(读 DB_DSN)。未采用分支的 Load()/PANGOLIN_ 前缀/MySQL 拆分字段(与 main 设计冲突)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-13 18:42:46 +08:00
wangjia
ed5eabea58
merge: 第三方拨测 API 适配 [tsk_-WC9smQK7YRt]
2026-06-13 17:32:26 +08:00
wangjia
bf1717a369
merge: 控制面联调 M6 [tsk_nuoKSM4Vt-zK]
...
# Conflicts:
# .gitignore
# client/lib/widgets/home_shell.dart
# client/lib/widgets/server_tile.dart
# client/pubspec.yaml
2026-06-13 17:32:26 +08:00
wangjia
b05d7ed416
merge: oapi-codegen 接线 + 生成代码入库 [tsk_Kp80nvHV2yLc]
...
# Conflicts:
# server/Makefile
# server/cmd/server/main.go
# server/go.mod
# server/go.sum
2026-06-13 17:32:25 +08:00
wangjia
605bfa1ec9
feat(nodes): gRPC server + Hub + Redis pub/sub cross-instance delivery (tsk__58l3wTLvaSn)
...
Implements AgentService gRPC server with all 6 RPCs (Enroll/Register/Heartbeat/
Subscribe/Ack/ReportUsage), Hub command routing with Redis ZSET at-least-once
persistence and cross-instance pub/sub delivery, LoadCache for node:load metrics,
NodeStore SQL interface + MySQL implementation, and full mTLS gRPC listener in main.
Integration tests: 18 tests covering full Enroll→Register→Heartbeat→Subscribe→Ack
flow, reconnect resume with last_command_id, and cross-instance pub/sub delivery
via bufconn + miniredis + mockNodeStore + real mTLS certificates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 17:30:42 +08:00
wangjia
8de32eb611
merge: 弹性节点基建:Terraform + 一键更换 [tsk_6u0FxmbC7Yeq]
...
# Conflicts:
# .gitignore
# infra/cloud-init/node.yaml.tmpl
2026-06-13 17:30:37 +08:00
wangjia
4e2a444572
merge: usage 用量 + 广告解锁 [tsk_1taxhtV2k3RP]
...
# Conflicts:
# server/internal/apierr/apierr.go
2026-06-13 17:30:36 +08:00
wangjia
34c9909608
merge: auth 模块 [tsk_2PFfyviECIXh]
...
# Conflicts:
# server/go.mod
2026-06-13 17:29:49 +08:00
wangjia
a47121f99b
feat(probe): add third-party dial-test adapter for 15C (tsk_-WC9smQK7YRt)
...
Implements ProberAgent interface + Alibaba Cloud CloudMonitor Synthetic adapter:
- ProbeTarget / VantageResult types aligned with 15A schema (L3 always nil)
- AliyunSyntheticAgent: CreateSiteMonitor → poll → map to VantageResult
- ISP vantage uses "3rd-{ISP}" prefix; Redis key probe:{node}:CN:{region}:3rd-*
- Degradation: API error/rate-limit/timeout → skip vantage, log + counter (no write)
- RunOnce(ctx, targets) shape for 15H assembly wiring
- Credentials via cfg only (env-var injection, zero hardcoded secrets)
- 6 new tests (mock vendor server): normalization, rate-limit, timeout, Redis keys,
failed-target L1, no-credentials check; all 22 probe-package tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 17:21:43 +08:00
wangjia
3bcf1d7b4c
merge: maestro/tsk_NU9JuUweHWMt [tsk_NU9JuUweHWMt]
2026-06-13 17:04:17 +08:00
wangjia
5a6b4a2dbb
merge: maestro/tsk__R8M4jEw43JR [tsk__R8M4jEw43JR]
2026-06-13 17:04:17 +08:00
wangjia
5a485fbcf6
merge: maestro/tsk_FrRarw9YO8Nf [tsk_FrRarw9YO8Nf]
2026-06-13 17:04:17 +08:00
wangjia
908f75de75
merge: maestro/tsk_VRzw-af__qWx [tsk_VRzw-af__qWx]
2026-06-13 17:04:17 +08:00