@rei-standard/amsg-server 2.6.0-next.23 → 2.6.0-next.25
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 +5 -0
- package/dist/adapters/d1.d.ts +27 -2
- package/dist/adapters/interface.d.ts +16 -4
- package/dist/{chunk-BNZLICCI.cjs → chunk-DZJHAAYS.cjs} +137 -27
- package/dist/{chunk-3JEWYDM4.mjs → chunk-X2ZEXRBI.mjs} +137 -27
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +25 -25
- package/dist/index.mjs +1 -1
- package/dist/lib/client-state-store.d.ts +7 -2
- package/dist/lib/outbox-store.d.ts +10 -5
- package/dist/lib/result-emitter.d.ts +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -224,6 +224,8 @@ createSingleUserCloudflareWorker((env) => ({ …, multipart })); // CF Worker
|
|
|
224
224
|
|
|
225
225
|
不配 = 两边都用默认值。发送节奏也按 `ttlMs` 排:片数多时自动收紧每片之间的间隔,保证整批分片在这个窗口内发完;收紧到下限还装不下就一片都不发。
|
|
226
226
|
|
|
227
|
+
`maxChunkBytes` 只用来**收窄**,有上限:每片原文经 base64url 膨胀 4/3、再套上分片信封后,必须仍装得进单条 push 的明文上限(约 3993 字节),换算下来原文一片最多约 2800 字节。配超了服务端在投递时抛 `MULTIPART_CHUNK_BYTES_TOO_LARGE` 的配置错误(错误信息带当前配置下允许的最大值),不会切出一批推送服务收不了的分片。
|
|
228
|
+
|
|
227
229
|
两边对不上的下场值得记一下:页面把 `maxChunks` 收窄到 32、服务端还按 128 切的话,分片到了接收端会被逐片拒收;节奏排得比窗口长的话,迟到的分片会被当过期丢掉。两种都是「页面上这条思考过程直接没有」,而服务端那边每一片都发成功、看不出任何异常。
|
|
228
230
|
|
|
229
231
|
### 思考过程没送到时的可见性
|
|
@@ -328,6 +330,7 @@ ctx / metadata / push 上)。
|
|
|
328
330
|
- `metadata` 是整体替换,不深合并——只改一个子字段的读-改-写流程见上一节。
|
|
329
331
|
- `avatarUrl` 显式传 `null` 是「不改」而不是「清空」(§6.2 的软清空策略要求非法头像被摘掉时保留旧头像,「摘掉」和「传了个 null」在这一层是同一件事)。
|
|
330
332
|
- 凭据三件套传 `null` 同样只是忽略:清掉任何一个,任务到点就发不出去。
|
|
333
|
+
- 凭据三件套只对**没存 `credRefs.chat`** 的任务是「凭据刷新」入口。任务已经通过 `credRefs.chat` 引用凭据的话,触发时以凭据表那行为准、内联只是表行缺失的兜底,改内联不会生效——这种组合返回 `409 TASK_USES_CRED_REFS`,换 Key 走 `PUT /llm-credentials` 覆盖对应凭据,或在请求里改用 `credRefs` 指向新凭据。
|
|
331
334
|
- `credRefs` 是整体替换(语义同 `metadata`),同样做存在性检查;与内联三件套在同一个请求里混着传返回 `400`。给存量内联任务补 `credRefs` 时不动已存的三件套——那份留作 fire 时表行缺失的兜底。
|
|
332
335
|
- `pushSubscription` 不收(`400 PUSH_SUBSCRIPTION_NOT_ACCEPTED`),它是用户级的一份,走 `PUT /push-subscription`。
|
|
333
336
|
- `userMessage` 给了就必须是字符串(口径与排程时一致):它到点要过正则切分,别的类型收进来只会在投递时炸。
|
|
@@ -575,6 +578,8 @@ clientStateTtl: {
|
|
|
575
578
|
|
|
576
579
|
一个坑说在前头:`PUT /client-state` 和 `writeState()` 的条件写护栏(entry 上的 `version`)落的就是 `updated_at` 这一列。护栏值传的是自增计数器之类的小整数时,这行的 `updated_at` 看起来就像 1970 年,第一次清理就会被扫走。要给某个命名空间配 TTL,就让它的写入方把 `version` 传成毫秒时间戳。
|
|
577
580
|
|
|
581
|
+
反过来,`updated_at` 大幅领先真实时间的行(设备时钟跑偏时同步上来的)要等真实时间追过去才轮得到清理。这种行不会卡住写入——条件写认得出它来自未来,照样覆盖。
|
|
582
|
+
|
|
578
583
|
`GET /capabilities` 的 features 里有 `client-state-ttl`。
|
|
579
584
|
|
|
580
585
|
## 循环任务的时区(`tzId`)
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -185,8 +185,17 @@ export class D1Adapter {
|
|
|
185
185
|
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
186
186
|
* newer overwrites. Values arrive pre-encrypted (the handler encrypts).
|
|
187
187
|
*
|
|
188
|
+
* 例外是「来自未来」的行:`updated_at` 晚于服务端当前时间的行一律放行覆盖。
|
|
189
|
+
* 比较值是客户端自己报的时间戳,设备时钟只要领先过真实时间(用户改过系统
|
|
190
|
+
* 时间、时区或日期误操作),那一刻同步上来的行就带着一个还没到的时刻;之后
|
|
191
|
+
* 这台设备发什么都比它「旧」,条件写全被无声跳过,云端那行要等真实时间追上
|
|
192
|
+
* 去才解得开——客户端删本地数据、重装都碰不到它。合法写入不可能来自未来,
|
|
193
|
+
* 所以这种行按脏数据处理:服务端的钟是可信的那一个,拿它当判据放行。一次
|
|
194
|
+
* 正常写入就把 `updated_at` 拉回现实,之后旧不盖新照常生效。
|
|
195
|
+
*
|
|
188
196
|
* `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
|
|
189
|
-
*
|
|
197
|
+
* 条件保证陈旧批次删不动更新写入的行(同样对未来时间戳的行放行,否则删一条
|
|
198
|
+
* 状态时切片行留在库里成孤儿)。两种形态——
|
|
190
199
|
* - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
|
|
191
200
|
* 大值旧写入留下的切片行(见 lib/state-chunks.js);
|
|
192
201
|
* - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
|
|
@@ -204,6 +213,9 @@ export class D1Adapter {
|
|
|
204
213
|
* @param {string} userId
|
|
205
214
|
* @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
|
|
206
215
|
* @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
|
|
216
|
+
* @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
|
|
217
|
+
* 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
|
|
218
|
+
* 自定义调用方不传时退回本机时钟。
|
|
207
219
|
* @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
|
|
208
220
|
* `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
|
|
209
221
|
*/
|
|
@@ -217,7 +229,7 @@ export class D1Adapter {
|
|
|
217
229
|
key?: string;
|
|
218
230
|
keyPrefix?: string;
|
|
219
231
|
updatedAt: number;
|
|
220
|
-
}
|
|
232
|
+
}>, now?: number): Promise<{
|
|
221
233
|
upserted: number;
|
|
222
234
|
skipped: number;
|
|
223
235
|
outcomes: boolean[];
|
|
@@ -372,6 +384,19 @@ export class D1Adapter {
|
|
|
372
384
|
* @returns {Promise<number>} 删掉的行数
|
|
373
385
|
*/
|
|
374
386
|
discardOutboxMessages(userId: string, messageIds: string[]): Promise<number>;
|
|
387
|
+
/**
|
|
388
|
+
* 把某条任务名下还没发出去的行一次删掉(取消 / 顶替时的 outbox 清理)。
|
|
389
|
+
*
|
|
390
|
+
* 判据与 discardOutboxMessages 相同:只删 delivered_at 与 acked_at 均为
|
|
391
|
+
* NULL 的行——已经推给设备 / 已 ack 的照旧留着。按 task_uuid 直删是为了不
|
|
392
|
+
* 受未 ack 积压量的影响:靠翻页扫描挑行的话,积压一大这条任务的行就落在
|
|
393
|
+
* 扫描上限之外(见 lib/outbox-store.js 的 discardUndeliveredPushesForTask)。
|
|
394
|
+
*
|
|
395
|
+
* @param {string} userId
|
|
396
|
+
* @param {string} taskUuid
|
|
397
|
+
* @returns {Promise<number>} 删掉的行数
|
|
398
|
+
*/
|
|
399
|
+
discardUndeliveredOutboxForTask(userId: string, taskUuid: string): Promise<number>;
|
|
375
400
|
/**
|
|
376
401
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
377
402
|
*
|
|
@@ -143,6 +143,9 @@ export type DbAdapter = {
|
|
|
143
143
|
* 旧写入留下的切片行,见 lib/state-chunks.js),带 `key` 的删这一个 key(删整条状态;前缀会连带
|
|
144
144
|
* 删掉同前缀的兄弟 key)。两种都只删 `updated_at <= updatedAt` 的行。自定义 adapter 忽略前缀形态
|
|
145
145
|
* 只损失存储卫生;忽略精确 key 形态则删不掉状态,`ctx.writeState()` 的删除会失效。
|
|
146
|
+
* `now` 是服务端当前时刻(epoch 毫秒):库里 `updated_at` 晚于它的行只可能来自跑偏的设备时钟,
|
|
147
|
+
* 写入与删除都对这种行放行,否则那一行会一直卡到真实时间追上去为止。忽略这个参数的自定义
|
|
148
|
+
* adapter 功能不变,只是没有这道解锁。
|
|
146
149
|
* `outcomes` 逐条报告 entries[i] 是否真的写入(缺席时调用方按物理行计数兜底)。
|
|
147
150
|
*/
|
|
148
151
|
upsertClientState?: (userId: string, entries: Array<{
|
|
@@ -155,7 +158,7 @@ export type DbAdapter = {
|
|
|
155
158
|
key?: string;
|
|
156
159
|
keyPrefix?: string;
|
|
157
160
|
updatedAt: number;
|
|
158
|
-
}
|
|
161
|
+
}>, now?: number) => Promise<{
|
|
159
162
|
upserted: number;
|
|
160
163
|
skipped: number;
|
|
161
164
|
outcomes?: boolean[];
|
|
@@ -280,11 +283,20 @@ export type DbAdapter = {
|
|
|
280
283
|
* 把它们补收回去。不实现 → 取消只挡住 Web Push 这一路。
|
|
281
284
|
*/
|
|
282
285
|
discardOutboxMessages?: (userId: string, messageIds: string[]) => Promise<number>;
|
|
286
|
+
/**
|
|
287
|
+
* (可选;单用户/D1)按 task_uuid 一次删掉该任务名下还没发出去的行
|
|
288
|
+
* (delivered_at 与 acked_at 均为 NULL)。取消 / 顶替的 outbox 清理优先走
|
|
289
|
+
* 它:一个来回,且不受未 ack 积压量的影响。不实现 → 退回
|
|
290
|
+
* listUnackedOutbox + discardOutboxMessages 的翻页扫描,扫描有行数上限
|
|
291
|
+
* (见 lib/outbox-store.js),积压超过上限时会漏撤并打日志。
|
|
292
|
+
*/
|
|
293
|
+
discardUndeliveredOutboxForTask?: (userId: string, taskUuid: string) => Promise<number>;
|
|
283
294
|
/**
|
|
284
295
|
* (可选;单用户/D1)未 ack 的行,id 升序游标翻页(GET /outbox)。行上要带
|
|
285
|
-
* `task_uuid` 和 `delivered_at
|
|
286
|
-
*
|
|
287
|
-
*
|
|
296
|
+
* `task_uuid` 和 `delivered_at`:适配器没实现 discardUndeliveredOutboxForTask
|
|
297
|
+
* 时,`DELETE /message` 和 supersedesUuid 顶替这两条路靠翻这份名单找出该任务
|
|
298
|
+
* 名下还没发出去的行,缺任一字段就挑不出来,那两条路上的 outbox 清理会静默
|
|
299
|
+
* 跳过。
|
|
288
300
|
*/
|
|
289
301
|
listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
|
|
290
302
|
/**
|
|
@@ -1058,7 +1058,8 @@ function planClientStateCleanup(ttl, now) {
|
|
|
1058
1058
|
}
|
|
1059
1059
|
return targets;
|
|
1060
1060
|
}
|
|
1061
|
-
async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
1061
|
+
async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
|
|
1062
|
+
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1062
1063
|
const physicalRows = [];
|
|
1063
1064
|
const cleanups = [];
|
|
1064
1065
|
const rootRowIndexes = [];
|
|
@@ -1111,7 +1112,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1111
1112
|
if (physicalRows.length === 0 && cleanups.length === 0) {
|
|
1112
1113
|
return { upserted: 0, skipped: 0, deleted: 0, skippedEntries: [] };
|
|
1113
1114
|
}
|
|
1114
|
-
const result = await db.upsertClientState(userId, physicalRows, cleanups);
|
|
1115
|
+
const result = await db.upsertClientState(userId, physicalRows, cleanups, nowFn());
|
|
1115
1116
|
let upserted = 0;
|
|
1116
1117
|
let skipped = 0;
|
|
1117
1118
|
const skippedEntries = [];
|
|
@@ -1204,7 +1205,7 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
1204
1205
|
...entry.version !== void 0 ? { version: entry.version } : {}
|
|
1205
1206
|
};
|
|
1206
1207
|
});
|
|
1207
|
-
return writeClientStateEntries({ db, userId, userKey, entries: normalized });
|
|
1208
|
+
return writeClientStateEntries({ db, userId, userKey, entries: normalized, now: nowFn });
|
|
1208
1209
|
};
|
|
1209
1210
|
return { readState, writeState };
|
|
1210
1211
|
}
|
|
@@ -1265,15 +1266,27 @@ async function discardUndeliveredPushes({ db, userId, pushes, sentIds }) {
|
|
|
1265
1266
|
var OUTBOX_SCAN_PAGE_SIZE = 100;
|
|
1266
1267
|
var OUTBOX_SCAN_MAX_ROWS = 5e3;
|
|
1267
1268
|
async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
|
|
1268
|
-
if (!db ||
|
|
1269
|
-
if (
|
|
1269
|
+
if (!db || !taskUuid) return;
|
|
1270
|
+
if (typeof db.discardUndeliveredOutboxForTask === "function") {
|
|
1271
|
+
try {
|
|
1272
|
+
await db.discardUndeliveredOutboxForTask(userId, taskUuid);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.warn("[amsg-server] outbox \u6309\u4EFB\u52A1\u64A4\u56DE\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof db.listUnackedOutbox !== "function" || typeof db.discardOutboxMessages !== "function") return;
|
|
1270
1279
|
const messageIds = [];
|
|
1280
|
+
let exhausted = false;
|
|
1271
1281
|
try {
|
|
1272
1282
|
let cursor = 0;
|
|
1273
1283
|
let scanned = 0;
|
|
1274
1284
|
while (scanned < OUTBOX_SCAN_MAX_ROWS) {
|
|
1275
1285
|
const rows = await db.listUnackedOutbox(userId, cursor, OUTBOX_SCAN_PAGE_SIZE);
|
|
1276
|
-
if (!rows || rows.length === 0)
|
|
1286
|
+
if (!rows || rows.length === 0) {
|
|
1287
|
+
exhausted = true;
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1277
1290
|
scanned += rows.length;
|
|
1278
1291
|
let nextCursor = cursor;
|
|
1279
1292
|
for (const row of rows) {
|
|
@@ -1284,12 +1297,20 @@ async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
|
|
|
1284
1297
|
}
|
|
1285
1298
|
if (nextCursor <= cursor) break;
|
|
1286
1299
|
cursor = nextCursor;
|
|
1287
|
-
if (rows.length < OUTBOX_SCAN_PAGE_SIZE)
|
|
1300
|
+
if (rows.length < OUTBOX_SCAN_PAGE_SIZE) {
|
|
1301
|
+
exhausted = true;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1288
1304
|
}
|
|
1289
1305
|
} catch (error) {
|
|
1290
1306
|
console.warn("[amsg-server] outbox \u67E5\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1291
1307
|
return;
|
|
1292
1308
|
}
|
|
1309
|
+
if (!exhausted) {
|
|
1310
|
+
console.warn(
|
|
1311
|
+
`[amsg-server] outbox \u626B\u63CF\u5230 ${OUTBOX_SCAN_MAX_ROWS} \u884C\u4E0A\u9650\u4ECD\u672A\u626B\u5B8C\uFF0C\u4EFB\u52A1 ${taskUuid} \u53EF\u80FD\u8FD8\u6709\u672A\u6295\u9012\u7684\u884C\u6CA1\u64A4\u6389\uFF08\u88AB\u53D6\u6D88\u4EFB\u52A1\u7684\u884C\u901A\u5E38\u662F\u6700\u65B0\u7684\uFF0C\u6B63\u597D\u5728\u4E0A\u9650\u4E4B\u5916\uFF09\u3002\u7ED9\u9002\u914D\u5668\u5B9E\u73B0 discardUndeliveredOutboxForTask \u53EF\u7ED5\u5F00\u8FD9\u4E2A\u4E0A\u9650\u3002`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1293
1314
|
await discardPushesFromOutbox({ db, userId, messageIds });
|
|
1294
1315
|
}
|
|
1295
1316
|
|
|
@@ -1352,10 +1373,17 @@ function createResultEmitter({
|
|
|
1352
1373
|
sessionId,
|
|
1353
1374
|
occurrenceMs,
|
|
1354
1375
|
webpush,
|
|
1355
|
-
now
|
|
1376
|
+
now,
|
|
1377
|
+
isCancelled
|
|
1356
1378
|
}) {
|
|
1357
1379
|
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1358
1380
|
let emitted = 0;
|
|
1381
|
+
const assertNotCancelled = () => {
|
|
1382
|
+
if (typeof isCancelled !== "function" || !isCancelled()) return;
|
|
1383
|
+
const error = new Error("\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF0C\u7ED3\u679C\u5DF2\u4E2D\u6B62");
|
|
1384
|
+
error.code = TASK_CANCELLED_CODE;
|
|
1385
|
+
throw error;
|
|
1386
|
+
};
|
|
1359
1387
|
const emitResult = async (payload) => {
|
|
1360
1388
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1361
1389
|
throw new TypeError("emitResult(payload) \u9700\u8981\u4E00\u4E2A\u666E\u901A\u5BF9\u8C61");
|
|
@@ -1367,9 +1395,10 @@ function createResultEmitter({
|
|
|
1367
1395
|
);
|
|
1368
1396
|
}
|
|
1369
1397
|
const seq = emitted++;
|
|
1398
|
+
const messageType = decryptedPayload.messageType || "auto";
|
|
1370
1399
|
const push = _amsgshared.buildResultPush.call(void 0, {
|
|
1371
|
-
messageType
|
|
1372
|
-
source: "scheduled",
|
|
1400
|
+
messageType,
|
|
1401
|
+
source: messageType === "instant" ? "instant" : "scheduled",
|
|
1373
1402
|
messageId: `${messageIdBase}_result_${seq}`,
|
|
1374
1403
|
sessionId,
|
|
1375
1404
|
...payload,
|
|
@@ -1383,9 +1412,11 @@ function createResultEmitter({
|
|
|
1383
1412
|
recurrenceType: decryptedPayload.recurrenceType || "none",
|
|
1384
1413
|
occurrenceMs
|
|
1385
1414
|
});
|
|
1415
|
+
assertNotCancelled();
|
|
1386
1416
|
await db.appendOutboxMessages(task.user_id, await toOutboxRows([push], userKey, nowFn()));
|
|
1387
1417
|
let pushed;
|
|
1388
1418
|
try {
|
|
1419
|
+
assertNotCancelled();
|
|
1389
1420
|
pushed = shouldSendPush(push, { outboxed: true }) ? await sendResultPush({ db, task, userKey, decryptedPayload, webpush, push }) : false;
|
|
1390
1421
|
} catch (error) {
|
|
1391
1422
|
await discardUndeliveredPushes({ db, userId: task.user_id, pushes: [push], sentIds: [] });
|
|
@@ -1585,7 +1616,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1585
1616
|
sessionId,
|
|
1586
1617
|
occurrenceMs,
|
|
1587
1618
|
webpush: ctx.webpush,
|
|
1588
|
-
now: nowFn
|
|
1619
|
+
now: nowFn,
|
|
1620
|
+
// run-tick 在投递 ctx 上挂的取消信号(与 guardWebpushWithLease 读的是同一
|
|
1621
|
+
// 个租约状态),emitResult 不发推送的那条路要靠它拦下已取消任务的落行。
|
|
1622
|
+
isCancelled: typeof ctx.isTaskCancelled === "function" ? ctx.isTaskCancelled : null
|
|
1589
1623
|
});
|
|
1590
1624
|
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1591
1625
|
let scheduledTaskCount = 0;
|
|
@@ -1771,6 +1805,9 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1771
1805
|
);
|
|
1772
1806
|
}
|
|
1773
1807
|
const cancelled = await ctx.db.deleteTaskByUuid(uuid, task.user_id);
|
|
1808
|
+
if (cancelled) {
|
|
1809
|
+
await discardUndeliveredPushesForTask({ db: ctx.db, userId: task.user_id, taskUuid: uuid });
|
|
1810
|
+
}
|
|
1774
1811
|
return { cancelled: !!cancelled };
|
|
1775
1812
|
};
|
|
1776
1813
|
const renewTask = async (uuid, nextSendAt) => {
|
|
@@ -2240,12 +2277,36 @@ function sleepFor(ctx, ms) {
|
|
|
2240
2277
|
}
|
|
2241
2278
|
function resolveMultipartOptions(ctx) {
|
|
2242
2279
|
const configured = ctx && ctx.multipart && typeof ctx.multipart === "object" ? ctx.multipart : {};
|
|
2243
|
-
|
|
2280
|
+
const resolved = {
|
|
2244
2281
|
maxChunkBytes: positiveIntegerOr(configured.maxChunkBytes, _amsgshared.DEFAULT_MULTIPART_CHUNK_BYTES),
|
|
2245
2282
|
maxChunks: positiveIntegerOr(configured.maxChunks, _amsgshared.DEFAULT_MULTIPART_MAX_CHUNKS),
|
|
2246
2283
|
maxTotalBytes: positiveIntegerOr(configured.maxTotalBytes, _amsgshared.DEFAULT_MULTIPART_MAX_TOTAL_BYTES),
|
|
2247
2284
|
ttlMs: positiveIntegerOr(configured.ttlMs, _amsgshared.DEFAULT_MULTIPART_TTL_MS)
|
|
2248
2285
|
};
|
|
2286
|
+
assertChunkBytesFitPushLimit(resolved);
|
|
2287
|
+
return resolved;
|
|
2288
|
+
}
|
|
2289
|
+
function assertChunkBytesFitPushLimit({ maxChunkBytes, maxChunks, ttlMs }) {
|
|
2290
|
+
const PROBE_CHUNK_BYTES = 3;
|
|
2291
|
+
const [probe] = _amsgshared.buildMultipartPushPayloads.call(void 0,
|
|
2292
|
+
{ messageKind: "reasoning" },
|
|
2293
|
+
{ serializedPayload: "x".repeat(PROBE_CHUNK_BYTES), maxChunkBytes: PROBE_CHUNK_BYTES, ttlMs }
|
|
2294
|
+
);
|
|
2295
|
+
const digitHeadroom = 2 * (String(maxChunks).length - 1);
|
|
2296
|
+
const envelopeOverhead = measurePushPayload(JSON.stringify(probe)).bytes - base64UrlLength(PROBE_CHUNK_BYTES) + digitHeadroom;
|
|
2297
|
+
const worstEnvelopeBytes = envelopeOverhead + base64UrlLength(maxChunkBytes);
|
|
2298
|
+
if (worstEnvelopeBytes <= MAX_PUSH_PAYLOAD_BYTES) return;
|
|
2299
|
+
let maxAllowed = Math.floor((MAX_PUSH_PAYLOAD_BYTES - envelopeOverhead) * 3 / 4);
|
|
2300
|
+
while (maxAllowed > 0 && envelopeOverhead + base64UrlLength(maxAllowed) > MAX_PUSH_PAYLOAD_BYTES) {
|
|
2301
|
+
maxAllowed--;
|
|
2302
|
+
}
|
|
2303
|
+
throw new DeploymentConfigError(
|
|
2304
|
+
`MULTIPART_CHUNK_BYTES_TOO_LARGE: multipart.maxChunkBytes = ${maxChunkBytes} \u5207\u51FA\u7684\u5206\u7247\u4FE1\u5C01\u6700\u574F ${worstEnvelopeBytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5355\u6761 push \u660E\u6587\u4E0A\u9650 ${MAX_PUSH_PAYLOAD_BYTES} \u5B57\u8282\uFF0C\u6BCF\u4E00\u7247\u90FD\u4F1A\u88AB\u63A8\u9001\u670D\u52A1\u62D2\u6536\u3002\u8FD9\u4E2A\u65CB\u94AE\u53EA\u7528\u4E8E\u6536\u7A84\uFF0C\u5F53\u524D\u914D\u7F6E\u4E0B\u6700\u5927 ${maxAllowed}`,
|
|
2305
|
+
{ code: "MULTIPART_CHUNK_BYTES_TOO_LARGE" }
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
function base64UrlLength(n) {
|
|
2309
|
+
return Math.ceil(n * 4 / 3);
|
|
2249
2310
|
}
|
|
2250
2311
|
function positiveIntegerOr(value, fallback) {
|
|
2251
2312
|
return Number.isInteger(value) && /** @type {number} */
|
|
@@ -3003,13 +3064,14 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3003
3064
|
groupsTakenThisTick.add(scopedKey);
|
|
3004
3065
|
return { taken: false, rawKey };
|
|
3005
3066
|
}
|
|
3006
|
-
function recordCancelled(task, status) {
|
|
3067
|
+
async function recordCancelled(task, status) {
|
|
3007
3068
|
results.cancelledTasks.push({
|
|
3008
3069
|
taskId: task.id,
|
|
3009
3070
|
reason: "\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF",
|
|
3010
3071
|
status
|
|
3011
3072
|
});
|
|
3012
3073
|
console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF08${status}\uFF09`);
|
|
3074
|
+
await discardUndeliveredPushesForTask({ db, userId: task.user_id, taskUuid: task.uuid });
|
|
3013
3075
|
}
|
|
3014
3076
|
async function payloadStillFresh(task) {
|
|
3015
3077
|
if (typeof db.getTaskByUuid !== "function" || !task.uuid) return true;
|
|
@@ -3191,7 +3253,8 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3191
3253
|
messageIdBase: task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_stale_${task.uuid || ""}`,
|
|
3192
3254
|
sessionId: task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_stale_${task.uuid || ""}`,
|
|
3193
3255
|
occurrenceMs,
|
|
3194
|
-
webpush: ctx.webpush
|
|
3256
|
+
webpush: ctx.webpush,
|
|
3257
|
+
isCancelled: () => lease.lost
|
|
3195
3258
|
});
|
|
3196
3259
|
const recurring = isRecurringType(recurrenceType);
|
|
3197
3260
|
const plan = recurring ? planNextOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId) : null;
|
|
@@ -3240,13 +3303,19 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3240
3303
|
try {
|
|
3241
3304
|
sendResult = await processSingleMessage(
|
|
3242
3305
|
task,
|
|
3243
|
-
{
|
|
3306
|
+
{
|
|
3307
|
+
...ctx,
|
|
3308
|
+
db,
|
|
3309
|
+
masterKey,
|
|
3310
|
+
webpush: guardWebpushWithLease(ctx.webpush, lease),
|
|
3311
|
+
isTaskCancelled: () => lease.lost
|
|
3312
|
+
},
|
|
3244
3313
|
masterKey,
|
|
3245
3314
|
{ userKey, payload: decryptedPayload }
|
|
3246
3315
|
);
|
|
3247
3316
|
} catch (error) {
|
|
3248
3317
|
if (lease.lost) {
|
|
3249
|
-
recordCancelled(task, "cancelled_mid_delivery");
|
|
3318
|
+
await recordCancelled(task, "cancelled_mid_delivery");
|
|
3250
3319
|
return;
|
|
3251
3320
|
}
|
|
3252
3321
|
await handleDeliveryFailure(
|
|
@@ -3261,7 +3330,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3261
3330
|
}
|
|
3262
3331
|
if (!sendResult.success) {
|
|
3263
3332
|
if (lease.lost) {
|
|
3264
|
-
recordCancelled(task, "cancelled_mid_delivery");
|
|
3333
|
+
await recordCancelled(task, "cancelled_mid_delivery");
|
|
3265
3334
|
return;
|
|
3266
3335
|
}
|
|
3267
3336
|
await handleDeliveryFailure(
|
|
@@ -3284,7 +3353,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3284
3353
|
if (recurrenceType === "none") {
|
|
3285
3354
|
markLeaseReleased(task.id);
|
|
3286
3355
|
if (rowVanished(await db.deleteTaskById(task.id))) {
|
|
3287
|
-
recordCancelled(task, "cancelled_after_delivery");
|
|
3356
|
+
await recordCancelled(task, "cancelled_after_delivery");
|
|
3288
3357
|
return;
|
|
3289
3358
|
}
|
|
3290
3359
|
results.deletedOnceOffTasks++;
|
|
@@ -3298,7 +3367,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3298
3367
|
...clearedPayload ? { encrypted_payload: clearedPayload } : {}
|
|
3299
3368
|
});
|
|
3300
3369
|
if (rowVanished(updated)) {
|
|
3301
|
-
recordCancelled(task, "cancelled_after_delivery");
|
|
3370
|
+
await recordCancelled(task, "cancelled_after_delivery");
|
|
3302
3371
|
return;
|
|
3303
3372
|
}
|
|
3304
3373
|
results.updatedRecurringTasks++;
|
|
@@ -3484,6 +3553,13 @@ function createUpdateMessageHandler(ctx) {
|
|
|
3484
3553
|
return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
|
|
3485
3554
|
}
|
|
3486
3555
|
const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
3556
|
+
if ((updates.apiUrl || updates.apiKey || updates.primaryModel) && hasChatCredRef(existingData)) {
|
|
3557
|
+
return { status: 409, body: { success: false, error: {
|
|
3558
|
+
code: "TASK_USES_CRED_REFS",
|
|
3559
|
+
message: "\u4EFB\u52A1\u5DF2\u901A\u8FC7 credRefs.chat \u5F15\u7528\u51ED\u636E\uFF0C\u89E6\u53D1\u65F6\u4EE5\u51ED\u636E\u8868\u4E3A\u51C6\uFF0C\u5185\u8054 apiUrl / apiKey / primaryModel \u7684\u66F4\u65B0\u4E0D\u4F1A\u751F\u6548\u3002\u6362 Key \u8BF7\u7528 PUT /llm-credentials \u8986\u76D6\u5BF9\u5E94\u51ED\u636E\uFF0C\u6216\u5728\u672C\u6B21\u8BF7\u6C42\u91CC\u6539\u7528 credRefs \u6307\u5411\u65B0\u51ED\u636E",
|
|
3560
|
+
details: { invalidFields: ["apiUrl", "apiKey", "primaryModel"].filter((name) => updates[name]) }
|
|
3561
|
+
} } };
|
|
3562
|
+
}
|
|
3487
3563
|
const promptUpdates = {};
|
|
3488
3564
|
if (updates.completePrompt) {
|
|
3489
3565
|
promptUpdates.completePrompt = updates.completePrompt;
|
|
@@ -4574,8 +4650,17 @@ var D1Adapter = class {
|
|
|
4574
4650
|
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
4575
4651
|
* newer overwrites. Values arrive pre-encrypted (the handler encrypts).
|
|
4576
4652
|
*
|
|
4653
|
+
* 例外是「来自未来」的行:`updated_at` 晚于服务端当前时间的行一律放行覆盖。
|
|
4654
|
+
* 比较值是客户端自己报的时间戳,设备时钟只要领先过真实时间(用户改过系统
|
|
4655
|
+
* 时间、时区或日期误操作),那一刻同步上来的行就带着一个还没到的时刻;之后
|
|
4656
|
+
* 这台设备发什么都比它「旧」,条件写全被无声跳过,云端那行要等真实时间追上
|
|
4657
|
+
* 去才解得开——客户端删本地数据、重装都碰不到它。合法写入不可能来自未来,
|
|
4658
|
+
* 所以这种行按脏数据处理:服务端的钟是可信的那一个,拿它当判据放行。一次
|
|
4659
|
+
* 正常写入就把 `updated_at` 拉回现实,之后旧不盖新照常生效。
|
|
4660
|
+
*
|
|
4577
4661
|
* `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
|
|
4578
|
-
*
|
|
4662
|
+
* 条件保证陈旧批次删不动更新写入的行(同样对未来时间戳的行放行,否则删一条
|
|
4663
|
+
* 状态时切片行留在库里成孤儿)。两种形态——
|
|
4579
4664
|
* - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
|
|
4580
4665
|
* 大值旧写入留下的切片行(见 lib/state-chunks.js);
|
|
4581
4666
|
* - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
|
|
@@ -4593,24 +4678,30 @@ var D1Adapter = class {
|
|
|
4593
4678
|
* @param {string} userId
|
|
4594
4679
|
* @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
|
|
4595
4680
|
* @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
|
|
4681
|
+
* @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
|
|
4682
|
+
* 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
|
|
4683
|
+
* 自定义调用方不传时退回本机时钟。
|
|
4596
4684
|
* @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
|
|
4597
4685
|
* `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
|
|
4598
4686
|
*/
|
|
4599
|
-
async upsertClientState(userId, entries, cleanups = []) {
|
|
4687
|
+
async upsertClientState(userId, entries, cleanups = [], now = Date.now()) {
|
|
4600
4688
|
const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
|
|
4601
4689
|
VALUES (?, ?, ?, ?, ?)
|
|
4602
4690
|
ON CONFLICT (user_id, namespace, key) DO UPDATE SET
|
|
4603
4691
|
value = excluded.value,
|
|
4604
4692
|
updated_at = excluded.updated_at
|
|
4605
|
-
WHERE excluded.updated_at >= client_state.updated_at
|
|
4693
|
+
WHERE excluded.updated_at >= client_state.updated_at
|
|
4694
|
+
OR client_state.updated_at > ?`;
|
|
4606
4695
|
const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
|
|
4607
|
-
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ?
|
|
4696
|
+
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ?
|
|
4697
|
+
AND (updated_at <= ? OR updated_at > ?)`;
|
|
4608
4698
|
const CLEANUP_KEY_SQL = `DELETE FROM client_state
|
|
4609
|
-
WHERE user_id = ? AND namespace = ? AND key = ?
|
|
4699
|
+
WHERE user_id = ? AND namespace = ? AND key = ?
|
|
4700
|
+
AND (updated_at <= ? OR updated_at > ?)`;
|
|
4610
4701
|
const buildStatements = () => [
|
|
4611
|
-
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt)),
|
|
4702
|
+
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)),
|
|
4612
4703
|
...entries.map(
|
|
4613
|
-
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
4704
|
+
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
|
|
4614
4705
|
)
|
|
4615
4706
|
];
|
|
4616
4707
|
let results;
|
|
@@ -4896,6 +4987,25 @@ var D1Adapter = class {
|
|
|
4896
4987
|
messageIds
|
|
4897
4988
|
);
|
|
4898
4989
|
}
|
|
4990
|
+
/**
|
|
4991
|
+
* 把某条任务名下还没发出去的行一次删掉(取消 / 顶替时的 outbox 清理)。
|
|
4992
|
+
*
|
|
4993
|
+
* 判据与 discardOutboxMessages 相同:只删 delivered_at 与 acked_at 均为
|
|
4994
|
+
* NULL 的行——已经推给设备 / 已 ack 的照旧留着。按 task_uuid 直删是为了不
|
|
4995
|
+
* 受未 ack 积压量的影响:靠翻页扫描挑行的话,积压一大这条任务的行就落在
|
|
4996
|
+
* 扫描上限之外(见 lib/outbox-store.js 的 discardUndeliveredPushesForTask)。
|
|
4997
|
+
*
|
|
4998
|
+
* @param {string} userId
|
|
4999
|
+
* @param {string} taskUuid
|
|
5000
|
+
* @returns {Promise<number>} 删掉的行数
|
|
5001
|
+
*/
|
|
5002
|
+
async discardUndeliveredOutboxForTask(userId, taskUuid) {
|
|
5003
|
+
const res = await this._db.prepare(
|
|
5004
|
+
`DELETE FROM message_outbox
|
|
5005
|
+
WHERE user_id = ? AND task_uuid = ? AND delivered_at IS NULL AND acked_at IS NULL`
|
|
5006
|
+
).bind(userId, taskUuid).run();
|
|
5007
|
+
return res.meta.changes || 0;
|
|
5008
|
+
}
|
|
4899
5009
|
/**
|
|
4900
5010
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
4901
5011
|
*
|
|
@@ -5213,7 +5323,7 @@ function createClientStateHandler(ctx) {
|
|
|
5213
5323
|
}
|
|
5214
5324
|
|
|
5215
5325
|
// src/server/lib/version.js
|
|
5216
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
5326
|
+
var SERVER_VERSION = true ? "2.6.0-next.25" : "0.0.0-dev";
|
|
5217
5327
|
|
|
5218
5328
|
// src/server/handlers/capabilities.js
|
|
5219
5329
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -1058,7 +1058,8 @@ function planClientStateCleanup(ttl, now) {
|
|
|
1058
1058
|
}
|
|
1059
1059
|
return targets;
|
|
1060
1060
|
}
|
|
1061
|
-
async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
1061
|
+
async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
|
|
1062
|
+
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1062
1063
|
const physicalRows = [];
|
|
1063
1064
|
const cleanups = [];
|
|
1064
1065
|
const rootRowIndexes = [];
|
|
@@ -1111,7 +1112,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1111
1112
|
if (physicalRows.length === 0 && cleanups.length === 0) {
|
|
1112
1113
|
return { upserted: 0, skipped: 0, deleted: 0, skippedEntries: [] };
|
|
1113
1114
|
}
|
|
1114
|
-
const result = await db.upsertClientState(userId, physicalRows, cleanups);
|
|
1115
|
+
const result = await db.upsertClientState(userId, physicalRows, cleanups, nowFn());
|
|
1115
1116
|
let upserted = 0;
|
|
1116
1117
|
let skipped = 0;
|
|
1117
1118
|
const skippedEntries = [];
|
|
@@ -1204,7 +1205,7 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
1204
1205
|
...entry.version !== void 0 ? { version: entry.version } : {}
|
|
1205
1206
|
};
|
|
1206
1207
|
});
|
|
1207
|
-
return writeClientStateEntries({ db, userId, userKey, entries: normalized });
|
|
1208
|
+
return writeClientStateEntries({ db, userId, userKey, entries: normalized, now: nowFn });
|
|
1208
1209
|
};
|
|
1209
1210
|
return { readState, writeState };
|
|
1210
1211
|
}
|
|
@@ -1265,15 +1266,27 @@ async function discardUndeliveredPushes({ db, userId, pushes, sentIds }) {
|
|
|
1265
1266
|
var OUTBOX_SCAN_PAGE_SIZE = 100;
|
|
1266
1267
|
var OUTBOX_SCAN_MAX_ROWS = 5e3;
|
|
1267
1268
|
async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
|
|
1268
|
-
if (!db ||
|
|
1269
|
-
if (
|
|
1269
|
+
if (!db || !taskUuid) return;
|
|
1270
|
+
if (typeof db.discardUndeliveredOutboxForTask === "function") {
|
|
1271
|
+
try {
|
|
1272
|
+
await db.discardUndeliveredOutboxForTask(userId, taskUuid);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.warn("[amsg-server] outbox \u6309\u4EFB\u52A1\u64A4\u56DE\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof db.listUnackedOutbox !== "function" || typeof db.discardOutboxMessages !== "function") return;
|
|
1270
1279
|
const messageIds = [];
|
|
1280
|
+
let exhausted = false;
|
|
1271
1281
|
try {
|
|
1272
1282
|
let cursor = 0;
|
|
1273
1283
|
let scanned = 0;
|
|
1274
1284
|
while (scanned < OUTBOX_SCAN_MAX_ROWS) {
|
|
1275
1285
|
const rows = await db.listUnackedOutbox(userId, cursor, OUTBOX_SCAN_PAGE_SIZE);
|
|
1276
|
-
if (!rows || rows.length === 0)
|
|
1286
|
+
if (!rows || rows.length === 0) {
|
|
1287
|
+
exhausted = true;
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1277
1290
|
scanned += rows.length;
|
|
1278
1291
|
let nextCursor = cursor;
|
|
1279
1292
|
for (const row of rows) {
|
|
@@ -1284,12 +1297,20 @@ async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
|
|
|
1284
1297
|
}
|
|
1285
1298
|
if (nextCursor <= cursor) break;
|
|
1286
1299
|
cursor = nextCursor;
|
|
1287
|
-
if (rows.length < OUTBOX_SCAN_PAGE_SIZE)
|
|
1300
|
+
if (rows.length < OUTBOX_SCAN_PAGE_SIZE) {
|
|
1301
|
+
exhausted = true;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1288
1304
|
}
|
|
1289
1305
|
} catch (error) {
|
|
1290
1306
|
console.warn("[amsg-server] outbox \u67E5\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1291
1307
|
return;
|
|
1292
1308
|
}
|
|
1309
|
+
if (!exhausted) {
|
|
1310
|
+
console.warn(
|
|
1311
|
+
`[amsg-server] outbox \u626B\u63CF\u5230 ${OUTBOX_SCAN_MAX_ROWS} \u884C\u4E0A\u9650\u4ECD\u672A\u626B\u5B8C\uFF0C\u4EFB\u52A1 ${taskUuid} \u53EF\u80FD\u8FD8\u6709\u672A\u6295\u9012\u7684\u884C\u6CA1\u64A4\u6389\uFF08\u88AB\u53D6\u6D88\u4EFB\u52A1\u7684\u884C\u901A\u5E38\u662F\u6700\u65B0\u7684\uFF0C\u6B63\u597D\u5728\u4E0A\u9650\u4E4B\u5916\uFF09\u3002\u7ED9\u9002\u914D\u5668\u5B9E\u73B0 discardUndeliveredOutboxForTask \u53EF\u7ED5\u5F00\u8FD9\u4E2A\u4E0A\u9650\u3002`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1293
1314
|
await discardPushesFromOutbox({ db, userId, messageIds });
|
|
1294
1315
|
}
|
|
1295
1316
|
|
|
@@ -1352,10 +1373,17 @@ function createResultEmitter({
|
|
|
1352
1373
|
sessionId,
|
|
1353
1374
|
occurrenceMs,
|
|
1354
1375
|
webpush,
|
|
1355
|
-
now
|
|
1376
|
+
now,
|
|
1377
|
+
isCancelled
|
|
1356
1378
|
}) {
|
|
1357
1379
|
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1358
1380
|
let emitted = 0;
|
|
1381
|
+
const assertNotCancelled = () => {
|
|
1382
|
+
if (typeof isCancelled !== "function" || !isCancelled()) return;
|
|
1383
|
+
const error = new Error("\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF0C\u7ED3\u679C\u5DF2\u4E2D\u6B62");
|
|
1384
|
+
error.code = TASK_CANCELLED_CODE;
|
|
1385
|
+
throw error;
|
|
1386
|
+
};
|
|
1359
1387
|
const emitResult = async (payload) => {
|
|
1360
1388
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1361
1389
|
throw new TypeError("emitResult(payload) \u9700\u8981\u4E00\u4E2A\u666E\u901A\u5BF9\u8C61");
|
|
@@ -1367,9 +1395,10 @@ function createResultEmitter({
|
|
|
1367
1395
|
);
|
|
1368
1396
|
}
|
|
1369
1397
|
const seq = emitted++;
|
|
1398
|
+
const messageType = decryptedPayload.messageType || "auto";
|
|
1370
1399
|
const push = buildResultPush({
|
|
1371
|
-
messageType
|
|
1372
|
-
source: "scheduled",
|
|
1400
|
+
messageType,
|
|
1401
|
+
source: messageType === "instant" ? "instant" : "scheduled",
|
|
1373
1402
|
messageId: `${messageIdBase}_result_${seq}`,
|
|
1374
1403
|
sessionId,
|
|
1375
1404
|
...payload,
|
|
@@ -1383,9 +1412,11 @@ function createResultEmitter({
|
|
|
1383
1412
|
recurrenceType: decryptedPayload.recurrenceType || "none",
|
|
1384
1413
|
occurrenceMs
|
|
1385
1414
|
});
|
|
1415
|
+
assertNotCancelled();
|
|
1386
1416
|
await db.appendOutboxMessages(task.user_id, await toOutboxRows([push], userKey, nowFn()));
|
|
1387
1417
|
let pushed;
|
|
1388
1418
|
try {
|
|
1419
|
+
assertNotCancelled();
|
|
1389
1420
|
pushed = shouldSendPush(push, { outboxed: true }) ? await sendResultPush({ db, task, userKey, decryptedPayload, webpush, push }) : false;
|
|
1390
1421
|
} catch (error) {
|
|
1391
1422
|
await discardUndeliveredPushes({ db, userId: task.user_id, pushes: [push], sentIds: [] });
|
|
@@ -1585,7 +1616,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1585
1616
|
sessionId,
|
|
1586
1617
|
occurrenceMs,
|
|
1587
1618
|
webpush: ctx.webpush,
|
|
1588
|
-
now: nowFn
|
|
1619
|
+
now: nowFn,
|
|
1620
|
+
// run-tick 在投递 ctx 上挂的取消信号(与 guardWebpushWithLease 读的是同一
|
|
1621
|
+
// 个租约状态),emitResult 不发推送的那条路要靠它拦下已取消任务的落行。
|
|
1622
|
+
isCancelled: typeof ctx.isTaskCancelled === "function" ? ctx.isTaskCancelled : null
|
|
1589
1623
|
});
|
|
1590
1624
|
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1591
1625
|
let scheduledTaskCount = 0;
|
|
@@ -1771,6 +1805,9 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1771
1805
|
);
|
|
1772
1806
|
}
|
|
1773
1807
|
const cancelled = await ctx.db.deleteTaskByUuid(uuid, task.user_id);
|
|
1808
|
+
if (cancelled) {
|
|
1809
|
+
await discardUndeliveredPushesForTask({ db: ctx.db, userId: task.user_id, taskUuid: uuid });
|
|
1810
|
+
}
|
|
1774
1811
|
return { cancelled: !!cancelled };
|
|
1775
1812
|
};
|
|
1776
1813
|
const renewTask = async (uuid, nextSendAt) => {
|
|
@@ -2240,12 +2277,36 @@ function sleepFor(ctx, ms) {
|
|
|
2240
2277
|
}
|
|
2241
2278
|
function resolveMultipartOptions(ctx) {
|
|
2242
2279
|
const configured = ctx && ctx.multipart && typeof ctx.multipart === "object" ? ctx.multipart : {};
|
|
2243
|
-
|
|
2280
|
+
const resolved = {
|
|
2244
2281
|
maxChunkBytes: positiveIntegerOr(configured.maxChunkBytes, DEFAULT_MULTIPART_CHUNK_BYTES),
|
|
2245
2282
|
maxChunks: positiveIntegerOr(configured.maxChunks, DEFAULT_MULTIPART_MAX_CHUNKS),
|
|
2246
2283
|
maxTotalBytes: positiveIntegerOr(configured.maxTotalBytes, DEFAULT_MULTIPART_MAX_TOTAL_BYTES),
|
|
2247
2284
|
ttlMs: positiveIntegerOr(configured.ttlMs, DEFAULT_MULTIPART_TTL_MS)
|
|
2248
2285
|
};
|
|
2286
|
+
assertChunkBytesFitPushLimit(resolved);
|
|
2287
|
+
return resolved;
|
|
2288
|
+
}
|
|
2289
|
+
function assertChunkBytesFitPushLimit({ maxChunkBytes, maxChunks, ttlMs }) {
|
|
2290
|
+
const PROBE_CHUNK_BYTES = 3;
|
|
2291
|
+
const [probe] = buildMultipartPushPayloads(
|
|
2292
|
+
{ messageKind: "reasoning" },
|
|
2293
|
+
{ serializedPayload: "x".repeat(PROBE_CHUNK_BYTES), maxChunkBytes: PROBE_CHUNK_BYTES, ttlMs }
|
|
2294
|
+
);
|
|
2295
|
+
const digitHeadroom = 2 * (String(maxChunks).length - 1);
|
|
2296
|
+
const envelopeOverhead = measurePushPayload(JSON.stringify(probe)).bytes - base64UrlLength(PROBE_CHUNK_BYTES) + digitHeadroom;
|
|
2297
|
+
const worstEnvelopeBytes = envelopeOverhead + base64UrlLength(maxChunkBytes);
|
|
2298
|
+
if (worstEnvelopeBytes <= MAX_PUSH_PAYLOAD_BYTES) return;
|
|
2299
|
+
let maxAllowed = Math.floor((MAX_PUSH_PAYLOAD_BYTES - envelopeOverhead) * 3 / 4);
|
|
2300
|
+
while (maxAllowed > 0 && envelopeOverhead + base64UrlLength(maxAllowed) > MAX_PUSH_PAYLOAD_BYTES) {
|
|
2301
|
+
maxAllowed--;
|
|
2302
|
+
}
|
|
2303
|
+
throw new DeploymentConfigError(
|
|
2304
|
+
`MULTIPART_CHUNK_BYTES_TOO_LARGE: multipart.maxChunkBytes = ${maxChunkBytes} \u5207\u51FA\u7684\u5206\u7247\u4FE1\u5C01\u6700\u574F ${worstEnvelopeBytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5355\u6761 push \u660E\u6587\u4E0A\u9650 ${MAX_PUSH_PAYLOAD_BYTES} \u5B57\u8282\uFF0C\u6BCF\u4E00\u7247\u90FD\u4F1A\u88AB\u63A8\u9001\u670D\u52A1\u62D2\u6536\u3002\u8FD9\u4E2A\u65CB\u94AE\u53EA\u7528\u4E8E\u6536\u7A84\uFF0C\u5F53\u524D\u914D\u7F6E\u4E0B\u6700\u5927 ${maxAllowed}`,
|
|
2305
|
+
{ code: "MULTIPART_CHUNK_BYTES_TOO_LARGE" }
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
function base64UrlLength(n) {
|
|
2309
|
+
return Math.ceil(n * 4 / 3);
|
|
2249
2310
|
}
|
|
2250
2311
|
function positiveIntegerOr(value, fallback) {
|
|
2251
2312
|
return Number.isInteger(value) && /** @type {number} */
|
|
@@ -3003,13 +3064,14 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3003
3064
|
groupsTakenThisTick.add(scopedKey);
|
|
3004
3065
|
return { taken: false, rawKey };
|
|
3005
3066
|
}
|
|
3006
|
-
function recordCancelled(task, status) {
|
|
3067
|
+
async function recordCancelled(task, status) {
|
|
3007
3068
|
results.cancelledTasks.push({
|
|
3008
3069
|
taskId: task.id,
|
|
3009
3070
|
reason: "\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF",
|
|
3010
3071
|
status
|
|
3011
3072
|
});
|
|
3012
3073
|
console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF08${status}\uFF09`);
|
|
3074
|
+
await discardUndeliveredPushesForTask({ db, userId: task.user_id, taskUuid: task.uuid });
|
|
3013
3075
|
}
|
|
3014
3076
|
async function payloadStillFresh(task) {
|
|
3015
3077
|
if (typeof db.getTaskByUuid !== "function" || !task.uuid) return true;
|
|
@@ -3191,7 +3253,8 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3191
3253
|
messageIdBase: task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_stale_${task.uuid || ""}`,
|
|
3192
3254
|
sessionId: task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_stale_${task.uuid || ""}`,
|
|
3193
3255
|
occurrenceMs,
|
|
3194
|
-
webpush: ctx.webpush
|
|
3256
|
+
webpush: ctx.webpush,
|
|
3257
|
+
isCancelled: () => lease.lost
|
|
3195
3258
|
});
|
|
3196
3259
|
const recurring = isRecurringType(recurrenceType);
|
|
3197
3260
|
const plan = recurring ? planNextOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId) : null;
|
|
@@ -3240,13 +3303,19 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3240
3303
|
try {
|
|
3241
3304
|
sendResult = await processSingleMessage(
|
|
3242
3305
|
task,
|
|
3243
|
-
{
|
|
3306
|
+
{
|
|
3307
|
+
...ctx,
|
|
3308
|
+
db,
|
|
3309
|
+
masterKey,
|
|
3310
|
+
webpush: guardWebpushWithLease(ctx.webpush, lease),
|
|
3311
|
+
isTaskCancelled: () => lease.lost
|
|
3312
|
+
},
|
|
3244
3313
|
masterKey,
|
|
3245
3314
|
{ userKey, payload: decryptedPayload }
|
|
3246
3315
|
);
|
|
3247
3316
|
} catch (error) {
|
|
3248
3317
|
if (lease.lost) {
|
|
3249
|
-
recordCancelled(task, "cancelled_mid_delivery");
|
|
3318
|
+
await recordCancelled(task, "cancelled_mid_delivery");
|
|
3250
3319
|
return;
|
|
3251
3320
|
}
|
|
3252
3321
|
await handleDeliveryFailure(
|
|
@@ -3261,7 +3330,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3261
3330
|
}
|
|
3262
3331
|
if (!sendResult.success) {
|
|
3263
3332
|
if (lease.lost) {
|
|
3264
|
-
recordCancelled(task, "cancelled_mid_delivery");
|
|
3333
|
+
await recordCancelled(task, "cancelled_mid_delivery");
|
|
3265
3334
|
return;
|
|
3266
3335
|
}
|
|
3267
3336
|
await handleDeliveryFailure(
|
|
@@ -3284,7 +3353,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3284
3353
|
if (recurrenceType === "none") {
|
|
3285
3354
|
markLeaseReleased(task.id);
|
|
3286
3355
|
if (rowVanished(await db.deleteTaskById(task.id))) {
|
|
3287
|
-
recordCancelled(task, "cancelled_after_delivery");
|
|
3356
|
+
await recordCancelled(task, "cancelled_after_delivery");
|
|
3288
3357
|
return;
|
|
3289
3358
|
}
|
|
3290
3359
|
results.deletedOnceOffTasks++;
|
|
@@ -3298,7 +3367,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
3298
3367
|
...clearedPayload ? { encrypted_payload: clearedPayload } : {}
|
|
3299
3368
|
});
|
|
3300
3369
|
if (rowVanished(updated)) {
|
|
3301
|
-
recordCancelled(task, "cancelled_after_delivery");
|
|
3370
|
+
await recordCancelled(task, "cancelled_after_delivery");
|
|
3302
3371
|
return;
|
|
3303
3372
|
}
|
|
3304
3373
|
results.updatedRecurringTasks++;
|
|
@@ -3484,6 +3553,13 @@ function createUpdateMessageHandler(ctx) {
|
|
|
3484
3553
|
return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
|
|
3485
3554
|
}
|
|
3486
3555
|
const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
3556
|
+
if ((updates.apiUrl || updates.apiKey || updates.primaryModel) && hasChatCredRef(existingData)) {
|
|
3557
|
+
return { status: 409, body: { success: false, error: {
|
|
3558
|
+
code: "TASK_USES_CRED_REFS",
|
|
3559
|
+
message: "\u4EFB\u52A1\u5DF2\u901A\u8FC7 credRefs.chat \u5F15\u7528\u51ED\u636E\uFF0C\u89E6\u53D1\u65F6\u4EE5\u51ED\u636E\u8868\u4E3A\u51C6\uFF0C\u5185\u8054 apiUrl / apiKey / primaryModel \u7684\u66F4\u65B0\u4E0D\u4F1A\u751F\u6548\u3002\u6362 Key \u8BF7\u7528 PUT /llm-credentials \u8986\u76D6\u5BF9\u5E94\u51ED\u636E\uFF0C\u6216\u5728\u672C\u6B21\u8BF7\u6C42\u91CC\u6539\u7528 credRefs \u6307\u5411\u65B0\u51ED\u636E",
|
|
3560
|
+
details: { invalidFields: ["apiUrl", "apiKey", "primaryModel"].filter((name) => updates[name]) }
|
|
3561
|
+
} } };
|
|
3562
|
+
}
|
|
3487
3563
|
const promptUpdates = {};
|
|
3488
3564
|
if (updates.completePrompt) {
|
|
3489
3565
|
promptUpdates.completePrompt = updates.completePrompt;
|
|
@@ -4574,8 +4650,17 @@ var D1Adapter = class {
|
|
|
4574
4650
|
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
4575
4651
|
* newer overwrites. Values arrive pre-encrypted (the handler encrypts).
|
|
4576
4652
|
*
|
|
4653
|
+
* 例外是「来自未来」的行:`updated_at` 晚于服务端当前时间的行一律放行覆盖。
|
|
4654
|
+
* 比较值是客户端自己报的时间戳,设备时钟只要领先过真实时间(用户改过系统
|
|
4655
|
+
* 时间、时区或日期误操作),那一刻同步上来的行就带着一个还没到的时刻;之后
|
|
4656
|
+
* 这台设备发什么都比它「旧」,条件写全被无声跳过,云端那行要等真实时间追上
|
|
4657
|
+
* 去才解得开——客户端删本地数据、重装都碰不到它。合法写入不可能来自未来,
|
|
4658
|
+
* 所以这种行按脏数据处理:服务端的钟是可信的那一个,拿它当判据放行。一次
|
|
4659
|
+
* 正常写入就把 `updated_at` 拉回现实,之后旧不盖新照常生效。
|
|
4660
|
+
*
|
|
4577
4661
|
* `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
|
|
4578
|
-
*
|
|
4662
|
+
* 条件保证陈旧批次删不动更新写入的行(同样对未来时间戳的行放行,否则删一条
|
|
4663
|
+
* 状态时切片行留在库里成孤儿)。两种形态——
|
|
4579
4664
|
* - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
|
|
4580
4665
|
* 大值旧写入留下的切片行(见 lib/state-chunks.js);
|
|
4581
4666
|
* - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
|
|
@@ -4593,24 +4678,30 @@ var D1Adapter = class {
|
|
|
4593
4678
|
* @param {string} userId
|
|
4594
4679
|
* @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
|
|
4595
4680
|
* @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
|
|
4681
|
+
* @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
|
|
4682
|
+
* 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
|
|
4683
|
+
* 自定义调用方不传时退回本机时钟。
|
|
4596
4684
|
* @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
|
|
4597
4685
|
* `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
|
|
4598
4686
|
*/
|
|
4599
|
-
async upsertClientState(userId, entries, cleanups = []) {
|
|
4687
|
+
async upsertClientState(userId, entries, cleanups = [], now = Date.now()) {
|
|
4600
4688
|
const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
|
|
4601
4689
|
VALUES (?, ?, ?, ?, ?)
|
|
4602
4690
|
ON CONFLICT (user_id, namespace, key) DO UPDATE SET
|
|
4603
4691
|
value = excluded.value,
|
|
4604
4692
|
updated_at = excluded.updated_at
|
|
4605
|
-
WHERE excluded.updated_at >= client_state.updated_at
|
|
4693
|
+
WHERE excluded.updated_at >= client_state.updated_at
|
|
4694
|
+
OR client_state.updated_at > ?`;
|
|
4606
4695
|
const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
|
|
4607
|
-
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ?
|
|
4696
|
+
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ?
|
|
4697
|
+
AND (updated_at <= ? OR updated_at > ?)`;
|
|
4608
4698
|
const CLEANUP_KEY_SQL = `DELETE FROM client_state
|
|
4609
|
-
WHERE user_id = ? AND namespace = ? AND key = ?
|
|
4699
|
+
WHERE user_id = ? AND namespace = ? AND key = ?
|
|
4700
|
+
AND (updated_at <= ? OR updated_at > ?)`;
|
|
4610
4701
|
const buildStatements = () => [
|
|
4611
|
-
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt)),
|
|
4702
|
+
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)),
|
|
4612
4703
|
...entries.map(
|
|
4613
|
-
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
4704
|
+
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
|
|
4614
4705
|
)
|
|
4615
4706
|
];
|
|
4616
4707
|
let results;
|
|
@@ -4896,6 +4987,25 @@ var D1Adapter = class {
|
|
|
4896
4987
|
messageIds
|
|
4897
4988
|
);
|
|
4898
4989
|
}
|
|
4990
|
+
/**
|
|
4991
|
+
* 把某条任务名下还没发出去的行一次删掉(取消 / 顶替时的 outbox 清理)。
|
|
4992
|
+
*
|
|
4993
|
+
* 判据与 discardOutboxMessages 相同:只删 delivered_at 与 acked_at 均为
|
|
4994
|
+
* NULL 的行——已经推给设备 / 已 ack 的照旧留着。按 task_uuid 直删是为了不
|
|
4995
|
+
* 受未 ack 积压量的影响:靠翻页扫描挑行的话,积压一大这条任务的行就落在
|
|
4996
|
+
* 扫描上限之外(见 lib/outbox-store.js 的 discardUndeliveredPushesForTask)。
|
|
4997
|
+
*
|
|
4998
|
+
* @param {string} userId
|
|
4999
|
+
* @param {string} taskUuid
|
|
5000
|
+
* @returns {Promise<number>} 删掉的行数
|
|
5001
|
+
*/
|
|
5002
|
+
async discardUndeliveredOutboxForTask(userId, taskUuid) {
|
|
5003
|
+
const res = await this._db.prepare(
|
|
5004
|
+
`DELETE FROM message_outbox
|
|
5005
|
+
WHERE user_id = ? AND task_uuid = ? AND delivered_at IS NULL AND acked_at IS NULL`
|
|
5006
|
+
).bind(userId, taskUuid).run();
|
|
5007
|
+
return res.meta.changes || 0;
|
|
5008
|
+
}
|
|
4899
5009
|
/**
|
|
4900
5010
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
4901
5011
|
*
|
|
@@ -5213,7 +5323,7 @@ function createClientStateHandler(ctx) {
|
|
|
5213
5323
|
}
|
|
5214
5324
|
|
|
5215
5325
|
// src/server/lib/version.js
|
|
5216
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
5326
|
+
var SERVER_VERSION = true ? "2.6.0-next.25" : "0.0.0-dev";
|
|
5217
5327
|
|
|
5218
5328
|
// src/server/handlers/capabilities.js
|
|
5219
5329
|
var SERVER_FEATURES = Object.freeze([
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
var
|
|
29
|
+
var _chunkDZJHAAYScjs = require('./chunk-DZJHAAYS.cjs');
|
|
30
30
|
require('./chunk-7NMQFTDJ.cjs');
|
|
31
31
|
|
|
32
32
|
|
|
@@ -56,4 +56,4 @@ require('./chunk-7NMQFTDJ.cjs');
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
exports.DEFAULT_MAX_REQUEST_BODY_BYTES =
|
|
59
|
+
exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkDZJHAAYScjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkDZJHAAYScjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkDZJHAAYScjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkDZJHAAYScjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkDZJHAAYScjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkDZJHAAYScjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkDZJHAAYScjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkDZJHAAYScjs.advanceOccurrence; exports.createD1Adapter = _chunkDZJHAAYScjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkDZJHAAYScjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkDZJHAAYScjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkDZJHAAYScjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkDZJHAAYScjs.decryptFromStorage; exports.decryptPayload = _chunkDZJHAAYScjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkDZJHAAYScjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkDZJHAAYScjs.encryptForStorage; exports.ensureSchema = _chunkDZJHAAYScjs.ensureSchema; exports.getSchemaVersion = _chunkDZJHAAYScjs.getSchemaVersion; exports.isNonRetryableError = _chunkDZJHAAYScjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkDZJHAAYScjs.isValidTimeZoneId; exports.measurePushPayload = _chunkDZJHAAYScjs.measurePushPayload; exports.nextFutureOccurrence = _chunkDZJHAAYScjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkDZJHAAYScjs.planNextOccurrence; exports.readRequestBody = _chunkDZJHAAYScjs.readRequestBody; exports.runScheduledTick = _chunkDZJHAAYScjs.runScheduledTick; exports.runTask = _chunkDZJHAAYScjs.runTask; exports.summarizeErrorCause = _chunkDZJHAAYScjs.summarizeErrorCause;
|
package/dist/cloudflare.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
var
|
|
74
|
+
var _chunkDZJHAAYScjs = require('./chunk-DZJHAAYS.cjs');
|
|
75
75
|
require('./chunk-7NMQFTDJ.cjs');
|
|
76
76
|
|
|
77
77
|
// src/server/adapters/factory.js
|
|
@@ -105,7 +105,7 @@ async function createAdapter(config) {
|
|
|
105
105
|
// src/server/handlers/init-tenant.js
|
|
106
106
|
function createInitTenantHandler(ctx) {
|
|
107
107
|
async function POST(headers, body) {
|
|
108
|
-
const initSecret =
|
|
108
|
+
const initSecret = _chunkDZJHAAYScjs.getHeader.call(void 0, headers, "x-init-secret");
|
|
109
109
|
if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
|
|
110
110
|
return {
|
|
111
111
|
status: 401,
|
|
@@ -118,7 +118,7 @@ function createInitTenantHandler(ctx) {
|
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
-
const parsed =
|
|
121
|
+
const parsed = _chunkDZJHAAYScjs.parseJsonBody.call(void 0, body);
|
|
122
122
|
if (!parsed.ok) {
|
|
123
123
|
return {
|
|
124
124
|
status: 400,
|
|
@@ -132,7 +132,7 @@ function createInitTenantHandler(ctx) {
|
|
|
132
132
|
const tenantId = payload.tenantId || void 0;
|
|
133
133
|
const driver = payload.driver;
|
|
134
134
|
const databaseUrl = payload.databaseUrl || payload.connectionString;
|
|
135
|
-
if (tenantId && !
|
|
135
|
+
if (tenantId && !_chunkDZJHAAYScjs.isValidUUIDv4.call(void 0, tenantId)) {
|
|
136
136
|
return {
|
|
137
137
|
status: 400,
|
|
138
138
|
body: {
|
|
@@ -252,7 +252,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
252
252
|
}
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
|
-
const data = await
|
|
255
|
+
const data = await _chunkDZJHAAYScjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
|
|
256
256
|
return { status: 200, body: { success: true, data } };
|
|
257
257
|
}
|
|
258
258
|
return { POST };
|
|
@@ -274,16 +274,16 @@ function encryptConfig(config, kekBuffer) {
|
|
|
274
274
|
const plaintext = Buffer.from(JSON.stringify(config), "utf8");
|
|
275
275
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
276
276
|
const authTag = cipher.getAuthTag();
|
|
277
|
-
return `v1.${
|
|
277
|
+
return `v1.${_chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, iv)}.${_chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, ciphertext)}`;
|
|
278
278
|
}
|
|
279
279
|
function decryptConfig(encrypted, kekBuffer) {
|
|
280
280
|
const parts = String(encrypted || "").split(".");
|
|
281
281
|
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
282
282
|
throw new Error("INVALID_TENANT_CONFIG");
|
|
283
283
|
}
|
|
284
|
-
const iv =
|
|
285
|
-
const authTag =
|
|
286
|
-
const ciphertext =
|
|
284
|
+
const iv = _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, parts[1]);
|
|
285
|
+
const authTag = _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, parts[2]);
|
|
286
|
+
const ciphertext = _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, parts[3]);
|
|
287
287
|
const decipher = _crypto.createDecipheriv.call(void 0, "aes-256-gcm", kekBuffer, iv);
|
|
288
288
|
decipher.setAuthTag(authTag);
|
|
289
289
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -351,7 +351,7 @@ function createTenantBlobStore(options) {
|
|
|
351
351
|
// src/server/tenant/token.js
|
|
352
352
|
|
|
353
353
|
function sign(input, secret) {
|
|
354
|
-
return
|
|
354
|
+
return _chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, _crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
|
|
355
355
|
}
|
|
356
356
|
function nowEpochSeconds() {
|
|
357
357
|
return Math.floor(Date.now() / 1e3);
|
|
@@ -367,8 +367,8 @@ function createTenantToken(params, secret) {
|
|
|
367
367
|
v: 1
|
|
368
368
|
};
|
|
369
369
|
const header = { alg: "HS256", typ: "JWT" };
|
|
370
|
-
const encodedHeader =
|
|
371
|
-
const encodedPayload =
|
|
370
|
+
const encodedHeader = _chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, _chunkDZJHAAYScjs.utf8.call(void 0, JSON.stringify(header)));
|
|
371
|
+
const encodedPayload = _chunkDZJHAAYScjs.bytesToBase64Url.call(void 0, _chunkDZJHAAYScjs.utf8.call(void 0, JSON.stringify(payload)));
|
|
372
372
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
373
373
|
const signature = sign(signingInput, secret);
|
|
374
374
|
return `${signingInput}.${signature}`;
|
|
@@ -387,17 +387,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
387
387
|
let receivedBytes;
|
|
388
388
|
let expectedBytes;
|
|
389
389
|
try {
|
|
390
|
-
receivedBytes =
|
|
391
|
-
expectedBytes =
|
|
390
|
+
receivedBytes = _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, receivedSignature);
|
|
391
|
+
expectedBytes = _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, expectedSignature);
|
|
392
392
|
} catch (e) {
|
|
393
393
|
throw new Error("INVALID_TENANT_AUTH");
|
|
394
394
|
}
|
|
395
|
-
if (!
|
|
395
|
+
if (!_chunkDZJHAAYScjs.timingSafeEqualBytes.call(void 0, receivedBytes, expectedBytes)) {
|
|
396
396
|
throw new Error("INVALID_TENANT_AUTH");
|
|
397
397
|
}
|
|
398
398
|
let payload;
|
|
399
399
|
try {
|
|
400
|
-
payload = JSON.parse(
|
|
400
|
+
payload = JSON.parse(_chunkDZJHAAYScjs.utf8Decode.call(void 0, _chunkDZJHAAYScjs.base64UrlToBytes.call(void 0, encodedPayload)));
|
|
401
401
|
} catch (e2) {
|
|
402
402
|
throw new Error("INVALID_TENANT_AUTH");
|
|
403
403
|
}
|
|
@@ -621,15 +621,15 @@ async function createReiServer(config) {
|
|
|
621
621
|
return {
|
|
622
622
|
handlers: {
|
|
623
623
|
initTenant: createInitTenantHandler(ctx),
|
|
624
|
-
getUserKey:
|
|
625
|
-
scheduleMessage:
|
|
624
|
+
getUserKey: _chunkDZJHAAYScjs.createGetUserKeyHandler.call(void 0, ctx),
|
|
625
|
+
scheduleMessage: _chunkDZJHAAYScjs.createScheduleMessageHandler.call(void 0, ctx),
|
|
626
626
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
627
|
-
updateMessage:
|
|
628
|
-
cancelMessage:
|
|
629
|
-
messages:
|
|
630
|
-
getMessage:
|
|
631
|
-
pushSubscription:
|
|
632
|
-
llmCredentials:
|
|
627
|
+
updateMessage: _chunkDZJHAAYScjs.createUpdateMessageHandler.call(void 0, ctx),
|
|
628
|
+
cancelMessage: _chunkDZJHAAYScjs.createCancelMessageHandler.call(void 0, ctx),
|
|
629
|
+
messages: _chunkDZJHAAYScjs.createMessagesHandler.call(void 0, ctx),
|
|
630
|
+
getMessage: _chunkDZJHAAYScjs.createGetMessageHandler.call(void 0, ctx),
|
|
631
|
+
pushSubscription: _chunkDZJHAAYScjs.createPushSubscriptionHandler.call(void 0, ctx),
|
|
632
|
+
llmCredentials: _chunkDZJHAAYScjs.createLlmCredentialsHandler.call(void 0, ctx)
|
|
633
633
|
}
|
|
634
634
|
};
|
|
635
635
|
}
|
|
@@ -695,4 +695,4 @@ async function createReiServer(config) {
|
|
|
695
695
|
|
|
696
696
|
|
|
697
697
|
|
|
698
|
-
exports.CORS_ALLOW_HEADERS =
|
|
698
|
+
exports.CORS_ALLOW_HEADERS = _chunkDZJHAAYScjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkDZJHAAYScjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkDZJHAAYScjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkDZJHAAYScjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkDZJHAAYScjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkDZJHAAYScjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkDZJHAAYScjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkDZJHAAYScjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkDZJHAAYScjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkDZJHAAYScjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkDZJHAAYScjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkDZJHAAYScjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkDZJHAAYScjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.DeploymentConfigError = _chunkDZJHAAYScjs.DeploymentConfigError; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkDZJHAAYScjs.MAX_PUSH_PAYLOAD_BYTES; exports.MAX_TASK_PAYLOAD_BYTES = _chunkDZJHAAYScjs.MAX_TASK_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkDZJHAAYScjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkDZJHAAYScjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkDZJHAAYScjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkDZJHAAYScjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkDZJHAAYScjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkDZJHAAYScjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkDZJHAAYScjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkDZJHAAYScjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkDZJHAAYScjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkDZJHAAYScjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkDZJHAAYScjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkDZJHAAYScjs.decryptFromStorage; exports.decryptPayload = _chunkDZJHAAYScjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkDZJHAAYScjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkDZJHAAYScjs.encryptForStorage; exports.ensureSchema = _chunkDZJHAAYScjs.ensureSchema; exports.getSchemaVersion = _chunkDZJHAAYScjs.getSchemaVersion; exports.hasChatCredRef = _chunkDZJHAAYScjs.hasChatCredRef; exports.isNonRetryableError = _chunkDZJHAAYScjs.isNonRetryableError; exports.isValidCredId = _chunkDZJHAAYScjs.isValidCredId; exports.isValidISO8601 = _chunkDZJHAAYScjs.isValidISO8601; exports.isValidTimeZoneId = _chunkDZJHAAYScjs.isValidTimeZoneId; exports.isValidUUID = _chunkDZJHAAYScjs.isValidUUID; exports.isValidUUIDv4 = _chunkDZJHAAYScjs.isValidUUIDv4; exports.isValidUrl = _chunkDZJHAAYScjs.isValidUrl; exports.measurePushPayload = _chunkDZJHAAYScjs.measurePushPayload; exports.nextFutureOccurrence = _chunkDZJHAAYScjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkDZJHAAYScjs.planNextOccurrence; exports.readRequestBody = _chunkDZJHAAYScjs.readRequestBody; exports.resolveLlmCredential = _chunkDZJHAAYScjs.resolveLlmCredential; exports.runScheduledTick = _chunkDZJHAAYScjs.runScheduledTick; exports.runTask = _chunkDZJHAAYScjs.runTask; exports.sanitizeErrorSummary = _chunkDZJHAAYScjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkDZJHAAYScjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkDZJHAAYScjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkDZJHAAYScjs.validateAvatarUrl; exports.validateCredRefs = _chunkDZJHAAYScjs.validateCredRefs; exports.validateCredValue = _chunkDZJHAAYScjs.validateCredValue; exports.validateLlmMessagesArray = _chunkDZJHAAYScjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkDZJHAAYScjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkDZJHAAYScjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
package/dist/index.mjs
CHANGED
|
@@ -29,7 +29,9 @@ export function planClientStateCleanup(ttl: Record<string, number> | null | unde
|
|
|
29
29
|
* - 删除:根行按精确 key 删,切片行按 key 前缀删,一个 key 的数据清干净。
|
|
30
30
|
*
|
|
31
31
|
* 两者都受同一套 last-write-wins 约束:`updatedAt` 比库里已有值旧的写入
|
|
32
|
-
*
|
|
32
|
+
* (或删除)不生效,陈旧批次盖不掉新数据。唯一的例外是库里那行「来自未来」
|
|
33
|
+
* (`updated_at` 晚于服务端当前时刻)——那种行只可能出自跑偏的设备时钟,不当
|
|
34
|
+
* 比较基准,写入放行,详见 adapters/d1.js 的 `upsertClientState`。
|
|
33
35
|
*
|
|
34
36
|
* 条件写护栏:条目可带可选的 `version`(毫秒时间戳或单调递增整数)。带了它,
|
|
35
37
|
* 比较用的就是这个值而不是 `updatedAt`——同一个 key 有多个写入方时(例如
|
|
@@ -48,12 +50,14 @@ export function planClientStateCleanup(ttl: Record<string, number> | null | unde
|
|
|
48
50
|
* @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number, version?: number }>} args.entries
|
|
49
51
|
* `version`(可选):条件写护栏值,见文件头。带了它,这条的 last-write-wins
|
|
50
52
|
* 比较用它(写进行的 updated_at 列);没带照旧用 `updatedAt`。
|
|
53
|
+
* @param {() => number} [args.now] - 取服务端当前时刻(测试可注入假时钟)。适配器
|
|
54
|
+
* 拿它认出库里「来自未来」的脏行。
|
|
51
55
|
* @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
|
|
52
56
|
* `upserted` / `skipped` 按逻辑条目计(切片行不计);`deleted` 是请求删除的
|
|
53
57
|
* key 数,不代表这些 key 原本一定存在。`skippedEntries` 逐条列出被
|
|
54
58
|
* last-write-wins 拦下的 key(适配器不回 outcomes 时为空数组——分不清是哪条)。
|
|
55
59
|
*/
|
|
56
|
-
export function writeClientStateEntries({ db, userId, userKey, entries }: {
|
|
60
|
+
export function writeClientStateEntries({ db, userId, userKey, entries, now }: {
|
|
57
61
|
db: {
|
|
58
62
|
upsertClientState: Function;
|
|
59
63
|
};
|
|
@@ -66,6 +70,7 @@ export function writeClientStateEntries({ db, userId, userKey, entries }: {
|
|
|
66
70
|
updatedAt: number;
|
|
67
71
|
version?: number;
|
|
68
72
|
}>;
|
|
73
|
+
now?: () => number;
|
|
69
74
|
}): Promise<{
|
|
70
75
|
upserted: number;
|
|
71
76
|
skipped: number;
|
|
@@ -70,16 +70,21 @@ export function discardUndeliveredPushes({ db, userId, pushes, sentIds }: {
|
|
|
70
70
|
/**
|
|
71
71
|
* 把某条任务名下「还没发出去的」行从 outbox 撤掉。
|
|
72
72
|
*
|
|
73
|
-
* 用在取消 /
|
|
74
|
-
*
|
|
75
|
-
* outbox
|
|
76
|
-
* `GET /outbox`
|
|
77
|
-
*
|
|
73
|
+
* 用在取消 / 顶替之后的每条收尾路上(`DELETE /message`、`supersedesUuid`、
|
|
74
|
+
* fire 内的 cancelTask、投递侧发现行已被取消):任务此前投递到一半失败过的话,
|
|
75
|
+
* 没发出去的那几段还躺在 outbox 里等重试,任务行删掉它们也不会跟着走。不撤的
|
|
76
|
+
* 话客户端下一次 `GET /outbox` 照样把它们补收回去——用户看到的就是「取消接口回
|
|
77
|
+
* 了成功,消息还是来了」。
|
|
78
78
|
*
|
|
79
79
|
* 判据与 discardUndeliveredPushes 一致:只撤 delivered_at 为 null 的行。已经推
|
|
80
80
|
* 给设备的那几条撤不回来,行留着让客户端照常 ack——取消的意思是「别再发后面
|
|
81
81
|
* 的」,不是「把已经收到的从收件箱里抹掉」。
|
|
82
82
|
*
|
|
83
|
+
* 优先走适配器的按任务删除(discardUndeliveredOutboxForTask):一个来回,且不
|
|
84
|
+
* 受未 ack 积压量的影响。没有这个方法的适配器(宿主自带的旧实现)退回翻页扫
|
|
85
|
+
* 描——被取消任务的行通常是最新的,积压超过扫描上限时正好扫不到它们,所以扫到
|
|
86
|
+
* 上限还没扫完必须吵出来,不能装作清干净了。
|
|
87
|
+
*
|
|
83
88
|
* 同样是 best-effort:适配器缺读/删任一侧就静默跳过,出错只记日志。取消 / 顶
|
|
84
89
|
* 替本身已经生效了,不该因为账本没清干净被翻成失败。
|
|
85
90
|
*
|
|
@@ -25,9 +25,14 @@
|
|
|
25
25
|
* @param {number|null} args.occurrenceMs
|
|
26
26
|
* @param {{ sendNotification: Function }|null} args.webpush
|
|
27
27
|
* @param {() => number} [args.now] - 取当前时刻(测试可注入假时钟)
|
|
28
|
+
* @param {(() => boolean)|null} [args.isCancelled] - 「这条任务是否已在投递期间
|
|
29
|
+
* 被取消 / 顶替」(run-tick 的租约心跳信号)。推送那条路的取消检查在
|
|
30
|
+
* webpush 那层(guardWebpushWithLease),但 `notification.show: false` 的结
|
|
31
|
+
* 果不发推送,只有这个显式读法能拦得住它——不给的话那条路上没有任何取消检
|
|
32
|
+
* 查点。
|
|
28
33
|
* @returns {ResultEmitter}
|
|
29
34
|
*/
|
|
30
|
-
export function createResultEmitter({ db, task, userKey, decryptedPayload, messageIdBase, sessionId, occurrenceMs, webpush, now, }: {
|
|
35
|
+
export function createResultEmitter({ db, task, userKey, decryptedPayload, messageIdBase, sessionId, occurrenceMs, webpush, now, isCancelled, }: {
|
|
31
36
|
db: import("../adapters/interface.js").DbAdapter;
|
|
32
37
|
task: any;
|
|
33
38
|
userKey: string;
|
|
@@ -39,6 +44,7 @@ export function createResultEmitter({ db, task, userKey, decryptedPayload, messa
|
|
|
39
44
|
sendNotification: Function;
|
|
40
45
|
} | null;
|
|
41
46
|
now?: () => number;
|
|
47
|
+
isCancelled?: (() => boolean) | null;
|
|
42
48
|
}): ResultEmitter;
|
|
43
49
|
export type ResultEmitter = {
|
|
44
50
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.6.0-next.
|
|
3
|
+
"version": "2.6.0-next.25",
|
|
4
4
|
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@netlify/blobs": "^8.1.0",
|
|
42
|
-
"@rei-standard/amsg-shared": "^0.4.0-next.
|
|
42
|
+
"@rei-standard/amsg-shared": "^0.4.0-next.9",
|
|
43
43
|
"web-push": "^3.6.7"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|