b255fdfb4e
ci.yml 跑在自建 gitea 的 mac-pangolin-2 runner 上,该 runner 不稳:重 job (go/flutter/golden/integration/e2e)频繁整体超时/失败(run 233、236 复现:所有 runs-on=mac 的 job 全挂、runs-on=ubuntu 的 nas 快扫描全过,证明是 mac runner 掉线而非代码)。既然开发就在 mac 上、原生工具齐全,把校验迁回本地。 - 新增 ci/check-local.sh:与 ci.yml 各 job 一一对应,dev mac 原生跑(仅 golden 因 Linux 权威基线走 docker)。分档:默认=静态闸+go build/test+flutter analyze/test (各带覆盖率闸 Go30%/Flutter28%);--full 加 golden/go-integration/e2e; --only <名> 单跑;--list 列项。缺工具标 SKIP 不算失败。shellcheck-clean。 - 删 .gitea/workflows/ci.yml(部署流水线 deploy-server/client/site 保留,发版 仍过 go test)。 - .githooks/pre-commit:秒级快闸不变,指引改指 ci/check-local.sh(原指 CI)。 - CLAUDE.md CI/CD 段重写 + ds-flow 闸表「CI」触发点改「check-local」。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
243 lines
19 KiB
Markdown
243 lines
19 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## 这个仓库是什么
|
||
|
||
Pangolin 是面向中国大陆用户的终端 VPN(科学上网)项目,**自研全栈**:
|
||
|
||
- `server/` —— Go 控制面 + 节点 agent + sing-box 数据面(HTTP API + gRPC mTLS)
|
||
- `client/` —— Flutter 客户端(内嵌 sing-box + TUN 全局代理)
|
||
- `web/` —— 用户中心(usercenter)+ 官网(website)
|
||
- `deploy/` —— 节点部署:`bootstrap/`(新机初始化)+ `single-node/`(整套自建栈)
|
||
- `design/` —— 设计 token 单源 + 规格;`docs/` / `plan/` —— 文档与计划
|
||
|
||
> 历史:早期借 EC2 上的 Marzban 临时顶现网。**本仓库已不再管理那套 EC2/marzban 部署**
|
||
> (相关 `deploy/edge|singbox|scripts|docker-compose.yml` 与 Gitea 自动部署 CD 已删除),
|
||
> 全面转向在自有 VPS 上跑自研栈。
|
||
|
||
## 部署目标:自有 VPS 节点
|
||
|
||
当前节点:**`pangolin1` = `103.119.13.48`**(Debian 12 bookworm,root,2 核 / ~1GB / 2G swap)。
|
||
单机部署:控制面 `pangolin-server` + 节点 `pangolin-agent` + 数据面 `sing-box` 三者同机,均 systemd 常驻。
|
||
|
||
- **SSH 免密**:本机 `~/.ssh/config` 有别名 `pangolin1`(密钥登录,密码登录已禁)→ 直接 `ssh pangolin1`。
|
||
- 已用 `deploy/bootstrap/init.sh` 加固(ufw + fail2ban + BBR + swap + pangolin 用户)。
|
||
- **内存仍紧(~1GB)**:控制面用 **SQLite**(见下),不要在这上面跑 MySQL 8。
|
||
- 改任何机器系统(装包/改配置/重装)前**先问用户**(只读操作除外)。
|
||
|
||
> 历史:早期节点为 RackNerd VPS `107.172.55.251`(别名 `racknerd`),已迁离;该别名/IP 作废,勿再用。
|
||
|
||
## deploy/ 结构
|
||
|
||
```bash
|
||
# ① 新机初始化(幂等,root 跑一次;后续每台新机复用)
|
||
# 远程:scp -r deploy/bootstrap pangolin1:/root/ && ssh pangolin1 'cd /root/bootstrap && bash init.sh'
|
||
deploy/bootstrap/init.sh # 加固/swap/BBR/pangolin用户/ufw/fail2ban/Telegram监控
|
||
deploy/bootstrap/monitor/ # pangolin-monitor(节点自报)+ deadman-watch(常在线主机探活)
|
||
|
||
# ② 整套自建栈(一台 VPS 跑 MySQL/Redis + 控制面 + agent + sing-box,端到端真连)
|
||
sudo VPS_IP=<公网IP> bash deploy/single-node/deploy.sh
|
||
```
|
||
|
||
`deploy/bootstrap/bootstrap.env`、`/etc/pangolin-monitor.env`、`deploy/single-node` 的
|
||
`/etc/pangolin*` 密钥均 **不入 git**。
|
||
|
||
## server/ 后端(Go)
|
||
|
||
- 二进制:`cmd/{server,agent,nodectl,migrate}`;`go build ./...` 直接编译,免确认。
|
||
- 控制面 HTTP API(`:8080`)+ gRPC agent 服务(`:9443`, mTLS);agent 自 enroll → 渲染
|
||
sing-box 配置 → `systemctl restart sing-box`。客户端连节点真实出网。
|
||
- **控制面 API 对外经 Cloudflare Tunnel**:`https://api.yanmeiai.com`(cloudflared 出站隧道,
|
||
不监听入站端口)→ 源站 `pangolin-server` 只绑 `127.0.0.1:8080`,不外露、不放行防火墙。
|
||
数据面 sing-box REALITY 仍独占入站 `:443`(未改动);gRPC agent mTLS 仍 `:9443`。详见
|
||
`docs/control-plane-tls-tunnel.html`。
|
||
|
||
### 数据层:多数据库(一个环境变量切换)
|
||
|
||
server 已与具体 DB 解绑(裸 SQL + 薄方言层,`internal/db/dialect.go`):
|
||
|
||
- `DB_DRIVER=mysql`(默认)| `sqlite`;`DB_DSN`(mysql DSN / sqlite 文件路径或 `:memory:`)。
|
||
- 迁移分 `server/migrations/{mysql,sqlite}/` 两套,`migrate` 按驱动选源(`golang-migrate`)。
|
||
- SQLite 用 `modernc.org/sqlite`(纯 Go 免 CGO)+ `_txlock=immediate`(等价 MySQL 行锁的悲观语义)。
|
||
- 测试:`go test ./...`(含 SQLite 实库测试,免 docker)+ `./server/run_sqlite_test.sh`;
|
||
MySQL 集成测试 `./server/run_mysql_test.sh`(需 docker)。
|
||
- **改 schema/查询**:upsert 用 `dialect.Upsert(...)`(中性 `EXCLUDED.col` 记法),行锁用
|
||
`dialect.LockForUpdate()`;时间等一律 Go 端算好传 `?`,**不要**用 `UTC_TIMESTAMP()`/
|
||
`NOW()`/`FIELD()` 等 MySQL 专属构造(已全部清除,加回会破坏可移植性)。
|
||
|
||
### 可配置分流(routing profile,类 Shadowrocket)
|
||
|
||
用户自定义路由规则(域名/IP/GeoIP/GeoSite × 直连/走隧道/拒绝),有序首命中,存服务端 per-user 档案:
|
||
- 表 `routing_profiles`(迁移 000028)· `internal/routing`(Profile/校验/Store)· `GET/POST /v1/me/routing`
|
||
(`httpapi/routing.go`)。写操作走 **POST**(仓库无 PUT 先例);校验非法整体 400 逐条错误、不半保存。
|
||
- **客户端不拼配置(铁律 ARCHITECTURE.md §3.1)**:客户端只编辑/存取档案,`BuildClientConfig`
|
||
(`httpapi/clientconfig.go`)在 connect 时读档案翻译进 `route.rules`——层级 **系统强制层**(hijack-dns/
|
||
LAN 直连/私有服务走隧道,恒在、用户不可越)→ **用户规则** → geoip-cn 国内分流 → FINAL;三模式
|
||
rule/global/direct(global/direct 忽略用户规则只改 FINAL)。
|
||
- **直连真生效**:IP 直连 value 并入 `route_exclude_address`(在 **TUN inbound** `tunIn` 非顶层 `route`)、
|
||
域名直连开 `dns.reverse_mapping`。**fail-safe**:档案空/坏/取失败 → 回退默认,`Profile==nil` 逐字节
|
||
等价旧行为(有测试钉)。GET 附只读 `system_locked_domains`(私有域名清单,PUT 不持久化)供 UI 标「系统强制不生效」。
|
||
- 客户端:设置页「分流规则」下钻(`routing_screen.dart` + `routing_provider`,Riverpod);改档案后连接态
|
||
自动重连使新规则生效。设计/计划见 `docs/configurable-proxy-{spec,plan}.html`。
|
||
|
||
## CI/CD
|
||
|
||
**校验(CI)已迁本地,不再上 pipeline**:原 `ci.yml`(push/PR)跑在 mac-pangolin-2 runner 上,
|
||
该 runner 不稳(重 job 频繁整体超时/失败),已**删除**。校验改为本地手动跑
|
||
**`bash ci/check-local.sh`**(dev mac 原生工具齐全):
|
||
- 默认档(快):shellcheck + 脱敏红线 + Android 明文 + 可移植SQL + OpenAPI + codegen漂移 +
|
||
ds-flow 三闸 + `go build/vet/test ./...` + `flutter analyze+test`(各带覆盖率闸)。
|
||
- `--full`:追加 golden(**docker Linux flutter**,mac 原生像素对不上基线)+ go integration
|
||
(testcontainers,docker)+ e2e-smoke。`--only <名>` 单跑,`--list` 列项。
|
||
- 秒级子集仍由 `.githooks/pre-commit` 提交时自动跑(`bash ci/install-hooks.sh` 启用一次);
|
||
完整校验靠 `ci/check-local.sh`。**改 CI 检查逻辑改 `ci/*.sh` 与本脚本,不要再加 workflow。**
|
||
|
||
`.gitea/workflows/` 现**只剩发版流水线**(deploy-server/client/site,tag 触发;runner 同为家里
|
||
NAS act_runner):
|
||
- **发版走 tag 触发**(cicd-design 已落地,取代旧「无部署」):`server-vX.Y.Z` → `deploy-server.yml`
|
||
= compile-backend → `test.sh server`(**`go test ./...`**,全绿才继续)→ Forgejo release →
|
||
`deploy-server.sh` 到 pangolin1(**备份DB→migrate up→换二进制→重启→本地 `/healthz` 权威闸**,
|
||
migrate 失败自动回滚DB+重启旧二进制)。同理 `client-v*`(deploy-client.yml)、`site-v*`。
|
||
- **⚠️ 发版必踩的私有依赖坑**:go.mod 依赖 **私有** `github.com/wangjia/codes`(真源在自建 gitea
|
||
`git.51yanmei.com/wangjia/codes`)。冷缓存 runner 上 `go build` 会走 direct git 到 github → 无凭证
|
||
挂(`could not read Username, terminal prompts disabled`)。`deploy-server.yml` 已加「私有依赖鉴权」步:
|
||
`GOPRIVATE=github.com/wangjia/codes` + `git config url."<FORGEJO_URL 带 oauth2:FORGEJO_TOKEN>/wangjia/codes.git".insteadOf https://github.com/wangjia/codes`。**新增任何私有 Go 依赖,编译前照此配鉴权。**
|
||
- **发版排障**:gitea `https://git.51yanmei.com/wangjia/pangolin/actions`;日志用 rbw「gitea 读写key」查
|
||
`/api/v1/repos/wangjia/pangolin/actions/runs/<url_id>/jobs`(step 结论)+
|
||
`/wangjia/pangolin/actions/runs/<url_id>/jobs/<job_db_id>/logs`(原始日志)。
|
||
- 节点**新机初始化**仍手动(scp+ssh 跑 bootstrap / single-node),不走 CI。
|
||
|
||
## 设计 Token 单源模型
|
||
|
||
`design/colors_and_type.css` 是**唯一 token 真相源**。改颜色/间距/圆角只改这一个文件,然后:
|
||
|
||
```bash
|
||
# Flutter token(生成 client/lib/pangolin_tokens.gen.dart)
|
||
node design/codegen/gen_flutter_tokens.mjs
|
||
|
||
# usercenter / website CSS token(由 prebuild/predev 钩子自动触发,也可手动)
|
||
cd web/usercenter && npm run gen:tokens
|
||
cd web/website && npm run gen:tokens
|
||
```
|
||
|
||
- `client/lib/pangolin_tokens.gen.dart` — **勿手改**,由生成器覆盖。
|
||
- `client/lib/pangolin_theme.dart` — 只含实现层(`PangolinScheme`/`PangolinText`/`PangolinTheme`),不含 token 数值。
|
||
- `design/flutter/` 已删除;Flutter 组件 canonical 实现在 `client/lib/widgets/`,规格在 `design/preview/`。
|
||
- **禁止**再向 `design/` 提交 Dart/TS 组件代码副本(会漂移)。
|
||
|
||
## 前端设计系统治理(ds-flow)
|
||
|
||
> **落地中**(分阶段收口,见计划 `docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md`
|
||
> / 阅读版 `docs/frontend-ds-refactor-plan.html` / todo #19)。以下是**目标模型与硬规则**;
|
||
> 标注「⏳」的部件正在建,未标注的已生效。参考样板 `~/code/jiu`。
|
||
|
||
**心智模型**:设计只有一个出生地(**原型单源**),代码永远是镜像;跨端副本是否走样由
|
||
**静态闸**在提交/本地校验(check-local)前拦截,像素是否还原由 **golden/fidelity 双级验收**兜底。主题:**light / dark 双主题**。
|
||
|
||
**原型单源** `design/prototype/`(⏳ 建设中,Phase 1):
|
||
- `tokens.css` — 令牌真源:基础 `:root`(主题无关标量:间距/圆角/字号/字体/阴影/动效)+ `[data-theme=dark]` 颜色覆盖块。
|
||
(当前真源仍是 `design/colors_and_type.css`,Phase 1 迁移后数值不变、结构规整)
|
||
- `atoms.css` — 公用组件原子(按钮/卡片/输入/语言下拉/徽章/状态药丸),**只引 `var(--token)`,禁硬编码**。
|
||
- `icons.js` — SVG sprite 单源(`<symbol id="i-*">`);website/usercenter/Flutter 三处图标集须 ⊆ 此集。
|
||
- `index.html` — 活登记页:主题切换 + 声明式色板 + 全组件/图标展示卡。**每个 atom 必须在此登记**。
|
||
- `serve.mjs` — 零依赖热重载预览;评审给 URL,**不截图**。
|
||
|
||
**三层治理**:
|
||
- **L1 设计系统**:新增颜色/组件/图标——**先登记原型,再同步代码**,无例外。跨端映射(状态词→图标)两端同集要有闸。
|
||
- **L2 屏级三态**(台账记 `design/CONTRACT.md`):`同步`=入 fidelity;`快照`=原型退役、golden+契约为准;`代码先行`=无原型屏、golden 唯一基准。
|
||
- **L3 新屏/改版**:design-first——原型 → serve 评审 → 契约 → 实现 → 验收 → 入同步态。
|
||
|
||
**codegen**:
|
||
- Flutter:`node design/codegen/gen_flutter_tokens.mjs` → `pangolin_tokens.gen.dart`(**勿手改**)。
|
||
- Web:`build-tokens.mjs`(website→`src/styles/tokens.gen.css` / usercenter→`public/colors_and_type.css`),
|
||
**只同源不重复生成设计决策**;靠同源闸逐值校验(不建跨端共享组件包,两端各自实现)。
|
||
|
||
**四道静态闸 —「违规谁拦」**(规则没上闸 = 没有规则):
|
||
|
||
| 闸 | 拦什么 | 何时 |
|
||
|---|---|---|
|
||
| 原型校验 `check-ds.mjs`(⏳ Phase 5) | 硬编码色/未定义 token/未登记组件/魔法数断点… 12 道 | pre-commit(动了原型)+ check-local |
|
||
| 跨端同源 `check-l1-sync.mjs`(⏳ Phase 2) | tokens 逐值/icons 同集/Web hex 白名单 | check-local |
|
||
| 代码色单源 `check_ds_code.mjs`(⏳ Phase 5) | Flutter 裸 `Color(0x)`/具名 `Colors.x`(`// ds-ignore: 理由` 豁免) | pre-commit(`--changed`)+ check-local |
|
||
| codegen 零 diff `ci/check-codegen-drift.sh`(**已生效**) | 重生成 token 后 `git diff` 非空即 fail | pre-commit + check-local |
|
||
|
||
**双级像素验收**:
|
||
- **golden**(**已有**,`client/test/golden/`):多主题回归自比(同渲染器),抓串色/漏 token;真字体加载防豆腐块、钉死 viewport/dpr/动态值。重录 `flutter test --update-goldens`,随功能 commit 入库。
|
||
- **fidelity**(⏳ Phase 5,本地体检**不进 CI**):原型 Chromium 截图 vs Flutter golden pixelmatch,逐屏阈值=实测残差+2pp;跨渲染器噪声大,故不入 CI。
|
||
|
||
**pre-commit**:`.githooks/pre-commit` 已写,**每台机需 `bash ci/install-hooks.sh` 启用一次**(设 `core.hooksPath=.githooks`)。
|
||
|
||
**发现硬编码色**:换 token;确属例外(`#fff/#000`/品牌 logo 固定色)加 `// ds-ignore: 理由` 或列白名单。
|
||
|
||
## client/ macOS 原生隧道(PacketTunnel 系统扩展 + 内嵌 libbox)
|
||
|
||
内嵌 sing-box(`Libbox.xcframework`)的 `NEPacketTunnelProvider` **系统扩展**(站外 Developer ID
|
||
分发)。让它能被 `sysextd` 加载并真正连通踩了一长串坑,**改这块前必读**
|
||
`docs/macos-sysext-realize-troubleshooting.html`。以下是铁律:
|
||
|
||
**构建 / 发版**
|
||
- 一律走 `scripts/local_test.sh`(`build`/`notarize`/`copy`/`run`):Developer ID 签名 + 公证 + staple。
|
||
- **每次构建必递增 `CFBundleVersion`**(`CURRENT_PROJECT_VERSION`)——否则 `sysextd` 视为同版本**不更新**,装上去跑的还是旧扩展。
|
||
- SIP 开启的机器只接受**已公证**的 sysext;`client/macos/sign_libbox.sh` 在构建期以 Developer ID 重签内嵌 Libbox。
|
||
|
||
**系统扩展能被 realize 的硬性要求**(缺一即 `code=4` / 静默拒)
|
||
- **自包含**:`PacketTunnel` target 设 `OTHER_LDFLAGS = ""`(切断继承项目级 CocoaPods 链接标志,否则会把 `flutter_secure_storage` 链进扩展);`Libbox.xcframework` **只 Link 不 Embed**(它是静态库)。验证:`otool -L` 扩展二进制应**零 `@rpath` 外部依赖**。
|
||
- **bundle 名 = 标识符**:`PRODUCT_NAME = com.pangolin.pangolin.PacketTunnel`。
|
||
- 扩展 `Info.plist` 必须有 **`NSSystemExtensionUsageDescription`**(网络扩展类别强制,主 app 的不顶用)。
|
||
- **App Group 用 macOS 原生格式 `<TeamID>.<name>`**(`BYL4KQHMTN.com.pangolin.pangolin`,非 iOS 的 `group.` 前缀);`NEMachServiceName` 以其为前缀。
|
||
- 沙箱扩展补 `network.client` / `network.server`;`get-task-allow=false` + 签名加 `--timestamp`。
|
||
|
||
**libbox / NetworkExtension 集成铁律**(改 `PacketTunnelProvider.swift` 注意)
|
||
- `startTunnel` **必须在后台队列**执行 libbox 启动(`DispatchQueue.global().async`)——否则 `startOrReloadService` 在 provider 队列同步阻塞,与 `openTun → setTunnelNetworkSettings` 回调**三方死锁**(隧道卡 connecting 永不完成)。
|
||
- `startOrReloadService(options:)` **传非空** `LibboxOverrideOptions()`(传 `nil` → libbox 解引用空指针 SIGSEGV,扩展进程崩溃)。
|
||
- `startDefaultInterfaceMonitor` 要**阻塞到首个 path 更新再返回**(否则 sing-box 启动期拿不到默认接口,报 `no available network interface`)。
|
||
|
||
**配置由服务端渲染,客户端不拼**
|
||
- sing-box 客户端配置由 `server/internal/httpapi/clientconfig.go::BuildClientConfig` 渲染、原样下发。
|
||
- **TUN 模式必须有 DNS 劫持**:`route.rules` 首条 `{"action":"hijack-dns","port":[53]}`(排在 LAN 直连规则前)——否则发往隧道 DNS(172.19.0.2:53)的查询被 `172.16.0.0/12` 吞去直连,域名解析失败,**隧道连上也打不开网站**。
|
||
- REALITY 数据口走 **节点 `endpoint` 的端口**(当前 443;受限网络常封高位端口如 11443,优先 443)。
|
||
|
||
**已知坑**
|
||
- 开发机若是 **macOS 26 (Tahoe)**:`sysextd` 报 `no policy, cannot allow apps outside /Applications`(app 在 /Applications 也报)是 **Apple 回归**,本机调试需关 SIP 后 `systemextensionsctl developer on`;真实用户(macOS 14/15)不受影响。
|
||
- #5 国内分流:客户端 `smartRoute` → `?split_cn=1` 下发远程 rule-set;**TODO 改本地 `.srs` 预取**,避免启动期下载。
|
||
|
||
## client/ 移动端(iOS/iPad + Android)原生隧道
|
||
|
||
同样内嵌 libbox,与 macOS 同「CommandServer 模型」,但形态/打包不同:
|
||
|
||
- **iOS/iPad**:`NEPacketTunnelProvider` 扩展(`client/ios/PacketTunnel/`),经 `NETunnelProviderManager` 管理(非 sysext);App Group 用 **iOS 的 `group.` 前缀**(`group.com.pangolin.pangolinVpn`)。`ctl_info`/`sockaddr_ctl` iOS SDK 不发,靠 `PacketTunnel-Bridging-Header.h` 手写声明。
|
||
- **Android**:`PangolinVpnService`(`client/android/.../PangolinVpnService.kt`),VpnService + libbox **同进程**;明文联调 API 需 manifest `usesCleartextTraffic`(Android 9+ 默认禁 http)。
|
||
- iOS/Android 与 macOS 同走 Dart `VpnNativeBridge`,所以 Dart 侧修复(状态/统计流)四端共享。
|
||
|
||
**libbox 构建(产物 gitignore,需手动生成)**——`scripts/build-libbox.sh`:
|
||
- Apple:`bash scripts/build-libbox.sh apple macos`(只 macOS,快)/ `apple ios`(iOS+sim)→ `Libbox.xcframework` 放 `client/{macos,ios}/Frameworks/`。
|
||
- Android:`bash scripts/build-libbox.sh android` → `Libbox.aar`,**重命名小写** `libbox.aar` 放 `app/kernel/dist/android/`。⚠️ gomobile 编 Android **强制 JDK 17**(JDK 21 直接拒);`export JAVA_HOME=/opt/homebrew/opt/openjdk@17/...` 再编。
|
||
- ⚠️ **Android libbox Java 包名是 `io.nekohasekai.libbox`**(不是 `libbox`),Kotlin import 用前者。
|
||
|
||
**真机装机(老忘——有现成脚本,别手搓 xcodebuild/签名)**:
|
||
- **iOS**:`API_URL=https://api.yanmeiai.com bash scripts/local_test.sh ipad "<设备名/id>"`(`local_test.sh ios-devices` 列已连设备)。脚本自动:flutter build ipa(**公司分发证书** Apple Distribution: Yanmei / Team `BYL4KQHMTN`,ad-hoc)→ 核验签名主体(防无声退回个人证书)→ `xcrun devicectl` 装机。`API_URL` 决定客户端连哪个控制面(prod = `https://api.yanmeiai.com`)。
|
||
- iOS libbox 是 **gitignore 产物**,新 worktree 常缺 → 从主仓拷免重建:`cp -R /Users/wangjia/code/pangolin/client/ios/Frameworks/Libbox.xcframework client/ios/Frameworks/`(routing 等功能不碰 libbox 接口,主仓那份兼容)。
|
||
- 新设备首次装报 `0xe8008012`(描述文件不含 UDID)→ 需 xcodebuild `-allowProvisioningUpdates -allowProvisioningDeviceRegistration`(脚本 die 里给了兜底命令);chen 的设备(chen-macbook/chen-iphone)已注册。装机走**公司**分发证书,禁个人开发证书(记忆 `ios-install-team-cert`)。
|
||
|
||
## 跨端实时统计 + urltest 延迟(连接页"延迟"的唯一正解)
|
||
|
||
连接页"延迟"= 内核 urltest(经 REALITY 真实出站测 RTT);**坑很深,改前必读**
|
||
`docs/connect-latency-urltest.html`。铁律:
|
||
|
||
- **服务端下发的 sing-box 配置没有 `clash_api`** → 内核不暴露出站组/urltest 历史 → libbox 的
|
||
Group 命令 / `/proxies` 全空。**每个原生端必须在起内核前给配置注入 `experimental.clash_api`
|
||
(127.0.0.1 本地监听)+ `cache_file`**,再经本地 HTTP 查 `/proxies`(读 history)+
|
||
`/group/<name>/delay`(刷新)取真实延迟(与 Windows desktop bridge 同法)。
|
||
- **libbox CommandClient 一连接只订一种命令**:`addCommand(Status)+addCommand(Group)` 同一 client
|
||
只第一个生效(Group 永不回调)。速率走 Status,urltest 走上面的 clash HTTP,别指望单 client 的 Group。
|
||
- **macOS 扩展是 root、容器与无 root 主 app 不同路径** → 主 app 连不上扩展的 command.sock;macOS/iOS
|
||
统计走 `NETunnelProviderSession.sendProviderMessage`(扩展内 StatsCollector 采集、app 拉取)。Android
|
||
同进程,直接 socket。
|
||
- **Dart 侧**:`VpnNativeBridge.statsStream/statusStream` 必须缓存成**共享广播流**(`asBroadcastStream`)——
|
||
否则多订阅者(连接页速度 + ConnectionController 回写延迟)抢 EventChannel 的单一原生 sink,后订阅者赢、
|
||
前者变哑(表现:速度正常但延迟一直 —)。`_onStats` 在自动连到已运行隧道(`_connectedNode==null`)时回退
|
||
`effectiveNode`;连接态 `_measure` 跳过直连实测(全局 TUN 会本地接住、返回假的几 ms)。
|
||
|
||
> 设备列表"最后在线"取 `last_seen`(连接/用量/~15s 会话轮询刷新),不是 `last_login`(仅登录那刻)。
|