@rei-standard/amsg-server 2.6.0-next.20 → 2.6.0-next.22
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.
- package/README.md +224 -11
- package/dist/adapters/d1.d.ts +28 -0
- package/dist/adapters/interface.d.ts +46 -5
- package/dist/adapters/neon.d.ts +7 -1
- package/dist/adapters/schema.d.ts +16 -0
- package/dist/{chunk-HTGYGGUH.mjs → chunk-5J73MSQ5.mjs} +1150 -283
- package/dist/{chunk-V6NZQ22S.cjs → chunk-7NMQFTDJ.cjs} +5 -1
- package/dist/{chunk-5FXVSC5O.mjs → chunk-GN44PST5.mjs} +5 -1
- package/dist/{chunk-WOXSQBAM.cjs → chunk-ILH32T3G.cjs} +1138 -271
- package/dist/cloudflare.cjs +7 -3
- package/dist/cloudflare.d.ts +1 -0
- package/dist/cloudflare.mjs +6 -2
- package/dist/index.cjs +40 -28
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.mjs +16 -4
- package/dist/lib/client-state-store.d.ts +19 -0
- package/dist/lib/errors.d.ts +129 -7
- package/dist/lib/message-processor.d.ts +18 -1
- package/dist/lib/outbox-store.d.ts +62 -0
- package/dist/lib/push-policy.d.ts +11 -0
- package/dist/lib/push-subscription-store.d.ts +23 -0
- package/dist/lib/request.d.ts +47 -0
- package/dist/lib/result-emitter.d.ts +54 -0
- package/dist/lib/run-tick.d.ts +9 -0
- package/dist/lib/task-projection.d.ts +8 -3
- package/dist/lib/validation.d.ts +35 -0
- package/dist/{neon-U6TQFKMJ.cjs → neon-KP2CPA57.cjs} +23 -15
- package/dist/{neon-Y4KEZO5C.mjs → neon-ZIMHALYI.mjs} +14 -6
- package/dist/{pg-GWJZBWAM.mjs → pg-QGC2XHUT.mjs} +13 -5
- package/dist/{pg-B324Q2R4.cjs → pg-RZPQGXR3.cjs} +22 -14
- package/dist/single-user.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,7 +15,46 @@ npm install @neondatabase/serverless
|
|
|
15
15
|
npm install pg
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## 两条部署线
|
|
19
|
+
|
|
20
|
+
| | 单用户线(推荐) | 多租户线 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 入口 | `createSingleUserCloudflareWorker` / `createSingleUserServer` | `createReiServer` |
|
|
23
|
+
| 数据库 | D1 | pg / neon |
|
|
24
|
+
| 一个部署 | 服务一个用户 | 服务多个租户(Blob 存租户配置 + token 鉴权) |
|
|
25
|
+
| 服务端收件箱(`GET /outbox` 补拉) | ✅ | ❌ |
|
|
26
|
+
| `client_state` 云端镜像 | ✅ | ❌ |
|
|
27
|
+
| 推送订阅、LLM 凭据、定时 / 周期任务 | ✅ | ✅ |
|
|
28
|
+
|
|
29
|
+
**新接入走单用户线。** 收件箱是这套 SDK 的到达保证:每条 payload 发出去之前先落一行 `message_outbox`,客户端上线 `GET /outbox?since=` 一条不少地补得回来。有了它,到了客户端不会弹通知的 payload(思考过程、工具请求、错误)就不必占用推送通道——这是[哪些 payload 会发推送](#哪些-payload-会发推送)那一节的前提,也是不去赌 iOS 订阅宽限期的唯一办法。
|
|
30
|
+
|
|
31
|
+
多租户线继续维护,任务、推送、凭据这些照常能用——但它的 pg / neon 适配器还没有 `message_outbox` 和 `client_state`,收件箱相关的端点返回 501,不会弹通知的 payload 也只能照旧推送。
|
|
32
|
+
|
|
33
|
+
> **TODO**:给 pg / neon 适配器补 `message_outbox` 那组方法(D1 的实现见 `src/server/adapters/d1.js`,建表 SQL 见 `adapters/schema.sqlite.js`)。补上之前,多租户线省不掉那些不会弹通知的推送。
|
|
34
|
+
|
|
35
|
+
## 快速使用(单用户线)
|
|
36
|
+
|
|
37
|
+
一个 Cloudflare Worker 装完两个入口:`fetch` 收客户端请求,`scheduled` 跑 cron 投递。
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createSingleUserCloudflareWorker, createD1Adapter } from '@rei-standard/amsg-server';
|
|
41
|
+
|
|
42
|
+
export default createSingleUserCloudflareWorker((env) => ({
|
|
43
|
+
db: createD1Adapter(env.DB),
|
|
44
|
+
masterKey: env.MASTER_KEY,
|
|
45
|
+
vapid: {
|
|
46
|
+
email: env.VAPID_EMAIL,
|
|
47
|
+
publicKey: env.VAPID_PUBLIC_KEY,
|
|
48
|
+
privateKey: env.VAPID_PRIVATE_KEY,
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
建表、绑定、cron 配置和 fire-time hook 的完整走法见 [`examples/cloudflare-single-user`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md)。
|
|
54
|
+
|
|
55
|
+
客户端那侧还差一步:**应用启动时拉一次收件箱**。不会弹通知的内容(思考过程、工具请求、错误)只落收件箱、不发推送,不补拉就等于没有——做法见 [`@rei-standard/amsg-client` README 的「上线补一次收件箱」](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/client/README.md#上线补一次收件箱)。
|
|
56
|
+
|
|
57
|
+
## 快速使用(多租户线)
|
|
19
58
|
|
|
20
59
|
```js
|
|
21
60
|
import { createReiServer } from '@rei-standard/amsg-server';
|
|
@@ -54,9 +93,9 @@ const rei = await createReiServer({
|
|
|
54
93
|
|
|
55
94
|
## 关于 `messageType: 'instant'`
|
|
56
95
|
|
|
57
|
-
> **两条 instant
|
|
58
|
-
> - **本端点的 `messageType: 'instant'`**(create task → process by UUID → delete task
|
|
59
|
-
> - **[@rei-standard/amsg-instant](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md)**:纯 SSE 流 + Web Push backup
|
|
96
|
+
> **两条 instant 路径:**
|
|
97
|
+
> - **本端点的 `messageType: 'instant'`**(create task → process by UUID → delete task):任务先写进数据库再处理,投递不绑在请求连接上——客户端断开也没关系,任务行还在,能继续跑、能重试,想跑多久跑多久。落进收件箱的那份客户端上线也补得回来。有数据库就走这条。
|
|
98
|
+
> - **[@rei-standard/amsg-instant](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md)**:纯 SSE 流 + Web Push backup,不需要数据库,跑得动无状态边缘运行时。处理挂在响应连接上,客户端一断开就只剩平台给的那点宽限期把活干完(Deno Deploy 实测 ≈20-30s);它也没有服务端收件箱,push 漏了的内容补不回来。这个包现在是维护态,新接入不从它起步。
|
|
60
99
|
|
|
61
100
|
## AI 接口 `apiUrl` 约束
|
|
62
101
|
|
|
@@ -142,6 +181,63 @@ const message = body.length <= remainingBytes ? body : body.slice(0, remainingBy
|
|
|
142
181
|
|
|
143
182
|
用比 64 字符更长的 uuid(`scheduleTask` 允许传任意字符串)就自己再多留一点。
|
|
144
183
|
|
|
184
|
+
### 哪些 payload 会发推送
|
|
185
|
+
|
|
186
|
+
一条 payload 出门有两条腿:**落进 `message_outbox`**(到达的保证,客户端上线 `GET /outbox?since=` 补拉)和**发一条 Web Push**(及时性,当场叫人回来看)。收件箱那条腿每条 payload 都走,推送这条腿只留给「到了客户端会弹通知」的那些。
|
|
187
|
+
|
|
188
|
+
| payload | 落收件箱 | 发推送 |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `content` / `result` | ✅ | ✅ |
|
|
191
|
+
| `reasoning` / `tool_request` / `error` | ✅ | ❌ |
|
|
192
|
+
| 任意 kind + `notification: { show: false }` | ✅ | ❌ |
|
|
193
|
+
| 任意 kind + `notification: { show: 'always' \| 'when-hidden' }` | ✅ | ✅ |
|
|
194
|
+
|
|
195
|
+
为什么这么分:订阅是按 `userVisibleOnly: true` 建的,每条 push 都欠用户一次可见反馈。`reasoning` / `tool_request` / `error` 在 Service Worker 那边是静默送给页面的,推过去不会有任何可见反馈,却要跟浏览器赊一次账——Firefox 对这类 push 有配额、超了退掉订阅,iOS 给新订阅几天宽限期、过后一条就吊销订阅,而且掉订阅是静默发生的,服务端只看得到后续推送返回 410。而这些内容在收件箱里一个字不少,客户端上线补拉就行(完整取舍见 `@rei-standard/amsg-sw` README 的「不展示通知的代价」一节)。
|
|
196
|
+
|
|
197
|
+
**想让某一条照样弹**,给它带上 `notification: { show: 'always' }`——判定读的是与 Service Worker 同一份规则(`@rei-standard/amsg-shared` 的 `notificationIntent`),宿主说了要弹,发送端就当它值得占用推送通道。逐条控制,不需要在服务端配开关。嫌打扰就配 `tag` 折叠加 `silent: true`,而不是不弹。`show: 'when-hidden'` 也照推(它到底弹不弹只有 Service Worker 当场知道),但那是给老部署留的兼容档,新代码在「一定弹」和「压根不推」里挑一个。
|
|
198
|
+
|
|
199
|
+
跳过推送的那条不标 `delivered_at`,行留在收件箱里等客户端补收;这正是能跳过的前提。
|
|
200
|
+
|
|
201
|
+
**前提是这个部署有收件箱。** 内置适配器里只有 D1 实现了 `message_outbox`(见[两条部署线](#两条部署线))。落不进收件箱时——适配器没实现,或者这一批落行失败——推送是这条内容唯一的腿,所有 payload 照旧全推,包括不会弹通知的那些。宁可跟浏览器违约一次,也不能让内容凭空消失。
|
|
202
|
+
|
|
203
|
+
agentic 链路的 `onAfterSend` / `onFireSettled` 回执里,`sentCount` 是这批走完了几段,`pushedCount` 是其中真的占用了推送通道的有几条。`sentCount === total` 照旧表示整批都到位了。
|
|
204
|
+
|
|
205
|
+
### 装不下就切片:`multipart`
|
|
206
|
+
|
|
207
|
+
思考过程(reasoning)常常一条 push 装不下。真要把它推出去时(宿主给它配了 `notification.show`,或者这个部署没有收件箱),服务端会把它切成分片逐条发,Service Worker 收齐后还原成原样再走正常派发。切多大一片、最多切几片、收齐前能等多久,都是接收端说了算——所以给 `installReiSW` 传了什么,就把同一份原样传给服务端:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
const multipart = { maxChunkBytes: 1800, maxChunks: 128, maxTotalBytes: 256_000, ttlMs: 60_000 };
|
|
211
|
+
|
|
212
|
+
installReiSW({ multipart }); // 页面
|
|
213
|
+
createReiServer({ tenant: { … }, multipart }); // 多租户
|
|
214
|
+
createSingleUserServer({ db, masterKey, multipart }); // 单用户
|
|
215
|
+
createSingleUserCloudflareWorker((env) => ({ …, multipart })); // CF Worker(cron 与 runTask 都认)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| 键 | 默认值 | 说明 |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `maxChunkBytes` | `1800` | 每片装多少字节原文 |
|
|
221
|
+
| `maxChunks` | `128` | 一条消息最多切几片,超了就不发 |
|
|
222
|
+
| `maxTotalBytes` | `256000` | 整条消息的原文上限,超了就不发 |
|
|
223
|
+
| `ttlMs` | `60000` | 接收端收到第一片之后,等齐剩下分片能等多久 |
|
|
224
|
+
|
|
225
|
+
不配 = 两边都用默认值。发送节奏也按 `ttlMs` 排:片数多时自动收紧每片之间的间隔,保证整批分片在这个窗口内发完;收紧到下限还装不下就一片都不发。
|
|
226
|
+
|
|
227
|
+
两边对不上的下场值得记一下:页面把 `maxChunks` 收窄到 32、服务端还按 128 切的话,分片到了接收端会被逐片拒收;节奏排得比窗口长的话,迟到的分片会被当过期丢掉。两种都是「页面上这条思考过程直接没有」,而服务端那边每一片都发成功、看不出任何异常。
|
|
228
|
+
|
|
229
|
+
### 思考过程没送到时的可见性
|
|
230
|
+
|
|
231
|
+
> 这一节说的是**推送真的发出去过、但发挂了**的情况。有收件箱的部署上思考过程只落行、不推送(见[哪些 payload 会发推送](#哪些-payload-会发推送)),那条路上内容没丢,也就不会有下面这些信号。
|
|
232
|
+
|
|
233
|
+
思考过程是正文之外的附赠内容:它没发出去不影响正文,任务照样算成功。这件事有三处看得见——
|
|
234
|
+
|
|
235
|
+
- 定时任务的 tick 汇总多一个 `details.reasoningSkippedTasks`:`[{ taskId, reason }]`。这些任务同时计在 `successCount` 里。
|
|
236
|
+
- instant 消息(`POST /schedule-message`)的成功响应带 `reasoningError`(字符串,只在思考过程没送到时出现)。
|
|
237
|
+
- 服务端日志各打一行:一行说原因,一行说是哪条任务。
|
|
238
|
+
|
|
239
|
+
刻意不写进 `last_error`:那一列说的是「上一次没发出去的原因」,一条正文已经送达的消息挂着它,客户端会当成这次投递失败了。
|
|
240
|
+
|
|
145
241
|
## 推送订阅(用户级)
|
|
146
242
|
|
|
147
243
|
推送订阅一个用户存一份,任务行不携带它,到点投递时现读。用户清了站点数据、重装了 PWA、或者推送服务轮换了 endpoint 之后,覆盖这一份就够了——所有已排的任务,包括角色在 fire 里给自己排的、客户端根本不知道存在的那些,下次触发读到的都是新订阅。
|
|
@@ -226,7 +322,7 @@ ctx / metadata / push 上)。
|
|
|
226
322
|
|
|
227
323
|
## 更新任务时能改哪些字段
|
|
228
324
|
|
|
229
|
-
`PUT /update-message` 的可写字段:`contactName` / `avatarUrl` / `userMessage` / `completePrompt` / `messages` / `nextSendAt` / `recurrenceType` / `tzId` / `metadata` / `maxTokens` / `temperature` / `splitPattern`、凭据三件套 `apiUrl` / `apiKey` / `primaryModel`,以及凭据引用 `credRefs`。
|
|
325
|
+
`PUT /update-message` 的可写字段:`contactName` / `avatarUrl` / `userMessage` / `completePrompt` / `messages` / `nextSendAt` / `recurrenceType` / `tzId` / `metadata` / `messageSubtype` / `maxTokens` / `temperature` / `splitPattern` / `llmExtraBody`、凭据三件套 `apiUrl` / `apiKey` / `primaryModel`,以及凭据引用 `credRefs`。
|
|
230
326
|
|
|
231
327
|
- `contactName` 必须是非空字符串(口径与排程时一致),空串 / `null` / 非字符串一律 `400`。用户给角色改了名之后,之前排的任务推送出来的通知标题(「来自 <contactName>」)靠它跟着改。
|
|
232
328
|
- `metadata` 是整体替换,不深合并——只改一个子字段的读-改-写流程见上一节。
|
|
@@ -234,6 +330,17 @@ ctx / metadata / push 上)。
|
|
|
234
330
|
- 凭据三件套传 `null` 同样只是忽略:清掉任何一个,任务到点就发不出去。
|
|
235
331
|
- `credRefs` 是整体替换(语义同 `metadata`),同样做存在性检查;与内联三件套在同一个请求里混着传返回 `400`。给存量内联任务补 `credRefs` 时不动已存的三件套——那份留作 fire 时表行缺失的兜底。
|
|
236
332
|
- `pushSubscription` 不收(`400 PUSH_SUBSCRIPTION_NOT_ACCEPTED`),它是用户级的一份,走 `PUT /push-subscription`。
|
|
333
|
+
- `userMessage` 给了就必须是字符串(口径与排程时一致):它到点要过正则切分,别的类型收进来只会在投递时炸。
|
|
334
|
+
- `messageSubtype` / `llmExtraBody` 显式传 `null` 是「改回默认」(分别是投递时的 `'chat'` 和「不透传额外参数」),不会被当成「不改」吞掉。
|
|
335
|
+
- 响应里的 `updatedFields` 只列真正落进这次更新的字段。请求里带了但没被应用的键——这个接口不接受的、拼错的、传了 `null` 走「不改」语义的——不会出现在里面。
|
|
336
|
+
|
|
337
|
+
## 取消 / 顶替时,没发出去的那几段也会撤掉
|
|
338
|
+
|
|
339
|
+
适配器实现了 outbox 那组方法(内置 D1 有)时,每条 push 在发出去之前会先落一行 `message_outbox`,客户端离线或推送服务抽风时靠 `GET /outbox` 补收。这就带来一个收尾问题:一条任务投递到一半失败过的话,没发出去的那几段还留在 outbox 里等补收,光删任务行它们不会跟着走。
|
|
340
|
+
|
|
341
|
+
所以 `DELETE /cancel-message` 和 `POST /schedule-message` 的 `supersedesUuid` 顶替,都会顺手把该任务名下还没发出去的行撤掉。已经推到设备上的分段不动——取消的意思是「别再发后面的」,不是「把用户已经收到的从收件箱里抹掉」,那几条留着让客户端照常 ack。
|
|
342
|
+
|
|
343
|
+
清理是 best-effort:适配器没实现 outbox、或者清理本身出错,都不影响取消 / 顶替的成功返回(任务行已经删掉了)。
|
|
237
344
|
|
|
238
345
|
## 推送自带任务身份
|
|
239
346
|
|
|
@@ -259,6 +366,7 @@ hook 在 `pushPayloads` 里自己写了这几个字段的话会被库覆盖:
|
|
|
259
366
|
| ctx 上的口子 | 干什么 |
|
|
260
367
|
|---|---|
|
|
261
368
|
| `readState(ns)` / `writeState(ns, entries)` | 读写 `client_state`,和客户端 `GET/PUT /client-state` 是同一份数据 |
|
|
369
|
+
| `emitResult(payload)` | 给客户端送一条**不是聊天内容**的结果(落收件箱 + 推送) |
|
|
262
370
|
| `scheduleTask(options)` | 给同一个用户再建一条定时任务 |
|
|
263
371
|
| `scratch` | 本次 fire 的便签对象,三个 hook 加上发送后的 `onAfterSend` 共享同一个引用,fire 结束即丢弃 |
|
|
264
372
|
|
|
@@ -278,14 +386,16 @@ hook 在 `pushPayloads` 里自己写了这几个字段的话会被库覆盖:
|
|
|
278
386
|
|
|
279
387
|
| hook | 什么时候调 | 载荷 |
|
|
280
388
|
|---|---|---|
|
|
281
|
-
| `onAfterSend` | fire 的 pushPayloads 逐段发完,或中途发挂 | `{ task, sentCount, total, error, scratch, readState, writeState }` |
|
|
282
|
-
| `onFireSettled` | 一次 fire 收尾——只要 `onBeforeFire` 被调用过,什么结局都调一次 | `{ task, status, skipReason, sentCount, total, iterations, error, scratch, readState, writeState }` |
|
|
283
|
-
| `onStaleSkip` | 任务错过触发时刻超过 60 分钟、这一次(或这几次)不再补发 | `{ reason, action, metadata, recurrenceType, occurrenceMs, skippedCount, skippedOccurrences, skippedTruncated, nextSendAt, readState, writeState }` |
|
|
389
|
+
| `onAfterSend` | fire 的 pushPayloads 逐段发完,或中途发挂 | `{ task, sentCount, pushedCount, total, error, scratch, readState, writeState, emitResult }` |
|
|
390
|
+
| `onFireSettled` | 一次 fire 收尾——只要 `onBeforeFire` 被调用过,什么结局都调一次 | `{ task, status, skipReason, sentCount, pushedCount, total, iterations, error, scratch, readState, writeState, emitResult }` |
|
|
391
|
+
| `onStaleSkip` | 任务错过触发时刻超过 60 分钟、这一次(或这几次)不再补发 | `{ reason, action, metadata, recurrenceType, occurrenceMs, skippedCount, skippedOccurrences, skippedTruncated, nextSendAt, readState, writeState, emitResult }` |
|
|
284
392
|
|
|
285
|
-
三个 hook 都自带 `readState` / `writeState
|
|
393
|
+
三个 hook 都自带 `readState` / `writeState` / `emitResult`,作用于当前用户,语义与 fire 级那套一致。`onStaleSkip` 尤其需要:服务停摆恢复后的第一跳里可能一次 fire 都没跑过,而那正是它要留痕迹的时候。
|
|
286
394
|
|
|
287
395
|
`onAfterSend` 的 `scratch` 与本次 fire 的 `onBeforeFire` / `onLLMOutput` 是同一个引用——「这次生成了哪几段正文」之类的上下文直接从这里读,不用自建按任务分格的登记表。全部成功时 `error` 为 `null`;第 k 段失败时 `sentCount = k`、`error` 带原始错误,且在错误往上抛之前调用完。
|
|
288
396
|
|
|
397
|
+
`sentCount` 与 `pushedCount` 数的是两件事:`sentCount` 是这批走完了几段(只落收件箱、没占推送通道的那些也算走完),`pushedCount` 是其中真的发了 Web Push 的有几条。判整批跑完没有看 `sentCount === total`。
|
|
398
|
+
|
|
289
399
|
`onFireSettled` 是「这次 fire 结束了」这一个信号,`status` 说明结局:
|
|
290
400
|
|
|
291
401
|
| status | 什么时候 |
|
|
@@ -337,10 +447,61 @@ const result = await ctx.scheduleTask({
|
|
|
337
447
|
| 单次 fire 的建任务条数 | 默认 **2 条**,factory 配置 `maxScheduledTasksPerFire` 可调(`0` = 不许自排) | `RangeError` | 模型自排后续本质上是条能无限延伸的链,没有上限就没人按停止键 |
|
|
338
448
|
| `uuid` 撞车 | 不当错误处理 | 返回 `{ created: false, reason: 'duplicate', uuid, task }` | fire 失败会整条重跑,宿主传一个由「任务 id + 触发时刻」推出来的确定性 uuid 就天然幂等 |
|
|
339
449
|
| `tzId` | 可用的 IANA 时区 id,或 `null` | `TypeError` | 认不出来的时区会让循环推进悄悄退回 UTC,用户设的钟点从此对不上 |
|
|
340
|
-
|
|
|
450
|
+
| 任务内容大小 | 与 `POST /schedule-message` 同一道闸门 | 抛 `RangeError`(`code: 'TASK_PAYLOAD_TOO_LARGE'`) | 往 `metadata` 里塞一坨大对象会顶穿存储的单行上限,不拦的话到落库那步才炸,报错看不出所以然 |
|
|
451
|
+
| 数据库适配器没有 `createTask` | — | 抛 `DeploymentConfigError`(`code: 'AGENTIC_SCHEDULE_UNSUPPORTED'`) | 静默成功会让宿主以为后续那条排上了,其实谁也不会触发它 |
|
|
341
452
|
|
|
342
453
|
`recurrenceType` 沿用排程接口那套 `none` / `daily` / `weekly`,别的值抛 `TypeError`。参数不合法的调用不占建任务额度;uuid 撞车占(那条任务其实已经建出来了)。
|
|
343
454
|
|
|
455
|
+
### `ctx.emitResult(payload)`
|
|
456
|
+
|
|
457
|
+
聊天正文之外的产出——整理好的一份数据、一条账目、后台生成的产物——用它送给客户端。
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
const { messageId, pushed } = await ctx.emitResult({
|
|
461
|
+
resultKind: 'fire-pack', // 必填:这类结果的名字,客户端按它分流
|
|
462
|
+
packId: 'pack_42', // 以下随便加,形状由你定
|
|
463
|
+
entries: [{ id: 1 }, { id: 2 }],
|
|
464
|
+
notification: { title: '整理好了', body: '点开看看' }, // 可选,见下
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
一条结果走两条路,缺一不可:
|
|
469
|
+
|
|
470
|
+
| 路 | 负责什么 |
|
|
471
|
+
|---|---|
|
|
472
|
+
| 落进 `message_outbox` | **到达**。客户端下次 `GET /outbox?since=` 一定拿得到——推送没送到、内容超过一条推送 4KB 的上限,都不会让它丢 |
|
|
473
|
+
| 发一条 Web Push | **及时**。跑完当场弹一下叫人回来看,而不是等客户端下次上线 |
|
|
474
|
+
|
|
475
|
+
客户端因此不必为每种结果各写一套轮询:补收机制已经在那儿了,结果跟聊天消息从同一个口子回来,靠 `messageKind === 'result'` 分开,再按 `resultKind` 分流。
|
|
476
|
+
|
|
477
|
+
**通知**默认弹(结果与聊天正文同待遇,其余 push 类型是静默送给页面)。标题正文在 `notification` 里自定义,字段与 SW 那套一致(`title` / `body` / `icon` / `tag` / …);不想弹就 `notification: { show: false }`。
|
|
478
|
+
|
|
479
|
+
> 带 `show: false` 的结果不发推送、只落收件箱(见[哪些 payload 会发推送](#哪些-payload-会发推送)):订阅按 `userVisibleOnly: true` 建,收到 push 却不弹通知,Firefox 按配额退订,iOS 在订阅的宽限期过后直接吊销(完整取舍见 `@rei-standard/amsg-sw` README 的「不展示通知的代价」一节)。结果这条本来就有收件箱兜底,客户端上线补拉拿得到。
|
|
480
|
+
|
|
481
|
+
**返回值**里 `pushed` 是「这次推送有没有真的发出去」。两种情况会是 `false`:带 `notification: { show: false }` 的结果按策略压根不推(见[哪些 payload 会发推送](#哪些-payload-会发推送)),以及推了但没送出去(订阅失效、推送服务抽风、payload 超过 4KB)。两种都不算失败,行还在收件箱里等补收,`emitResult` 也不会因此抛错。
|
|
482
|
+
|
|
483
|
+
**落行失败会抛**:收件箱是到达的保证,静默丢掉正是这个能力要修的病。适配器没有 `message_outbox`(自定义适配器)时同样抛,`code` 是 `OUTBOX_UNSUPPORTED`。
|
|
484
|
+
|
|
485
|
+
**取消**与聊天分段同待遇:结果行上带 `task_uuid`,`DELETE /message` 取消、`supersedesUuid` 顶替时,这条任务名下**还没送到**的结果一起撤掉;已经推到设备上的留着让客户端照常 ack(推出去的撤不回来)。
|
|
486
|
+
|
|
487
|
+
**重试**:`messageId` 缺省值掺了任务 id 与本次名义触发时刻,同一次触发重跑时第 n 条结果拿到的还是同一个 id,收件箱靠 `(user_id, message_id)` 唯一约束天然去重,不会补出第二条。想自己控制就在 payload 里传 `messageId`。
|
|
488
|
+
|
|
489
|
+
`GET /capabilities` 的 features 里有 `emit-result`。
|
|
490
|
+
|
|
491
|
+
### hook 契约违约算确定性失败
|
|
492
|
+
|
|
493
|
+
宿主 hook 返回了库不认的东西(`onBeforeFire` 的返回形状、`onLLMOutput` 的决策标签),或者建后续任务时 `createTask` 没把行交回来——这些错误带 `permanent: true` 和一个稳定的 `code`(`AGENTIC_BAD_BEFORE_FIRE` / `AGENTIC_BAD_DECISION` / `AGENTIC_SCHEDULE_FAILED` / `TASK_PAYLOAD_TOO_LARGE`),投递侧据此跳过退避阶梯:一次性任务直接标 `failed`,循环任务作废本次 occurrence。重试也是同一个结果,而每重试一轮都要把 `onBeforeFire` 和一整轮 LLM 重跑一遍。
|
|
494
|
+
|
|
495
|
+
分界线是「谁写错了」:契约由宿主代码定死,重掷一次还是同一个形状;而模型这一轮掷出了什么则是每轮都可能不同的。所以「tool-request 决策里没有能解析的 `toolCalls`」(`AGENTIC_EMPTY_TOOL_REQUEST`)和「轮数用尽也没等到 `finish` / `skip-push`」(`AGENTIC_LOOP_EXCEEDED`)带 `code` 但不带 `permanent`,留在退避阶梯上——隔两分钟重掷一次多半就正常收尾了,判终态的话一次性任务第一次掷歪就永久 `failed`,行离开 `pending` 之后连 `PUT /update-message` 都救不回来(回 409)。
|
|
496
|
+
|
|
497
|
+
### 部署配错了算可重试
|
|
498
|
+
|
|
499
|
+
部署缺了必要的能力——没配 `onLLMOutput` / `executeToolCalls`,或者自定义适配器没有 `createTask` / `deleteTaskByUuid` / `getTaskByUuid` / `upsertClientState`——抛的是 `DeploymentConfigError`:带同样的 `code`(`AGENTIC_CONFIG_ERROR` / `AGENTIC_SCHEDULE_UNSUPPORTED` / `AGENTIC_CANCEL_UNSUPPORTED` / `AGENTIC_RENEW_UNSUPPORTED` / `AGENTIC_STATE_WRITE_UNSUPPORTED`),但**不带** `permanent`,走的是普通的退避阶梯。
|
|
500
|
+
|
|
501
|
+
因为坏的不是这条任务,是这个部署:同一个坏部署下每条到点的任务都会撞同一个错,判终态等于把那段时间里每一条一次性任务都永久标 `failed`,配置改好重新部署也捞不回来(行已不在 `pending`,`PUT /update-message` 回 409)。留在阶梯上的话,配置一修好,下一跳就正常发出去。VAPID 配错回的 400 / 401 / 403 是同一个道理,见下面的推送失败分级。
|
|
502
|
+
|
|
503
|
+
`AGENTIC_TOTAL_TIMEOUT`(整条 fire 链超出 `totalTimeoutMs`)也走退避重试:这一轮慢不代表下一轮也慢。
|
|
504
|
+
|
|
344
505
|
`GET /capabilities` 的 features 里有 `agentic-schedule-task`,前端可以据此判断部署的 worker 认不认这条链路。
|
|
345
506
|
|
|
346
507
|
## 导出(新增)
|
|
@@ -368,6 +529,54 @@ const result = await ctx.scheduleTask({
|
|
|
368
529
|
- `send-notifications`
|
|
369
530
|
- `Authorization: Bearer <cronToken>` 或 `?token=<cronToken>`
|
|
370
531
|
|
|
532
|
+
## 请求体可以压缩(`Content-Encoding: gzip`)
|
|
533
|
+
|
|
534
|
+
带 `Content-Encoding: gzip` 的请求体在读出来的那一步自动解压,单用户 Worker 上每个带 body 的端点都认(`POST /schedule-message`、`PUT /client-state`、`PUT /llm-credentials`……)。客户端把大 body 压了再传能省下几倍传输量,两边都不用改端点。
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
await fetch(`${baseUrl}/client-state`, {
|
|
538
|
+
method: 'PUT',
|
|
539
|
+
headers: { ...encryptionHeaders, 'Content-Encoding': 'gzip' },
|
|
540
|
+
body: await gzip(JSON.stringify(encryptedEnvelope)),
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
几条边界:
|
|
545
|
+
|
|
546
|
+
| 情况 | 结果 |
|
|
547
|
+
|---|---|
|
|
548
|
+
| 没有这个头 | 原样读,行为与以前一字不差 |
|
|
549
|
+
| 说是 gzip、字节却是明文 | 按明文处理。有些边缘网关会替你解开请求体却留着这个头,照着头再解一次只会解出乱码 |
|
|
550
|
+
| `br` / `deflate` 之类 | `415 UNSUPPORTED_CONTENT_ENCODING`,不猜着解 |
|
|
551
|
+
| 解压后超过上限 | `413 REQUEST_BODY_TOO_LARGE`。默认 32MB,config 的 `maxRequestBodyBytes` 可调 |
|
|
552
|
+
| 声明了 gzip 但数据是坏的 | `400 INVALID_CONTENT_ENCODING` |
|
|
553
|
+
|
|
554
|
+
上限只管压缩这条路:几百 KB 的压缩数据能展开成几个 GB,不设上限等于把内存交给调用方决定。不压缩的请求体不受它约束。
|
|
555
|
+
|
|
556
|
+
自己包路由的宿主用 `readRequestBody(request, { maxBytes })` 代替 `await request.text()` 就能得到同样的行为,`GET /capabilities` 的 features 里有 `gzip-request-body`。
|
|
557
|
+
|
|
558
|
+
## `client_state` 的过期清理(`clientStateTtl`)
|
|
559
|
+
|
|
560
|
+
`client_state` 默认不过期:写进去的是宿主的数据,库不替它决定什么时候该没。
|
|
561
|
+
|
|
562
|
+
但「大内容旁路」那类用法写的是一次性内容——一条 push 塞不下的正文先写进状态、push 里只带一个引用键,客户端取走之后没人再回来删它。给这类命名空间配上天数,cron 每跳顺手清一次:
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
clientStateTtl: {
|
|
566
|
+
fire_pack: 7, // fire_pack 下超过 7 天没更新的条目自动清掉
|
|
567
|
+
scratch_pad: 1,
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
- **逐个命名空间开**,没写进配置的一个都不动。
|
|
572
|
+
- 判据是行本来就有的 `updated_at` 列,不加列——升级后老库不用改表结构,也就没有「表没跟上、cron 静默挂在缺的那一列上」这一说。
|
|
573
|
+
- 大值分块存储的切片行跟着根行一起走,不留读不出来的垃圾行。
|
|
574
|
+
- 天数不是正数的条目会被跳过并告警一次;清理本身失败只记日志,不影响这一跳的投递。
|
|
575
|
+
|
|
576
|
+
一个坑说在前头:`PUT /client-state` 和 `writeState()` 的条件写护栏(entry 上的 `version`)落的就是 `updated_at` 这一列。护栏值传的是自增计数器之类的小整数时,这行的 `updated_at` 看起来就像 1970 年,第一次清理就会被扫走。要给某个命名空间配 TTL,就让它的写入方把 `version` 传成毫秒时间戳。
|
|
577
|
+
|
|
578
|
+
`GET /capabilities` 的 features 里有 `client-state-ttl`。
|
|
579
|
+
|
|
371
580
|
## 循环任务的时区(`tzId`)
|
|
372
581
|
|
|
373
582
|
`daily` / `weekly` 任务可以带一个 IANA 时区 id:
|
|
@@ -520,11 +729,13 @@ const result = await runTask(ctx, uuid);
|
|
|
520
729
|
|---|---|
|
|
521
730
|
| `stage` | 在哪一段炸的:`config` = 构建配置时(少了 binding、环境变量丢了),`request` = 路由或处理器抛错 |
|
|
522
731
|
| `name` | 错误类型(`error.name`,认不出来时是 `Error`) |
|
|
523
|
-
| `message` | 错误消息,长得像凭据的串已遮掉、超长截断到 500
|
|
732
|
+
| `message` | 错误消息,长得像凭据的串已遮掉、超长截断到 500 字符。`stage: 'config'` 的响应回给跨域调用方时没有这个字段,见下 |
|
|
524
733
|
| `code` | 错误自带的 `code` 字符串,有才带 |
|
|
525
734
|
|
|
526
735
|
只带错误类型和消息文本:密钥、用户数据、任务正文都不在 `error.message` 上,也不往这里放。
|
|
527
736
|
|
|
737
|
+
`stage: 'config'` 那条路多一层收敛:配置都没建起来时这个部署允许哪些 origin 无从得知,响应头只能回显来访 Origin,于是任意第三方页面一个 `fetch` 就能读到这条响应。而构建期异常的原文往往就是部署信息本身(`env.DB is undefined` 报的是 binding 名)。所以跨域读到的那份 `cause` 只有 `stage` / `name` / `code`,`message` 不出去;同源请求和不带 `Origin` 的调用(`curl`、服务端之间调用)照旧拿全文,`wrangler tail` 里也一直有。配置一修好,响应立刻回到部署自己那套 CORS,`stage: 'request'` 的 500 不受这层影响。
|
|
738
|
+
|
|
528
739
|
cron 那条路上没有调用方能读到响应,所以另开两个出口:
|
|
529
740
|
|
|
530
741
|
```js
|
|
@@ -551,6 +762,7 @@ export default createSingleUserCloudflareWorker(buildConfig, {
|
|
|
551
762
|
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION` — 表结构自查与补齐
|
|
552
763
|
- `summarizeErrorCause` — 把异常压成响应体里 `error.cause` 那个形状(自己包一层路由、想回同样形状时用同一份)
|
|
553
764
|
- `NonRetryableError` / `isNonRetryableError` — hook 侧标注「重试也好不了」的失败
|
|
765
|
+
- `readRequestBody` / `DEFAULT_MAX_REQUEST_BODY_BYTES` — 请求正文的读取口(`Content-Encoding: gzip` 在这一步还原),自己包路由时代替 `await request.text()`
|
|
554
766
|
- `createWebCryptoWebPush` — 纯 Web Crypto 的 Web Push 发送器(不依赖 `web-push` 包)
|
|
555
767
|
- `createTenantToken` / `verifyTenantToken`
|
|
556
768
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
@@ -565,6 +777,7 @@ export default createSingleUserCloudflareWorker(buildConfig, {
|
|
|
565
777
|
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION`
|
|
566
778
|
- `summarizeErrorCause` / `NonRetryableError` / `isNonRetryableError`
|
|
567
779
|
- `createWebCryptoWebPush` / `measurePushPayload` / `MAX_PUSH_PAYLOAD_BYTES` / `WEB_PUSH_MAX_BODY_BYTES` / `WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES`
|
|
780
|
+
- `readRequestBody` / `DEFAULT_MAX_REQUEST_BODY_BYTES`
|
|
568
781
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
569
782
|
|
|
570
783
|
## 运行环境与要求
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -235,6 +235,23 @@ export class D1Adapter {
|
|
|
235
235
|
value: string;
|
|
236
236
|
updated_at: number;
|
|
237
237
|
}>>;
|
|
238
|
+
/**
|
|
239
|
+
* 例行清理:把指定命名空间下太久没更新的条目删掉(run-tick 每跳顺手调,
|
|
240
|
+
* 宿主配了 `clientStateTtl` 才会调)。
|
|
241
|
+
*
|
|
242
|
+
* 不限用户——「这个命名空间只留最近 N 天」是命名空间级的约定,单用户部署
|
|
243
|
+
* 下也就是这一个用户的行。指令由 lib/client-state-store.js 的
|
|
244
|
+
* `planClientStateCleanup` 算好(含大值切片所在的保留命名空间),这里只负
|
|
245
|
+
* 责照着删。
|
|
246
|
+
*
|
|
247
|
+
* @param {Array<{ namespace: string, updatedBefore: number }>} targets
|
|
248
|
+
* `updatedBefore` 是 epoch 毫秒,与 `updated_at` 列同一把尺子。
|
|
249
|
+
* @returns {Promise<number>} 删掉的行数
|
|
250
|
+
*/
|
|
251
|
+
cleanupClientState(targets?: Array<{
|
|
252
|
+
namespace: string;
|
|
253
|
+
updatedBefore: number;
|
|
254
|
+
}>): Promise<number>;
|
|
238
255
|
/**
|
|
239
256
|
* Wipe every entry of this user.
|
|
240
257
|
*
|
|
@@ -344,6 +361,17 @@ export class D1Adapter {
|
|
|
344
361
|
* @returns {Promise<number>}
|
|
345
362
|
*/
|
|
346
363
|
markOutboxDelivered(userId: string, messageIds: string[], deliveredAt: number): Promise<number>;
|
|
364
|
+
/**
|
|
365
|
+
* 把这一批还没发出去的行删掉(任务投递到一半被取消 / 顶替时用)。
|
|
366
|
+
*
|
|
367
|
+
* 只删 delivered_at 仍为 NULL 的行:已经推给设备的那几条撤不回来,行留着让
|
|
368
|
+
* 客户端照常 ack。已 ack 的行更不动。
|
|
369
|
+
*
|
|
370
|
+
* @param {string} userId
|
|
371
|
+
* @param {string[]} messageIds
|
|
372
|
+
* @returns {Promise<number>} 删掉的行数
|
|
373
|
+
*/
|
|
374
|
+
discardOutboxMessages(userId: string, messageIds: string[]): Promise<number>;
|
|
347
375
|
/**
|
|
348
376
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
349
377
|
*
|
|
@@ -7,8 +7,20 @@ export type TaskRow = {
|
|
|
7
7
|
next_send_at: string;
|
|
8
8
|
status: string;
|
|
9
9
|
retry_count: number;
|
|
10
|
-
created_at
|
|
11
|
-
updated_at
|
|
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;
|
|
12
24
|
};
|
|
13
25
|
export type InsertTaskParams = {
|
|
14
26
|
user_id: string;
|
|
@@ -52,6 +64,9 @@ export type DbAdapter = {
|
|
|
52
64
|
getTaskByUuid: (uuid: string, userId: string) => Promise<TaskRow | null>;
|
|
53
65
|
/**
|
|
54
66
|
* Fetch a single pending task by uuid only (used by instant processing).
|
|
67
|
+
* 返回的行要和 `getPendingTasks` 是同一套列(`TASK_DELIVERY_COLUMNS`):
|
|
68
|
+
* `runTask` 拿这一行走同一条投递链,少了 `retry_after`,退避守卫读到的永远
|
|
69
|
+
* 是 undefined,还在等重试的任务会被当场再跑一遍。
|
|
55
70
|
*/
|
|
56
71
|
getTaskByUuidOnly: (uuid: string) => Promise<TaskRow | null>;
|
|
57
72
|
/**
|
|
@@ -158,6 +173,16 @@ export type DbAdapter = {
|
|
|
158
173
|
* (optional; single-user/D1 only) Delete every entry of this user; returns rows deleted.
|
|
159
174
|
*/
|
|
160
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>;
|
|
161
186
|
/**
|
|
162
187
|
* 这个用户当前登记的 Web Push 订阅(`subscription` 是密文,解密在上层)。没有登记过 → null。
|
|
163
188
|
* 一个用户一份:任务行不携带订阅,到点投递时读这里。
|
|
@@ -216,6 +241,12 @@ export type DbAdapter = {
|
|
|
216
241
|
* (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
|
|
217
242
|
* pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
|
|
218
243
|
* 它复活。不实现 → 心跳自动关闭,退回一次性长租约(claimLeaseMs)。
|
|
244
|
+
*
|
|
245
|
+
* 返回值现在还是「这条任务是不是被取消/顶替了」的信号:明确返回 `false`
|
|
246
|
+
* 会中止这次投递剩下的推送(行已经不在了,再发就是「取消接口回了成功,
|
|
247
|
+
* 消息照样送达」)。所以匹配不到行时必须返回 `false`,不能返回 undefined
|
|
248
|
+
* ——什么都不返回等于关掉这条信号,取消撞上投递时又会照发。抛错不算行没
|
|
249
|
+
* 了,只当作这次没续上,下个心跳再试。
|
|
219
250
|
*/
|
|
220
251
|
renewTaskLease?: (taskId: number, leaseUntil: string | Date) => Promise<boolean>;
|
|
221
252
|
/**
|
|
@@ -244,7 +275,16 @@ export type DbAdapter = {
|
|
|
244
275
|
*/
|
|
245
276
|
markOutboxDelivered?: (userId: string, messageIds: string[], deliveredAt: number) => Promise<number>;
|
|
246
277
|
/**
|
|
247
|
-
* (可选;单用户/D1
|
|
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 清理会静默跳过。
|
|
248
288
|
*/
|
|
249
289
|
listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
|
|
250
290
|
/**
|
|
@@ -254,9 +294,10 @@ export type DbAdapter = {
|
|
|
254
294
|
/**
|
|
255
295
|
* (可选;单用户/D1)outbox 例行清理(runScheduledTick 每跳顺手调)。
|
|
256
296
|
*
|
|
257
|
-
* outbox
|
|
297
|
+
* outbox 这几个方法要么都实现、要么都不实现:缺写入侧的(append / mark),
|
|
258
298
|
* 发送链路静默跳过落行;缺读取侧的(list / ack),`GET /outbox` 与
|
|
259
|
-
* `POST /outbox/ack` 返回 501
|
|
299
|
+
* `POST /outbox/ack` 返回 501;缺 discard(或取消那条路上缺 list),取消只挡
|
|
300
|
+
* 住 Web Push。内置只有 D1 实现(与 client_state 同待遇)。
|
|
260
301
|
*/
|
|
261
302
|
cleanupOutbox?: (opts: {
|
|
262
303
|
ackedBeforeMs?: number;
|
package/dist/adapters/neon.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export class NeonAdapter {
|
|
|
5
5
|
private _connectionString;
|
|
6
6
|
/** @private */
|
|
7
7
|
private _sql;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
10
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
11
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
12
|
+
*
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
9
15
|
private _getSql;
|
|
10
16
|
initSchema(): Promise<{
|
|
11
17
|
columnsCreated: number;
|
|
@@ -39,3 +39,19 @@ export const LLM_CREDENTIALS_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS llm_cred
|
|
|
39
39
|
export const VERIFY_TABLE_SQL: "\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public'\n AND table_name = 'scheduled_messages'\n";
|
|
40
40
|
export const COLUMNS_SQL: "\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'scheduled_messages'\n ORDER BY ordinal_position\n";
|
|
41
41
|
export const UPDATABLE_COLUMNS: Set<string>;
|
|
42
|
+
/**
|
|
43
|
+
* 任务行的 SELECT 列集,同样是三个适配器共用一份(列名不分方言,加列只改这
|
|
44
|
+
* 里)。分成两份,是因为两条链路要的东西本来就不一样:
|
|
45
|
+
*
|
|
46
|
+
* - `TASK_DELIVERY_COLUMNS`:投递链路读的行(`getPendingTasks` /
|
|
47
|
+
* `getTaskByUuidOnly`)。除了发消息本身要用的字段,还必须带 `retry_after`
|
|
48
|
+
* —— run-tick 的退避守卫就是拿这一列判断「这条还在等重试,现在别跑」,列
|
|
49
|
+
* 不在行里,守卫读到的永远是 undefined,等于没有守卫。
|
|
50
|
+
* - `TASK_DETAIL_COLUMNS`:读接口返回的行(`getTaskByUuid` / `listTasks`),
|
|
51
|
+
* 多了 `last_error` 和两个时间戳,不带 `lease_until` 这类内部调度列。
|
|
52
|
+
*
|
|
53
|
+
* 收进来之前每个适配器各写各的 SELECT 列表,加列时漏掉其中一个不会有任何报
|
|
54
|
+
* 错,只会在那种部署上静默少一列。
|
|
55
|
+
*/
|
|
56
|
+
export const TASK_DELIVERY_COLUMNS: "id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count";
|
|
57
|
+
export const TASK_DETAIL_COLUMNS: "id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at";
|