Skip to content

GET /api/v1/notifications 从不解析它声明的请求 schema —— cursor 被静默丢弃(SDK 分页永远第一页),limit 默认 20 声明 vs 50 实现 #6361

Description

@qq9340100

发现于 #5792(Stage A / #3877 notification 族响应体一致性测试)的前置测量。不在该 PR 修——修法是 #3676(删声明)与 #3847(改实现)方向相反的判断题,按纪律另立单。

事实(实测,origin/main@4d552af3f)

DEFAULT_NOTIFICATION_ROUTESGET /api/v1/notifications 声明了 ListNotificationsRequestSchema(见 packages/spec/src/api/protocol.zod.ts:914-919):

read:   z.boolean().optional()
type:   z.string().optional()
limit:  z.number().default(20)
cursor: z.string().optional()

服务端(packages/runtime/src/domains/notifications.ts:106-112)只读三个,且不经过这个 schema:

const read  = query?.read === undefined ? undefined : String(query.read) === 'true';
const limit = query?.limit ? Number(query.limit) : undefined;
const type  = query?.type ? String(query.type) : undefined;
const result = await inbox.listInbox(userId, { read, type, limit });

于是两条声明落空:

声明 实际 后果
cursor 谁都不读 见下面实测
limit 默认 20 未传时 forward undefined,MessagingService.listInbox 自己 clamp 到 50(messaging-service.ts:289) 声明的默认值从来没有生效过

cursor 的调用方是存在的:client.notifications.list({ cursor }) 确实把它拼进 query
(packages/client/src/index.ts:3679-3688)。所以这不是「无人使用的声明」,是「有人发、服务端静默忽略」。

实测(真实 boot:sqlite-wasm + ObjectQL + service-messaging + hono + dispatcher,60 条未读):

page1 = GET /api/v1/notifications?limit=5
        ids: ["YmkEfraba3S3E8GQ","N4eT1bK5MEGu-6j3","PsGcTsTjf0RqAOO-","6xLrm7tJadih3ccL","tZx2EQQ4MfeKKfWp"]
page2 = GET /api/v1/notifications?limit=5&cursor=tZx2EQQ4MfeKKfWp
        ids: ["YmkEfraba3S3E8GQ","N4eT1bK5MEGu-6j3","PsGcTsTjf0RqAOO-","6xLrm7tJadih3ccL","tZx2EQQ4MfeKKfWp"]
page2 === page1 ?  true

即:一个照着 SDK 声明分页的调用者会无限循环在第一页上,不报错,不 400。这正是 #3676
「request 声明了服务器不读的过滤器」那一类,只是这次调用方真的会发。

为什么此前没被发现

判断题(不猜,列证据供裁决)

A. 删声明(#3676 方向) —— 从 ListNotificationsRequestSchema 摘掉 cursor,把 limit
的默认值改成实现真正用的 50(或去掉 default,让「未传 = 由服务端决定窗口」成为显式契约),
同时摘掉 client.notifications.listcursor 参数。

B. 改实现(#3847 方向) —— 真的实现 cursor 分页(listInboxcreated_at + id 游标取下一页,
响应回填 cursor),并把 query 接到 ListNotificationsRequestSchema 上(默认值随之生效)。

  • 业务需求轴:目前没有实测拉动。收件箱的深翻页在 objectui 里走的是 setup app 的
    sys_inbox_message 列表视图(InboxPopover.tsx:129),已有自己的分页。
  • 长远合理性轴:通知列表迟早需要分页,但「迟早」不是本轮的证据。
  • 代价:listInbox 的读路径要改(受 receipt join 与 in-memory read 过滤影响,游标语义要想清楚),
    是真实设计工作,不是机械替换。

建议:A(删声明 + 把 limit 默认对齐实现),理由是三轴里业务需求轴给不出拉动,而
#4127 已在同一事实上裁过同一方向;创业期能力扩张从紧,分页词汇可以在真的有业务拉动时
连同实现一起回来。但这是公开契约变更,按纪律不猜——请维护者定。

关联

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions