fix(hub): 12 处 list 端点把越界 pageSize 塌回默认 50 而非 clamp 到上限(统一 config.ClampPageSize) - #2235
Merged
Merged
Conversation
…lampPageSize 单点实现 (#2154) 12 个 list 入口用同一个错误形态归一化越界 pageSize: if pageSize <= 0 || pageSize > 200 { pageSize = defaultXPageSize } // 50 上分支是错的:请求 201 行(openapi 共享 PageSize 声明 maximum: 200,handler 层还先 clamp 到 config.MaxPageLimit=500)会静默得到 **50 行 + HTTP 200**。 按分页方式分两档严重度(逐个端点核实,未照抄探索报告): - cursor 分页(workspaces / skills / public skills / agent profiles / public profiles / execution targets / provider bindings / mcp servers / public mcp / audit events):nextCursor 由实际返回行推导,**不丢数据**,但客户端只拿到 请求量的 1/4、往返 ~4×,且端点实际强制的上限与声明契约不一致。 - offset 分页 / 无游标: · ListNotifications 返回**裸数组**(hubClient.ts:166 request<HubNotification[]>,无 hasMore/nextCursor),limit 塌回而 offset 照给 → 按「请求的 limit」推进 offset 的消费者会**跳行丢数据**。全 app 树 grep 未发现 listNotifications 的 UI 调用点,故定性 **latent**,非活体事故。 · service/workspace.ListThreadMessages(handler/workspace.go:206 调用, handler 先 clamp 到 500)固定 offset 0、**无游标** → 线程面板请求最多 500 条却静默只渲染 50 条,用户无法翻到其余部分。这是**活体可见截断**。 对探索报告结论的更正:「10 个在产端点静默截断 + 裸数组无 hasMore」部分不成立。 workspace 等 cursor 端点响应含 page.hasMore(handler/workspace.go:103-105), 不会让客户端误判到底;裸数组只出现在 notifications 与 thread messages 两处。 同仓已有两处写对的形态(clamp 到上限):repository/message.go:48 (GetMessagesIncrement)与 repository/agent_team_assignments.go:160 —— 这是 「其余 12 处是抄错而非有意策略」的证据。 改法: - config 新增 ClampPageSize(requested, max, def) 单点实现(<=0→def、>max→ **max**、其余原样),并把裸 200 字面量提为 config.MaxListPageSize(= openapi PageSize 的 maximum;改它即契约变更,测试里钉死)。 - 13 处站点改为调用它(12 处 repository + service/workspace.ListThreadMessages)。 - handler 侧 clamp 一律不动,理由见 PR 正文「不做的事」。 测试(行为级,不是读代码): - config/paging_test.go:三分支表驱动 + 「max 由调用方传入」+ MaxListPageSize == 200 的契约钉。 - repository/pagination_clamp_test.go:12 个入口 × 3 个方向 = 36 例。共享夹具 缺 6 张表(workspaces/skills/execution_targets/provider_bindings/ mcp_servers/audit_events),用 AutoMigrate 在测试内局部补齐,不改共享夹具。 红证据(修复前):12/12 子测试 expected 200 actual 50(notifications 与 messages 为 100→50);绿:36/36。 另两个方向防止「修成新的忽略客户端意图」:in-range 30 → 正好 30 行; 非正数 0/-5 → 仍是默认 50 行(旧行为里正确的那一半必须保留)。 门禁(全部跑在本 HEAD): - go build ./... / go vet ./... 干净 - go test ./internal/repository/... ./internal/config/... ./internal/service/... ./internal/handler/... -short -count=1 → **30 包 ok**(无既有测试依赖旧塌回) - golangci-lint run ./... **0 issues**(清缓存后跑) - gosec -fmt=json ./... | verify-gosec-gates.sh CLEAN - verify-openapi-contract / verify-doc-ssot / verify-conventions / verify-hub-layering / verify-hub-pure-packages / verify-quality-debt-ratchet 全部 rc=0 - git diff --check 干净 证据等级:L0(sqlite 行为测试 + 静态门禁)。clamp 是纯 Go 整数分支、与 DB 引擎无关,故未加 L1;未做真实客户端翻页 E2E。 Co-authored-by: Cursor <cursor@vectorcontrol.tech>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 2, 2026
DeliciousBuding
added a commit
that referenced
this pull request
Sep 2, 2026
…2154)(#2237) ## 这条 lane 是什么 实施 lane E(**纯文档**)= #2154 Feynman 文档探索批的切片 C「对外文档诚实批」。 唯一非 `.md` 改动是第 6 条的 `scripts/verify/quality-debt-baseline.json`(任务书明示的唯一例外)。 **未改任何 `.go` / `.ts` / `.tsx` / `.yaml` 契约文件。** 分支已 `git fetch origin && git rebase origin/master`;上游新增的 `dc7df53d`(纯 Go:edge lifecycle safego 去重 + panic observer)与本分支 9 个文件**零重叠,rebase 无冲突**,因此没有触发「两边都保留 + 行数预算重新核算」的解冲突流程。 合并时 HEAD = `b017f0f4`,base = `94aef98a`(origin/master);lane push 时的 HEAD 为 `8f2ade25`(base `dc7df53d`)。7 个 commit,每个 commit 一次门禁。 **订正(主机侧合并前补记,原文此处写的是「故意不再 rebase」,与最终实况不符)**:push 后 `origin/master` 由 `dc7df53d` 连续前进到 `94aef98a`(#2234/#2235/#2236/#2239/#2240),本仓 `required_status_checks.strict: true` 使 PR 转 `BEHIND`,故本分支**已按纪律 rebase 到 `94aef98a`**,HEAD 由 `8f2ade25` 变为 `b017f0f4`。rebase 前后**写集逐字节零变化**:`git diff --stat 8f2ade2 b017f0f -- AGENTS.md CHANGELOG.md CONTRIBUTING.md README.md README_EN.md SECURITY.md docs/ scripts/verify/quality-debt-baseline.json` **输出为空**;两者全量 diff 只含 master 自身的 `app/pnpm-lock.yaml`+`app/pnpm-workspace.yaml`(#2240)与 `edge-server/internal/events/**`(#2234/#2239),与本 lane 9 个文件零重叠;7 个 commit 逐条 subject 一一对应。因此正文里所有本地实测结论原样成立。rebase 后按原文要求**重跑并复现**:`scripts/verify/verify-doc-ssot.py` → `doc SSOT ok`(verifier-map 66 script paths / 58 CI files、AGENTS.md 96 paths)、`wc -l AGENTS.md` → **284**(≤300 预算)、`git diff --check` 干净;CI run `33666303378` 在 `b017f0f4` 上**全绿**(22 successful / 17 skipped / 0 failing / 0 pending,含 go-hub、go-edge、windows-go、backend-required、frontend-required、ui-required、validate、CodeRabbit),`mergeStateStatus: CLEAN`。 --- ## 逐条三段式证据(文档原话 → 代码/CI 事实 → 改后原话) > 7 条逐字引证与改后原话全文见本 PR 正文(GitHub 侧永久保留),squash commit 只收判定结论以免历史膨胀。 | # | 条目 | 复核判定 | |---|---|---| | 1 | `CONTRIBUTING.md:36` 称 `make test` 跑前端 vitest | ✅ 成立(四处口径改指明 Makefile 目标名) | | 2 | `CHANGELOG.md:5-7`「暂无未发布变更」 | ✅ 成立(改为声明 SSOT 与生成方式) | | 3 | `SECURITY.md:23` 安全门禁工具名指错 | ✅ 成立,且原文另有一处更严重的不诚实 | | 4 | `README.md` / `README_EN.md` |⚠️ 部分成立(禁用词那条报告说法不准确,但假声明本身成立) | | 5 | `verifier-map.md` + `docs/architecture/README.md` 双向差集 | ✅ 成立(补 12 行 + 修 2 处不诚实 + 补 1 行索引) | | 6 | `quality-debt-baseline.json` 的 `issue` 归属 |⚠️ 报告部分成立:6 条里只敢改 2 条 | | 7 | `AGENTS.md` §12 加「CHANGELOG owner」 | ✅ 净减 2 行守住行数预算(284≤300) | ## 复核后判定「不成立 / 已过期」而跳过或改判的条目 | # | 报告说法 | 实测 | 处置 | |---|---|---|---| | 4b | 「路线图/roadmap 被门禁主动禁止却仍存在」 | `verify-doc-ssot.py` 只禁**根级文件 `ROADMAP.md`**(`:100`)、**路径 `docs/roadmap`**(`:109`)、**正则 `ROADMAP\.md`**(`:200`);没有任何规则禁「路线图」这个词,门禁本来就跑得绿 | 报告说法**不成立**,已在 PR 正文写明。但底下的**假声明成立**(`docs/` 里确实没有路线图),故仍按事实改,改法换成门禁理由本身陈述的真事实(roadmap 在 GitHub issues) | | 6 | web lint 债的真实归属是 1575 | `gh issue view 1581` 正文原话「**#1575 只负责 Desktop ESLint,不覆盖 Web**」 | 报告此点**不成立**,改判为 **#1581** | | 6 | i18n callsite 债的真实归属是 1612 | `#1612` 是 PR「docs(progress): MASTER 同步」,`files` 只有 `docs/progress/MASTER.md`;而仓内三处(checks.yml:2126 / verifier-map:27 / CHANGELOG:44)一致引用 #1612 | 报告此点**不成立**(且暴露更大问题:全仓的 #1612 引用可疑)。**不改**,登记 #2154 待裁决 | | 6 | 5/6 条都该改 | 只有 2 条能拿到「该 issue 明确以这笔债为标的」的正文证据 | **只改 2 条**,另 3 条按任务书要求不猜号 | | 1 | 四处口径自相矛盾 | `docs/developer-quickstart.md:122-123` 其实是**正确**的那一处 | quickstart **未改**,只改 CONTRIBUTING(错的那处)+ AGENTS(歧义的那处) | | 2 | 若不成立才补真实条目 | SSOT 判断**成立** | 按要求**没有**手写 Unreleased 列表 | --- ## 门禁表(原跑于 HEAD `8f2ade25` / base `dc7df53d`;rebase 到 `b017f0f4` / base `94aef98a` 后写集零变化,doc 门禁已重跑复现、CI 已全绿重证) | 门禁 | 命令 | 结果 | |---|---|---| | 文档 SSOT(主门禁) | `python3 scripts/verify/verify-doc-ssot.py` | ✅ `doc SSOT ok`;verifier-map **66** 脚本路径 / **58** CI 文件全部存在;AGENTS.md **96** 个反引号路径全部存在;`DOC-README-PARITY` PASS | | CI 结构合同 | `python3 scripts/verify/verify-ci-gates.py` | ✅ `ci gate policy ok` | | 质量债棘轮(动了 baseline) | `python3 scripts/verify/verify-quality-debt-ratchet.py` | ✅ **9 pass / 0 fail** | | 质量债棘轮负向自测 | `python3 scripts/verify/tests/verify-quality-debt-ratchet.Tests.py` | ✅ **15 tests OK** | | skill 白名单 | `python3 scripts/verify/verify-project-skills.py` | ✅ rc=0(`skills root absent (.agents removed) — whitelist gate trivially passes`) | | conventions 方法 SSOT | `python3 scripts/verify/verify-conventions.py` | ✅ `Passed: 1 \| Failed: 0` | | doc-ssot 负向自测 | `python3 scripts/verify/tests/verify-doc-entrypoints.Tests.py` | ✅ `Ran 1 test … OK`(证明主门禁没被我的改动弄钝) | | 空白/冲突标记 | `git diff --check origin/master..HEAD` | ✅ clean | | AGENTS.md 行数 | `wc -l AGENTS.md` | ✅ **284** ≤ 300 | | **GitHub Actions(本 PR 真实 run)** | run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) | ✅ **22 SUCCESS / 17 SKIPPED / 0 非绿**;7 个 required 聚合全绿:`validate` `go-edge` `go-hub` `windows-go` `windows-frontend` `backend-required` `frontend-required` | | 其他行数预算 | `wc -l` | ✅ CHANGELOG.md 80/90、CONTRIBUTING.md 58/90、verifier-map.md 87/120、docs/architecture/README.md 28/40 | **按纪律未跑**:`go test`、`go build`、vitest、coverage、全量 golangci-lint、docker、`make *`(4 核机 + 并行 lane)。 **golangci-lint 幽灵**:本 lane 未跑 golangci-lint,未遇到指向已删除 worktree 路径的缓存幽灵 issue。 --- ## 未验证项(诚实声明) 1. ~~**没有跑任何 CI**~~ → **已验证(本条从「未验证」升级为「已验证」,PR 开出后回写)**:GitHub Actions run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) 结果 **22 SUCCESS / 17 SKIPPED / 0 非绿**,7 个 required 聚合(`validate`/`go-edge`/`go-hub`/`windows-go`/`windows-frontend`/`backend-required`/`frontend-required`)全部 SUCCESS。`validate` 是承载 `verify-doc-ssot.py` + `verify-ci-gates.py` + `verify-quality-debt-ratchet.py` + `verify-conventions.py` + `verify-project-skills.py` 的 job,它 SUCCESS ⇒ 本 PR 全部 9 个文件的改动在 CI 上被同一套门禁判绿,不只是我本地判绿。`go-*` 侧也跑了(因为 `scripts/verify/**` 在 `changes` job 的 `go` 路径过滤里,baseline JSON 改动触发了 Go lane),`go-hub` 的 golangci-lint + 覆盖率门禁 SUCCESS ⇒ 未出现缓存幽灵。 **顺带活体印证第 3 条的改法**:`Vuln scan (pnpm audit prod+full)`、`Vuln scan (govulncheck)`、`Vuln scan (cargo audit)`、全部 `frontend-*`、`Visual QA *`、`Design CSS syntax` 在本 PR 均为 **SKIPPED** —— 正是我写进 `SECURITY.md` 的「三个 vuln-scan job 都经 `changes` job 路径过滤触发,不是每次 push 全量扫描」的实时证据(本 PR 不含 `app/**` 改动)。 2. **markdown 渲染只在本地按 CommonMark 规则推断**,没有在 GitHub 上肉眼看过渲染结果。两处需要 review 时确认:`docs/architecture/README.md` 新增行、`verifier-map.md` 宏观四行并入主表后是否真的渲染成表格。 3. **`gh issue view` 读到的是 issue/PR 的当前标题与正文**,不能证明「该 issue 在软门禁被引入的那一刻就是 owner」。desktop→#1575 / web→#1581 的判定依据是两个 issue 正文**逐字点名了对应的 baseline 条目与 step 名**,这是我能拿到的最强证据,但仍属文档考古而非当事人确认。 4. **tag `v0.6.1` 与 master 历史脱钩**这件事我只做了 `git merge-base --is-ancestor` / `git merge-base` 两个命令的验证,**没有**去查 release.yml 的历史 run 是否真的因此失败过,也没有验证 git-cliff 在无前序 tag 时的实际输出长度。它超出纯文档 lane 范围,只登记不动手。 5. **未改任何产品代码**,因此第 3/5 条里所有关于「门禁 fail-closed」的描述都是**读脚本源码 + workflow YAML 得出**,不是我实跑这些门禁观察到的红/绿。唯一实跑过的是 `verify-doc-ssot.py` / `verify-ci-gates.py` / `verify-quality-debt-ratchet.py` / `verify-conventions.py` / `verify-project-skills.py` 及两个负向自测。 --- ## 需要人工裁决 / 后续 issue(已同步登记 #2154) 1. quality-debt baseline 3 条 `issue` 归属待确认:`frontend-mobile: Lint (mobile rules)`、`validate: Verify i18n callsites ratchet`、`vuln-scan-rust: cargo clippy (advisory)`(现值均为可证伪的 1573)。 2. 全仓 `#1612` 引用可疑(checks.yml:2126 / verifier-map:27 / CHANGELOG:44 三处),需定位 i18n callsite ratchet 的真实接线 issue/PR。 3. tag `v0.6.1` 不在 master 祖先链上 ⇒ `release.yml:42` tag-guard 与 git-cliff `--latest` 的前序 tag 解析都受影响,下一次打 tag 前需裁决(重打 tag / 调整 cliff 调用 / 接受全量分组)。 4. `cliff.toml` 的 `^security` commit parser 是**死分支**(提交类型白名单不含 `security`):要么给白名单加 `security`,要么删掉这个 parser 并改用 label/其它机制披露安全修复。本 PR 只把 SECURITY.md 的承诺改成与现状一致,没动 cliff.toml(属产品配置,非纯文档 lane 范围)。 5. `scripts/verify/tests/merge-coverprofiles.Tests.py` 与 `scripts/verify/tests/verify-real-e2e-artifacts.Tests.py` **存在于磁盘但没有任何 workflow 调用**(`grep .github/workflows/` 零命中)⇒ 两个负向自测是死的。我在 verifier-map 里因此**没有**把它们写成「负向自测」(只登记了脚本本体),避免制造新的假绿声明;是否接线请裁决。 6. #1575 / #1581 均已 CLOSED,但对应的两条 `continue-on-error` 软门禁**仍在 checks.yml 里活着**(`verify-quality-debt-ratchet.py` 的 zombie 检查 PASS 即证明这点),且两条的 `review_by` 都是 `2026-10-01`。即「偿还 ESLint 债并移除软门禁」的 issue 关了、软门禁没移除。属治理不一致,非本 lane 范围。 ## 流程事故记录(不影响代码,但影响交付物可信度,故如实记) 开出本 PR 后、往 #2154 贴登记评论时,**另一条并行 lane(Lane A,#2154 评论 `5513247382`)在同一分钟覆写了 `/tmp/pr-body.md`** —— 两条 lane 用了同一个临时文件名。后果与处置: - **PR #2237 正文未受影响**:`gh pr create` 在覆写发生前已执行完毕。事后用 `gh pr view 2237 --json body` 回读实测 21035 字符,首句「## 这条 lane 是什么」、末句「Closes 无(本 PR 是 #2154 的切片 C…)」,且 `grep -c "toast.actionUnavailable"`(对方正文特征串)= 0 ⇒ 内容是我的、完整的。 - **#2154 的首版评论被污染**:拼评论时读到的是对方正文,等于把我的抬头 + Lane A 的正文贴了上去。已从 PR 正文回读重建、用 `gh api -X PATCH .../issues/comments/5513238457` **原地编辑**修正(不新贴第二条制造噪声),并复核修正后正文里对方 lane 的 5 个特征串(`§6.4`/`§6.5`/`desktop forward`/`regenerate 是否另开 lane`/`i18n 资源面 lane`)全部 0 命中、我的 4 个结构节各 1 次。 - **教训(供主机侧收进并行 lane 纪律)**:多 lane 并行时临时文件必须用 lane 唯一路径。本 lane 后续已改用 `/tmp/laneE-doc-honesty-2237/`。这与 `AGENTS.md`「一个 worktree 同时只放一个写 agent」是同一类风险,但发生在 worktree 之外的共享 `/tmp`,现有规则没覆盖到。 Closes 无(本 PR 是 #2154 的切片 C,#2154 由主机侧统一收口,不在此自动关单)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
结论先说
12 个 list 入口用同一个错误形态归一化越界
pageSize:上分支是错的:请求 201 行(
api/openapi.yaml的共享PageSize声明maximum: 200,handler 层还先 clamp 到config.MaxPageLimit=500)会静默得到 50 行 + HTTP 200。关联:#2154(Popper 后端架构探索批 P1-1;严重度分级与"裸数组"范围经主机侧逐端点核实后修正,见下)
严重度按分页方式分两档(逐个核实,非照抄报告)
① cursor 分页 — 契约违背 + 4× 往返,不丢数据
ListWorkspaces/ListSkills/ListPublicSkills/ListAgentProfiles/ListPublicProfiles/ListExecutionTargets/ListProviderBindings/ListMCPServers/ListPublicMCPServers/ListAuditEventsnextCursor由实际返回行推导,所以客户端仍能翻完,只是每页只有请求量的 1/4;同时端点实际强制的上限(50)与声明的契约(200)不一致。② offset 分页 / 无游标 — 真截断
ListNotificationsapp/shared/src/hub/hubClient.ts:166=request<HubNotification[]>,无hasMore/nextCursor),limit 塌回而 offset 照给listNotifications的 UI 调用点service/workspace.ListThreadMessages(handler/workspace.go:206,handler 先 clamp 到 500)对探索报告的两处更正(已核实):报告称「10 个在产端点静默截断、裸数组无
hasMore」。实测 cursor 端点的响应含page.hasMore(handler/workspace.go:103-105),不会让客户端误判到底;裸数组只出现在上表两处。故本 PR 只声明上表的影响面。"是抄错而非策略"的证据:同仓已有两处写对的形态(clamp 到上限)——
repository/message.go:48(GetMessagesIncrement)与repository/agent_team_assignments.go:160。改法
config.ClampPageSize(requested, max, def)单点实现:<=0 → def、> max → **max**、其余原样。200字面量(10 处重复)提为config.MaxListPageSize,并在测试里钉住== 200:改它就是改契约,必须显式。service/workspace.ListThreadMessages)。max/def做成参数而非硬编码常量:仓里合法存在两个上限(通用 list = 200,message 族 =MaxMessagePageLimit100,document 族 =MaxPageLimit500),由调用方传自己端点声明的那个。测试(行为级:种真实行数、数返回行数,不读代码)
config/paging_test.goMaxListPageSize == 200契约钉repository/pagination_clamp_test.go12/12子测试expected: 200 / actual: 50(notifications 与 messages 为100 → 50)。36/36。30→ 正好 30 行;非正数0/-5→ 仍是默认 50 行(旧行为里正确的那一半必须保留)。AutoMigrate在测试内局部补齐,不改共享夹具。门禁(全部跑在本 HEAD)
go build ./.../go vet ./...go test ./internal/repository/... ./internal/config/... ./internal/service/... ./internal/handler/... -short -count=1golangci-lint run ./...(清缓存后)gosec -fmt=json ./... | verify-gosec-gates.shverify-openapi-contractverify-doc-ssot/verify-conventions/verify-hub-layering/verify-hub-pure-packages/verify-quality-debt-ratchetgit diff --check证据等级:L0(sqlite 行为测试 + 静态门禁)。clamp 是纯 Go 整数分支、与 DB 引擎无关,故未加 L1;未做真实客户端翻页 E2E。
不做的事(附完整清点,已登记 #2154)
不删 handler 侧的 13 处 clamp。 对本 PR 修的端点它们确实已成死代码(handler clamp 到 500,repository clamp 到 200/100,前者永不决定性),但:
market.go:49/session.go:346/agent.go:374/agent_team_runs.go:128四处的下游走 service 层,要证明"repository 侧确有等价约束"需逐个穿透;handler/message.go:105是活的——它把 limit 压到 100,恰好掩盖了 repository 的塌回(这也是消息列表没暴露本 bug 的原因);故拆为后续切片:把边界收敛为「repository 单一属主」,逐端点证明后再删 handler clamp(清点已在 #2154)。同时不改 openapi(
PageSize maximum: 200现在才真的成立;documents/custom-agents 用MaxPageLimit=500与共享PageSize的三方不一致属报告 P1-2,另片处理)。