@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +599 -24
  3. package/dist/adapters/d1.d.ts +416 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +306 -0
  6. package/dist/adapters/neon.d.ts +88 -0
  7. package/dist/adapters/pg-shared.d.ts +161 -0
  8. package/dist/adapters/pg.d.ts +84 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +49 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/chunk-E7OWP3VL.cjs +135 -0
  13. package/dist/chunk-GN44PST5.mjs +148 -0
  14. package/dist/chunk-KOJQVOYS.mjs +5847 -0
  15. package/dist/chunk-QHVID3V2.cjs +5847 -0
  16. package/dist/chunk-ZGGF4GMA.mjs +135 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +37 -0
  18. package/dist/cloudflare.cjs +39 -2
  19. package/dist/cloudflare.d.ts +10 -2
  20. package/dist/cloudflare.mjs +40 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state.d.ts +7 -0
  24. package/dist/handlers/get-message.d.ts +3 -0
  25. package/dist/handlers/get-user-key.d.ts +3 -0
  26. package/dist/handlers/init-tenant.d.ts +23 -0
  27. package/dist/handlers/llm-credentials.d.ts +5 -0
  28. package/dist/handlers/messages.d.ts +3 -0
  29. package/dist/handlers/outbox.d.ts +7 -0
  30. package/dist/handlers/push-subscription.d.ts +5 -0
  31. package/dist/handlers/schedule-message.d.ts +3 -0
  32. package/dist/handlers/send-notifications.d.ts +3 -0
  33. package/dist/handlers/single-user-init.d.ts +13 -0
  34. package/dist/handlers/update-message.d.ts +3 -0
  35. package/dist/handlers/vapid-public-key.d.ts +19 -0
  36. package/dist/index.cjs +133 -44
  37. package/dist/index.d.cts +131 -766
  38. package/dist/index.d.ts +131 -766
  39. package/dist/index.mjs +125 -36
  40. package/dist/lib/agentic-fire.d.ts +108 -0
  41. package/dist/lib/client-state-store.d.ts +80 -0
  42. package/dist/lib/constant-time.d.ts +14 -0
  43. package/dist/lib/db-errors.d.ts +10 -0
  44. package/dist/lib/encryption.d.ts +48 -0
  45. package/dist/lib/errors.d.ts +215 -0
  46. package/dist/lib/llm-credentials-store.d.ts +147 -0
  47. package/dist/lib/llm.d.ts +1 -0
  48. package/dist/lib/message-processor.d.ts +73 -0
  49. package/dist/lib/outbox-store.d.ts +95 -0
  50. package/dist/lib/push-subscription-store.d.ts +94 -0
  51. package/dist/lib/recurrence.d.ts +51 -0
  52. package/dist/lib/request.d.ts +182 -0
  53. package/dist/lib/result-emitter.d.ts +54 -0
  54. package/dist/lib/run-tick.d.ts +115 -0
  55. package/dist/lib/schema-version.d.ts +50 -0
  56. package/dist/lib/state-accessors.d.ts +70 -0
  57. package/dist/lib/state-chunks.d.ts +55 -0
  58. package/dist/lib/task-projection.d.ts +92 -0
  59. package/dist/lib/validation.d.ts +97 -0
  60. package/dist/lib/version.d.ts +7 -0
  61. package/dist/lib/webcrypto-utils.d.ts +1 -0
  62. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  63. package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
  64. package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
  65. package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
  66. package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
  67. package/dist/single-user.d.ts +95 -0
  68. package/dist/tenant/blob-store.d.ts +30 -0
  69. package/dist/tenant/context.d.ts +47 -0
  70. package/dist/tenant/single-user-context.d.ts +29 -0
  71. package/dist/tenant/token.d.ts +26 -0
  72. package/package.json +3 -3
  73. package/dist/chunk-5ENJGVZX.mjs +0 -72
  74. package/dist/chunk-GN5QTMCL.cjs +0 -1870
  75. package/dist/chunk-PVNLB6TF.mjs +0 -1870
  76. package/dist/chunk-RGECD4OH.cjs +0 -72
  77. package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
  78. package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
  79. package/dist/cloudflare.d.cts +0 -2
  80. package/dist/neon-CJl66EGy.d.cts +0 -251
  81. package/dist/neon-DYvGnCzx.d.ts +0 -251
  82. package/dist/pg-07S-u_H4.d.cts +0 -246
  83. package/dist/pg-Du-pN_UT.d.ts +0 -246
  84. package/dist/schema-C8OnYk6j.d.cts +0 -74
  85. package/dist/schema-C8OnYk6j.d.ts +0 -74
@@ -0,0 +1,306 @@
1
+ export type TaskRow = {
2
+ id: number;
3
+ user_id: string;
4
+ uuid: string;
5
+ encrypted_payload: string;
6
+ message_type: string;
7
+ next_send_at: string;
8
+ status: string;
9
+ retry_count: number;
10
+ created_at?: string;
11
+ updated_at?: string;
12
+ /**
13
+ * 退避时刻。投递链路读的行(getPendingTasks / getTaskByUuidOnly)必须带上
14
+ * 它,读接口返回的行(getTaskByUuid / listTasks)不带。
15
+ */
16
+ retry_after?: string | null;
17
+ /**
18
+ * 上一次投递失败的脱敏摘要(JSON 串)。读接口返回的行才带。
19
+ *
20
+ * 两条链路各要一套列,内置适配器统一从 adapters/schema.js 的
21
+ * `TASK_DELIVERY_COLUMNS` / `TASK_DETAIL_COLUMNS` 取,三种方言共用一份。
22
+ */
23
+ last_error?: string | null;
24
+ };
25
+ export type InsertTaskParams = {
26
+ user_id: string;
27
+ uuid: string;
28
+ encrypted_payload: string;
29
+ next_send_at: string;
30
+ message_type: string;
31
+ };
32
+ export type InitSchemaResult = {
33
+ columnsCreated: number;
34
+ indexesCreated: number;
35
+ indexesFailed: number;
36
+ columns: any[];
37
+ indexes: any[];
38
+ };
39
+ export type DbAdapter = {
40
+ /**
41
+ * Create the scheduled_messages table and all indexes.
42
+ */
43
+ initSchema: () => Promise<InitSchemaResult>;
44
+ /**
45
+ * (可选)活库里现在实际有哪些表 / 列 / 索引,只读不写。
46
+ * `getSchemaVersion` / `ensureSchema`(lib/schema-version.js)拿它跟这一版
47
+ * 需要的清单对照;不实现的适配器调那两个函数会抛错。内置只有 D1 实现。
48
+ */
49
+ describeSchema?: () => Promise<{
50
+ tables: Record<string, string[]>;
51
+ indexes: string[];
52
+ }>;
53
+ /**
54
+ * Drop the scheduled_messages table (CASCADE).
55
+ */
56
+ dropSchema: () => Promise<void>;
57
+ /**
58
+ * Insert a new task row and return the created record.
59
+ */
60
+ createTask: (params: InsertTaskParams) => Promise<TaskRow>;
61
+ /**
62
+ * Fetch a single pending task by uuid + user_id.
63
+ */
64
+ getTaskByUuid: (uuid: string, userId: string) => Promise<TaskRow | null>;
65
+ /**
66
+ * Fetch a single pending task by uuid only (used by instant processing).
67
+ * 返回的行要和 `getPendingTasks` 是同一套列(`TASK_DELIVERY_COLUMNS`):
68
+ * `runTask` 拿这一行走同一条投递链,少了 `retry_after`,退避守卫读到的永远
69
+ * 是 undefined,还在等重试的任务会被当场再跑一遍。
70
+ */
71
+ getTaskByUuidOnly: (uuid: string) => Promise<TaskRow | null>;
72
+ /**
73
+ * (可选)这条 uuid 现在是什么状态——不限用户,也不限状态。`runTask` 用它
74
+ * 把「这条已经跑完进终态了」和「压根没这条」分开回报;不实现时两种都归
75
+ * `not_found`。
76
+ */
77
+ getTaskStatusByUuidOnly?: (uuid: string) => Promise<{
78
+ status: string;
79
+ } | null>;
80
+ /**
81
+ * Partially update a task row by its numeric id.
82
+ * 实现了 `claimTask` 的适配器还要认 `lease_until` 和 `retry_after`(含写
83
+ * null):投递收尾时 runScheduledTick 用前者把租约放掉,用后者写/清投递失败
84
+ * 的退避时刻。
85
+ */
86
+ updateTaskById: (taskId: number, updates: any) => Promise<TaskRow | null>;
87
+ /**
88
+ * Update a pending task's encrypted_payload (and optional index fields) by uuid + user_id.
89
+ */
90
+ updateTaskByUuid: (uuid: string, userId: string, encryptedPayload: string, extraFields?: any) => Promise<TaskRow | null>;
91
+ /**
92
+ * Delete a task by numeric id. Returns true if a row was affected.
93
+ */
94
+ deleteTaskById: (taskId: number) => Promise<boolean>;
95
+ /**
96
+ * Delete a task by uuid + user_id. Returns true if a row was affected.
97
+ */
98
+ deleteTaskByUuid: (uuid: string, userId: string) => Promise<boolean>;
99
+ /**
100
+ * Fetch pending tasks whose next_send_at <= NOW(), ordered ASC.
101
+ * 跳过两种还不该动的行:租约没到期的(`lease_until`,有人正在跑)、退避没到
102
+ * 点的(`retry_after`,上次投递失败在等重试)。两列都是空或已过期才算待发。
103
+ */
104
+ getPendingTasks: (limit?: number) => Promise<TaskRow[]>;
105
+ /**
106
+ * 领取一条到点的任务,返回是否领到。cron 每分钟一跳而一次投递可能跑几分
107
+ * 钟,runScheduledTick 靠它保证同一行同时只被一个 tick 跑。
108
+ * 三个条件同时成立才领得走:行仍是 pending;`lease_until` 为空或已过期
109
+ * (没别人正在跑);`next_send_at` 还等于 `expectedNextSendAt`(读这行时
110
+ * 拿到的值,用户中途改了排期就不发了)。领到就把 `lease_until` 写成
111
+ * `leaseUntil`,`next_send_at` 不动。
112
+ * 传了非空 `serializeGroup` 时还多一个条件:同一分组里没有别的行拿着未到期
113
+ * 的租约(同一分组同时只跑一条,`runScheduledTick` 的 `serializeBy` 用)。
114
+ * 领到时把这个值写进 `serialize_group` 列。判定和占位必须在同一条语句里,
115
+ * 「先查再占」中间的空档会让两个 tick 同时进同一个分组。
116
+ * 自定义适配器可以不实现(runScheduledTick 会退回不占位的行为,代价是慢
117
+ * 任务可能被下一跳重复触发);实现了但忽略第四个参数的,分组串行退化成只在
118
+ * 同一跳内生效。
119
+ */
120
+ claimTask?: (taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null) => Promise<boolean>;
121
+ /**
122
+ * List tasks for a user with optional filters and pagination.
123
+ */
124
+ listTasks: (userId: string, opts: {
125
+ status?: string;
126
+ limit?: number;
127
+ offset?: number;
128
+ }) => Promise<{
129
+ tasks: TaskRow[];
130
+ total: number;
131
+ }>;
132
+ /**
133
+ * Delete completed / failed tasks older than `days` (default 7).
134
+ */
135
+ cleanupOldTasks: (days?: number) => Promise<number>;
136
+ /**
137
+ * Return the status string of a task (used to distinguish 404 from 409).
138
+ */
139
+ getTaskStatus: (uuid: string, userId: string) => Promise<string | null>;
140
+ /**
141
+ * (optional; single-user/D1 only) Batch upsert of client state, last-write-wins on updatedAt.
142
+ * `cleanups` 先于 upsert 在同一事务里删旧行,两种形态:带 `keyPrefix` 的按 key 前缀删(清理大值
143
+ * 旧写入留下的切片行,见 lib/state-chunks.js),带 `key` 的删这一个 key(删整条状态;前缀会连带
144
+ * 删掉同前缀的兄弟 key)。两种都只删 `updated_at <= updatedAt` 的行。自定义 adapter 忽略前缀形态
145
+ * 只损失存储卫生;忽略精确 key 形态则删不掉状态,`ctx.writeState()` 的删除会失效。
146
+ * `outcomes` 逐条报告 entries[i] 是否真的写入(缺席时调用方按物理行计数兜底)。
147
+ */
148
+ upsertClientState?: (userId: string, entries: Array<{
149
+ namespace: string;
150
+ key: string;
151
+ value: string;
152
+ updatedAt: number;
153
+ }>, cleanups?: Array<{
154
+ namespace: string;
155
+ key?: string;
156
+ keyPrefix?: string;
157
+ updatedAt: number;
158
+ }>) => Promise<{
159
+ upserted: number;
160
+ skipped: number;
161
+ outcomes?: boolean[];
162
+ }>;
163
+ /**
164
+ * (optional; single-user/D1 only) All entries of one namespace; values still encrypted.
165
+ */
166
+ getClientState?: (userId: string, namespace: string) => Promise<Array<{
167
+ namespace: string;
168
+ key: string;
169
+ value: string;
170
+ updated_at: number;
171
+ }>>;
172
+ /**
173
+ * (optional; single-user/D1 only) Delete every entry of this user; returns rows deleted.
174
+ */
175
+ clearClientState?: (userId: string) => Promise<number>;
176
+ /**
177
+ * (可选;单用户/D1)按命名空间清掉 `updated_at` 早于 `updatedBefore`(epoch 毫秒)
178
+ * 的行,不限用户。宿主配了 `clientStateTtl` 时 runScheduledTick 每跳顺手调;
179
+ * 指令由 lib/client-state-store.js 的 `planClientStateCleanup` 算好(含大值切片
180
+ * 所在的保留命名空间)。不实现 → 不清理,与不配 TTL 时行为一致。
181
+ */
182
+ cleanupClientState?: (targets: Array<{
183
+ namespace: string;
184
+ updatedBefore: number;
185
+ }>) => Promise<number>;
186
+ /**
187
+ * 这个用户当前登记的 Web Push 订阅(`subscription` 是密文,解密在上层)。没有登记过 → null。
188
+ * 一个用户一份:任务行不携带订阅,到点投递时读这里。
189
+ */
190
+ getPushSubscription?: (userId: string) => Promise<{
191
+ subscription: string;
192
+ updated_at: number;
193
+ } | null>;
194
+ /**
195
+ * 覆盖写这个用户的订阅(`updatedAt` 是 epoch 毫秒)。没有 last-write-wins 比较——
196
+ * 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
197
+ */
198
+ upsertPushSubscription?: (userId: string, encryptedSubscription: string, updatedAt: number) => Promise<boolean>;
199
+ /**
200
+ * 删掉这个用户的订阅;返回是否真的删掉了一行。
201
+ *
202
+ * 上面三个方法要么都实现、要么都不实现:缺任何一个,`PUT/GET/DELETE
203
+ * /push-subscription` 返回 501,`POST /schedule-message` 也会拒绝建任务
204
+ * (建了也永远发不出去)。内置的 D1 / pg / neon 适配器都实现了。
205
+ */
206
+ deletePushSubscription?: (userId: string) => Promise<boolean>;
207
+ /**
208
+ * 批量覆盖写这个用户的 LLM 凭据(`encryptedValue` 是密文,加密在上层)。
209
+ * 已存在的行覆盖 encrypted_value 并刷 updated_at。返回实际写入/覆盖的行数。
210
+ */
211
+ upsertLlmCredentials?: (userId: string, entries: Array<{
212
+ credId: string;
213
+ encryptedValue: string;
214
+ }>) => Promise<number>;
215
+ /**
216
+ * 按 cred_id 批量读凭据行(密文原样返回)。排程时的存在性检查和 fire 时的
217
+ * 解析共用这一个口。
218
+ */
219
+ getLlmCredentials?: (userId: string, credIds: string[]) => Promise<Array<{
220
+ cred_id: string;
221
+ encrypted_value: string;
222
+ updated_at: string;
223
+ }>>;
224
+ /**
225
+ * 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出这个
226
+ * 方法)。按 cred_id 排序。
227
+ */
228
+ listLlmCredentials?: (userId: string) => Promise<Array<{
229
+ cred_id: string;
230
+ updated_at: string;
231
+ }>>;
232
+ /**
233
+ * 删凭据:数组删指定那几行,null 删全部。返回删掉的行数。
234
+ *
235
+ * llm_credentials 四个方法要么都实现、要么都不实现:缺任何一个,
236
+ * `PUT/GET/DELETE /llm-credentials` 返回 501,带 `credRefs` 的
237
+ * `POST /schedule-message` 也会被拒。内置的 D1 / pg / neon 适配器都实现了。
238
+ */
239
+ deleteLlmCredentials?: (userId: string, credIds: string[] | null) => Promise<number>;
240
+ /**
241
+ * (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
242
+ * pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
243
+ * 它复活。不实现 → 心跳自动关闭,退回一次性长租约(claimLeaseMs)。
244
+ *
245
+ * 返回值现在还是「这条任务是不是被取消/顶替了」的信号:明确返回 `false`
246
+ * 会中止这次投递剩下的推送(行已经不在了,再发就是「取消接口回了成功,
247
+ * 消息照样送达」)。所以匹配不到行时必须返回 `false`,不能返回 undefined
248
+ * ——什么都不返回等于关掉这条信号,取消撞上投递时又会照发。抛错不算行没
249
+ * 了,只当作这次没续上,下个心跳再试。
250
+ */
251
+ renewTaskLease?: (taskId: number, leaseUntil: string | Date) => Promise<boolean>;
252
+ /**
253
+ * (可选)状态 + last_error 列(脱敏失败摘要的 JSON 串)。GET /message 用
254
+ * 它把「为什么失败」透给已失败的行;不实现时退回 getTaskStatus(409 里就
255
+ * 没有 lastError)。
256
+ */
257
+ getTaskStatusInfo?: (uuid: string, userId: string) => Promise<{
258
+ status: string;
259
+ last_error: string | null;
260
+ } | null>;
261
+ /**
262
+ * (可选)建新任务的同一事务里取消旧的那条(POST /schedule-message 的
263
+ * supersedesUuid)。不实现时 handler 退回「先删再建」两步(失去原子性)。
264
+ */
265
+ createTaskSuperseding?: (params: InsertTaskParams, supersedesUuid: string) => Promise<TaskRow & {
266
+ superseded: boolean;
267
+ }>;
268
+ /**
269
+ * (可选;单用户/D1)push 发送前把整批落进 message_outbox(密文 payload),
270
+ * (user_id, message_id) 冲突时更新未 ack 的行、不动已 ack 的。
271
+ */
272
+ appendOutboxMessages?: (userId: string, rows: Array<any>) => Promise<number>;
273
+ /**
274
+ * (可选;单用户/D1)把发出去的段标 delivered_at。
275
+ */
276
+ markOutboxDelivered?: (userId: string, messageIds: string[], deliveredAt: number) => Promise<number>;
277
+ /**
278
+ * (可选;单用户/D1)把还没发出去的行删掉。任务投递到一半被取消 / 顶替时,
279
+ * 剩下那几条 push 已经落了行却不会再发,不撤掉的话客户端会从 `GET /outbox`
280
+ * 把它们补收回去。不实现 → 取消只挡住 Web Push 这一路。
281
+ */
282
+ discardOutboxMessages?: (userId: string, messageIds: string[]) => Promise<number>;
283
+ /**
284
+ * (可选;单用户/D1)未 ack 的行,id 升序游标翻页(GET /outbox)。行上要带
285
+ * `task_uuid` 和 `delivered_at`:`DELETE /message` 和 supersedesUuid 顶替这两
286
+ * 条路靠翻这份名单找出该任务名下还没发出去的行(没有按 task_uuid 查的读法),
287
+ * 缺任一字段就挑不出来,那两条路上的 outbox 清理会静默跳过。
288
+ */
289
+ listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
290
+ /**
291
+ * (可选;单用户/D1)客户端确认收到(POST /outbox/ack,幂等)。
292
+ */
293
+ ackOutboxMessages?: (userId: string, messageIds: string[], ackedAt: number) => Promise<number>;
294
+ /**
295
+ * (可选;单用户/D1)outbox 例行清理(runScheduledTick 每跳顺手调)。
296
+ *
297
+ * outbox 这几个方法要么都实现、要么都不实现:缺写入侧的(append / mark),
298
+ * 发送链路静默跳过落行;缺读取侧的(list / ack),`GET /outbox` 与
299
+ * `POST /outbox/ack` 返回 501;缺 discard(或取消那条路上缺 list),取消只挡
300
+ * 住 Web Push。内置只有 D1 实现(与 client_state 同待遇)。
301
+ */
302
+ cleanupOutbox?: (opts: {
303
+ ackedBeforeMs?: number;
304
+ allBeforeMs?: number;
305
+ }) => Promise<number>;
306
+ };
@@ -0,0 +1,88 @@
1
+ export class NeonAdapter {
2
+ /** @param {string} connectionString */
3
+ constructor(connectionString: string);
4
+ /** @private */
5
+ private _connectionString;
6
+ /** @private */
7
+ private _sql;
8
+ /**
9
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
10
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
11
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
12
+ *
13
+ * @private
14
+ */
15
+ private _getSql;
16
+ initSchema(): Promise<{
17
+ columnsCreated: number;
18
+ indexesCreated: number;
19
+ indexesFailed: number;
20
+ columns: {
21
+ table: string;
22
+ name: any;
23
+ type: any;
24
+ nullable: boolean;
25
+ }[];
26
+ indexes: ({
27
+ name: string;
28
+ status: string;
29
+ description: string;
30
+ critical: boolean;
31
+ error?: undefined;
32
+ } | {
33
+ name: string;
34
+ status: string;
35
+ description: string;
36
+ critical: boolean;
37
+ error: any;
38
+ })[];
39
+ }>;
40
+ dropSchema(): Promise<void>;
41
+ createTask(params: any): Promise<Record<string, any>>;
42
+ getTaskByUuid(uuid: any, userId: any): Promise<Record<string, any>>;
43
+ getTaskByUuidOnly(uuid: any): Promise<Record<string, any>>;
44
+ /**
45
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
46
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
47
+ *
48
+ * @param {string} uuid
49
+ * @returns {Promise<{ status: string }|null>}
50
+ */
51
+ getTaskStatusByUuidOnly(uuid: string): Promise<{
52
+ status: string;
53
+ } | null>;
54
+ updateTaskById(taskId: any, updates: any): Promise<Record<string, any>>;
55
+ updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<Record<string, any>>;
56
+ deleteTaskById(taskId: any): Promise<boolean>;
57
+ deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
58
+ getPendingTasks(limit?: number): Promise<Record<string, any>[]>;
59
+ claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
60
+ renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
61
+ listTasks(userId: any, opts?: {}): Promise<{
62
+ tasks: Record<string, any>[];
63
+ total: number;
64
+ }>;
65
+ cleanupOldTasks(days?: number): Promise<number>;
66
+ getTaskStatus(uuid: any, userId: any): Promise<any>;
67
+ getTaskStatusInfo(uuid: any, userId: any): Promise<{
68
+ status: string;
69
+ last_error: string | null;
70
+ }>;
71
+ getPushSubscription(userId: any): Promise<{
72
+ subscription: string;
73
+ updated_at: number;
74
+ }>;
75
+ upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
76
+ deletePushSubscription(userId: any): Promise<boolean>;
77
+ upsertLlmCredentials(userId: any, entries: any): Promise<number>;
78
+ getLlmCredentials(userId: any, credIds: any): Promise<{
79
+ cred_id: string;
80
+ encrypted_value: string;
81
+ updated_at: string;
82
+ }[]>;
83
+ listLlmCredentials(userId: any): Promise<{
84
+ cred_id: string;
85
+ updated_at: string;
86
+ }[]>;
87
+ deleteLlmCredentials(userId: any, credIds?: any): Promise<number>;
88
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * pg / neon 共用的 Postgres 查询实现。
3
+ *
4
+ * 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
5
+ * 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
6
+ * 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
7
+ * 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
8
+ *
9
+ * D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
10
+ *
11
+ * @typedef {(text: string, params?: any[]) => Promise<any[]>} PgQuery
12
+ */
13
+ /**
14
+ * 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
15
+ * 本次投递期间别的 tick 领不走它。
16
+ *
17
+ * 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
18
+ * 任务列表要读它、循环任务推进下一次也要拿它当基准。
19
+ *
20
+ * 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
21
+ * 过。WHERE 里的两个条件各管一件事:
22
+ * - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
23
+ * 也不会把行焊死,租约到期后自然可以被接手。
24
+ * - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
25
+ * 这一跳就不该再按旧时刻发。
26
+ *
27
+ * 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
28
+ * 要改表。
29
+ *
30
+ * 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
31
+ * 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
32
+ *
33
+ * 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
34
+ * 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
35
+ * 里完成——但那只对「抢同一行」成立;READ COMMITTED 下两个并发 tick 各领
36
+ * 同组的**不同**行时,各自的 NOT EXISTS 子查询都看不到对方尚未提交的租约,
37
+ * 也没有行锁冲突逼它重查(写偏斜)。所以占位成功后再回头查一次:真撞上了
38
+ * 就把自己刚写的租约放掉、这一跳不跑——两边都让也没事,行保持 pending,
39
+ * 下一跳重试。分组门只看租约,不看 `retry_after`:等着重试的任务其实闲着,
40
+ * 不该把同分组的其他任务一起堵住。
41
+ *
42
+ * @param {PgQuery} query
43
+ * @param {number} taskId
44
+ * @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
45
+ * @param {string|Date} leaseUntil - 租期末尾
46
+ * @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
47
+ * @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
48
+ * 任务正在跑、排期被改过、或行已不是 pending
49
+ */
50
+ export function claimTask(query: PgQuery, taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
51
+ /**
52
+ * 投递期间的租约续期(run-tick 的心跳用)。只在行仍是 pending 且确实持有
53
+ * 租约(lease_until 非空)时生效——收尾把租约放掉之后,迟到的心跳不会把
54
+ * 租约复活。
55
+ *
56
+ * @param {PgQuery} query
57
+ * @param {number} taskId
58
+ * @param {string|Date} leaseUntil - 新的租期末尾
59
+ * @returns {Promise<boolean>} true = 续上了
60
+ */
61
+ export function renewTaskLease(query: PgQuery, taskId: number, leaseUntil: string | Date): Promise<boolean>;
62
+ /**
63
+ * 状态 + 失败摘要(GET /message 用它把「为什么失败」透给已失败的行)。
64
+ *
65
+ * @param {PgQuery} query
66
+ * @param {string} uuid
67
+ * @param {string} userId
68
+ * @returns {Promise<{ status: string, last_error: string|null }|null>}
69
+ */
70
+ export function getTaskStatusInfo(query: PgQuery, uuid: string, userId: string): Promise<{
71
+ status: string;
72
+ last_error: string | null;
73
+ } | null>;
74
+ /**
75
+ * 这个用户当前登记的推送订阅(密文原样返回,解密在上层)。
76
+ *
77
+ * @param {PgQuery} query
78
+ * @param {string} userId
79
+ * @returns {Promise<{ subscription: string, updated_at: number }|null>}
80
+ */
81
+ export function getPushSubscription(query: PgQuery, userId: string): Promise<{
82
+ subscription: string;
83
+ updated_at: number;
84
+ } | null>;
85
+ /**
86
+ * 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
87
+ * 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
88
+ *
89
+ * @param {PgQuery} query
90
+ * @param {string} userId
91
+ * @param {string} encryptedSubscription
92
+ * @param {number} updatedAt - epoch 毫秒
93
+ * @returns {Promise<boolean>}
94
+ */
95
+ export function upsertPushSubscription(query: PgQuery, userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
96
+ /**
97
+ * 删掉这个用户的订阅(设置页「停止接收推送」)。
98
+ *
99
+ * @param {PgQuery} query
100
+ * @param {string} userId
101
+ * @returns {Promise<boolean>} true = 确实删掉了一行
102
+ */
103
+ export function deletePushSubscription(query: PgQuery, userId: string): Promise<boolean>;
104
+ /**
105
+ * 批量 upsert 这个用户的凭据(value 已是密文,加密在上层)。已存在的行覆盖
106
+ * encrypted_value 并刷 updated_at,created_at 保留首次写入的时刻。
107
+ *
108
+ * @param {PgQuery} query
109
+ * @param {string} userId
110
+ * @param {Array<{ credId: string, encryptedValue: string }>} entries
111
+ * @param {string} now - ISO8601 时间戳(与 SQLite 侧同口径的 TEXT)
112
+ * @returns {Promise<number>} 实际写入/覆盖的行数
113
+ */
114
+ export function upsertLlmCredentials(query: PgQuery, userId: string, entries: Array<{
115
+ credId: string;
116
+ encryptedValue: string;
117
+ }>, now: string): Promise<number>;
118
+ /**
119
+ * 按 cred_id 批量读这个用户的凭据行(密文原样返回,解密在上层)。
120
+ *
121
+ * @param {PgQuery} query
122
+ * @param {string} userId
123
+ * @param {string[]} credIds
124
+ * @returns {Promise<Array<{ cred_id: string, encrypted_value: string, updated_at: string }>>}
125
+ */
126
+ export function getLlmCredentials(query: PgQuery, userId: string, credIds: string[]): Promise<Array<{
127
+ cred_id: string;
128
+ encrypted_value: string;
129
+ updated_at: string;
130
+ }>>;
131
+ /**
132
+ * 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at)。
133
+ *
134
+ * @param {PgQuery} query
135
+ * @param {string} userId
136
+ * @returns {Promise<Array<{ cred_id: string, updated_at: string }>>}
137
+ */
138
+ export function listLlmCredentials(query: PgQuery, userId: string): Promise<Array<{
139
+ cred_id: string;
140
+ updated_at: string;
141
+ }>>;
142
+ /**
143
+ * 删凭据。`credIds` 传数组删指定那几行;传 null 删这个用户的全部。
144
+ *
145
+ * @param {PgQuery} query
146
+ * @param {string} userId
147
+ * @param {string[]|null} credIds
148
+ * @returns {Promise<number>} 删掉的行数
149
+ */
150
+ export function deleteLlmCredentials(query: PgQuery, userId: string, credIds?: string[] | null): Promise<number>;
151
+ /**
152
+ * pg / neon 共用的 Postgres 查询实现。
153
+ *
154
+ * 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
155
+ * 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
156
+ * 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
157
+ * 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
158
+ *
159
+ * D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
160
+ */
161
+ export type PgQuery = (text: string, params?: any[]) => Promise<any[]>;
@@ -0,0 +1,84 @@
1
+ export class PgAdapter {
2
+ /** @param {string} connectionString */
3
+ constructor(connectionString: string);
4
+ /** @private */
5
+ private _connectionString;
6
+ /** @private */
7
+ private _pool;
8
+ /** @private */
9
+ private _getPool;
10
+ /** @private */
11
+ private _query;
12
+ initSchema(): Promise<{
13
+ columnsCreated: number;
14
+ indexesCreated: number;
15
+ indexesFailed: number;
16
+ columns: {
17
+ table: string;
18
+ name: any;
19
+ type: any;
20
+ nullable: boolean;
21
+ }[];
22
+ indexes: ({
23
+ name: string;
24
+ status: string;
25
+ description: string;
26
+ critical: boolean;
27
+ error?: undefined;
28
+ } | {
29
+ name: string;
30
+ status: string;
31
+ description: string;
32
+ critical: boolean;
33
+ error: any;
34
+ })[];
35
+ }>;
36
+ dropSchema(): Promise<void>;
37
+ createTask(params: any): Promise<any[]>;
38
+ getTaskByUuid(uuid: any, userId: any): Promise<any[]>;
39
+ getTaskByUuidOnly(uuid: any): Promise<any[]>;
40
+ /**
41
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
42
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
43
+ *
44
+ * @param {string} uuid
45
+ * @returns {Promise<{ status: string }|null>}
46
+ */
47
+ getTaskStatusByUuidOnly(uuid: string): Promise<{
48
+ status: string;
49
+ } | null>;
50
+ updateTaskById(taskId: any, updates: any): Promise<any[]>;
51
+ updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<any[]>;
52
+ deleteTaskById(taskId: any): Promise<boolean>;
53
+ deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
54
+ getPendingTasks(limit?: number): Promise<any[][]>;
55
+ claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
56
+ renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
57
+ listTasks(userId: any, opts?: {}): Promise<{
58
+ tasks: any[][];
59
+ total: number;
60
+ }>;
61
+ cleanupOldTasks(days?: number): Promise<number>;
62
+ getTaskStatus(uuid: any, userId: any): Promise<any>;
63
+ getTaskStatusInfo(uuid: any, userId: any): Promise<{
64
+ status: string;
65
+ last_error: string | null;
66
+ }>;
67
+ getPushSubscription(userId: any): Promise<{
68
+ subscription: string;
69
+ updated_at: number;
70
+ }>;
71
+ upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
72
+ deletePushSubscription(userId: any): Promise<boolean>;
73
+ upsertLlmCredentials(userId: any, entries: any): Promise<number>;
74
+ getLlmCredentials(userId: any, credIds: any): Promise<{
75
+ cred_id: string;
76
+ encrypted_value: string;
77
+ updated_at: string;
78
+ }[]>;
79
+ listLlmCredentials(userId: any): Promise<{
80
+ cred_id: string;
81
+ updated_at: string;
82
+ }[]>;
83
+ deleteLlmCredentials(userId: any, credIds?: any): Promise<number>;
84
+ }