# AGENTS.md 本文件是 NetStable 项目的维护入口。任何自动化代理、Codex 会话或人工维护者在改动本仓库前都应先读这里。 ## 基本原则 - 用最简单、直接、改动最少的实现,优先复用现有文件、函数和类型。 - 仅为当前实际需求或明显重复逻辑增加抽象,不为未来扩展提前设计。 - 替换实现时移除旧路径,避免新旧双轨。 - 不做顺手重构。除非它直接服务当前问题,否则不要改命名、目录结构、样式或部署流程。 - 不回滚用户或其他维护者的改动。遇到不相关的脏工作区,忽略;遇到相关冲突,先读懂再处理。 - 所有影响生产行为的改动都必须有验证证据:至少跑相关测试;改到公共路径时跑 `go test ./... -count=1`。 - 完成代码改动后,提交并对齐 Gitea,除非用户明确要求不要提交或不要推送。 ## 项目概况 NetStable 是一个独立 Go Web/CLI 测速项目,用于测试用户浏览器或 CLI 到部署服务器的网络质量。 核心能力: - 浏览器测速:用户打开网页后测试下载、上传、延迟和稳定性。 - CLI 测速:同一个二进制可作为客户端运行,支持 HTTP 和 `iperf3` 模式。 - 全局锁:同一时间只允许一个用户真正测速,其它用户进入 FIFO 队列。 - 半截记录:已经产生样本的中断/停止测试也必须保存为历史记录;还没轮到且没有样本的排队取消不生成空记录。 - 历史记录:完整样本曲线、摘要、脱敏 IP、地区、运营商写入 JSONL。 - 域名访问:生产主入口是 `https://speedtest.hzct.obscura.work/`。 ## 仓库信息 - 本地路径:`/Users/kanshan/Documents/speedtest` - Gitea 仓库:`https://git.aiot.ml/kanshan/speedtest` - 远端名:`origin` - 远端主分支:`main` - 当前本地工作分支通常是:`codex/go-network-stability-tester` - 推送当前 HEAD 到 Gitea 主分支: ```bash git push origin HEAD:main ``` 提交前后检查: ```bash git status --short --branch git log --oneline --decorate -5 git remote -v git ls-remote origin refs/heads/main ``` 不要提交 `dist/`、本地 JSONL 记录、日志或临时文件;这些已在 `.gitignore` 中排除。 ## 代码结构 - `cmd/netstable/main.go` - CLI 入口。 - 默认不带子命令时启动服务端。 - `serve` 启动 Web 服务。 - `client` 运行 HTTP/iperf3 客户端测速。 - `internal/web/server.go` - HTTP API、网页静态资源、队列、全局锁、记录完成、下载/上传接口、iperf3 会话。 - 这是生产行为最关键的文件,修改前必须读相关测试。 - 测试 SSE 探测端点(`/api/tests/{id}/events`)已移除;浏览器和 CLI 测速统一走 download/upload/complete,页面只保留 `/api/health/events` 心跳。 - `internal/client/client.go` - HTTP CLI 客户端。 - 负责创建会话、排队等待、下载/上传阶段、complete/cancel。 - 已产生样本后中断,必须走 partial complete,不能只 cancel。 - `internal/client/iperf3.go` - CLI iperf3 模式。 - 服务端创建一次性高端口,客户端执行本机 `iperf3`,完成后回写记录。 - `internal/probe` - TCP/HTTP 探测样本和摘要计算。 - `internal/store` - JSONL 历史记录读写。 - 单条记录可能很大,读取端已提高到 64 MiB 单行上限。 - `internal/geo` - IP 地区/运营商解析。 - 优先内置 `ip2region_v4.xdb`,再走在线 fallback。 - `internal/limit` - 程序级带宽限速。 - 生产当前默认不限速。 - `web/static` - 前端 HTML/CSS/JS。 - 一键 CLI 命令由 `web/static/app.js` 根据当前 `window.location.origin` 生成。 - `data/ip2region_v4.xdb` - 内置 IPv4 数据库。 - `deploy/netstable.service` - systemd 服务模板。 - `.gitea/workflows/ci.yml` - Gitea Actions:测试、vet、release artifact。 ## 关键业务约束 这些约束比局部代码风格更重要,不能破坏。 1. 同一时间只能有一个真正测速会话。 - `Server.activeID` 是全局锁。 - 普通 Web/CLI HTTP 测试和 iperf3 测试必须共用同一个锁。 - 其它请求必须排队,而不是并发跑测速。 2. 队列必须 FIFO。 - 新请求在有 active 会话时进入 `Server.queue`。 - active 完成、取消或过期后只提升队列头。 - 手动停止或 CLI 中断必须释放队列。 3. 已产生样本的半截测试必须保存。 - CLI HTTP 中断:如果 `samples` 非空,先 complete partial,再返回原始中断错误。 - 浏览器停止/刷新:如果 `state.samples.length > 0`,优先 complete partial。 - 还没轮到、没有样本的取消只释放队列,不写空记录。 4. complete 成功写入记录后才释放会话。 - 如果 `Store.Append` 失败,complete 应返回 500,并保留会话以便重试。 - 不能先 `takeSession` 再写盘,否则会丢记录。 5. 历史记录必须保护隐私。 - API 输出的 IP 必须脱敏。 - ISP 缺失时显示 `未知运营商`。 - 不要在前端、日志或 API 输出完整用户 IP,除非用户明确要求调试且范围受控。 6. 大样本记录必须可保存和读取。 - complete body 上限:64 MiB。 - JSONL 单行读取上限:64 MiB。 - 不要把限制改回 2 MiB 或默认 scanner 64 KiB。 7. 峰值口径不能用单次极短请求直接当最大值。 - 现有逻辑使用持续窗口计算 max,避免计时误差导致几百 Mbps 的假峰值。 8. 生产默认不限速。 - `-bandwidth-limit-mbps 0` 表示不限速。 - 如果用户要求限速,必须同时更新页面显示和服务启动参数。 ## 常用开发命令 本地测试: ```bash go test ./... -count=1 go vet ./... ``` 运行服务端: ```bash go run ./cmd/netstable serve -listen 127.0.0.1:18080 -data /tmp/netstable-records.jsonl -geo-base '' ``` 运行 CLI HTTP 测试: ```bash go run ./cmd/netstable client -server http://127.0.0.1:18080 -duration 15 ``` 构建本机二进制: ```bash make build ``` 构建 Linux release 包: ```bash make release VERSION=v0.1.0 ``` 生成文件: - `dist/netstable-v0.1.0-linux-amd64.tar.gz` - `dist/netstable-v0.1.0-linux-arm64.tar.gz` - `dist/checksums.txt` 更新 IP 数据库: ```bash make update-ip-db ``` 更新数据库后必须检查 `THIRD_PARTY.md` 和 `LICENSES/ip2region-LICENSE.md` 是否仍然准确。 ## 测试要求 最小规则: - 修改 Go 行为:跑对应 package 测试,再跑 `go test ./... -count=1`。 - 修改公共服务端路径:同时跑 `go vet ./...`。 - 修改 release/deploy/CI:跑 `make release VERSION=v0.1.0`。 - 修改前端 JS:跑 `node --check web/static/app.js` 和 `go test ./web -count=1`。 - 修改生产相关配置:先本地校验配置,再线上验证接口。 重点回归测试名称: - `TestCompleteEndpointAcceptsLargeSamplePayload` - `TestCompleteEndpointCanRetryWhenPersistFails` - `TestRunPersistsPartialRecordWhenCanceledAfterSamples` - `TestRunCompletesBrowserCompatibleSpeedTest` - `TestRunIperf3CompletesOneOffServerSession` - `TestCancelActiveTestPromotesQueuedSession` - `TestCancelQueuedTestRemovesItAndUpdatesPositions` - `TestEventsEndpointIsRemoved` - `TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax` - `TestStoreListsLargeRecordsWithFullSampleCurves` ## API 速查 - `GET /` - Web UI。 - `GET /api/config` - 返回带宽限制标签。 - `GET /api/records?limit=100` - 返回历史记录,输出前会脱敏 IP。 - `limit` 范围:1 到 1000。 - `GET /api/health/events?limit=1` - SSE 心跳,用于页面连接状态。 - `POST /api/tests` - 创建 HTTP/Web 测速会话。 - 返回 queue/download/upload/complete/cancel URL。 - `GET /api/tests/{id}/queue` - 查询排队位置。 - `POST /api/tests/{id}/complete` - 提交浏览器或 HTTP CLI 样本并写历史。 - `POST /api/tests/{id}/cancel` - 取消还未产生样本的会话或释放队列。 - `GET /api/download?testId={id}&bytes={n}` - 下载测速数据。 - `POST /api/upload?testId={id}` - 上传测速数据。 - `POST /api/iperf3/sessions` - 创建一次性 iperf3 服务端会话。 - `POST /api/iperf3/sessions/{id}/complete` - 提交 iperf3 结果并写历史。 - `GET /downloads/netstable-linux-amd64.tar.gz` - CLI 一键下载包。 - `GET /downloads/netstable-linux-arm64.tar.gz` - CLI 一键下载包。 ## 生产环境概览 最后核对时间:2026-09-15 Asia/Shanghai。 ### 主入口 - URL:`https://speedtest.hzct.obscura.work/` - HTTPS:Caddy 自动申请 Let's Encrypt 证书。 - 证书核对结果: - Subject:`CN = speedtest.hzct.obscura.work` - Issuer:`Let's Encrypt` - 当前证书有效期:`2026-08-09` 到 `2026-11-07` - HTTP 会 308 跳转 HTTPS。 ### 生产节点 | 节点 | 用途 | 状态备注 | | --- | --- | --- | | `103.46.93.244` | 主域名节点,Caddy 80/443 反代到本机 NetStable 18080 | HTTPS/API 正常;2026-09-15 检查时 SSH 连接被远端关闭,部署前先恢复 SSH | | `110.42.44.35` | 直连备用节点,`http://110.42.44.35:18080/` | 2026-09-15 HTTP/API 正常,SSH 可用 | | `103.46.93.38` | 历史直连节点,`http://103.46.93.38:18080/` | 2026-09-15 检查时 HTTP 空响应,SSH host key 变化;不要盲目信任或部署,必须先人工确认主机身份 | ### 主节点端口 `103.46.93.244` 预期监听: - `:80` Caddy HTTP,自动跳转 HTTPS。 - `:443` Caddy HTTPS。 - `:18080` NetStable 服务。 - `127.0.0.1:2019` Caddy admin。 ### 主节点 Caddy 配置 路径: ```bash /etc/caddy/Caddyfile ``` 关键站点块: ```caddyfile speedtest.hzct.obscura.work { reverse_proxy 127.0.0.1:18080 } ``` 最近已知备份: ```bash /etc/caddy/Caddyfile.bak-20260610110050 ``` 修改 Caddy 后必须校验: ```bash caddy fmt --overwrite /etc/caddy/Caddyfile caddy validate --config /etc/caddy/Caddyfile systemctl reload caddy systemctl is-active caddy ``` 验证 HTTPS: ```bash curl -fsS -I https://speedtest.hzct.obscura.work/ curl -fsS https://speedtest.hzct.obscura.work/api/config | jq . echo | openssl s_client -servername speedtest.hzct.obscura.work -connect speedtest.hzct.obscura.work:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates ``` 查看 Caddy 证书日志: ```bash journalctl -u caddy --no-pager -n 100 ``` ### systemd 服务 模板文件: ```bash deploy/netstable.service ``` 线上预期服务名: ```bash netstable.service ``` 预期服务配置: ```ini ExecStart=/usr/local/bin/netstable -listen 0.0.0.0:18080 -data /var/lib/netstable/records.jsonl -downloads-dir /var/lib/netstable/downloads ``` 常用命令: ```bash systemctl status netstable --no-pager -l systemctl restart netstable systemctl is-active netstable journalctl -u netstable --no-pager -n 100 ``` 注意: - 当前模板没有启用 `-trust-proxy-headers`。 - 如果要通过 Caddy 获取真实客户端 IP,必须同时满足: - NetStable 只接受可信代理流量,或公网 `18080` 被防火墙挡住。 - 服务启动参数显式加 `-trust-proxy-headers`。 - 不要在 `18080` 仍公网直连时直接开启 `-trust-proxy-headers`,否则用户可伪造 `X-Forwarded-For`。 ### 生产路径 - 二进制:`/usr/local/bin/netstable` - 历史记录:`/var/lib/netstable/records.jsonl` - CLI 下载包目录:`/var/lib/netstable/downloads` - amd64 下载包:`/var/lib/netstable/downloads/netstable-linux-amd64.tar.gz` - arm64 下载包:`/var/lib/netstable/downloads/netstable-linux-arm64.tar.gz` ## 发布和部署手册 ### 1. 发布前检查 ```bash git status --short --branch go test ./... -count=1 go vet ./... make release VERSION=v0.1.0 ``` 确认生成包: ```bash ls -lh dist cat dist/checksums.txt ``` ### 2. 部署到可 SSH 节点 优先用 SSH 标准输入流上传。不要直接 `scp` 覆盖 `/usr/local/bin/netstable`,运行中的 Linux 二进制可能导致写入失败。 单节点部署模板: ```bash host=110.42.44.35 ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'mkdir -p /var/lib/netstable/downloads' ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /tmp/netstable.new' < dist/netstable-v0.1.0-linux-amd64/netstable ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /var/lib/netstable/downloads/netstable-linux-amd64.tar.gz' < dist/netstable-v0.1.0-linux-amd64.tar.gz ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /var/lib/netstable/downloads/netstable-linux-arm64.tar.gz' < dist/netstable-v0.1.0-linux-arm64.tar.gz ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'install -m 0755 /tmp/netstable.new /usr/local/bin/netstable && rm -f /tmp/netstable.new && systemctl restart netstable && systemctl is-active netstable' ``` 多节点部署前先确认每台 SSH 身份和服务状态,不要无脑批量推。 ### 3. 部署后验证 本地哈希: ```bash shasum -a 256 dist/netstable-v0.1.0-linux-amd64/netstable ``` 远端哈希: ```bash ssh root@SERVER 'sha256sum /usr/local/bin/netstable' ``` 直连节点检查: ```bash curl -fsS "http://SERVER:18080/api/config" | jq . curl -fsS "http://SERVER:18080/api/records?limit=1" | jq . curl -fsS -r 0-127 "http://SERVER:18080/downloads/netstable-linux-amd64.tar.gz" >/tmp/netstable-range.bin wc -c /tmp/netstable-range.bin ``` 域名检查: ```bash curl -fsS -I https://speedtest.hzct.obscura.work/ curl -fsS https://speedtest.hzct.obscura.work/api/config | jq . curl -fsS "https://speedtest.hzct.obscura.work/api/records?limit=1" | jq . curl -fsS "https://speedtest.hzct.obscura.work/api/health/events?limit=1" curl -fsS -r 0-127 "https://speedtest.hzct.obscura.work/downloads/netstable-linux-amd64.tar.gz" >/tmp/netstable-https-range.bin wc -c /tmp/netstable-https-range.bin ``` ### 4. 推送 Gitea ```bash git status --short --branch git add git commit -m "" git push origin HEAD:main git ls-remote origin refs/heads/main ``` 如果用户只要求文档变更,也要提交并推送,除非用户明确不要。 ## 回滚手册 ### 代码回滚 优先回滚到上一个已知好提交,重新构建并部署。 ```bash git log --oneline --decorate -10 git checkout go test ./... -count=1 make release VERSION=v0.1.0 ``` 然后按“部署到可 SSH 节点”重新上传。 不要对用户工作区执行 `git reset --hard`,除非用户明确要求。 ### Caddy 回滚 先找备份: ```bash ls -lh /etc/caddy/Caddyfile.bak-* ``` 恢复: ```bash cp /etc/caddy/Caddyfile.bak-YYYYMMDDHHMMSS /etc/caddy/Caddyfile caddy validate --config /etc/caddy/Caddyfile systemctl reload caddy systemctl is-active caddy ``` ### 数据回滚 历史记录是 append-only JSONL。修复坏记录时优先备份再处理: ```bash cp /var/lib/netstable/records.jsonl /var/lib/netstable/records.jsonl.bak-$(date +%Y%m%d%H%M%S) ``` 不要直接清空生产记录,除非用户明确要求。 ## 排障手册 ### 记录接口 500:`failed to load records` 常见原因: - JSONL 单行超过读取限制。 - 某行 JSON 损坏。 - 文件权限或磁盘问题。 检查: ```bash journalctl -u netstable --no-pager -n 100 ls -lh /var/lib/netstable/records.jsonl tail -n 3 /var/lib/netstable/records.jsonl ``` 本项目已经把单行读取上限提高到 64 MiB;不要改回默认 scanner 行为。 ### complete 保存失败 原则: - 写记录失败时,会话必须保留,客户端可以重试。 - 如果重试变成 404,说明 complete 路径又被改坏了。 优先跑: ```bash go test ./internal/web -run TestCompleteEndpointCanRetryWhenPersistFails -count=1 -v ``` ### 用户停止后队列不释放 检查: - 浏览器停止时有没有调用 partial complete 或 cancel。 - CLI 中断时有没有调用 partial complete 或 cancel。 - `takeSession`、`finishSession`、`promoteNextLocked` 是否被绕过。 优先跑: ```bash go test ./internal/client -run TestRunCancelsQueuedSessionWhenContextStops -count=1 -v go test ./internal/web -run 'TestCancelActiveTestPromotesQueuedSession|TestCancelQueuedTestRemovesItAndUpdatesPositions' -count=1 -v ``` ### 峰值速度明显虚高 不要直接相信单个样本的 `mbps`。检查持续吞吐统计: ```bash go test ./internal/web -run 'TestBrowserSummaryUsesSustainedTransferRatesForMax|TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax' -count=1 -v ``` ### 域名 HTTPS 失败 检查: ```bash curl -I https://speedtest.hzct.obscura.work/ systemctl status caddy --no-pager -l journalctl -u caddy --no-pager -n 100 caddy validate --config /etc/caddy/Caddyfile ``` 如果证书过期或申请失败,先看 Caddy ACME 日志。不要手动复制不明来源证书覆盖 Caddy 管理目录。 ### 直连 18080 正常但域名记录 IP 不准 原因通常是反向代理后没有启用可信代理头,或启用了但公网 18080 仍可被用户直连伪造。 正确处理顺序: 1. 先决定是否只允许通过 Caddy 访问主节点。 2. 用防火墙限制公网直接访问 18080,或让 NetStable 只监听 `127.0.0.1:18080`。 3. 再给服务加 `-trust-proxy-headers`。 4. 测试 IP 脱敏、ISP 和地区显示。 不要只加 `-trust-proxy-headers`。 ### SSH host key 变化 `103.46.93.38` 在 2026-09-15 检查时出现 host key changed。处理方式: 1. 不要直接 `ssh-keygen -R` 后继续部署。 2. 先通过控制台或可信渠道确认服务器是否重装、迁移或 IP 复用。 3. 确认无误后再更新 `~/.ssh/known_hosts`。 4. 更新后先只跑只读检查,再部署。 ## 安全和隐私 - 不要把完整用户 IP 写到公开 API 响应。 - 不要把 Gitea token、密码、SSH 私钥写入仓库、日志或最终回复。 - Gitea 凭据由本机 Git credential helper 管理;不要打印明文。 - 生产记录包含用户网络信息,下载或复制前先确认用途。 - 对外开放的下载文件名必须限制在已知二进制包名,不能做任意文件下载。 ## CI 和 Release Gitea Actions 文件: ```bash .gitea/workflows/ci.yml ``` 当前行为: - push 到 `main`、`master`、`codex/**`:运行测试和 vet。 - pull request:运行测试和 vet。 - tag `v*` 或手动触发:构建 release tar.gz 和 checksums,并上传 artifact。 如果改 Makefile、release 包内容或运行时依赖,必须同步检查 CI。 ## 最终交付检查清单 代码或文档改动完成后: ```bash git status --short --branch go test ./... -count=1 go vet ./... ``` 按改动类型补充: - 前端 JS:`node --check web/static/app.js` - release:`make release VERSION=v0.1.0` - 域名/证书:`curl -I https://speedtest.hzct.obscura.work/` 然后提交并推送: ```bash git add git commit -m "" git push origin HEAD:main ``` 最终回复必须说明: - 改了什么。 - 跑了哪些验证。 - 是否已提交和推送 Gitea。 - 如涉及生产,列出线上验证结果或明确说明未部署。