{"owner":"nxtrace","repo":"NTrace-core","hasSkills":true,"hasMcp":true,"mcpConfig":{"mcpServers":{"NTrace-core":{"command":"npx","args":["-y","@modelcontextprotocol/server-NTrace-core"]}}},"found":["CLAUDE.md","AGENTS.md"],"skills":{"CLAUDE.md":"每次执行命令前均需阅读项目根目录下的 ./AGENTS.md 文件","AGENTS.md":"# NTrace-core 项目记忆文件（2026-03 快照，rev-3）\n\n# 供 LLM 在后续会话中快速加载上下文，减少重复分析。\n\n## 项目概览\n\n- 名称：NextTrace (NTrace-core)\n- 仓库：github.com/nxtrace/NTrace-core\n- 模块：`github.com/nxtrace/NTrace-core`\n- 语言：Go（`go 1.26.5`）\n- 入口：`main.go -> cmd.Execute()`\n- 核心能力：ICMP/TCP/UDP traceroute、GeoIP/RDNS、MTR 连续探测、Web/API、多平台构建\n\n## 构建与测试（必须遵守）\n\n- 常用命令：\n  - 构建：`go build ./...`\n  - 测试：`go test ./...`\n- 交叉编译脚本：`.cross_compile.sh`\n- Darwin 下 `trace/internal/icmp_darwin.go` 已不再使用 `//go:linkname`，改为\n  `syscall.Socket` + `os.NewFile` + 自定义 `icmpPacketConn`（实现 `net.PacketConn` /\n  `net.Conn` / `syscall.Conn` + `ReadMsgIP` 以满足 `x/net/internal/socket.ipConn`\n  接口），并在 `ReadFrom` 中调用 `stripIPv4Header` 剥离 macOS DGRAM ICMP socket\n  返回的外层 IP 头。\n\n## Git 与提交信息规范（必须遵守）\n\n- Commit message 必须使用 Conventional Commits 风格：`type(scope): 具体行为`。\n- subject 必须描述该提交实际改变的行为或约束，不能只写“处理 review 问题”“修复剩余问题”“调整代码”等泛化内容。\n- review finding 修复提交也必须写明具体修复点，例如 timeout、fallback、权限校验、stdout/stderr 分离；若一个提交覆盖多个 finding，用 body bullet 简短列出。\n- 一个提交包含多个互不相关主题时，优先拆分提交；确实需要放在一起时，subject 写主影响，body 写清每个具体改动。\n- 提交或 push PR 前必须检查 `git log --oneline main..HEAD`，发现泛化、误导或与 diff 不匹配的 commit message，应先 reword/amend。\n- 改写已推送 PR 分支历史后，只使用 `git push --force-with-lease`，不要用无保护的强推覆盖远端新提交。\n- 在本工作区发布 PR 时，目标仓库必须是 `nxtrace/NTrace-dev`；禁止直接向 `nxtrace/NTrace-core` 创建 PR。\n- 如果 GitHub 工具或远端推断结果指向 `nxtrace/NTrace-core`，必须改为 `nxtrace/NTrace-dev` 或停止确认，不能直接创建 PR。\n\n## 当前 CLI 语义（重点）\n\n### 常规 traceroute 路径\n\n- `--table`：现在是\"最终汇总表\"模式（一次探测完成后输出汇总表），不再是旧的异步 table 刷新模式。\n- `--route-path`：仍由 `reporter.New(...).Print()` 负责（与 MTR report 无关）。\n\n### 独立 `--mtu` 路径\n\n- `--mtu`：独立 UDP path-MTU / tracepath 风格模式，不复用普通 `trace.Traceroute` / MTR / Web 路径。\n- flavor 可用性：仅 `nexttrace` / `nexttrace-tiny` 包含；`ntr` 不注册该 flag。\n- 输出：\n  - `TTY`：当前 TTL 占位后原地更新，边探测边刷行。\n  - `非 TTY`：TTL 定稿后逐行流式输出，不使用 renderer 自己的 ANSI 控制序列。\n  - `--json`：输出独立 mtu schema；`hop.geo` 已存在。\n- 参数语义：\n  - 复用 `--data-provider`、`--language`、`--no-rdns`、`--always-rdns`、`--dot-server`。\n  - `--mtu` 仍只支持 UDP；显式 `--tcp` 冲突报错。\n- Geo/RDNS：\n  - `trace/mtu` 自带独立 metadata helper，不依赖普通 `trace.Hop.fetchIPData`。\n  - 流式事件会先输出基础 hop，再在同一 TTL 内补一条带 Geo/RDNS 的 update，最后 `ttl_final` 定稿。\n  - macOS 上曾有 `Warning: macOS --mtu support is experimental.` 提示，现已删除；不要再假设 CLI 会打印这句。\n\n### `--psize` / `--tos` 语义与平台差异\n\n- `--psize` 现在统一对齐 `mtr -s/--psize`：\n  - 用户输入语义是“含 IP + 当前探测协议头的总字节数”。\n  - 内部 `trace.Config.PktSize` 仍保存 payload bytes。\n  - 未显式传入时，不再固定默认 `52`，而是按协议/IP 族自动取最小合法值：\n    - ICMPv4 / UDPv4 = `28`\n    - TCPv4 = `44`\n    - ICMPv6 = `48`\n    - UDPv6 = `50`\n    - TCPv6 = `64`\n  - 负数 `--psize` 表示“每个 probe 独立随机”，CLI 允许 `--psize -84` 这种写法并会在解析前归一化。\n- `--tos` / `-Q`：\n  - 范围固定 `0..255`。\n  - `--mtu` 与 Globalping 显式传 `--psize` / `--tos` 会直接报不支持。\n- 平台发送路径差异（这是后续判断 bug 的关键记忆）：\n  - Linux / 其他 Unix：\n    - `ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生 socket/raw socket 路径。\n    - `--tos` 只是在现有路径上设置 `TOS/TrafficClass`，不会切换实现。\n  - macOS：\n    - 与 Linux 类似，`ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生发送路径。\n    - `--tos` 同样只是在现有路径上设置 `TOS/TrafficClass`。\n  - Windows：\n    - `TCP/UDP` 的 IPv4/IPv6 一直走 WinDivert raw send。\n    - `ICMPv4` 一直走 socket path（`SetTOS` / `SetTTL`）。\n    - `ICMPv6`：\n      - 默认或 `--tos 0`：继续走原生 socket path，只设置 `HopLimit`，保持与 `v1.5.2` 一致。\n      - 非零 `--tos`：切到 WinDivert raw send，直接发送完整 `IPv6 + ICMPv6` 报文，因为 Windows 的 `x/net/ipv6.PacketConn` 不能可靠设置 `TrafficClass`。\n    - 因此，Windows 上只有“`ICMPv6` 且 `--tos != 0`”这个组合会额外依赖 WinDivert 发送能力；README 中英两份都已写明。\n\n### 间隔默认值（分层体系）\n\n- `-z/--send-time`：每包间隔，默认 `defaultPacketIntervalMs = 50` ms。\n- `-i/--ttl-time`：\n  - **常规 traceroute**：TTL 分组间隔，默认 `defaultTracerouteTTLIntervalMs = 300` ms。\n  - **MTR 模式**：`normalizeMTRTraceConfig()` 始终覆盖为 `defaultMTRInternalTTLIntervalMs = 0` ms（各 TTL 间不间隔）。\n  - MTR 每跳探测间隔由 `-i` 显式传值 或 默认 1000ms 决定（见下文 `-q/-i` 语义）。`-z/--send-time` 在 MTR 模式下被忽略。\n\n### MTR 相关参数\n\n- `-t/--mtr`：开启 MTR 交互模式（TTY 全屏 TUI）。\n- `-r/--report`：MTR 报告模式（非交互），隐式开启 MTR。\n- `-w/--wide`：宽报告模式，隐式等价 `--mtr --report --wide`。\n- `--raw`：与 MTR 组合时进入 **MTR raw 流式模式**（`runMTRRaw`），不再与 MTR 冲突。\n- 有效 MTR 开关：`effectiveMTR = mtr || report || wide`。\n- MTR 三路分支（`chooseMTRRunMode`）：\n  1. `effectiveMTRRaw` → `runMTRRaw`（流式行输出，适合管道/脚本）\n  2. `effectiveReport` → `runMTRReport`（非交互报告表）\n  3. 默认 → `runMTRTUI`（全屏 TUI）\n- MTR 冲突参数（会直接报错退出）：`--table` `--classic` `--json` `--output` `--route-path` `--from` `--fast-trace` `--file` `--deploy`。\n  - **注意**：`--raw` 不再是冲突参数。\n\n### MTR 中 `-q/-i/-y` 的新语义\n\n- `-q/--queries`：\n  - 在 MTR report 下表示每跳探测次数，默认 10（仅当用户未显式传 `-q`）。\n  - 在 MTR TUI 下表示每跳最大探测次数，未显式传时默认无限运行。\n- `-i/--ttl-time`：\n  - 在 MTR 下表示每跳探测间隔毫秒，默认 1000ms（仅当用户未显式传 `-i`）。\n  - 各 TTL 间内部扫描间隔固定 0ms（`normalizeMTRTraceConfig` 覆盖为 `defaultMTRInternalTTLIntervalMs = 0`）。\n  - `-z/--send-time` 在 MTR 模式下被忽略。\n- `-y/--ipinfo <0..4>`：\n  - TUI 初始 Host 显示模式，默认 0（IP/PTR only）。\n  - 0=Base(IP/PTR) 1=ASN 2=City 3=Owner 4=Full\n  - 仅 TUI 模式生效，report/raw 不受影响。\n\n### MTR report wide / non-wide 区别\n\n- **wide 模式**（`-w` 或 `--mtr --report --wide`）：\n  - 查询 GeoIP，显示完整 host 信息（ASN + geo + MPLS）。\n- **非 wide 模式**（`-r` 或 `--mtr --report`）：\n  - `normalizeMTRReportConfig` 设 `IPGeoSource=nil`（不查 geo）、`AlwaysWaitRDNS=true`。\n  - 显示 `formatCompactReportHost`：仅 IP/PTR + ASN，无 geo 列。\n\n## MTR 运行链路（重要文件）\n\n- 入口与调度：`cmd/mtr_mode.go`（~315 行）\n  - `runMTRTUI(...)` / `runMTRReport(...)` / `runMTRRaw(...)`\n  - `normalizeMTRTraceConfig(conf)` / `normalizeMTRReportConfig(conf, wide)`\n  - `buildAPIInfo(...)` / `buildRawAPIInfoLine(...)`\n  - MTR CLI 现在统一使用 `signal.NotifyContext(...)` 管理 Ctrl-C / SIGTERM；不再保留额外的 `sigCh` + goroutine 等待器。\n- 交互控制：`cmd/mtr_ui.go`\n  - alternate screen + raw mode\n  - 输入状态机 `mtrInputParser`（字节流，吞掉 CSI/SS3/OSC/鼠标/焦点等序列）\n  - Enter/Leave 显式关闭输入扩展模式：1000/1002/1003/1006/1015/1004/2004\n  - Quit 路径会先判空 `cancel`，因此 `newMTRUI(nil, ...)` / 测试注入 nil 不会 panic。\n- 核心探测循环：`trace/mtr_runner.go`\n  - `RunMTR` / `mtrLoop` / `RunMTRRaw`\n  - 支持暂停、重置、流式预览（`ProgressThrottle` 默认 200ms）\n  - ICMP 持久引擎 + TCP/UDP fallback\n- 统计聚合：`trace/mtr_stats.go`\n  - `MTRAggregator` / `MTRHopStat`\n  - unknown 合并策略：单路径时把 unknown 合并到唯一已知路径，避免同 TTL 分裂成 waiting + 真实 IP 两行\n- 输出层：\n  - TUI：`printer/mtr_tui.go`\n  - table/report：`printer/mtr_table.go`\n  - raw 行格式化：`printer.FormatMTRRawLine(rec)`\n  - TUI 颜色：`printer/mtr_tui_color.go`\n\n## MTR 交互行为（当前）\n\n- `q`/`Q`/`Ctrl-C`：退出\n- `p`：暂停\n- `SPACE`：恢复\n- `r`：重置统计\n- `y`：切换 Host 显示模式（IP/PTR → ASN → City → Owner → Full → 循环）\n- `n`：切换 Host 基名显示（PTR-or-IP / IP-only）\n- `e`：切换 MPLS 标签显示（toggle MPLS on/off）\n\n## MTR 显示与统计规则（当前）\n\n- Host 显示支持 5 种模式（`-y/--ipinfo` 设初始值，`y` 键运行时循环）：\n  - `HostModeBase=0`：仅 IP/PTR，无 ASN 前缀\n  - `HostModeASN=1` / `HostModeCity=2` / `HostModeOwner=3` / `HostModeFull=4`\n  - `HostNamePTRorIP` / `HostNameIPOnly`\n- 默认语言：`cn`（`--language en` 才优先英文字段）\n- waiting 判定：`loss >= 99.95 && IP==\"\" && Host==\"\"`\n  - 显示为 `(waiting for reply)`\n  - 指标列（Loss/Snt/Last/Avg/Best/Wrst/StDev）留空\n- TUI Host 对齐（重要，已从 tab 改为手动空格）：\n  - `buildTUIHostParts(stat, mode, nameMode, lang, showIPs)` 生成结构化 parts\n  - `computeTUIASNWidth(stats, ...)` 扫描所有 hop 确定 ASN 列最大宽度\n  - `formatTUIHost(parts, asnW)` 用 `padRight(asn, asnW)` + 空格拼接（不用 `\\t`）\n  - ASN 为空但 IP 已知时填 `\"AS???\"` 占位符，保证列对齐（HostModeBase 除外，该模式不显示 ASN）\n  - waiting hop 不填占位符\n- compact report host（非 wide report）：\n  - `formatCompactReportHost(stat, nameMode, lang)` 仅输出 hostname/IP + ASN\n- TUI 其他特性：\n  - 终端宽度自适应 + CJK 宽度计算（go-runewidth）\n  - 窄屏右锚定指标区\n  - 动态 hop 前缀宽度（覆盖 3 位/4 位 TTL）\n  - MPLS 独立续行显示\n  - 紧凑指标列宽度：Loss=5 Snt=3 RTT=7 RTTMin=5\n\n## MTR 目的地检测与高 TTL 丢弃\n\n- 当 `knownFinalTTL` 已确定后，所有 `TTL > knownFinalTTL` 的调度槽位被标记为 `disabled`。\n- disabled TTL 的探测回包（包括在途探测返回的 dst-ip 回复）**一律丢弃**，不折叠、不计入任何统计。\n- **MaxPerHop 上限检查**（`states[originTTL].completed + inFlightCount >= MaxPerHop`）：\n  - 调度时使用 `completed + inFlightCount >= MaxPerHop` 防止超发。\n  - 完成时仍检查 `completed >= MaxPerHop` 丢弃溢出结果。\n- `originTTL < curFinal` 时（更低 TTL 先到 dst-ip → 降低 `knownFinalTTL`）：\n  - 保存 `oldFinal`，更新 `knownFinalTTL = originTTL`，disable 所有 `originTTL+1..maxHops`。\n  - 调用 `agg.ClearHop(oldFinal)`：清除旧 finalTTL 的聚合数据（避免幽灵行），**不合并**到新 finalTTL。\n  - 新 finalTTL 由独立的 per-hop 调度器自行积累新鲜探测数据，不存在 Snt 膨胀问题。\n- 调度状态（`inFlightCount`/`nextAt`/`consecutiveErrs`）更新在 `originTTL`。\n- 统计聚合（`completed++`/`agg.Update`/`onProbe`）均使用 `originTTL`（不再有 `accountTTL` 分离）。\n\n## MTR Per-Hop 调度器关键设计（当前）\n\n- **多 in-flight 探测**：每 TTL 允许最多 `MaxInFlightPerHop`（默认 3）个并发探测。\n  - `mtrHopState.inFlightCount` 是计数器（非 bool）。\n  - 这解决了高丢包 hop 因超时阻塞导致 Snt 积累速率远低于低丢包 hop 的问题。\n- **nextAt 基于发送时间**：`launchProbe` 时设 `nextAt = now + hopInterval`。\n  - 不再等探测完成才设 nextAt，调度器可在超时探测还在飞行中时为同一 TTL 发射新探测。\n  - 这保证了所有 TTL 的 Snt 积累速率大致相同，不受丢包率影响。\n- **全局并发限制**：`inFlight`（全局计数器）< `parallelism` 仍然有效。\n- **`MaxInFlightPerHop` 配置**：`mtrSchedulerConfig.MaxInFlightPerHop`，默认动态计算。\n  - 动态默认 = `ceil(Timeout / HopInterval) + 1`（至少 1）。\n  - 例：`Timeout=2s, HopInterval=1s` → 默认 3；`Timeout=2s, HopInterval=200ms` → 默认 11。\n  - 用户显式设置 > 0 时优先使用用户值。\n\n## MTR 引擎关键机制（易踩坑）\n\n- 目的地提前停止：\n  - `knownFinalTTL`（持久缓存）用于缩短后续探测的 TTL 上界；高 TTL 标记 disabled 后不再调度。\n- seq 16 位回卷处理：\n  - `seqWillWrap(...)` 触发 `rotateEngine(...)`\n  - 轮换 echoID 并重建 listener，协议层隔离新旧回包。\n- 额外安全网：\n  - onICMP 中有 RTT 合理性检查（`<=0` 或 `>timeout` 丢弃）。\n- 流式预览：\n  - 仅已发送 TTL 才会参与预览；未发送 TTL 保持 nil 槽位，避免提前计入 Snt/Loss。\n\n## Web Console / WebSocket（server/）\n\n### WS 架构（`server/ws_handler.go`，~451 行）\n\n- **异步写模型**：`wsTraceSession` 使用 `sendCh`（buffered channel，1024）+ `writeLoop` goroutine。\n  - 调用方通过 `send(envelope)` 非阻塞投递；channel 满时返回 `errWSSlowConsumer`。\n  - `writeLoop` 从 `sendCh` 取消息，`SetWriteDeadline` + `WriteJSON`。\n- **关闭路径**：\n  - `closeWithCode(code, reason)`：异常关闭（slow consumer / write error），关 `stopCh` + 发 close frame。\n  - `finish()`：正常结束，`sendMu` 下关 `sendCh`，等 `writerDone`，再关 conn。\n  - 两者均幂等（`closeOnce` / `finishOnce`）。\n- **可测试性**：`wsConn` 接口 + `fakeWSConn` mock（`server/ws_handler_test.go`）。\n- **常量**：`wsSendQueueSize=1024`，`wsWriteTimeout=5s`。\n\n### Web MTR 调度模式（重要变更）\n\n- **已从 round-based 迁移到 per-hop 调度**。\n- `runMTRTrace()`：\n  - 优先读 `HopIntervalMs`，fallback `IntervalMs`，再缺省 1000ms。\n  - `MaxRounds` → `MaxPerHop`（0 = 无限运行直到客户端断开）。\n  - 不再使用 legacy round-based 的 `Interval` / `RunRound`。\n- `executeMTRRaw()` 两路分支：\n  - `HopInterval > 0`：per-hop 模式，仅在 NextTrace API/FastIP 初始化阶段短暂加锁；长期探测不再依赖 `SrcDev` / `DisableMPLS` 等进程级全局。\n  - fallback：legacy round-based 模式（保留兼容），`RunRound` 回调内 per-round 锁定。\n  - `trace/runMTRRawRoundBased()` 也会先做 `normalizeRuntimeConfig(&cfg)`，因此 legacy raw 路径同样能继承 `SourceDevice`；`DisableMPLS` 不再从全局反向覆盖会话配置。\n- `traceRequest` 新增 `HopIntervalMs` 字段（`json:\"hop_interval_ms\"`），与 `IntervalMs` 解耦。\n- 前端 MTR 请求现在发送 `hop_interval_ms=1000`，不再把旧的 `interval_ms=2000` 当默认值。\n- 前端 raw 聚合键现在按 TTL 折叠，避免同一 hop 的 timeout / success 被拆成两行。\n\n### 前端渲染节流（`server/web/assets/app.js`）\n\n- MTR raw 消息通过 `scheduleMTRRender()` 节流，最小间隔 100ms，优先 `requestAnimationFrame`。\n- `cancelScheduledMTRRender()` 在 `clearResult`、socket close/error 路径调用，避免孤儿回调。\n- `flushMTRRender()` 立即执行挂起渲染。\n\n### 其他 server 文件\n\n- `server/server.go`：Gin 路由注册\n- `server/handlers.go`：REST 接口\n- `server/mtr.go`：MTR 专用 handler 逻辑\n- `server/trace_handler.go`：traceroute handler\n- `server/cache_handler.go`：缓存\n\n## Deploy WebUI / MCP / Agent Skill（2026-04 追加）\n\n- MCP 只属于完整版 `nexttrace`，`nexttrace-tiny` / `ntr` 不注册 WebUI/MCP flags。\n- `--mcp` 只能与 `--deploy` 同用；单独传 `--mcp` 必须报错。\n- `--deploy --mcp` 不提供 stdio MCP，只通过 deploy 网络服务在 `/mcp` 暴露 Streamable HTTP，使用官方 `github.com/modelcontextprotocol/go-sdk`。\n- `server.RunWithOptions(options, onReady)` 是 deploy 新入口；`server.Options` 包含 `ListenAddr`、`EnableMCP`、`AuthEnabled`、`DeployToken`。\n- deploy 鉴权规则：\n  - 监听 `127.0.0.1`、`::1`、`localhost` 默认免 token。\n  - 监听 `0.0.0.0`、`::`、非 loopback IP/host 时默认启用 token。\n  - 外网监听且用户未提供 token 时启动生成随机 token 并输出到 stdout。\n  - `--deploy-token` 优先于 `NEXTTRACE_DEPLOY_TOKEN`；手动 token 不回显。\n  - `AuthEnabled=true` 但 token 为空时必须 fail closed。\n  - WebUI 走 `/auth/login`，成功后写 HttpOnly cookie。\n  - API/MCP/WS 支持 `Authorization: Bearer <token>`、`X-NextTrace-Token` 和 cookie。\n  - 不支持 URL query token，尤其 WebSocket 不允许 query token。\n- auth middleware 覆盖 `/`、`/assets/*`、`/api/*`、`/ws/trace`、`/mcp`。\n- `/mcp` 不要用 `router.Any(\"/mcp\")`，会与 Gin 的静态资源 wildcard `/*path` 冲突；当前只注册 `GET`、`POST`、`DELETE`。\n- MCP 与 Web 共用 `internal/service`，不要在 MCP tool 中拼 CLI 参数再反调 CLI。\n- MCP tool 输出以 `structuredContent` 为主；schema 要把参数状态分成 `supported`、`not_applicable`、`not_yet_supported`。\n- 当前 MCP tools：\n  - `nexttrace_capabilities`\n  - `nexttrace_traceroute`\n  - `nexttrace_mtr_report`\n  - `nexttrace_mtr_raw`\n  - `nexttrace_mtu_trace`\n  - `nexttrace_speed_test`\n  - `nexttrace_annotate_ips`\n  - `nexttrace_geo_lookup`\n  - `nexttrace_globalping_trace`\n  - `nexttrace_globalping_limits`\n  - `nexttrace_globalping_get_measurement`\n- Globalping MCP 使用 service 专用模型，不复用 CLI `--from` 的单结果输出模型；`nexttrace_globalping_trace` 应返回 `measurement_id`、`status`、`probes_count` 和按 probe 展开的 `results[]`。\n- Globalping MCP 当前支持 `target`、`locations[]`、`limit`、`protocol`、`port`、`packets`、`ip_version`；不支持本地 `source/dev/dot_server/packet_size/tos/ttl_interval`。\n- Repo Skill 位于 `skills/nexttrace/`，入口是 `skills/nexttrace/SKILL.md`；更新 MCP tool 或参数时必须同步 skill references。\n- deploy/MCP 相关回归建议至少覆盖：\n  - `go test ./...`\n  - `node --test server/web/assets/*.test.cjs`\n  - `go test -tags flavor_tiny ./cmd ./server`\n  - `go test -tags flavor_ntr ./cmd ./server`\n  - 本地 MCP smoke，traceroute 在 macOS 可显式传 `source_device`（例如 `en8`）。\n\n## DoT 与 Geo DNS\n\n- `--dot-server` 不仅影响目标域名解析，也影响 GeoIP API / NextTrace API FastIP 的域名解析链路。\n- 关键文件：`util/dns_resolver.go`\n  - `SetGeoDNSResolver(dotServer)`\n  - `WithGeoDNSResolver(dotServer, fn)`：为 Web/API 请求提供作用域化的 resolver 切换；不同 resolver 串行切换，相同 resolver 允许安全嵌套，避免 `GetSourceWithGeoDNS` + 外层作用域组合时死锁。\n  - `geoResolverOverride` 的读写现在也走 `geoMu`，避免测试覆盖 resolver 时的数据竞争。\n  - `LookupHostForGeo(ctx, host)`：IP 字面量短路 -> DoT -> 失败时按配置 fallback 系统 DNS\n- `cmd/cmd.go` 在早期阶段（fast-trace / ws 初始化之前）注入 DoT 解析策略，避免早期分支绕过。\n- `server/trace_handler.go` 通过 `ipgeo.GetSourceWithGeoDNS(...)` + `WithGeoDNSResolver(...)` 让 Web/API 请求也遵守 `dot_server`，包括 NextTrace API/FastIP 初始化阶段。\n- Geo HTTP 请求统一走 `util.NewGeoHTTPClient(...)`（`util/http_client_geo.go`），其 Transport 现在从默认 Transport `Clone()` 而来，保留代理/HTTP2/连接池等标准行为。\n\n## NextTrace API FastIP 与 MTR 首行\n\n- `util/latency.go`：\n  - `FastIPMetaCache` 缓存节点元数据（IP/Latency/NodeName）\n  - `SuppressFastIPOutput` 可抑制彩色横幅\n- `GetFastIP(...)` 的 DNS 阶段现在显式受 `timeout` 限制；`FastIPMetaCache` 也改为在 fallback/default IP 决定后再写入，避免缓存空 IP。\n- MTR 模式在进入 TUI 前会设 `SuppressFastIPOutput=true`，避免污染主终端历史。\n- MTR TUI/report 首行 `APIInfo` 由 `cmd/mtr_mode.go` 的 `buildAPIInfo(...)` 生成（仅 NextTrace API 且有元数据时显示）。\n- MTR raw 首行由 `buildRawAPIInfoLine(...)` 生成（格式略不同，包含延迟信息）。\n\n## `--source` / `--dev` 现状\n\n- `--dev` 在 `cmd/cmd.go` 先解析网卡并推导 `srcAddr`（已处理非 `*net.IPNet` 地址类型，避免 panic）。\n- `trace.Config` 现在显式携带 `SourceDevice` / `DisableMPLS`，Darwin TCP/UDP 抓包与 MPLS 解析优先走会话级配置，不再依赖 Web 侧临时改写全局变量。\n- `trace.Config` 也显式携带 `Context`；`TracerouteWithContext(...)` 通过把上游 ctx 传入各 tracer 的 `signal.NotifyContext(...)` 基底，让 TCP/UDP fallback MTR 可以响应取消。\n- Windows 下 ICMP/TCP/UDP 的 `--dev` 都必须保持 v1.6.2 兼容行为：先解析指定网卡对应的 source IP，写入 `SrcAddr`，再清空 `SourceDevice`，让后续路径按 source address 继续运行；不要把 Windows `--dev` 改成拒绝执行或报错退出。\n- 这只是通过 source IP 影响 Windows 路由选择，不代表 WinDivert 或 socket 已支持真实按设备绑定；README 需要如实说明可能不准确。独立 `--mtu` 同样按 source-address 发送，但可额外保留 device 名用于本地 MTU 查询。\n- MTR 标题显示源信息来自：\n  - `--source`（最高优先）\n  - `--dev` 推导\n  - UDP dial fallback\n- 相关函数：`cmd/mtr_mode.go -> resolveSrcIP(...)`\n\n## CI 与工具链（当前）\n\n- `go.mod`: `go 1.26.5`\n- GitHub Actions：\n  - `.github/workflows/build.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/test.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/regression.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/golangci-lint.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - test workflow 中 `GOTOOLCHAIN=go1.26.5+auto`\n  - build matrix 已移除 `windows/arm`\n- `.cross_compile.sh` 与 workflow 里的 `go build` 现在都用数组构造 `-tags` 参数，避免 shell word-splitting；脚本也会把当前 `GOARM` 传给 `compress_with_upx`，使 linux/armv7 目标能命中对应压缩分支。\n- `ipgeo/ipdbone.go` 不再原地修改全局 `defaultClient.httpClient.Timeout`；超时覆盖会通过克隆 client（复用 token cache / token init，同步替换整个 HTTP client）实现，避免 dial timeout 与 client timeout 脱节。\n\n## 关键文件导航\n\n- CLI 主调度：`cmd/cmd.go`（~855 行）\n- MTR 参数/流程：`cmd/mtr_mode.go`（~315 行）\n- MTR 交互输入：`cmd/mtr_ui.go`\n- MTR 引擎：`trace/mtr_runner.go`\n- MTR 聚合：`trace/mtr_stats.go`\n- MTR TUI：`printer/mtr_tui.go`（~691 行）\n- MTR table/report：`printer/mtr_table.go`（~625 行）\n- MTR TUI 颜色：`printer/mtr_tui_color.go`\n- WS handler：`server/ws_handler.go`（~449 行）\n- 前端：`server/web/assets/app.js`\n- deploy auth：`server/auth.go`\n- deploy MCP：`server/mcp.go`\n- Web/MCP 共享服务层：`internal/service/`\n- Repo Agent Skill：`skills/nexttrace/`\n- Geo DoT 解析：`util/dns_resolver.go`\n- Geo HTTP 客户端：`util/http_client_geo.go`\n- FastIP：`util/latency.go`\n\n## 2026-03 Gocyclo 重构快照\n\n- 第一波低风险重构已落地：\n  - `ipgeo.Filter` 改为 CIDR 规则表驱动。\n  - `util.DomainLookUp` 拆成 resolver / lookup / family filter / interactive select 四段。\n  - `server.prepareTrace`、`normalizeTarget`、`trace.Traceroute`、`trace.Hop.fetchIPData` 已改成薄协调器。\n  - `GetMTUByIPForDevice`、`GetICMPResponsePayload`、`parseIPDBOneResponse`、`GlobalpingFormatLocation` 已拆 helper。\n- 第二波主流程/输出层已部分落地：\n  - `cmd.Execute` 已拆成 parser 注册 helper、启动模式 helper、运行时调度 helper。\n  - `fast_trace.FastTest` / `testFile` 已拆成交互选择、源地址推导、文件目标解析、单目标执行。\n  - `server.mtrAggregator.Update`、`wshandle.messageSendHandler` 已改成薄入口。\n  - `printer.RealtimePrinter`、`RealtimePrinterWithRouter`、`tracelog.RealtimePrinter` 现在共用 `internal/hoprender` 的 hop attempt 分组逻辑。\n- 第三波协议层已开始落地：\n  - `trace/internal/icmp_common.go`、`tcp_common.go`、`udp_common.go` 已改成“读包循环 + 共享解码 helper + 回调派发”结构。\n  - 新增 `trace/internal/icmp_decode.go`，集中处理 ICMPv4/v6 解析、echo reply 匹配、内嵌目标 IP 校验、内嵌 ICMP seq 提取。\n  - 新增 `trace/internal/icmp_decode_test.go`，覆盖 IPv4/IPv6 echo reply、error payload、目标 IP 校验、内嵌 seq 提取。\n  - `trace/internal/udp_unix.go` 的 `SendUDP` 已拆成 IPv4/IPv6 独立发送 helper；`trace/udp_ipv4.go` 的 `send()` 也已拆成配额检查、构包、超时守护、发送记账四段。\n  - Windows 协议层新增 `trace/internal/windivert_sniff_windows.go`，把 WinDivert sniff handle 打开、收包、ICMP/TCP 解码下沉为共享 helper；`icmp_windows.go` / `tcp_windows.go` / `udp_windows.go` 的 sniff 入口已变成薄协调器。\n  - Darwin `trace/internal/icmp_darwin.go:ListenPacket` 已拆成 socket spec、接口绑定、bind sockaddr、finalize packet conn 四段；`trace/internal/tcp_darwin.go:ListenTCP` 也改成设备选择 + BPF + 共享 TCP reply 解码 helper。\n  - 新增 `trace/internal/tcp_probe_decode.go` 与 `trace/internal/tcp_probe_decode_test.go`，集中处理 TCP probe reply 的 seq 还原、peer IP 提取与 IPv4/IPv6 解析，供 Darwin/Windows TCP sniff 共用。\n- 当前已知剩余高复杂度主要集中在：\n  - `printer/mtr_*` 渲染层\n  - `cmd/mtr_ui.go` 输入状态机\n  - `trace/globalping.go` 的主流程函数\n  - `trace/mtr_runner.go` 中仍未拆薄的 ICMP round handler（`probeRound` / `onICMP`）\n  - 少量收尾函数：`fast_trace ipv6.go`、`reporter/reporter.go`、`trace/mtr_raw.go`\n\n## 2026-03 Gocyclo 重构快照（追加）\n\n- MTR 核心热点已完成一轮收敛：\n  - `trace/mtr_scheduler.go:runMTRScheduler` 已改成薄入口，核心状态与分支移动到 `trace/mtr_scheduler_runtime.go`。\n  - `trace/mtr_stats.go:Update` / `MigrateStats` 已拆成按 hop 分组、累加器合并、裁剪 helper；新增 `trace/mtr_stats_helpers.go`。\n  - `trace/mtr_runner.go:mtrLoop` 已改成薄入口，取消/重置/暂停/预览/backoff 分支移动到 `trace/mtr_loop_runtime.go`。\n- MTR 输出层与输入层热点也已收敛：\n  - `printer/mtr_tui.go:mtrTUIRenderWithWidth` 已拆成布局扫描、三行头部构建、host part 预构建、MPLS 续行渲染四段。\n  - `printer/mtr_table.go` 的 host 组装和 `MTRReportPrint` 已改成共享 host-part 拼接 helper + report header/row helper。\n  - `cmd/mtr_ui.go:(*mtrInputParser).Feed` 已拆成按状态分发的 parser helper。\n- 最后一批业务流程热点也已拆薄：\n  - `trace/globalping.go:GlobalpingTraceroute` 已拆成 client 构建、measurement 请求、结果解码、hop limit 推导、结果组装五段。\n  - `trace/mtr_runner.go:(*mtrICMPEngine).onICMP` / `probeRound` 已拆成 reply 校验、notify 清理、目的地 TTL 识别、round 准备、发包 sweep、等待回包、结果构建多个 helper。\n  - `fast_trace/fast_trace ipv6.go:FastTestv6`、`reporter/reporter.go:generateRouteReportNode`、`trace/mtr_raw.go:buildMTRRawRecordFromProbe` 也已分别拆成选择分发、route-node 属性构建、raw record metadata 填充 helper。\n- 当前本地复杂度扫描结果：\n  - `go run /tmp/checkcyclo.go .` 已无 `>15` 函数输出。\n  - `go test ./...` 通过。\n\n## 仍需记住的残余风险（非阻断）\n\n- `closeWithCode` 中 `closed.Store(true)` 在 `closeOnce.Do` 外部，理论上有微小竞态窗口（实际无害，因 `sendMu` 保护；且无法简单移入 Once 内部，否则第二个调用者无法设置 closed）。\n"},"files":{"CLAUDE.md":"每次执行命令前均需阅读项目根目录下的 ./AGENTS.md 文件","AGENTS.md":"# NTrace-core 项目记忆文件（2026-03 快照，rev-3）\n\n# 供 LLM 在后续会话中快速加载上下文，减少重复分析。\n\n## 项目概览\n\n- 名称：NextTrace (NTrace-core)\n- 仓库：github.com/nxtrace/NTrace-core\n- 模块：`github.com/nxtrace/NTrace-core`\n- 语言：Go（`go 1.26.5`）\n- 入口：`main.go -> cmd.Execute()`\n- 核心能力：ICMP/TCP/UDP traceroute、GeoIP/RDNS、MTR 连续探测、Web/API、多平台构建\n\n## 构建与测试（必须遵守）\n\n- 常用命令：\n  - 构建：`go build ./...`\n  - 测试：`go test ./...`\n- 交叉编译脚本：`.cross_compile.sh`\n- Darwin 下 `trace/internal/icmp_darwin.go` 已不再使用 `//go:linkname`，改为\n  `syscall.Socket` + `os.NewFile` + 自定义 `icmpPacketConn`（实现 `net.PacketConn` /\n  `net.Conn` / `syscall.Conn` + `ReadMsgIP` 以满足 `x/net/internal/socket.ipConn`\n  接口），并在 `ReadFrom` 中调用 `stripIPv4Header` 剥离 macOS DGRAM ICMP socket\n  返回的外层 IP 头。\n\n## Git 与提交信息规范（必须遵守）\n\n- Commit message 必须使用 Conventional Commits 风格：`type(scope): 具体行为`。\n- subject 必须描述该提交实际改变的行为或约束，不能只写“处理 review 问题”“修复剩余问题”“调整代码”等泛化内容。\n- review finding 修复提交也必须写明具体修复点，例如 timeout、fallback、权限校验、stdout/stderr 分离；若一个提交覆盖多个 finding，用 body bullet 简短列出。\n- 一个提交包含多个互不相关主题时，优先拆分提交；确实需要放在一起时，subject 写主影响，body 写清每个具体改动。\n- 提交或 push PR 前必须检查 `git log --oneline main..HEAD`，发现泛化、误导或与 diff 不匹配的 commit message，应先 reword/amend。\n- 改写已推送 PR 分支历史后，只使用 `git push --force-with-lease`，不要用无保护的强推覆盖远端新提交。\n- 在本工作区发布 PR 时，目标仓库必须是 `nxtrace/NTrace-dev`；禁止直接向 `nxtrace/NTrace-core` 创建 PR。\n- 如果 GitHub 工具或远端推断结果指向 `nxtrace/NTrace-core`，必须改为 `nxtrace/NTrace-dev` 或停止确认，不能直接创建 PR。\n\n## 当前 CLI 语义（重点）\n\n### 常规 traceroute 路径\n\n- `--table`：现在是\"最终汇总表\"模式（一次探测完成后输出汇总表），不再是旧的异步 table 刷新模式。\n- `--route-path`：仍由 `reporter.New(...).Print()` 负责（与 MTR report 无关）。\n\n### 独立 `--mtu` 路径\n\n- `--mtu`：独立 UDP path-MTU / tracepath 风格模式，不复用普通 `trace.Traceroute` / MTR / Web 路径。\n- flavor 可用性：仅 `nexttrace` / `nexttrace-tiny` 包含；`ntr` 不注册该 flag。\n- 输出：\n  - `TTY`：当前 TTL 占位后原地更新，边探测边刷行。\n  - `非 TTY`：TTL 定稿后逐行流式输出，不使用 renderer 自己的 ANSI 控制序列。\n  - `--json`：输出独立 mtu schema；`hop.geo` 已存在。\n- 参数语义：\n  - 复用 `--data-provider`、`--language`、`--no-rdns`、`--always-rdns`、`--dot-server`。\n  - `--mtu` 仍只支持 UDP；显式 `--tcp` 冲突报错。\n- Geo/RDNS：\n  - `trace/mtu` 自带独立 metadata helper，不依赖普通 `trace.Hop.fetchIPData`。\n  - 流式事件会先输出基础 hop，再在同一 TTL 内补一条带 Geo/RDNS 的 update，最后 `ttl_final` 定稿。\n  - macOS 上曾有 `Warning: macOS --mtu support is experimental.` 提示，现已删除；不要再假设 CLI 会打印这句。\n\n### `--psize` / `--tos` 语义与平台差异\n\n- `--psize` 现在统一对齐 `mtr -s/--psize`：\n  - 用户输入语义是“含 IP + 当前探测协议头的总字节数”。\n  - 内部 `trace.Config.PktSize` 仍保存 payload bytes。\n  - 未显式传入时，不再固定默认 `52`，而是按协议/IP 族自动取最小合法值：\n    - ICMPv4 / UDPv4 = `28`\n    - TCPv4 = `44`\n    - ICMPv6 = `48`\n    - UDPv6 = `50`\n    - TCPv6 = `64`\n  - 负数 `--psize` 表示“每个 probe 独立随机”，CLI 允许 `--psize -84` 这种写法并会在解析前归一化。\n- `--tos` / `-Q`：\n  - 范围固定 `0..255`。\n  - `--mtu` 与 Globalping 显式传 `--psize` / `--tos` 会直接报不支持。\n- 平台发送路径差异（这是后续判断 bug 的关键记忆）：\n  - Linux / 其他 Unix：\n    - `ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生 socket/raw socket 路径。\n    - `--tos` 只是在现有路径上设置 `TOS/TrafficClass`，不会切换实现。\n  - macOS：\n    - 与 Linux 类似，`ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生发送路径。\n    - `--tos` 同样只是在现有路径上设置 `TOS/TrafficClass`。\n  - Windows：\n    - `TCP/UDP` 的 IPv4/IPv6 一直走 WinDivert raw send。\n    - `ICMPv4` 一直走 socket path（`SetTOS` / `SetTTL`）。\n    - `ICMPv6`：\n      - 默认或 `--tos 0`：继续走原生 socket path，只设置 `HopLimit`，保持与 `v1.5.2` 一致。\n      - 非零 `--tos`：切到 WinDivert raw send，直接发送完整 `IPv6 + ICMPv6` 报文，因为 Windows 的 `x/net/ipv6.PacketConn` 不能可靠设置 `TrafficClass`。\n    - 因此，Windows 上只有“`ICMPv6` 且 `--tos != 0`”这个组合会额外依赖 WinDivert 发送能力；README 中英两份都已写明。\n\n### 间隔默认值（分层体系）\n\n- `-z/--send-time`：每包间隔，默认 `defaultPacketIntervalMs = 50` ms。\n- `-i/--ttl-time`：\n  - **常规 traceroute**：TTL 分组间隔，默认 `defaultTracerouteTTLIntervalMs = 300` ms。\n  - **MTR 模式**：`normalizeMTRTraceConfig()` 始终覆盖为 `defaultMTRInternalTTLIntervalMs = 0` ms（各 TTL 间不间隔）。\n  - MTR 每跳探测间隔由 `-i` 显式传值 或 默认 1000ms 决定（见下文 `-q/-i` 语义）。`-z/--send-time` 在 MTR 模式下被忽略。\n\n### MTR 相关参数\n\n- `-t/--mtr`：开启 MTR 交互模式（TTY 全屏 TUI）。\n- `-r/--report`：MTR 报告模式（非交互），隐式开启 MTR。\n- `-w/--wide`：宽报告模式，隐式等价 `--mtr --report --wide`。\n- `--raw`：与 MTR 组合时进入 **MTR raw 流式模式**（`runMTRRaw`），不再与 MTR 冲突。\n- 有效 MTR 开关：`effectiveMTR = mtr || report || wide`。\n- MTR 三路分支（`chooseMTRRunMode`）：\n  1. `effectiveMTRRaw` → `runMTRRaw`（流式行输出，适合管道/脚本）\n  2. `effectiveReport` → `runMTRReport`（非交互报告表）\n  3. 默认 → `runMTRTUI`（全屏 TUI）\n- MTR 冲突参数（会直接报错退出）：`--table` `--classic` `--json` `--output` `--route-path` `--from` `--fast-trace` `--file` `--deploy`。\n  - **注意**：`--raw` 不再是冲突参数。\n\n### MTR 中 `-q/-i/-y` 的新语义\n\n- `-q/--queries`：\n  - 在 MTR report 下表示每跳探测次数，默认 10（仅当用户未显式传 `-q`）。\n  - 在 MTR TUI 下表示每跳最大探测次数，未显式传时默认无限运行。\n- `-i/--ttl-time`：\n  - 在 MTR 下表示每跳探测间隔毫秒，默认 1000ms（仅当用户未显式传 `-i`）。\n  - 各 TTL 间内部扫描间隔固定 0ms（`normalizeMTRTraceConfig` 覆盖为 `defaultMTRInternalTTLIntervalMs = 0`）。\n  - `-z/--send-time` 在 MTR 模式下被忽略。\n- `-y/--ipinfo <0..4>`：\n  - TUI 初始 Host 显示模式，默认 0（IP/PTR only）。\n  - 0=Base(IP/PTR) 1=ASN 2=City 3=Owner 4=Full\n  - 仅 TUI 模式生效，report/raw 不受影响。\n\n### MTR report wide / non-wide 区别\n\n- **wide 模式**（`-w` 或 `--mtr --report --wide`）：\n  - 查询 GeoIP，显示完整 host 信息（ASN + geo + MPLS）。\n- **非 wide 模式**（`-r` 或 `--mtr --report`）：\n  - `normalizeMTRReportConfig` 设 `IPGeoSource=nil`（不查 geo）、`AlwaysWaitRDNS=true`。\n  - 显示 `formatCompactReportHost`：仅 IP/PTR + ASN，无 geo 列。\n\n## MTR 运行链路（重要文件）\n\n- 入口与调度：`cmd/mtr_mode.go`（~315 行）\n  - `runMTRTUI(...)` / `runMTRReport(...)` / `runMTRRaw(...)`\n  - `normalizeMTRTraceConfig(conf)` / `normalizeMTRReportConfig(conf, wide)`\n  - `buildAPIInfo(...)` / `buildRawAPIInfoLine(...)`\n  - MTR CLI 现在统一使用 `signal.NotifyContext(...)` 管理 Ctrl-C / SIGTERM；不再保留额外的 `sigCh` + goroutine 等待器。\n- 交互控制：`cmd/mtr_ui.go`\n  - alternate screen + raw mode\n  - 输入状态机 `mtrInputParser`（字节流，吞掉 CSI/SS3/OSC/鼠标/焦点等序列）\n  - Enter/Leave 显式关闭输入扩展模式：1000/1002/1003/1006/1015/1004/2004\n  - Quit 路径会先判空 `cancel`，因此 `newMTRUI(nil, ...)` / 测试注入 nil 不会 panic。\n- 核心探测循环：`trace/mtr_runner.go`\n  - `RunMTR` / `mtrLoop` / `RunMTRRaw`\n  - 支持暂停、重置、流式预览（`ProgressThrottle` 默认 200ms）\n  - ICMP 持久引擎 + TCP/UDP fallback\n- 统计聚合：`trace/mtr_stats.go`\n  - `MTRAggregator` / `MTRHopStat`\n  - unknown 合并策略：单路径时把 unknown 合并到唯一已知路径，避免同 TTL 分裂成 waiting + 真实 IP 两行\n- 输出层：\n  - TUI：`printer/mtr_tui.go`\n  - table/report：`printer/mtr_table.go`\n  - raw 行格式化：`printer.FormatMTRRawLine(rec)`\n  - TUI 颜色：`printer/mtr_tui_color.go`\n\n## MTR 交互行为（当前）\n\n- `q`/`Q`/`Ctrl-C`：退出\n- `p`：暂停\n- `SPACE`：恢复\n- `r`：重置统计\n- `y`：切换 Host 显示模式（IP/PTR → ASN → City → Owner → Full → 循环）\n- `n`：切换 Host 基名显示（PTR-or-IP / IP-only）\n- `e`：切换 MPLS 标签显示（toggle MPLS on/off）\n\n## MTR 显示与统计规则（当前）\n\n- Host 显示支持 5 种模式（`-y/--ipinfo` 设初始值，`y` 键运行时循环）：\n  - `HostModeBase=0`：仅 IP/PTR，无 ASN 前缀\n  - `HostModeASN=1` / `HostModeCity=2` / `HostModeOwner=3` / `HostModeFull=4`\n  - `HostNamePTRorIP` / `HostNameIPOnly`\n- 默认语言：`cn`（`--language en` 才优先英文字段）\n- waiting 判定：`loss >= 99.95 && IP==\"\" && Host==\"\"`\n  - 显示为 `(waiting for reply)`\n  - 指标列（Loss/Snt/Last/Avg/Best/Wrst/StDev）留空\n- TUI Host 对齐（重要，已从 tab 改为手动空格）：\n  - `buildTUIHostParts(stat, mode, nameMode, lang, showIPs)` 生成结构化 parts\n  - `computeTUIASNWidth(stats, ...)` 扫描所有 hop 确定 ASN 列最大宽度\n  - `formatTUIHost(parts, asnW)` 用 `padRight(asn, asnW)` + 空格拼接（不用 `\\t`）\n  - ASN 为空但 IP 已知时填 `\"AS???\"` 占位符，保证列对齐（HostModeBase 除外，该模式不显示 ASN）\n  - waiting hop 不填占位符\n- compact report host（非 wide report）：\n  - `formatCompactReportHost(stat, nameMode, lang)` 仅输出 hostname/IP + ASN\n- TUI 其他特性：\n  - 终端宽度自适应 + CJK 宽度计算（go-runewidth）\n  - 窄屏右锚定指标区\n  - 动态 hop 前缀宽度（覆盖 3 位/4 位 TTL）\n  - MPLS 独立续行显示\n  - 紧凑指标列宽度：Loss=5 Snt=3 RTT=7 RTTMin=5\n\n## MTR 目的地检测与高 TTL 丢弃\n\n- 当 `knownFinalTTL` 已确定后，所有 `TTL > knownFinalTTL` 的调度槽位被标记为 `disabled`。\n- disabled TTL 的探测回包（包括在途探测返回的 dst-ip 回复）**一律丢弃**，不折叠、不计入任何统计。\n- **MaxPerHop 上限检查**（`states[originTTL].completed + inFlightCount >= MaxPerHop`）：\n  - 调度时使用 `completed + inFlightCount >= MaxPerHop` 防止超发。\n  - 完成时仍检查 `completed >= MaxPerHop` 丢弃溢出结果。\n- `originTTL < curFinal` 时（更低 TTL 先到 dst-ip → 降低 `knownFinalTTL`）：\n  - 保存 `oldFinal`，更新 `knownFinalTTL = originTTL`，disable 所有 `originTTL+1..maxHops`。\n  - 调用 `agg.ClearHop(oldFinal)`：清除旧 finalTTL 的聚合数据（避免幽灵行），**不合并**到新 finalTTL。\n  - 新 finalTTL 由独立的 per-hop 调度器自行积累新鲜探测数据，不存在 Snt 膨胀问题。\n- 调度状态（`inFlightCount`/`nextAt`/`consecutiveErrs`）更新在 `originTTL`。\n- 统计聚合（`completed++`/`agg.Update`/`onProbe`）均使用 `originTTL`（不再有 `accountTTL` 分离）。\n\n## MTR Per-Hop 调度器关键设计（当前）\n\n- **多 in-flight 探测**：每 TTL 允许最多 `MaxInFlightPerHop`（默认 3）个并发探测。\n  - `mtrHopState.inFlightCount` 是计数器（非 bool）。\n  - 这解决了高丢包 hop 因超时阻塞导致 Snt 积累速率远低于低丢包 hop 的问题。\n- **nextAt 基于发送时间**：`launchProbe` 时设 `nextAt = now + hopInterval`。\n  - 不再等探测完成才设 nextAt，调度器可在超时探测还在飞行中时为同一 TTL 发射新探测。\n  - 这保证了所有 TTL 的 Snt 积累速率大致相同，不受丢包率影响。\n- **全局并发限制**：`inFlight`（全局计数器）< `parallelism` 仍然有效。\n- **`MaxInFlightPerHop` 配置**：`mtrSchedulerConfig.MaxInFlightPerHop`，默认动态计算。\n  - 动态默认 = `ceil(Timeout / HopInterval) + 1`（至少 1）。\n  - 例：`Timeout=2s, HopInterval=1s` → 默认 3；`Timeout=2s, HopInterval=200ms` → 默认 11。\n  - 用户显式设置 > 0 时优先使用用户值。\n\n## MTR 引擎关键机制（易踩坑）\n\n- 目的地提前停止：\n  - `knownFinalTTL`（持久缓存）用于缩短后续探测的 TTL 上界；高 TTL 标记 disabled 后不再调度。\n- seq 16 位回卷处理：\n  - `seqWillWrap(...)` 触发 `rotateEngine(...)`\n  - 轮换 echoID 并重建 listener，协议层隔离新旧回包。\n- 额外安全网：\n  - onICMP 中有 RTT 合理性检查（`<=0` 或 `>timeout` 丢弃）。\n- 流式预览：\n  - 仅已发送 TTL 才会参与预览；未发送 TTL 保持 nil 槽位，避免提前计入 Snt/Loss。\n\n## Web Console / WebSocket（server/）\n\n### WS 架构（`server/ws_handler.go`，~451 行）\n\n- **异步写模型**：`wsTraceSession` 使用 `sendCh`（buffered channel，1024）+ `writeLoop` goroutine。\n  - 调用方通过 `send(envelope)` 非阻塞投递；channel 满时返回 `errWSSlowConsumer`。\n  - `writeLoop` 从 `sendCh` 取消息，`SetWriteDeadline` + `WriteJSON`。\n- **关闭路径**：\n  - `closeWithCode(code, reason)`：异常关闭（slow consumer / write error），关 `stopCh` + 发 close frame。\n  - `finish()`：正常结束，`sendMu` 下关 `sendCh`，等 `writerDone`，再关 conn。\n  - 两者均幂等（`closeOnce` / `finishOnce`）。\n- **可测试性**：`wsConn` 接口 + `fakeWSConn` mock（`server/ws_handler_test.go`）。\n- **常量**：`wsSendQueueSize=1024`，`wsWriteTimeout=5s`。\n\n### Web MTR 调度模式（重要变更）\n\n- **已从 round-based 迁移到 per-hop 调度**。\n- `runMTRTrace()`：\n  - 优先读 `HopIntervalMs`，fallback `IntervalMs`，再缺省 1000ms。\n  - `MaxRounds` → `MaxPerHop`（0 = 无限运行直到客户端断开）。\n  - 不再使用 legacy round-based 的 `Interval` / `RunRound`。\n- `executeMTRRaw()` 两路分支：\n  - `HopInterval > 0`：per-hop 模式，仅在 NextTrace API/FastIP 初始化阶段短暂加锁；长期探测不再依赖 `SrcDev` / `DisableMPLS` 等进程级全局。\n  - fallback：legacy round-based 模式（保留兼容），`RunRound` 回调内 per-round 锁定。\n  - `trace/runMTRRawRoundBased()` 也会先做 `normalizeRuntimeConfig(&cfg)`，因此 legacy raw 路径同样能继承 `SourceDevice`；`DisableMPLS` 不再从全局反向覆盖会话配置。\n- `traceRequest` 新增 `HopIntervalMs` 字段（`json:\"hop_interval_ms\"`），与 `IntervalMs` 解耦。\n- 前端 MTR 请求现在发送 `hop_interval_ms=1000`，不再把旧的 `interval_ms=2000` 当默认值。\n- 前端 raw 聚合键现在按 TTL 折叠，避免同一 hop 的 timeout / success 被拆成两行。\n\n### 前端渲染节流（`server/web/assets/app.js`）\n\n- MTR raw 消息通过 `scheduleMTRRender()` 节流，最小间隔 100ms，优先 `requestAnimationFrame`。\n- `cancelScheduledMTRRender()` 在 `clearResult`、socket close/error 路径调用，避免孤儿回调。\n- `flushMTRRender()` 立即执行挂起渲染。\n\n### 其他 server 文件\n\n- `server/server.go`：Gin 路由注册\n- `server/handlers.go`：REST 接口\n- `server/mtr.go`：MTR 专用 handler 逻辑\n- `server/trace_handler.go`：traceroute handler\n- `server/cache_handler.go`：缓存\n\n## Deploy WebUI / MCP / Agent Skill（2026-04 追加）\n\n- MCP 只属于完整版 `nexttrace`，`nexttrace-tiny` / `ntr` 不注册 WebUI/MCP flags。\n- `--mcp` 只能与 `--deploy` 同用；单独传 `--mcp` 必须报错。\n- `--deploy --mcp` 不提供 stdio MCP，只通过 deploy 网络服务在 `/mcp` 暴露 Streamable HTTP，使用官方 `github.com/modelcontextprotocol/go-sdk`。\n- `server.RunWithOptions(options, onReady)` 是 deploy 新入口；`server.Options` 包含 `ListenAddr`、`EnableMCP`、`AuthEnabled`、`DeployToken`。\n- deploy 鉴权规则：\n  - 监听 `127.0.0.1`、`::1`、`localhost` 默认免 token。\n  - 监听 `0.0.0.0`、`::`、非 loopback IP/host 时默认启用 token。\n  - 外网监听且用户未提供 token 时启动生成随机 token 并输出到 stdout。\n  - `--deploy-token` 优先于 `NEXTTRACE_DEPLOY_TOKEN`；手动 token 不回显。\n  - `AuthEnabled=true` 但 token 为空时必须 fail closed。\n  - WebUI 走 `/auth/login`，成功后写 HttpOnly cookie。\n  - API/MCP/WS 支持 `Authorization: Bearer <token>`、`X-NextTrace-Token` 和 cookie。\n  - 不支持 URL query token，尤其 WebSocket 不允许 query token。\n- auth middleware 覆盖 `/`、`/assets/*`、`/api/*`、`/ws/trace`、`/mcp`。\n- `/mcp` 不要用 `router.Any(\"/mcp\")`，会与 Gin 的静态资源 wildcard `/*path` 冲突；当前只注册 `GET`、`POST`、`DELETE`。\n- MCP 与 Web 共用 `internal/service`，不要在 MCP tool 中拼 CLI 参数再反调 CLI。\n- MCP tool 输出以 `structuredContent` 为主；schema 要把参数状态分成 `supported`、`not_applicable`、`not_yet_supported`。\n- 当前 MCP tools：\n  - `nexttrace_capabilities`\n  - `nexttrace_traceroute`\n  - `nexttrace_mtr_report`\n  - `nexttrace_mtr_raw`\n  - `nexttrace_mtu_trace`\n  - `nexttrace_speed_test`\n  - `nexttrace_annotate_ips`\n  - `nexttrace_geo_lookup`\n  - `nexttrace_globalping_trace`\n  - `nexttrace_globalping_limits`\n  - `nexttrace_globalping_get_measurement`\n- Globalping MCP 使用 service 专用模型，不复用 CLI `--from` 的单结果输出模型；`nexttrace_globalping_trace` 应返回 `measurement_id`、`status`、`probes_count` 和按 probe 展开的 `results[]`。\n- Globalping MCP 当前支持 `target`、`locations[]`、`limit`、`protocol`、`port`、`packets`、`ip_version`；不支持本地 `source/dev/dot_server/packet_size/tos/ttl_interval`。\n- Repo Skill 位于 `skills/nexttrace/`，入口是 `skills/nexttrace/SKILL.md`；更新 MCP tool 或参数时必须同步 skill references。\n- deploy/MCP 相关回归建议至少覆盖：\n  - `go test ./...`\n  - `node --test server/web/assets/*.test.cjs`\n  - `go test -tags flavor_tiny ./cmd ./server`\n  - `go test -tags flavor_ntr ./cmd ./server`\n  - 本地 MCP smoke，traceroute 在 macOS 可显式传 `source_device`（例如 `en8`）。\n\n## DoT 与 Geo DNS\n\n- `--dot-server` 不仅影响目标域名解析，也影响 GeoIP API / NextTrace API FastIP 的域名解析链路。\n- 关键文件：`util/dns_resolver.go`\n  - `SetGeoDNSResolver(dotServer)`\n  - `WithGeoDNSResolver(dotServer, fn)`：为 Web/API 请求提供作用域化的 resolver 切换；不同 resolver 串行切换，相同 resolver 允许安全嵌套，避免 `GetSourceWithGeoDNS` + 外层作用域组合时死锁。\n  - `geoResolverOverride` 的读写现在也走 `geoMu`，避免测试覆盖 resolver 时的数据竞争。\n  - `LookupHostForGeo(ctx, host)`：IP 字面量短路 -> DoT -> 失败时按配置 fallback 系统 DNS\n- `cmd/cmd.go` 在早期阶段（fast-trace / ws 初始化之前）注入 DoT 解析策略，避免早期分支绕过。\n- `server/trace_handler.go` 通过 `ipgeo.GetSourceWithGeoDNS(...)` + `WithGeoDNSResolver(...)` 让 Web/API 请求也遵守 `dot_server`，包括 NextTrace API/FastIP 初始化阶段。\n- Geo HTTP 请求统一走 `util.NewGeoHTTPClient(...)`（`util/http_client_geo.go`），其 Transport 现在从默认 Transport `Clone()` 而来，保留代理/HTTP2/连接池等标准行为。\n\n## NextTrace API FastIP 与 MTR 首行\n\n- `util/latency.go`：\n  - `FastIPMetaCache` 缓存节点元数据（IP/Latency/NodeName）\n  - `SuppressFastIPOutput` 可抑制彩色横幅\n- `GetFastIP(...)` 的 DNS 阶段现在显式受 `timeout` 限制；`FastIPMetaCache` 也改为在 fallback/default IP 决定后再写入，避免缓存空 IP。\n- MTR 模式在进入 TUI 前会设 `SuppressFastIPOutput=true`，避免污染主终端历史。\n- MTR TUI/report 首行 `APIInfo` 由 `cmd/mtr_mode.go` 的 `buildAPIInfo(...)` 生成（仅 NextTrace API 且有元数据时显示）。\n- MTR raw 首行由 `buildRawAPIInfoLine(...)` 生成（格式略不同，包含延迟信息）。\n\n## `--source` / `--dev` 现状\n\n- `--dev` 在 `cmd/cmd.go` 先解析网卡并推导 `srcAddr`（已处理非 `*net.IPNet` 地址类型，避免 panic）。\n- `trace.Config` 现在显式携带 `SourceDevice` / `DisableMPLS`，Darwin TCP/UDP 抓包与 MPLS 解析优先走会话级配置，不再依赖 Web 侧临时改写全局变量。\n- `trace.Config` 也显式携带 `Context`；`TracerouteWithContext(...)` 通过把上游 ctx 传入各 tracer 的 `signal.NotifyContext(...)` 基底，让 TCP/UDP fallback MTR 可以响应取消。\n- Windows 下 ICMP/TCP/UDP 的 `--dev` 都必须保持 v1.6.2 兼容行为：先解析指定网卡对应的 source IP，写入 `SrcAddr`，再清空 `SourceDevice`，让后续路径按 source address 继续运行；不要把 Windows `--dev` 改成拒绝执行或报错退出。\n- 这只是通过 source IP 影响 Windows 路由选择，不代表 WinDivert 或 socket 已支持真实按设备绑定；README 需要如实说明可能不准确。独立 `--mtu` 同样按 source-address 发送，但可额外保留 device 名用于本地 MTU 查询。\n- MTR 标题显示源信息来自：\n  - `--source`（最高优先）\n  - `--dev` 推导\n  - UDP dial fallback\n- 相关函数：`cmd/mtr_mode.go -> resolveSrcIP(...)`\n\n## CI 与工具链（当前）\n\n- `go.mod`: `go 1.26.5`\n- GitHub Actions：\n  - `.github/workflows/build.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/test.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/regression.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/golangci-lint.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - test workflow 中 `GOTOOLCHAIN=go1.26.5+auto`\n  - build matrix 已移除 `windows/arm`\n- `.cross_compile.sh` 与 workflow 里的 `go build` 现在都用数组构造 `-tags` 参数，避免 shell word-splitting；脚本也会把当前 `GOARM` 传给 `compress_with_upx`，使 linux/armv7 目标能命中对应压缩分支。\n- `ipgeo/ipdbone.go` 不再原地修改全局 `defaultClient.httpClient.Timeout`；超时覆盖会通过克隆 client（复用 token cache / token init，同步替换整个 HTTP client）实现，避免 dial timeout 与 client timeout 脱节。\n\n## 关键文件导航\n\n- CLI 主调度：`cmd/cmd.go`（~855 行）\n- MTR 参数/流程：`cmd/mtr_mode.go`（~315 行）\n- MTR 交互输入：`cmd/mtr_ui.go`\n- MTR 引擎：`trace/mtr_runner.go`\n- MTR 聚合：`trace/mtr_stats.go`\n- MTR TUI：`printer/mtr_tui.go`（~691 行）\n- MTR table/report：`printer/mtr_table.go`（~625 行）\n- MTR TUI 颜色：`printer/mtr_tui_color.go`\n- WS handler：`server/ws_handler.go`（~449 行）\n- 前端：`server/web/assets/app.js`\n- deploy auth：`server/auth.go`\n- deploy MCP：`server/mcp.go`\n- Web/MCP 共享服务层：`internal/service/`\n- Repo Agent Skill：`skills/nexttrace/`\n- Geo DoT 解析：`util/dns_resolver.go`\n- Geo HTTP 客户端：`util/http_client_geo.go`\n- FastIP：`util/latency.go`\n\n## 2026-03 Gocyclo 重构快照\n\n- 第一波低风险重构已落地：\n  - `ipgeo.Filter` 改为 CIDR 规则表驱动。\n  - `util.DomainLookUp` 拆成 resolver / lookup / family filter / interactive select 四段。\n  - `server.prepareTrace`、`normalizeTarget`、`trace.Traceroute`、`trace.Hop.fetchIPData` 已改成薄协调器。\n  - `GetMTUByIPForDevice`、`GetICMPResponsePayload`、`parseIPDBOneResponse`、`GlobalpingFormatLocation` 已拆 helper。\n- 第二波主流程/输出层已部分落地：\n  - `cmd.Execute` 已拆成 parser 注册 helper、启动模式 helper、运行时调度 helper。\n  - `fast_trace.FastTest` / `testFile` 已拆成交互选择、源地址推导、文件目标解析、单目标执行。\n  - `server.mtrAggregator.Update`、`wshandle.messageSendHandler` 已改成薄入口。\n  - `printer.RealtimePrinter`、`RealtimePrinterWithRouter`、`tracelog.RealtimePrinter` 现在共用 `internal/hoprender` 的 hop attempt 分组逻辑。\n- 第三波协议层已开始落地：\n  - `trace/internal/icmp_common.go`、`tcp_common.go`、`udp_common.go` 已改成“读包循环 + 共享解码 helper + 回调派发”结构。\n  - 新增 `trace/internal/icmp_decode.go`，集中处理 ICMPv4/v6 解析、echo reply 匹配、内嵌目标 IP 校验、内嵌 ICMP seq 提取。\n  - 新增 `trace/internal/icmp_decode_test.go`，覆盖 IPv4/IPv6 echo reply、error payload、目标 IP 校验、内嵌 seq 提取。\n  - `trace/internal/udp_unix.go` 的 `SendUDP` 已拆成 IPv4/IPv6 独立发送 helper；`trace/udp_ipv4.go` 的 `send()` 也已拆成配额检查、构包、超时守护、发送记账四段。\n  - Windows 协议层新增 `trace/internal/windivert_sniff_windows.go`，把 WinDivert sniff handle 打开、收包、ICMP/TCP 解码下沉为共享 helper；`icmp_windows.go` / `tcp_windows.go` / `udp_windows.go` 的 sniff 入口已变成薄协调器。\n  - Darwin `trace/internal/icmp_darwin.go:ListenPacket` 已拆成 socket spec、接口绑定、bind sockaddr、finalize packet conn 四段；`trace/internal/tcp_darwin.go:ListenTCP` 也改成设备选择 + BPF + 共享 TCP reply 解码 helper。\n  - 新增 `trace/internal/tcp_probe_decode.go` 与 `trace/internal/tcp_probe_decode_test.go`，集中处理 TCP probe reply 的 seq 还原、peer IP 提取与 IPv4/IPv6 解析，供 Darwin/Windows TCP sniff 共用。\n- 当前已知剩余高复杂度主要集中在：\n  - `printer/mtr_*` 渲染层\n  - `cmd/mtr_ui.go` 输入状态机\n  - `trace/globalping.go` 的主流程函数\n  - `trace/mtr_runner.go` 中仍未拆薄的 ICMP round handler（`probeRound` / `onICMP`）\n  - 少量收尾函数：`fast_trace ipv6.go`、`reporter/reporter.go`、`trace/mtr_raw.go`\n\n## 2026-03 Gocyclo 重构快照（追加）\n\n- MTR 核心热点已完成一轮收敛：\n  - `trace/mtr_scheduler.go:runMTRScheduler` 已改成薄入口，核心状态与分支移动到 `trace/mtr_scheduler_runtime.go`。\n  - `trace/mtr_stats.go:Update` / `MigrateStats` 已拆成按 hop 分组、累加器合并、裁剪 helper；新增 `trace/mtr_stats_helpers.go`。\n  - `trace/mtr_runner.go:mtrLoop` 已改成薄入口，取消/重置/暂停/预览/backoff 分支移动到 `trace/mtr_loop_runtime.go`。\n- MTR 输出层与输入层热点也已收敛：\n  - `printer/mtr_tui.go:mtrTUIRenderWithWidth` 已拆成布局扫描、三行头部构建、host part 预构建、MPLS 续行渲染四段。\n  - `printer/mtr_table.go` 的 host 组装和 `MTRReportPrint` 已改成共享 host-part 拼接 helper + report header/row helper。\n  - `cmd/mtr_ui.go:(*mtrInputParser).Feed` 已拆成按状态分发的 parser helper。\n- 最后一批业务流程热点也已拆薄：\n  - `trace/globalping.go:GlobalpingTraceroute` 已拆成 client 构建、measurement 请求、结果解码、hop limit 推导、结果组装五段。\n  - `trace/mtr_runner.go:(*mtrICMPEngine).onICMP` / `probeRound` 已拆成 reply 校验、notify 清理、目的地 TTL 识别、round 准备、发包 sweep、等待回包、结果构建多个 helper。\n  - `fast_trace/fast_trace ipv6.go:FastTestv6`、`reporter/reporter.go:generateRouteReportNode`、`trace/mtr_raw.go:buildMTRRawRecordFromProbe` 也已分别拆成选择分发、route-node 属性构建、raw record metadata 填充 helper。\n- 当前本地复杂度扫描结果：\n  - `go run /tmp/checkcyclo.go .` 已无 `>15` 函数输出。\n  - `go test ./...` 通过。\n\n## 仍需记住的残余风险（非阻断）\n\n- `closeWithCode` 中 `closed.Store(true)` 在 `closeOnce.Do` 外部，理论上有微小竞态窗口（实际无害，因 `sendMu` 保护；且无法简单移入 Once 内部，否则第二个调用者无法设置 closed）。\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"每次执行命令前均需阅读项目根目录下的 ./AGENTS.md 文件","category":"root","tokens":9},{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# NTrace-core 项目记忆文件（2026-03 快照，rev-3）\n\n# 供 LLM 在后续会话中快速加载上下文，减少重复分析。\n\n## 项目概览\n\n- 名称：NextTrace (NTrace-core)\n- 仓库：github.com/nxtrace/NTrace-core\n- 模块：`github.com/nxtrace/NTrace-core`\n- 语言：Go（`go 1.26.5`）\n- 入口：`main.go -> cmd.Execute()`\n- 核心能力：ICMP/TCP/UDP traceroute、GeoIP/RDNS、MTR 连续探测、Web/API、多平台构建\n\n## 构建与测试（必须遵守）\n\n- 常用命令：\n  - 构建：`go build ./...`\n  - 测试：`go test ./...`\n- 交叉编译脚本：`.cross_compile.sh`\n- Darwin 下 `trace/internal/icmp_darwin.go` 已不再使用 `//go:linkname`，改为\n  `syscall.Socket` + `os.NewFile` + 自定义 `icmpPacketConn`（实现 `net.PacketConn` /\n  `net.Conn` / `syscall.Conn` + `ReadMsgIP` 以满足 `x/net/internal/socket.ipConn`\n  接口），并在 `ReadFrom` 中调用 `stripIPv4Header` 剥离 macOS DGRAM ICMP socket\n  返回的外层 IP 头。\n\n## Git 与提交信息规范（必须遵守）\n\n- Commit message 必须使用 Conventional Commits 风格：`type(scope): 具体行为`。\n- subject 必须描述该提交实际改变的行为或约束，不能只写“处理 review 问题”“修复剩余问题”“调整代码”等泛化内容。\n- review finding 修复提交也必须写明具体修复点，例如 timeout、fallback、权限校验、stdout/stderr 分离；若一个提交覆盖多个 finding，用 body bullet 简短列出。\n- 一个提交包含多个互不相关主题时，优先拆分提交；确实需要放在一起时，subject 写主影响，body 写清每个具体改动。\n- 提交或 push PR 前必须检查 `git log --oneline main..HEAD`，发现泛化、误导或与 diff 不匹配的 commit message，应先 reword/amend。\n- 改写已推送 PR 分支历史后，只使用 `git push --force-with-lease`，不要用无保护的强推覆盖远端新提交。\n- 在本工作区发布 PR 时，目标仓库必须是 `nxtrace/NTrace-dev`；禁止直接向 `nxtrace/NTrace-core` 创建 PR。\n- 如果 GitHub 工具或远端推断结果指向 `nxtrace/NTrace-core`，必须改为 `nxtrace/NTrace-dev` 或停止确认，不能直接创建 PR。\n\n## 当前 CLI 语义（重点）\n\n### 常规 traceroute 路径\n\n- `--table`：现在是\"最终汇总表\"模式（一次探测完成后输出汇总表），不再是旧的异步 table 刷新模式。\n- `--route-path`：仍由 `reporter.New(...).Print()` 负责（与 MTR report 无关）。\n\n### 独立 `--mtu` 路径\n\n- `--mtu`：独立 UDP path-MTU / tracepath 风格模式，不复用普通 `trace.Traceroute` / MTR / Web 路径。\n- flavor 可用性：仅 `nexttrace` / `nexttrace-tiny` 包含；`ntr` 不注册该 flag。\n- 输出：\n  - `TTY`：当前 TTL 占位后原地更新，边探测边刷行。\n  - `非 TTY`：TTL 定稿后逐行流式输出，不使用 renderer 自己的 ANSI 控制序列。\n  - `--json`：输出独立 mtu schema；`hop.geo` 已存在。\n- 参数语义：\n  - 复用 `--data-provider`、`--language`、`--no-rdns`、`--always-rdns`、`--dot-server`。\n  - `--mtu` 仍只支持 UDP；显式 `--tcp` 冲突报错。\n- Geo/RDNS：\n  - `trace/mtu` 自带独立 metadata helper，不依赖普通 `trace.Hop.fetchIPData`。\n  - 流式事件会先输出基础 hop，再在同一 TTL 内补一条带 Geo/RDNS 的 update，最后 `ttl_final` 定稿。\n  - macOS 上曾有 `Warning: macOS --mtu support is experimental.` 提示，现已删除；不要再假设 CLI 会打印这句。\n\n### `--psize` / `--tos` 语义与平台差异\n\n- `--psize` 现在统一对齐 `mtr -s/--psize`：\n  - 用户输入语义是“含 IP + 当前探测协议头的总字节数”。\n  - 内部 `trace.Config.PktSize` 仍保存 payload bytes。\n  - 未显式传入时，不再固定默认 `52`，而是按协议/IP 族自动取最小合法值：\n    - ICMPv4 / UDPv4 = `28`\n    - TCPv4 = `44`\n    - ICMPv6 = `48`\n    - UDPv6 = `50`\n    - TCPv6 = `64`\n  - 负数 `--psize` 表示“每个 probe 独立随机”，CLI 允许 `--psize -84` 这种写法并会在解析前归一化。\n- `--tos` / `-Q`：\n  - 范围固定 `0..255`。\n  - `--mtu` 与 Globalping 显式传 `--psize` / `--tos` 会直接报不支持。\n- 平台发送路径差异（这是后续判断 bug 的关键记忆）：\n  - Linux / 其他 Unix：\n    - `ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生 socket/raw socket 路径。\n    - `--tos` 只是在现有路径上设置 `TOS/TrafficClass`，不会切换实现。\n  - macOS：\n    - 与 Linux 类似，`ICMP/TCP/UDP` 的 IPv4/IPv6 都走原生发送路径。\n    - `--tos` 同样只是在现有路径上设置 `TOS/TrafficClass`。\n  - Windows：\n    - `TCP/UDP` 的 IPv4/IPv6 一直走 WinDivert raw send。\n    - `ICMPv4` 一直走 socket path（`SetTOS` / `SetTTL`）。\n    - `ICMPv6`：\n      - 默认或 `--tos 0`：继续走原生 socket path，只设置 `HopLimit`，保持与 `v1.5.2` 一致。\n      - 非零 `--tos`：切到 WinDivert raw send，直接发送完整 `IPv6 + ICMPv6` 报文，因为 Windows 的 `x/net/ipv6.PacketConn` 不能可靠设置 `TrafficClass`。\n    - 因此，Windows 上只有“`ICMPv6` 且 `--tos != 0`”这个组合会额外依赖 WinDivert 发送能力；README 中英两份都已写明。\n\n### 间隔默认值（分层体系）\n\n- `-z/--send-time`：每包间隔，默认 `defaultPacketIntervalMs = 50` ms。\n- `-i/--ttl-time`：\n  - **常规 traceroute**：TTL 分组间隔，默认 `defaultTracerouteTTLIntervalMs = 300` ms。\n  - **MTR 模式**：`normalizeMTRTraceConfig()` 始终覆盖为 `defaultMTRInternalTTLIntervalMs = 0` ms（各 TTL 间不间隔）。\n  - MTR 每跳探测间隔由 `-i` 显式传值 或 默认 1000ms 决定（见下文 `-q/-i` 语义）。`-z/--send-time` 在 MTR 模式下被忽略。\n\n### MTR 相关参数\n\n- `-t/--mtr`：开启 MTR 交互模式（TTY 全屏 TUI）。\n- `-r/--report`：MTR 报告模式（非交互），隐式开启 MTR。\n- `-w/--wide`：宽报告模式，隐式等价 `--mtr --report --wide`。\n- `--raw`：与 MTR 组合时进入 **MTR raw 流式模式**（`runMTRRaw`），不再与 MTR 冲突。\n- 有效 MTR 开关：`effectiveMTR = mtr || report || wide`。\n- MTR 三路分支（`chooseMTRRunMode`）：\n  1. `effectiveMTRRaw` → `runMTRRaw`（流式行输出，适合管道/脚本）\n  2. `effectiveReport` → `runMTRReport`（非交互报告表）\n  3. 默认 → `runMTRTUI`（全屏 TUI）\n- MTR 冲突参数（会直接报错退出）：`--table` `--classic` `--json` `--output` `--route-path` `--from` `--fast-trace` `--file` `--deploy`。\n  - **注意**：`--raw` 不再是冲突参数。\n\n### MTR 中 `-q/-i/-y` 的新语义\n\n- `-q/--queries`：\n  - 在 MTR report 下表示每跳探测次数，默认 10（仅当用户未显式传 `-q`）。\n  - 在 MTR TUI 下表示每跳最大探测次数，未显式传时默认无限运行。\n- `-i/--ttl-time`：\n  - 在 MTR 下表示每跳探测间隔毫秒，默认 1000ms（仅当用户未显式传 `-i`）。\n  - 各 TTL 间内部扫描间隔固定 0ms（`normalizeMTRTraceConfig` 覆盖为 `defaultMTRInternalTTLIntervalMs = 0`）。\n  - `-z/--send-time` 在 MTR 模式下被忽略。\n- `-y/--ipinfo <0..4>`：\n  - TUI 初始 Host 显示模式，默认 0（IP/PTR only）。\n  - 0=Base(IP/PTR) 1=ASN 2=City 3=Owner 4=Full\n  - 仅 TUI 模式生效，report/raw 不受影响。\n\n### MTR report wide / non-wide 区别\n\n- **wide 模式**（`-w` 或 `--mtr --report --wide`）：\n  - 查询 GeoIP，显示完整 host 信息（ASN + geo + MPLS）。\n- **非 wide 模式**（`-r` 或 `--mtr --report`）：\n  - `normalizeMTRReportConfig` 设 `IPGeoSource=nil`（不查 geo）、`AlwaysWaitRDNS=true`。\n  - 显示 `formatCompactReportHost`：仅 IP/PTR + ASN，无 geo 列。\n\n## MTR 运行链路（重要文件）\n\n- 入口与调度：`cmd/mtr_mode.go`（~315 行）\n  - `runMTRTUI(...)` / `runMTRReport(...)` / `runMTRRaw(...)`\n  - `normalizeMTRTraceConfig(conf)` / `normalizeMTRReportConfig(conf, wide)`\n  - `buildAPIInfo(...)` / `buildRawAPIInfoLine(...)`\n  - MTR CLI 现在统一使用 `signal.NotifyContext(...)` 管理 Ctrl-C / SIGTERM；不再保留额外的 `sigCh` + goroutine 等待器。\n- 交互控制：`cmd/mtr_ui.go`\n  - alternate screen + raw mode\n  - 输入状态机 `mtrInputParser`（字节流，吞掉 CSI/SS3/OSC/鼠标/焦点等序列）\n  - Enter/Leave 显式关闭输入扩展模式：1000/1002/1003/1006/1015/1004/2004\n  - Quit 路径会先判空 `cancel`，因此 `newMTRUI(nil, ...)` / 测试注入 nil 不会 panic。\n- 核心探测循环：`trace/mtr_runner.go`\n  - `RunMTR` / `mtrLoop` / `RunMTRRaw`\n  - 支持暂停、重置、流式预览（`ProgressThrottle` 默认 200ms）\n  - ICMP 持久引擎 + TCP/UDP fallback\n- 统计聚合：`trace/mtr_stats.go`\n  - `MTRAggregator` / `MTRHopStat`\n  - unknown 合并策略：单路径时把 unknown 合并到唯一已知路径，避免同 TTL 分裂成 waiting + 真实 IP 两行\n- 输出层：\n  - TUI：`printer/mtr_tui.go`\n  - table/report：`printer/mtr_table.go`\n  - raw 行格式化：`printer.FormatMTRRawLine(rec)`\n  - TUI 颜色：`printer/mtr_tui_color.go`\n\n## MTR 交互行为（当前）\n\n- `q`/`Q`/`Ctrl-C`：退出\n- `p`：暂停\n- `SPACE`：恢复\n- `r`：重置统计\n- `y`：切换 Host 显示模式（IP/PTR → ASN → City → Owner → Full → 循环）\n- `n`：切换 Host 基名显示（PTR-or-IP / IP-only）\n- `e`：切换 MPLS 标签显示（toggle MPLS on/off）\n\n## MTR 显示与统计规则（当前）\n\n- Host 显示支持 5 种模式（`-y/--ipinfo` 设初始值，`y` 键运行时循环）：\n  - `HostModeBase=0`：仅 IP/PTR，无 ASN 前缀\n  - `HostModeASN=1` / `HostModeCity=2` / `HostModeOwner=3` / `HostModeFull=4`\n  - `HostNamePTRorIP` / `HostNameIPOnly`\n- 默认语言：`cn`（`--language en` 才优先英文字段）\n- waiting 判定：`loss >= 99.95 && IP==\"\" && Host==\"\"`\n  - 显示为 `(waiting for reply)`\n  - 指标列（Loss/Snt/Last/Avg/Best/Wrst/StDev）留空\n- TUI Host 对齐（重要，已从 tab 改为手动空格）：\n  - `buildTUIHostParts(stat, mode, nameMode, lang, showIPs)` 生成结构化 parts\n  - `computeTUIASNWidth(stats, ...)` 扫描所有 hop 确定 ASN 列最大宽度\n  - `formatTUIHost(parts, asnW)` 用 `padRight(asn, asnW)` + 空格拼接（不用 `\\t`）\n  - ASN 为空但 IP 已知时填 `\"AS???\"` 占位符，保证列对齐（HostModeBase 除外，该模式不显示 ASN）\n  - waiting hop 不填占位符\n- compact report host（非 wide report）：\n  - `formatCompactReportHost(stat, nameMode, lang)` 仅输出 hostname/IP + ASN\n- TUI 其他特性：\n  - 终端宽度自适应 + CJK 宽度计算（go-runewidth）\n  - 窄屏右锚定指标区\n  - 动态 hop 前缀宽度（覆盖 3 位/4 位 TTL）\n  - MPLS 独立续行显示\n  - 紧凑指标列宽度：Loss=5 Snt=3 RTT=7 RTTMin=5\n\n## MTR 目的地检测与高 TTL 丢弃\n\n- 当 `knownFinalTTL` 已确定后，所有 `TTL > knownFinalTTL` 的调度槽位被标记为 `disabled`。\n- disabled TTL 的探测回包（包括在途探测返回的 dst-ip 回复）**一律丢弃**，不折叠、不计入任何统计。\n- **MaxPerHop 上限检查**（`states[originTTL].completed + inFlightCount >= MaxPerHop`）：\n  - 调度时使用 `completed + inFlightCount >= MaxPerHop` 防止超发。\n  - 完成时仍检查 `completed >= MaxPerHop` 丢弃溢出结果。\n- `originTTL < curFinal` 时（更低 TTL 先到 dst-ip → 降低 `knownFinalTTL`）：\n  - 保存 `oldFinal`，更新 `knownFinalTTL = originTTL`，disable 所有 `originTTL+1..maxHops`。\n  - 调用 `agg.ClearHop(oldFinal)`：清除旧 finalTTL 的聚合数据（避免幽灵行），**不合并**到新 finalTTL。\n  - 新 finalTTL 由独立的 per-hop 调度器自行积累新鲜探测数据，不存在 Snt 膨胀问题。\n- 调度状态（`inFlightCount`/`nextAt`/`consecutiveErrs`）更新在 `originTTL`。\n- 统计聚合（`completed++`/`agg.Update`/`onProbe`）均使用 `originTTL`（不再有 `accountTTL` 分离）。\n\n## MTR Per-Hop 调度器关键设计（当前）\n\n- **多 in-flight 探测**：每 TTL 允许最多 `MaxInFlightPerHop`（默认 3）个并发探测。\n  - `mtrHopState.inFlightCount` 是计数器（非 bool）。\n  - 这解决了高丢包 hop 因超时阻塞导致 Snt 积累速率远低于低丢包 hop 的问题。\n- **nextAt 基于发送时间**：`launchProbe` 时设 `nextAt = now + hopInterval`。\n  - 不再等探测完成才设 nextAt，调度器可在超时探测还在飞行中时为同一 TTL 发射新探测。\n  - 这保证了所有 TTL 的 Snt 积累速率大致相同，不受丢包率影响。\n- **全局并发限制**：`inFlight`（全局计数器）< `parallelism` 仍然有效。\n- **`MaxInFlightPerHop` 配置**：`mtrSchedulerConfig.MaxInFlightPerHop`，默认动态计算。\n  - 动态默认 = `ceil(Timeout / HopInterval) + 1`（至少 1）。\n  - 例：`Timeout=2s, HopInterval=1s` → 默认 3；`Timeout=2s, HopInterval=200ms` → 默认 11。\n  - 用户显式设置 > 0 时优先使用用户值。\n\n## MTR 引擎关键机制（易踩坑）\n\n- 目的地提前停止：\n  - `knownFinalTTL`（持久缓存）用于缩短后续探测的 TTL 上界；高 TTL 标记 disabled 后不再调度。\n- seq 16 位回卷处理：\n  - `seqWillWrap(...)` 触发 `rotateEngine(...)`\n  - 轮换 echoID 并重建 listener，协议层隔离新旧回包。\n- 额外安全网：\n  - onICMP 中有 RTT 合理性检查（`<=0` 或 `>timeout` 丢弃）。\n- 流式预览：\n  - 仅已发送 TTL 才会参与预览；未发送 TTL 保持 nil 槽位，避免提前计入 Snt/Loss。\n\n## Web Console / WebSocket（server/）\n\n### WS 架构（`server/ws_handler.go`，~451 行）\n\n- **异步写模型**：`wsTraceSession` 使用 `sendCh`（buffered channel，1024）+ `writeLoop` goroutine。\n  - 调用方通过 `send(envelope)` 非阻塞投递；channel 满时返回 `errWSSlowConsumer`。\n  - `writeLoop` 从 `sendCh` 取消息，`SetWriteDeadline` + `WriteJSON`。\n- **关闭路径**：\n  - `closeWithCode(code, reason)`：异常关闭（slow consumer / write error），关 `stopCh` + 发 close frame。\n  - `finish()`：正常结束，`sendMu` 下关 `sendCh`，等 `writerDone`，再关 conn。\n  - 两者均幂等（`closeOnce` / `finishOnce`）。\n- **可测试性**：`wsConn` 接口 + `fakeWSConn` mock（`server/ws_handler_test.go`）。\n- **常量**：`wsSendQueueSize=1024`，`wsWriteTimeout=5s`。\n\n### Web MTR 调度模式（重要变更）\n\n- **已从 round-based 迁移到 per-hop 调度**。\n- `runMTRTrace()`：\n  - 优先读 `HopIntervalMs`，fallback `IntervalMs`，再缺省 1000ms。\n  - `MaxRounds` → `MaxPerHop`（0 = 无限运行直到客户端断开）。\n  - 不再使用 legacy round-based 的 `Interval` / `RunRound`。\n- `executeMTRRaw()` 两路分支：\n  - `HopInterval > 0`：per-hop 模式，仅在 NextTrace API/FastIP 初始化阶段短暂加锁；长期探测不再依赖 `SrcDev` / `DisableMPLS` 等进程级全局。\n  - fallback：legacy round-based 模式（保留兼容），`RunRound` 回调内 per-round 锁定。\n  - `trace/runMTRRawRoundBased()` 也会先做 `normalizeRuntimeConfig(&cfg)`，因此 legacy raw 路径同样能继承 `SourceDevice`；`DisableMPLS` 不再从全局反向覆盖会话配置。\n- `traceRequest` 新增 `HopIntervalMs` 字段（`json:\"hop_interval_ms\"`），与 `IntervalMs` 解耦。\n- 前端 MTR 请求现在发送 `hop_interval_ms=1000`，不再把旧的 `interval_ms=2000` 当默认值。\n- 前端 raw 聚合键现在按 TTL 折叠，避免同一 hop 的 timeout / success 被拆成两行。\n\n### 前端渲染节流（`server/web/assets/app.js`）\n\n- MTR raw 消息通过 `scheduleMTRRender()` 节流，最小间隔 100ms，优先 `requestAnimationFrame`。\n- `cancelScheduledMTRRender()` 在 `clearResult`、socket close/error 路径调用，避免孤儿回调。\n- `flushMTRRender()` 立即执行挂起渲染。\n\n### 其他 server 文件\n\n- `server/server.go`：Gin 路由注册\n- `server/handlers.go`：REST 接口\n- `server/mtr.go`：MTR 专用 handler 逻辑\n- `server/trace_handler.go`：traceroute handler\n- `server/cache_handler.go`：缓存\n\n## Deploy WebUI / MCP / Agent Skill（2026-04 追加）\n\n- MCP 只属于完整版 `nexttrace`，`nexttrace-tiny` / `ntr` 不注册 WebUI/MCP flags。\n- `--mcp` 只能与 `--deploy` 同用；单独传 `--mcp` 必须报错。\n- `--deploy --mcp` 不提供 stdio MCP，只通过 deploy 网络服务在 `/mcp` 暴露 Streamable HTTP，使用官方 `github.com/modelcontextprotocol/go-sdk`。\n- `server.RunWithOptions(options, onReady)` 是 deploy 新入口；`server.Options` 包含 `ListenAddr`、`EnableMCP`、`AuthEnabled`、`DeployToken`。\n- deploy 鉴权规则：\n  - 监听 `127.0.0.1`、`::1`、`localhost` 默认免 token。\n  - 监听 `0.0.0.0`、`::`、非 loopback IP/host 时默认启用 token。\n  - 外网监听且用户未提供 token 时启动生成随机 token 并输出到 stdout。\n  - `--deploy-token` 优先于 `NEXTTRACE_DEPLOY_TOKEN`；手动 token 不回显。\n  - `AuthEnabled=true` 但 token 为空时必须 fail closed。\n  - WebUI 走 `/auth/login`，成功后写 HttpOnly cookie。\n  - API/MCP/WS 支持 `Authorization: Bearer <token>`、`X-NextTrace-Token` 和 cookie。\n  - 不支持 URL query token，尤其 WebSocket 不允许 query token。\n- auth middleware 覆盖 `/`、`/assets/*`、`/api/*`、`/ws/trace`、`/mcp`。\n- `/mcp` 不要用 `router.Any(\"/mcp\")`，会与 Gin 的静态资源 wildcard `/*path` 冲突；当前只注册 `GET`、`POST`、`DELETE`。\n- MCP 与 Web 共用 `internal/service`，不要在 MCP tool 中拼 CLI 参数再反调 CLI。\n- MCP tool 输出以 `structuredContent` 为主；schema 要把参数状态分成 `supported`、`not_applicable`、`not_yet_supported`。\n- 当前 MCP tools：\n  - `nexttrace_capabilities`\n  - `nexttrace_traceroute`\n  - `nexttrace_mtr_report`\n  - `nexttrace_mtr_raw`\n  - `nexttrace_mtu_trace`\n  - `nexttrace_speed_test`\n  - `nexttrace_annotate_ips`\n  - `nexttrace_geo_lookup`\n  - `nexttrace_globalping_trace`\n  - `nexttrace_globalping_limits`\n  - `nexttrace_globalping_get_measurement`\n- Globalping MCP 使用 service 专用模型，不复用 CLI `--from` 的单结果输出模型；`nexttrace_globalping_trace` 应返回 `measurement_id`、`status`、`probes_count` 和按 probe 展开的 `results[]`。\n- Globalping MCP 当前支持 `target`、`locations[]`、`limit`、`protocol`、`port`、`packets`、`ip_version`；不支持本地 `source/dev/dot_server/packet_size/tos/ttl_interval`。\n- Repo Skill 位于 `skills/nexttrace/`，入口是 `skills/nexttrace/SKILL.md`；更新 MCP tool 或参数时必须同步 skill references。\n- deploy/MCP 相关回归建议至少覆盖：\n  - `go test ./...`\n  - `node --test server/web/assets/*.test.cjs`\n  - `go test -tags flavor_tiny ./cmd ./server`\n  - `go test -tags flavor_ntr ./cmd ./server`\n  - 本地 MCP smoke，traceroute 在 macOS 可显式传 `source_device`（例如 `en8`）。\n\n## DoT 与 Geo DNS\n\n- `--dot-server` 不仅影响目标域名解析，也影响 GeoIP API / NextTrace API FastIP 的域名解析链路。\n- 关键文件：`util/dns_resolver.go`\n  - `SetGeoDNSResolver(dotServer)`\n  - `WithGeoDNSResolver(dotServer, fn)`：为 Web/API 请求提供作用域化的 resolver 切换；不同 resolver 串行切换，相同 resolver 允许安全嵌套，避免 `GetSourceWithGeoDNS` + 外层作用域组合时死锁。\n  - `geoResolverOverride` 的读写现在也走 `geoMu`，避免测试覆盖 resolver 时的数据竞争。\n  - `LookupHostForGeo(ctx, host)`：IP 字面量短路 -> DoT -> 失败时按配置 fallback 系统 DNS\n- `cmd/cmd.go` 在早期阶段（fast-trace / ws 初始化之前）注入 DoT 解析策略，避免早期分支绕过。\n- `server/trace_handler.go` 通过 `ipgeo.GetSourceWithGeoDNS(...)` + `WithGeoDNSResolver(...)` 让 Web/API 请求也遵守 `dot_server`，包括 NextTrace API/FastIP 初始化阶段。\n- Geo HTTP 请求统一走 `util.NewGeoHTTPClient(...)`（`util/http_client_geo.go`），其 Transport 现在从默认 Transport `Clone()` 而来，保留代理/HTTP2/连接池等标准行为。\n\n## NextTrace API FastIP 与 MTR 首行\n\n- `util/latency.go`：\n  - `FastIPMetaCache` 缓存节点元数据（IP/Latency/NodeName）\n  - `SuppressFastIPOutput` 可抑制彩色横幅\n- `GetFastIP(...)` 的 DNS 阶段现在显式受 `timeout` 限制；`FastIPMetaCache` 也改为在 fallback/default IP 决定后再写入，避免缓存空 IP。\n- MTR 模式在进入 TUI 前会设 `SuppressFastIPOutput=true`，避免污染主终端历史。\n- MTR TUI/report 首行 `APIInfo` 由 `cmd/mtr_mode.go` 的 `buildAPIInfo(...)` 生成（仅 NextTrace API 且有元数据时显示）。\n- MTR raw 首行由 `buildRawAPIInfoLine(...)` 生成（格式略不同，包含延迟信息）。\n\n## `--source` / `--dev` 现状\n\n- `--dev` 在 `cmd/cmd.go` 先解析网卡并推导 `srcAddr`（已处理非 `*net.IPNet` 地址类型，避免 panic）。\n- `trace.Config` 现在显式携带 `SourceDevice` / `DisableMPLS`，Darwin TCP/UDP 抓包与 MPLS 解析优先走会话级配置，不再依赖 Web 侧临时改写全局变量。\n- `trace.Config` 也显式携带 `Context`；`TracerouteWithContext(...)` 通过把上游 ctx 传入各 tracer 的 `signal.NotifyContext(...)` 基底，让 TCP/UDP fallback MTR 可以响应取消。\n- Windows 下 ICMP/TCP/UDP 的 `--dev` 都必须保持 v1.6.2 兼容行为：先解析指定网卡对应的 source IP，写入 `SrcAddr`，再清空 `SourceDevice`，让后续路径按 source address 继续运行；不要把 Windows `--dev` 改成拒绝执行或报错退出。\n- 这只是通过 source IP 影响 Windows 路由选择，不代表 WinDivert 或 socket 已支持真实按设备绑定；README 需要如实说明可能不准确。独立 `--mtu` 同样按 source-address 发送，但可额外保留 device 名用于本地 MTU 查询。\n- MTR 标题显示源信息来自：\n  - `--source`（最高优先）\n  - `--dev` 推导\n  - UDP dial fallback\n- 相关函数：`cmd/mtr_mode.go -> resolveSrcIP(...)`\n\n## CI 与工具链（当前）\n\n- `go.mod`: `go 1.26.5`\n- GitHub Actions：\n  - `.github/workflows/build.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/test.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/regression.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - `.github/workflows/golangci-lint.yml` 使用 `setup-go@v7` + `go-version: 1.26.5`\n  - test workflow 中 `GOTOOLCHAIN=go1.26.5+auto`\n  - build matrix 已移除 `windows/arm`\n- `.cross_compile.sh` 与 workflow 里的 `go build` 现在都用数组构造 `-tags` 参数，避免 shell word-splitting；脚本也会把当前 `GOARM` 传给 `compress_with_upx`，使 linux/armv7 目标能命中对应压缩分支。\n- `ipgeo/ipdbone.go` 不再原地修改全局 `defaultClient.httpClient.Timeout`；超时覆盖会通过克隆 client（复用 token cache / token init，同步替换整个 HTTP client）实现，避免 dial timeout 与 client timeout 脱节。\n\n## 关键文件导航\n\n- CLI 主调度：`cmd/cmd.go`（~855 行）\n- MTR 参数/流程：`cmd/mtr_mode.go`（~315 行）\n- MTR 交互输入：`cmd/mtr_ui.go`\n- MTR 引擎：`trace/mtr_runner.go`\n- MTR 聚合：`trace/mtr_stats.go`\n- MTR TUI：`printer/mtr_tui.go`（~691 行）\n- MTR table/report：`printer/mtr_table.go`（~625 行）\n- MTR TUI 颜色：`printer/mtr_tui_color.go`\n- WS handler：`server/ws_handler.go`（~449 行）\n- 前端：`server/web/assets/app.js`\n- deploy auth：`server/auth.go`\n- deploy MCP：`server/mcp.go`\n- Web/MCP 共享服务层：`internal/service/`\n- Repo Agent Skill：`skills/nexttrace/`\n- Geo DoT 解析：`util/dns_resolver.go`\n- Geo HTTP 客户端：`util/http_client_geo.go`\n- FastIP：`util/latency.go`\n\n## 2026-03 Gocyclo 重构快照\n\n- 第一波低风险重构已落地：\n  - `ipgeo.Filter` 改为 CIDR 规则表驱动。\n  - `util.DomainLookUp` 拆成 resolver / lookup / family filter / interactive select 四段。\n  - `server.prepareTrace`、`normalizeTarget`、`trace.Traceroute`、`trace.Hop.fetchIPData` 已改成薄协调器。\n  - `GetMTUByIPForDevice`、`GetICMPResponsePayload`、`parseIPDBOneResponse`、`GlobalpingFormatLocation` 已拆 helper。\n- 第二波主流程/输出层已部分落地：\n  - `cmd.Execute` 已拆成 parser 注册 helper、启动模式 helper、运行时调度 helper。\n  - `fast_trace.FastTest` / `testFile` 已拆成交互选择、源地址推导、文件目标解析、单目标执行。\n  - `server.mtrAggregator.Update`、`wshandle.messageSendHandler` 已改成薄入口。\n  - `printer.RealtimePrinter`、`RealtimePrinterWithRouter`、`tracelog.RealtimePrinter` 现在共用 `internal/hoprender` 的 hop attempt 分组逻辑。\n- 第三波协议层已开始落地：\n  - `trace/internal/icmp_common.go`、`tcp_common.go`、`udp_common.go` 已改成“读包循环 + 共享解码 helper + 回调派发”结构。\n  - 新增 `trace/internal/icmp_decode.go`，集中处理 ICMPv4/v6 解析、echo reply 匹配、内嵌目标 IP 校验、内嵌 ICMP seq 提取。\n  - 新增 `trace/internal/icmp_decode_test.go`，覆盖 IPv4/IPv6 echo reply、error payload、目标 IP 校验、内嵌 seq 提取。\n  - `trace/internal/udp_unix.go` 的 `SendUDP` 已拆成 IPv4/IPv6 独立发送 helper；`trace/udp_ipv4.go` 的 `send()` 也已拆成配额检查、构包、超时守护、发送记账四段。\n  - Windows 协议层新增 `trace/internal/windivert_sniff_windows.go`，把 WinDivert sniff handle 打开、收包、ICMP/TCP 解码下沉为共享 helper；`icmp_windows.go` / `tcp_windows.go` / `udp_windows.go` 的 sniff 入口已变成薄协调器。\n  - Darwin `trace/internal/icmp_darwin.go:ListenPacket` 已拆成 socket spec、接口绑定、bind sockaddr、finalize packet conn 四段；`trace/internal/tcp_darwin.go:ListenTCP` 也改成设备选择 + BPF + 共享 TCP reply 解码 helper。\n  - 新增 `trace/internal/tcp_probe_decode.go` 与 `trace/internal/tcp_probe_decode_test.go`，集中处理 TCP probe reply 的 seq 还原、peer IP 提取与 IPv4/IPv6 解析，供 Darwin/Windows TCP sniff 共用。\n- 当前已知剩余高复杂度主要集中在：\n  - `printer/mtr_*` 渲染层\n  - `cmd/mtr_ui.go` 输入状态机\n  - `trace/globalping.go` 的主流程函数\n  - `trace/mtr_runner.go` 中仍未拆薄的 ICMP round handler（`probeRound` / `onICMP`）\n  - 少量收尾函数：`fast_trace ipv6.go`、`reporter/reporter.go`、`trace/mtr_raw.go`\n\n## 2026-03 Gocyclo 重构快照（追加）\n\n- MTR 核心热点已完成一轮收敛：\n  - `trace/mtr_scheduler.go:runMTRScheduler` 已改成薄入口，核心状态与分支移动到 `trace/mtr_scheduler_runtime.go`。\n  - `trace/mtr_stats.go:Update` / `MigrateStats` 已拆成按 hop 分组、累加器合并、裁剪 helper；新增 `trace/mtr_stats_helpers.go`。\n  - `trace/mtr_runner.go:mtrLoop` 已改成薄入口，取消/重置/暂停/预览/backoff 分支移动到 `trace/mtr_loop_runtime.go`。\n- MTR 输出层与输入层热点也已收敛：\n  - `printer/mtr_tui.go:mtrTUIRenderWithWidth` 已拆成布局扫描、三行头部构建、host part 预构建、MPLS 续行渲染四段。\n  - `printer/mtr_table.go` 的 host 组装和 `MTRReportPrint` 已改成共享 host-part 拼接 helper + report header/row helper。\n  - `cmd/mtr_ui.go:(*mtrInputParser).Feed` 已拆成按状态分发的 parser helper。\n- 最后一批业务流程热点也已拆薄：\n  - `trace/globalping.go:GlobalpingTraceroute` 已拆成 client 构建、measurement 请求、结果解码、hop limit 推导、结果组装五段。\n  - `trace/mtr_runner.go:(*mtrICMPEngine).onICMP` / `probeRound` 已拆成 reply 校验、notify 清理、目的地 TTL 识别、round 准备、发包 sweep、等待回包、结果构建多个 helper。\n  - `fast_trace/fast_trace ipv6.go:FastTestv6`、`reporter/reporter.go:generateRouteReportNode`、`trace/mtr_raw.go:buildMTRRawRecordFromProbe` 也已分别拆成选择分发、route-node 属性构建、raw record metadata 填充 helper。\n- 当前本地复杂度扫描结果：\n  - `go run /tmp/checkcyclo.go .` 已无 `>15` 函数输出。\n  - `go test ./...` 通过。\n\n## 仍需记住的残余风险（非阻断）\n\n- `closeWithCode` 中 `closed.Store(true)` 在 `closeOnce.Do` 外部，理论上有微小竞态窗口（实际无害，因 `sendMu` 保护；且无法简单移入 Once 内部，否则第二个调用者无法设置 closed）。\n","category":"root","tokens":4979}]}