背景
#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
同上
⚠️ 同上
缺口清单(按影响排序)
🔴 4 个端点完全无 LIMIT :/client/contacts、/client/contacts/friend-requests、/web/custom-agents、/client/messages/:id/reactions、/web/devices — [数据库] repository 查询模式审计(热表/索引/事务) #2102 的兜底可能未覆盖(需确认),即使覆盖了也无 API 层契约告知客户端被截断
🔴 /client/sessions 静默截断 :LIMIT 500 无分页参数,活跃用户超 500 会话时丢失数据且无信号
❌ /web/documents repo 上限与全局不一致 :repo 硬限 200 vs MaxPageLimit=500,API 层声称接受 500 但实际只返 200
⚠️ 分页语义三轨并行 :cursor(pageCursor) / offset(limit+offset) / seq(before_seq/after_seq) / timestamp(after) 四种游标语义共存,客户端需为每种端点实现不同翻页逻辑
⚠️ 搜索端点无分页 :/client/messages/search、/client/sessions/:id/messages/search、/client/sessions/search 均无分页参数,结果超限静默截断
⚠️ agent-team 子资源全部无分页 :runs/tasks/events/assignments 四个子资源列表均靠硬编码 LIMIT,长 run 必然截断
修复切片建议(≤3 条)
P0 补 LIMIT + 截断信号 :给 4 个无 LIMIT 端点加硬上限 + 响应中返回 truncated: true 或 hasMore;/client/sessions 加 cursor 分页或至少返回 truncated 标志
P1 统一 documents 上限 :repository/document.go:30 的 filter.Limit > 200 改为 config.MaxPageLimit,或 API 层声明上限为 200 并在 openapi.yaml 标注
P2 搜索端点加分页 :SearchMessages / SearchSessions 增加 pageCursor + pageSize 参数,复用 A 族模板;长期考虑将 offset 族的 notifications 也迁移到 cursor
基线
主检出:master(commit 以 git log -1 --oneline 为准)
config 常量:hub-server/internal/config/constants.go(DefaultPaginationLimit=50, MaxPageLimit=500, MaxMessagePageLimit=100, MaxIncrementalMessageLimit=500)
[数据库] repository 查询模式审计(热表/索引/事务) #2102 已加 repository 层兜底,本次审计聚焦 API 契约层
未覆盖面
edge-server 端点(本次范围限定 hub-server)
WebSocket 事件流的分片/背压契约
openapi.yaml 中分页参数的声明完整性(仅审计了运行时行为)
前端对各端点截断信号的实际处理(是否展示"加载更多"/"结果已截断")
背景
#2102 给无 LIMIT 的 repository 查询加了硬上限兜底,但 API 层的分页契约(参数名、默认值、上限、语义)未系统盘点。本 issue 是只读审计结果,不含代码变更。
分页契约矩阵
A. cursor+pageSize 族(✅ 统一模式)
参数:
pageCursor+pageSize,默认 50,上限MaxPageLimit=500,响应{nextCursor, hasMore}。GET /web/agent-profilesagent_profile.go:146GET /web/mcp-serversmcp_server.go:97GET /web/skillsskill.go:91GET /web/execution-targetsexecution_target.go:230GET /web/provider-bindingsprovider_binding.go:42GET /web/projectsworkspace.go:85GET /web/market/profilesmarket.go:40GET /web/audit-eventsaudit.go:41判定:这 8 个端点契约一致,可作为后续端点的参考模板。
B. limit+offset 族(⚠️ 唯一)
GET /client/notificationsnotification.go:30缺口:全仓唯一使用 offset 分页的端点,与其他端点 cursor 语义不一致。offset 在高频写入场景下有漂移风险。
C. limit-only 族(⚠️ 仅 limit 截断,无翻页能力)
GET /client/sessions/:id/messagesmessage.go:80GET /client/sessions/:id/messages/syncmessage.go:126GET /web/documentsdocument.go:88GET /web/projects/:id/threads/:threadId/messagesworkspace.go:198GET /web/agent-tasks/:id/eventsagent.go:246D. 无分页参数族(🔴 只有硬编码 LIMIT,客户端无法翻页/不知被截断)
GET /client/sessionssession.go:140session.go:89)GET /client/sessions/searchsession.go:331session.go:75)GET /client/contactscontact.go:125friendship.go:88)GET /client/contacts/friend-requestscontact.go:80friendship.go:74)GET /client/messages/searchmessage.go:408message.go:224)GET /client/sessions/:id/messages/searchmessage.go:434GET /client/sessions/:id/pinsmessage.go:343message.go:140)GET /client/messages/:id/reactionsmessage.go:321message_reaction.go:19)GET /web/custom-agentscustom_agent.go:73agent.go:72)GET /web/devicesdevice.go:93device.go:47)GET /web/agent-teamsagent_team_crud.go:36agent_team_teams.go:23)GET /web/agent-teams/:id/runsagent_team_runs.go:37agent_team_runs.go:55)GET /web/agent-teams/:id/runs/:run_id/tasksagent_team_runs.go:93agent_team_tasks.go:29)GET /web/agent-teams/:id/runs/:run_id/eventsagent_team_runs.go:111agent_team_events.go:104)GET /web/agent-teams/:id/runs/:run_id/assignmentsagent_team_assignments.go:95agent_team_assignments.go:25)GET /web/projects/:id/threadsworkspace.go:136session.go:103)GET /web/agent-tasks/:id/approvalsagent.go:285GET /web/agent-tasks/:id/artifactsagent.go:337缺口清单(按影响排序)
/client/contacts、/client/contacts/friend-requests、/web/custom-agents、/client/messages/:id/reactions、/web/devices— [数据库] repository 查询模式审计(热表/索引/事务) #2102 的兜底可能未覆盖(需确认),即使覆盖了也无 API 层契约告知客户端被截断/client/sessions静默截断:LIMIT 500 无分页参数,活跃用户超 500 会话时丢失数据且无信号/web/documentsrepo 上限与全局不一致:repo 硬限 200 vs MaxPageLimit=500,API 层声称接受 500 但实际只返 200/client/messages/search、/client/sessions/:id/messages/search、/client/sessions/search均无分页参数,结果超限静默截断修复切片建议(≤3 条)
truncated: true或hasMore;/client/sessions加 cursor 分页或至少返回truncated标志repository/document.go:30的filter.Limit > 200改为config.MaxPageLimit,或 API 层声明上限为 200 并在 openapi.yaml 标注SearchMessages/SearchSessions增加pageCursor+pageSize参数,复用 A 族模板;长期考虑将 offset 族的 notifications 也迁移到 cursor基线
master(commit 以git log -1 --oneline为准)hub-server/internal/config/constants.go(DefaultPaginationLimit=50, MaxPageLimit=500, MaxMessagePageLimit=100, MaxIncrementalMessageLimit=500)未覆盖面