Skip to content

[后端] API 分页契约审计(参数面/语义一致性/截断暴露) #2136

Description

@DeliciousBuding

背景

#2102 给无 LIMIT 的 repository 查询加了硬上限兜底,但 API 层的分页契约(参数名、默认值、上限、语义)未系统盘点。本 issue 是只读审计结果,不含代码变更。

分页契约矩阵

A. cursor+pageSize 族(✅ 统一模式)

参数:pageCursor + pageSize,默认 50,上限 MaxPageLimit=500,响应 {nextCursor, hasMore}

端点 handler repo 模式
GET /web/agent-profiles agent_profile.go:146 pageSize+1 cursor
GET /web/mcp-servers mcp_server.go:97 pageSize+1 cursor
GET /web/skills skill.go:91 pageSize+1 cursor
GET /web/execution-targets execution_target.go:230 pageSize+1 cursor
GET /web/provider-bindings provider_binding.go:42 pageSize+1 cursor
GET /web/projects workspace.go:85 pageSize+1 cursor
GET /web/market/profiles market.go:40 pageSize+1 cursor
GET /web/audit-events audit.go:41 pageSize+1 cursor

判定:这 8 个端点契约一致,可作为后续端点的参考模板。

B. limit+offset 族(⚠️ 唯一)

端点 handler 默认/上限
GET /client/notifications notification.go:30 limit(50)/offset, MaxPageLimit=500

缺口:全仓唯一使用 offset 分页的端点,与其他端点 cursor 语义不一致。offset 在高频写入场景下有漂移风险。

C. limit-only 族(⚠️ 仅 limit 截断,无翻页能力)

端点 handler 游标/上限 缺口
GET /client/sessions/:id/messages message.go:80 before_seq + limit, MaxMessagePageLimit=100 seq 游标非通用 cursor;客户端需自行维护 seq 状态
GET /client/sessions/:id/messages/sync message.go:126 after_seq + limit, MaxIncrementalMessageLimit=500 增量同步专用,上限与消息列表不同
GET /web/documents document.go:88 limit + after(timestamp), repo 硬限 200 ❌ repo 上限 200 ≠ 全局 MaxPageLimit=500;after 是时间戳非 opaque cursor
GET /web/projects/:id/threads/:threadId/messages workspace.go:198 limit(50)/MaxPageLimit 无 cursor/offset,只能取首页
GET /web/agent-tasks/:id/events agent.go:246 after_seq + limit, maxAgentEventsPerQuery=2000 seq 游标;默认 2000 远大于其他端点

D. 无分页参数族(🔴 只有硬编码 LIMIT,客户端无法翻页/不知被截断)

端点 handler repo 硬限 风险等级
GET /client/sessions session.go:140 LIMIT 500 (session.go:89) 🔴 高频入口,>500 会话静默截断
GET /client/sessions/search session.go:331 LIMIT 20 (session.go:75) ⚠️ 搜索上限极低且不可调
GET /client/contacts contact.go:125 无 LIMIT (friendship.go:88) 🔴 好友多时 OOM/大 payload
GET /client/contacts/friend-requests contact.go:80 无 LIMIT (friendship.go:74) 🔴 无上限
GET /client/messages/search message.go:408 MaxMessagePageLimit=100 (message.go:224) ⚠️ 搜索无分页参数,>100 静默截断
GET /client/sessions/:id/messages/search message.go:434 MaxMessagePageLimit=100 ⚠️ 同上
GET /client/sessions/:id/pins message.go:343 Limit(100) (message.go:140) ⚠️ >100 pin 截断
GET /client/messages/:id/reactions message.go:321 无 LIMIT (message_reaction.go:19) 🟡 单消息 reaction 通常少,但无防护
GET /web/custom-agents custom_agent.go:73 无 LIMIT (agent.go:72) 🔴 自定义 agent 多时截断/OOM
GET /web/devices device.go:93 无 LIMIT (device.go:47) 🟡 设备数通常少
GET /web/agent-teams agent_team_crud.go:36 Limit(200) (agent_team_teams.go:23) ⚠️ >200 team 截断
GET /web/agent-teams/:id/runs agent_team_runs.go:37 Limit(200) (agent_team_runs.go:55) ⚠️ >200 run 截断
GET /web/agent-teams/:id/runs/:run_id/tasks agent_team_runs.go:93 Limit(500) (agent_team_tasks.go:29) ⚠️ >500 task 截断
GET /web/agent-teams/:id/runs/:run_id/events agent_team_runs.go:111 Limit(10000) (agent_team_events.go:104) ⚠️ 超大 payload 风险(~10MB)
GET /web/agent-teams/:id/runs/:run_id/assignments agent_team_assignments.go:95 Limit(500) (agent_team_assignments.go:25) ⚠️ >500 截断
GET /web/projects/:id/threads workspace.go:136 LIMIT 500 (session.go:103) ⚠️ >500 thread 截断
GET /web/agent-tasks/:id/approvals agent.go:285 间接吃 maxAgentEventsPerQuery=2000 ⚠️ 全量加载再投影
GET /web/agent-tasks/:id/artifacts agent.go:337 同上 ⚠️ 同上

缺口清单(按影响排序)

  1. 🔴 4 个端点完全无 LIMIT/client/contacts/client/contacts/friend-requests/web/custom-agents/client/messages/:id/reactions/web/devices[数据库] repository 查询模式审计(热表/索引/事务) #2102 的兜底可能未覆盖(需确认),即使覆盖了也无 API 层契约告知客户端被截断
  2. 🔴 /client/sessions 静默截断:LIMIT 500 无分页参数,活跃用户超 500 会话时丢失数据且无信号
  3. /web/documents repo 上限与全局不一致:repo 硬限 200 vs MaxPageLimit=500,API 层声称接受 500 但实际只返 200
  4. ⚠️ 分页语义三轨并行:cursor(pageCursor) / offset(limit+offset) / seq(before_seq/after_seq) / timestamp(after) 四种游标语义共存,客户端需为每种端点实现不同翻页逻辑
  5. ⚠️ 搜索端点无分页/client/messages/search/client/sessions/:id/messages/search/client/sessions/search 均无分页参数,结果超限静默截断
  6. ⚠️ agent-team 子资源全部无分页:runs/tasks/events/assignments 四个子资源列表均靠硬编码 LIMIT,长 run 必然截断

修复切片建议(≤3 条)

  1. P0 补 LIMIT + 截断信号:给 4 个无 LIMIT 端点加硬上限 + 响应中返回 truncated: truehasMore/client/sessions 加 cursor 分页或至少返回 truncated 标志
  2. P1 统一 documents 上限repository/document.go:30filter.Limit > 200 改为 config.MaxPageLimit,或 API 层声明上限为 200 并在 openapi.yaml 标注
  3. P2 搜索端点加分页SearchMessages / SearchSessions 增加 pageCursor + pageSize 参数,复用 A 族模板;长期考虑将 offset 族的 notifications 也迁移到 cursor

基线

未覆盖面

  • edge-server 端点(本次范围限定 hub-server)
  • WebSocket 事件流的分片/背压契约
  • openapi.yaml 中分页参数的声明完整性(仅审计了运行时行为)
  • 前端对各端点截断信号的实际处理(是否展示"加载更多"/"结果已截断")

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium prioritysize-MMedium effort后端Hub Server、Edge-Hub 通信和后端服务

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions