Skip to content

fix: #50 Host 端口热加载与 Host 离线错误码语义(SDK/CLI/docs) - #54

Open
grapedev-bot wants to merge 4 commits into
masterfrom
feat/issue-50
Open

grapedev-bot wants to merge 4 commits into
masterfrom
feat/issue-50

Conversation

@grapedev-bot

@grapedev-bot grapedev-bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

概述

Issue #50 返工(Spec v2)。分支已 rebase 到最新 master(4 个原子提交,mergeable=true),
按「证据或降级二选一」口径重写端口热加载实现,并明确问题二的跨仓边界。

证据门结论:无网关推送证据 → 采用 Host 侧刷新 + 强制降级提示

  • 本执行环境无法访问网关:devbridge-s2.hwtunnel.com DNS 不可解析、无隧道凭据、无 devbridge 二进制。
    因此未取得「网关在 port create/update/delete 后主动下发全量端口通知」的协议证据
    (没有新的 relay channel / RelayRequest 载荷样本,也没有下发时机与频率数据)。
  • 结论:不采用依赖网关推送的实现路径。PR 旧实现(每个 relay channel 阻塞 2s 探测通知)已移除,
    该路径既有「每数据通道最多阻塞 2s」的性能风险,前提也未验证。
  • 已实现方案 A2:Host 侧按需刷新端口集。每次接受 relay channel 时调用 ListPorts,
    1s 去抖(portRefreshMinInterval)、单次 2s 超时(portRefreshTimeout),与缓存端口集 diff 后
    应用到本次 channel 的 ForwardFromRemotePort;启动时先刷新一次。
    • 静态端口模式(HostConfig.Ports 非空):不刷新、不调用 API,语义仍是「需重启」。
    • --token 模式(无 REST 凭据):不刷新,沿用首 channel 端口通知握手。
    • API 失败 fail-soft:保留最近端口集,不中断 Host 会话。
    • HostConfig.OnPortsChanged(added, removed, current) 仅在 diff 非空时回调。
  • 已实现降级 B:port create / port update / port delete 成功后统一输出中/英提示——
    动态端口模式约 1s 自动同步;静态模式给出可复制命令 devbridge host <tunnelId> -p <ports>。
    删除端口此前无任何提示,本次补齐,不再静默成功。

提交(4 组)

  1. feat(sdk): #50 Host 端口集运行期刷新(ListPorts 按需 + 1s 去抖,不依赖网关推送)
  2. feat(cli): #50 端口变更生效性提示(中/英 i18n)与 show Host 在线状态
  3. docs: #50 端口生效机制、Host 在线状态与公网状态码语义(中/英)
  4. docs: #50 任务文档 v2(证据门结论、方案 A2/降级 B、验证矩阵)

验证记录

项目 状态 证据
端口集刷新(新增/删除/diff 幂等/去抖/失败降级/超时/取消) 已验证 go-sdk: go test -race -count=1 ./... 通过
无 REST 凭据(--token)不刷新 已验证 TestPortSetRefresherWithoutAPICredential
并发刷新 已验证 TestPortNotifierConcurrentUpdates(-race)
降级提示含重启命令(中/英) 已验证 cli: go test ./... 通过,TestPortChangeHintShowsRestartCommand
show Host 在线状态 已验证 TestTunnelStatusHostOnline + cli/cmd/tunnel.go
文档中/英 + 格式 已验证 npm run lint:docs:0 issues
原始复现场景 ≤2s 返回 200 未验证 本环境无隧道凭据、网关不可达
网关端口变更推送协议证据 无法在本仓库验证 仓库无 Relay/网关代码
公网 404 vs 502/503 + Retry-After 无法在本仓库验证 网关(Relay)侧实现,跨仓依赖

待有凭据环境执行的端到端复测(原始复现场景)

devbridge host <tunnel-id> -p 18080            # 运行中
devbridge port create <tunnel-id> -p 18081 --protocol auto --deny-anonymous
date -Is; curl -s -o /dev/null -w '%{http_code} %{time_total}\n' \
  https://<tunnel-id>-18081.devbridge-s2.hwtunnel.com/

验收口径:port create 后 ≤2s 内 200,无需重启/重连 Host;提交命令、时间戳、状态码、time_total。

问题二(Host 离线返回 404)边界

本仓库仅含 CLI / go-sdk / docs,无 Relay 代码,文档变更不构成缺陷修复。本 PR 在本仓库交付:

  • devbridge show 输出「Host 在线状态」(由 status.hostConnectionCount 派生);
  • SDK TunnelStatus.HostOnline() 判定辅助(nil 安全);
  • docs 中/英:公网 404 tunnel not found / port not bound 与 503 + Retry-After (host offline)
    语义表、服务端边界说明、新增端口不可达排查步骤。

公网 404 与 502/503 + Retry-After 的判定由网关实现,已在 docs 与 Issue #50 记录跨仓依赖;
未端到端验证前 Issue 不关闭,关闭流程为 QA 通过 → pending_review → 人工确认。

跨仓依赖待办

  • 网关(Relay)侧仓库地址/维护者:待维护者提供后回填 Issue [Bug] 运行中隧道新增端口需重启 Host;Host 离线返回 404 而非 502/503 #50 依赖链接;
  • 网关侧:确认端口变更对运行中 Host 的可见性与下发能力(若推送能力成立,可作为 A2 的加速路径);
  • 网关侧:404 tunnel not found 与 502/503 + Retry-After (host offline) 判定;
  • 端到端复测记录(上表)回填本 PR 与 Issue。

测试命令

cd go-sdk && go build ./... && go vet ./... && go test -race -count=1 ./...
cd cli && go build ./... && go vet ./... && go test ./...
cd docs && npm run lint:docs

@grapedev-bot

Copy link
Copy Markdown
Collaborator Author

[QA Agent] 审查报告

配置检查

  • ✅ Title:fix: #50 Host 端口热加载与 Host 离线错误码语义(SDK/CLI/docs),清晰描述变更
  • ⚠️ Labels:PR 无分类标签(建议补 bug 或 type/bug)
  • ⚠️ Assignee:未设置责任人(grapedev-bot 机器人创建,可不阻塞)
  • ✅ Issue 关联:body 明确「自动实现 Issue [Bug] 运行中隧道新增端口需重启 Host;Host 离线返回 404 而非 502/503 #50」(未用 Fixes/Closes 关键字,合并后不会自动关闭 Issue,需人工关闭或补 Fixes #50)

变更检视

  • 首次审查(无历史 SHA 断点),head 4ef461e(feat/issue-50 → master,5 提交,18 文件,+481/-41)
  • 已核对 Coding Agent 声明:提交数(5)、分支(feat/issue-50)、测试文件(go-sdk/host_test.go 6 用例 + cli i18n 测试)与 pulls files/get 返回一致

代码审查

结论: ✅ 通过(整体质量良好;以下 2 个 Optional 建议与若干 FYI,无阻塞项)

级别 文件 行号 问题 建议
Optional docs/guide/tunnels.md 51 「上传和下载流量统计」重复出现两次,疑似改写残留 删去重复短语
Optional docs/guide/ports.md 37 静态端口模式定义「或通过 --token 直接指定端口启动」与代码不符:--token 模式下 HostConfig.Ports 为空(CLI 忽略 --ports),实际走网关动态端口模式(host.md 53 行描述正确) 修正为「仅 --ports/-p 显式指定」
FYI cli/cmd/tunnel.go 110-113 hostOnline 用 result.Status.HostOnline()(nil 安全),而下方连接数/流量用局部拷贝 status,两种判空风格并存 可统一为 status.HostOnline()(行为等价,非必改)
FYI cli/cmd/port.go 171-178 port delete 未输出热加载提示(create/update 有);host.md 67 行却提到「port delete 也自动生效」 可选补提示保持一致性

正确性核对(重点验证项)

  1. 热加载核心逻辑(go-sdk/host.go):
    • startHostAcceptLoop 现对每个 relay channel 都起 goroutine 走 handleRelayOrNotification 探测,修复了旧代码只在首个 channel 解析通知、后续通知被当数据转发的根因(与本 PR 诊断一致);
    • readPortNotification 探测保留已读流转发(非通知 channel 不丢数据),4096 字节缓冲足够容纳端口通知 JSON;
    • portNotifier 增加 sync.Mutex,update/currentPorts 均加锁,accept loop goroutine 与 ready 等待的并发访问已线程安全;-race 下全绿;
    • 端口集 diff(diffPorts)幂等:相同端口集不触发回调(有测试覆盖);
    • 静态端口模式忽略网关刷新(applyPortNotification 分支),与文档「静态模式需重启」语义一致;
    • CLI 侧配套修改 resolveHostTunnelPorts:API 解析端口返回 nil(动态模式),显式 --ports 仍走静态模式并校验——与 docs 两种模式的定义闭环。
  2. formatPortList 副作用:对传入 slice 做 sort.Ints(原地排序)。当前调用点(added/removed/current diff 结果与 currentPorts 拷贝)均为临时切片,无别名风险;注意未来若传入共享 slice 会被原地改序。
  3. TunnelStatus.HostOnline():nil 安全,show 输出用 nil-receiver 调用合法;测试覆盖 nil/零值/在线三态。

安全扫描

结论: ✅ 安全

  • 无新增依赖(sort/strconv/strings 均标准库);
  • 无硬编码凭据/token;网关 JSON 通知按结构体严格解析(@type 字段精确匹配 RelayRequest),非法输入按数据通道原样转发,无注入面;
  • PR body 中的 <!-- grape-auto-comment --> 为平台自动标记,非代码问题。

测试验证(本机复跑)

  • go-sdk:go build ./... ✅、go vet ./... ✅、go test -race -count=1 ./... ✅(新增 6 用例含子用例全部 PASS);
  • cli:go build ./... ✅、go vet ./... ✅、go test -count=1 ./... ✅(TestIssue50MessagesNonEmpty PASS);
  • docs:markdownlint 8 个变更文件仅 1 处 MD034 警告(docs/issue-50/spec.md:3 裸 URL,Optional 顺手可修)。

合并检查

⚠️ 请合并本 PR

审查已通过,但流水线需要 PR 合并才能继续(合并后将自动触发 SRE 部署验证)。
请维护者操作:在 GitCode 页面点击【Merge】合并本 PR。

grapedev-bot added 4 commits November 6, 2026 17:13
证据门:本环境无法访问网关(DNS 不可解析、无隧道凭据),未取得网关端口变更
推送协议证据,故不采用「每个 relay channel 探测 RelayRequest 通知」的推送路径。

改为 Host 侧主动刷新端口集(方案 A2):
- portSetRefresher 在接受 relay channel 时调用 ListPorts,1s 去抖、单次 2s 超时,
  diff 后应用到本次 channel 的 ForwardFromRemotePort;启动时先刷新一次。
- 静态端口模式(HostConfig.Ports 非空)不刷新、不调用 API,语义仍是「需重启」。
- 无 REST 凭据(--token 模式)不刷新,沿用首 channel 端口通知握手。
- API 失败 fail-soft 保留最近端口集;OnPortsChanged 仅在 diff 非空时回调。
- 移除每 channel 探测通知的旧实现(每数据 channel 最多阻塞 2s,且前提无证据)。

测试:diff 幂等、静态忽略、增删端口、去抖、失败/超时/取消、无凭据、并发 -race。
- port create/update/delete 成功后统一输出生效提示:动态端口模式约 1s 自动同步;
  静态模式输出重启命令 devbridge host <tunnelId> -p <ports>,不再静默成功。
- show 输出「Host 在线状态」(SDK TunnelStatus.HostOnline,nil 安全)。
- 既有隧道保持动态端口模式(不 pin 端口),由 Host 运行期刷新。
- 测试:中/英文案双全 + 提示必须包含重启命令。
- ports.md:动态 vs 静态端口模式生效机制对照表与刷新说明。
- host.md:端口刷新、Host 在线状态、limits 账户级边界。
- tunnels.md / cli.md:show 输出 Host 在线状态字段。
- troubleshooting.md:公网 404/503 语义表(404 tunnel not found / port not bound、
  503+Retry-After host offline)、服务端边界与跨仓依赖 #50、新端口不可达排查。
- 中文同步 docs/zh 对应四篇文档,markdownlint 通过。
spec/plan/task 按 Spec v2 重写,记录:无网关推送证据 → A2 + 降级 B;
问题二仅交付客户端语义并标注跨仓依赖;逐项标注已验证/未验证/无法在本仓库验证。
@grapedev-bot

Copy link
Copy Markdown
Collaborator Author

[Coding Agent] 已按 Issue #50 Spec v2 返工并 force-push(head 028d95e,4 个原子提交,已 rebase 到最新 master)。

  • 证据门:本环境无法访问网关(DNS 不可解析、无隧道凭据),未取得端口变更推送协议证据 → 不采用推送路径;改为 Host 侧 ListPorts 按需刷新(1s 去抖 / 2s 超时 / diff 应用)。
  • 降级 B:port create/update/delete 成功输出中/英生效提示(静态模式含重启命令),delete 补齐。
  • 问题二:仅交付 show 在线状态、SDK 判定辅助、docs 语义与排障;公网 404/502/503+Retry-After 属网关跨仓依赖。
  • 本地验证:go-sdk go test -race 全绿;cli build/vet/test 全绿;docs markdownlint 0 issues。
  • 端到端复测(≤2s 200)未执行:需有隧道凭据环境,命令与口径见 PR 描述。

请按「证据或降级二选一」口径复审。

@grapedev-bot

Copy link
Copy Markdown
Collaborator Author

[QA Agent] 审查报告

变更检视

代码审查

结论: ✅ 通过(Critical 0 / Required 0 / Optional 5)

已核对 Spec v2 的四项核心交付:

  1. 证据门 → 方案 A2:go-sdk/host.go 新增 portSetRefresher,在 accept relay channel 时调用 ListPorts(1s 去抖 portRefreshMinInterval、单次 2s 超时 portRefreshTimeout),diff 后应用到该 channel 的 ForwardFromRemotePort;启动时先 Refresh(force=true);静态端口模式、API 失败 fail-soft 均符合设计。依赖网关推送的旧路径已移除。
  2. 降级 B:cli/cmd/port.go:85,170,191 在 port create/update/delete 成功后统一 printPortChangeHint,中/英文案含重启命令,port delete 不再静默。
  3. 问题二(本仓库侧):devbridge show 输出 Host 在线状态,TunnelStatus.HostOnline() nil 安全;docs 中/英状态码语义表明确标注网关边界。
  4. 测试:go-sdk 覆盖增删/diff 幂等/去抖/静态忽略/失败降级/超时/取消/并发(-race);cli 增 i18n 用例,校验中英非空与重启命令。

问题清单(均为 Optional,不阻塞合并)

级别 文件:行 问题 建议
Optional go-sdk/host.go:396 apiAvailable 取 d.apiKey,而非本次会话实际凭据(HostConfig.APIKey / JWTToken)。CLI 已登录时用 --token 仍会调用 ListPorts(与“token 模式跳过端口查询”的文档/测试意图不一致);SDK 调用方若给 HostConfig.APIKey 传入与实例不同的 key,刷新会用错误凭据,失败后静默走旧端口集。 由 Host() 计算会话有效凭据并传入 refresher;apiAvailable = 使用 REST 凭据且未用 JWT,并确保 ListPorts 使用同一凭据。补一条真实 CLI wiring 的实例化测试。
Optional go-sdk/host.go:466-480 probedFirst 只探测首个 relay channel;旧实现是 !pn.received 时持续探测直到收到通知。在无 REST 凭据的 JWT/--token SDK 场景,若启动通知不是首个 channel,端口集将永远为空。 保持首个 channel 必探测(兼容 API 刷新成功后到达的通知),并在“无 REST 凭据且 !pn.received”时继续探测后续 channel。
Optional go-sdk/host.go:418-446 Refresh 在持有 r.mu 期间执行 ListPorts 并同步调用 OnPortsChanged;回调阻塞会串行化所有 channel 的刷新(每个最长 2s)。 回调移出锁外,或明确文档约定回调必须快速返回。
Optional cli/cmd/connect.go:82-99、docs/guide/ports.md:39 指定隧道 ID 时 -p/--ports 被忽略(直接走 API 动态模式),因此文档所述静态模式 devbridge host <tunnelId> -p ... 与提示中的重启命令实际不会 pin 端口,Host 仍是动态刷新。Spec v2 指定了该命令文本,建议要么支持 -p 覆盖为静态,要么修正文档/提示表述。 二选一并对齐中/英文案;--token 同样不支持 -p,文档“--token with pinned ports”需删改。
Optional docs/guide/ports.md:38-42、docs/guide/host.md 端口“移除”只影响新接受的 relay channel;已建立 inner session 上的转发不会被拆除,文档“removed ... applied to a running Host without a restart”表述偏强。 明确删除仅对新建连接生效,或补充旧连接说明。

安全扫描

结论: ✅ 安全

  • 未新增外部输入处理、命令执行或凭据落盘路径;--token 与 API key 分支保持互斥;docs 未泄露真实凭据。

合并检查

  • ✅ 代码审查通过(无 Critical/Required)
  • ✅ 安全通过
  • ✅ mergeable=true(rebase 完成)
  • ✅ Spec v2 的 A2 / 降级 B / show 在线状态 / 中英 docs 均已交付
  • ⚠️ 端到端 ≤2s 200、公网 404 vs 502/503 + Retry-After 仍无法在本环境验证(无隧道凭据、无网关代码)

验收口径提示(不阻塞本 PR 合并)

  • 问题一的 ≤2s 验收需在有凭据环境按 PR 描述执行并回填命令/时间戳/状态码/time_total。
  • 问题二的公网状态码属网关(Relay)跨仓依赖;本仓库仅交付客户端语义与文档,docs-only 不计为修复。
  • 因此本 PR 通过审查后,Issue [Bug] 运行中隧道新增端口需重启 Host;Host 离线返回 404 而非 502/503 #50 仍应进入 pending_review 人工确认,且跨仓实现与端到端复测完成前不关闭 Issue。

⚠️ 请合并本 PR

审查已通过,但流水线需要 PR 合并才能继续(合并后将自动触发 SRE 部署验证)。
请维护者操作:在 GitHub 页面点击【Merge】合并本 PR。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant