@rei-standard/amsg-server 2.6.0-next.20 → 2.6.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -7
- 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-V6NZQ22S.cjs → chunk-7NMQFTDJ.cjs} +5 -1
- package/dist/{chunk-5FXVSC5O.mjs → chunk-GN44PST5.mjs} +5 -1
- package/dist/{chunk-HTGYGGUH.mjs → chunk-KOJQVOYS.mjs} +1125 -276
- package/dist/{chunk-WOXSQBAM.cjs → chunk-QHVID3V2.cjs} +1114 -265
- 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-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
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
4
4
|
* @property {Object} vapid - { email, publicKey, privateKey }
|
|
5
5
|
* @property {import('../adapters/interface.js').DbAdapter} db
|
|
6
|
+
* @property {{ maxChunkBytes?: number, maxChunks?: number, maxTotalBytes?: number, ttlMs?: number }} [multipart]
|
|
7
|
+
* 分片传输的限额,宿主传给 `installReiSW` 的那一份原样传过来即可(见
|
|
8
|
+
* resolveMultipartOptions)。不传 = 两边都用默认值。
|
|
6
9
|
*/
|
|
7
10
|
/**
|
|
8
11
|
* Process a single database task row: decrypt → generate content → push.
|
|
@@ -34,12 +37,16 @@ export function processSingleMessage(task: import("../adapters/interface.js").Ta
|
|
|
34
37
|
* @param {number} [maxRetries=2]
|
|
35
38
|
* @param {string} [userId]
|
|
36
39
|
* @param {string} [providedMasterKey]
|
|
37
|
-
* @returns {Promise<{ success: boolean, messagesSent?: number, retriesUsed?: number, error?: Object }>}
|
|
40
|
+
* @returns {Promise<{ success: boolean, messagesSent?: number, retriesUsed?: number, reasoningError?: string, error?: Object }>}
|
|
41
|
+
* `reasoningError` 只在正文都发出去了、思考过程那一条没发成时出现(思考过程是
|
|
42
|
+
* 附赠内容,它发不出去不算这条消息失败)。调用方拿它提示用户这次没有思考过程,
|
|
43
|
+
* 不带这个字段就是整轮都送到了。
|
|
38
44
|
*/
|
|
39
45
|
export function processMessagesByUuid(uuid: string, ctx: ProcessorContext, maxRetries?: number, userId?: string, providedMasterKey?: string): Promise<{
|
|
40
46
|
success: boolean;
|
|
41
47
|
messagesSent?: number;
|
|
42
48
|
retriesUsed?: number;
|
|
49
|
+
reasoningError?: string;
|
|
43
50
|
error?: any;
|
|
44
51
|
}>;
|
|
45
52
|
export { normalizeAiApiUrl } from "./llm.js";
|
|
@@ -53,4 +60,14 @@ export type ProcessorContext = {
|
|
|
53
60
|
*/
|
|
54
61
|
vapid: any;
|
|
55
62
|
db: import("../adapters/interface.js").DbAdapter;
|
|
63
|
+
/**
|
|
64
|
+
* 分片传输的限额,宿主传给 `installReiSW` 的那一份原样传过来即可(见
|
|
65
|
+
* resolveMultipartOptions)。不传 = 两边都用默认值。
|
|
66
|
+
*/
|
|
67
|
+
multipart?: {
|
|
68
|
+
maxChunkBytes?: number;
|
|
69
|
+
maxChunks?: number;
|
|
70
|
+
maxTotalBytes?: number;
|
|
71
|
+
ttlMs?: number;
|
|
72
|
+
};
|
|
56
73
|
};
|
|
@@ -18,6 +18,18 @@ export function appendPushesToOutbox({ db, userId, userKey, pushes }: {
|
|
|
18
18
|
userKey: CryptoKey | string;
|
|
19
19
|
pushes: any[];
|
|
20
20
|
}): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* 把 push 对象转成 message_outbox 的行(payload 加密)。
|
|
23
|
+
*
|
|
24
|
+
* 抽出来是给 `ctx.emitResult()` 用的:它落的行要跟推送链路落的完全同构——
|
|
25
|
+
* 列一样、加密方式一样、身份字段的取法一样,客户端补收时才不用区分是谁写的。
|
|
26
|
+
*
|
|
27
|
+
* @param {Object[]} pushes - 定稿后的 push 对象
|
|
28
|
+
* @param {CryptoKey|string} userKey
|
|
29
|
+
* @param {number} createdAt - epoch 毫秒
|
|
30
|
+
* @returns {Promise<Object[]>}
|
|
31
|
+
*/
|
|
32
|
+
export function toOutboxRows(pushes: any[], userKey: CryptoKey | string, createdAt: number): Promise<any[]>;
|
|
21
33
|
/**
|
|
22
34
|
* 把发出去的那部分标成 delivered(发送半途失败时只标已发出的段)。
|
|
23
35
|
*
|
|
@@ -31,3 +43,53 @@ export function markPushesDelivered({ db, userId, messageIds }: {
|
|
|
31
43
|
userId: string;
|
|
32
44
|
messageIds: string[];
|
|
33
45
|
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 把这一批 push 里「没发出去的那些」从 outbox 撤掉。
|
|
48
|
+
*
|
|
49
|
+
* 取消撞上投递时用:整批 push 在开发之前就落进了 outbox,取消只拦住了 Web
|
|
50
|
+
* Push 这一路,剩下的行不撤掉,客户端下一次 GET /outbox 会照样把它们拉回去
|
|
51
|
+
* ——用户看到的就是「取消接口回了成功,消息还是来了」。
|
|
52
|
+
*
|
|
53
|
+
* 「哪些算没发出去」这条判据收在这里:投递侧只要把整批 push 和已发出的
|
|
54
|
+
* messageId 交过来就行。
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} args
|
|
57
|
+
* @param {Object} args.db
|
|
58
|
+
* @param {string} args.userId
|
|
59
|
+
* @param {Array<{ messageId: string }>} args.pushes - 落进 outbox 的整批 push
|
|
60
|
+
* @param {string[]} args.sentIds - 已经发出去的 messageId
|
|
61
|
+
*/
|
|
62
|
+
export function discardUndeliveredPushes({ db, userId, pushes, sentIds }: {
|
|
63
|
+
db: any;
|
|
64
|
+
userId: string;
|
|
65
|
+
pushes: Array<{
|
|
66
|
+
messageId: string;
|
|
67
|
+
}>;
|
|
68
|
+
sentIds: string[];
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* 把某条任务名下「还没发出去的」行从 outbox 撤掉。
|
|
72
|
+
*
|
|
73
|
+
* 用在取消 / 顶替只碰了任务行的那两条路上(`DELETE /message` 与
|
|
74
|
+
* `supersedesUuid`):任务此前投递到一半失败过的话,没发出去的那几段还躺在
|
|
75
|
+
* outbox 里等重试,任务行删掉它们也不会跟着走。不撤的话客户端下一次
|
|
76
|
+
* `GET /outbox` 照样把它们补收回去——用户看到的就是「取消接口回了成功,消息还
|
|
77
|
+
* 是来了」。
|
|
78
|
+
*
|
|
79
|
+
* 判据与 discardUndeliveredPushes 一致:只撤 delivered_at 为 null 的行。已经推
|
|
80
|
+
* 给设备的那几条撤不回来,行留着让客户端照常 ack——取消的意思是「别再发后面
|
|
81
|
+
* 的」,不是「把已经收到的从收件箱里抹掉」。
|
|
82
|
+
*
|
|
83
|
+
* 同样是 best-effort:适配器缺读/删任一侧就静默跳过,出错只记日志。取消 / 顶
|
|
84
|
+
* 替本身已经生效了,不该因为账本没清干净被翻成失败。
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} args
|
|
87
|
+
* @param {Object} args.db
|
|
88
|
+
* @param {string} args.userId
|
|
89
|
+
* @param {string} args.taskUuid - 被取消 / 被顶替的任务 uuid
|
|
90
|
+
*/
|
|
91
|
+
export function discardUndeliveredPushesForTask({ db, userId, taskUuid }: {
|
|
92
|
+
db: any;
|
|
93
|
+
userId: string;
|
|
94
|
+
taskUuid: string;
|
|
95
|
+
}): Promise<void>;
|
|
@@ -28,9 +28,32 @@ export function savePushSubscription({ db, userId, userKey, subscription, update
|
|
|
28
28
|
}): Promise<{
|
|
29
29
|
updatedAt: number;
|
|
30
30
|
}>;
|
|
31
|
+
/**
|
|
32
|
+
* 把一行 push_subscriptions 解成明文订阅。行不在、或订阅列是空的 → null;
|
|
33
|
+
* 密文解不开(换过 masterKey 之类)、解出来不是 JSON → 抛。
|
|
34
|
+
*
|
|
35
|
+
* 单独拎出来是为了让调用方分得开两类失败:`db.getPushSubscription()` 抛出来
|
|
36
|
+
* 的是基础设施问题(表没建、读超时),换个时候重试有救;这里抛出来的是这一
|
|
37
|
+
* 行的密文废了,重试多少次都一样。两者压在同一个 try 里就只能一起处理,读不
|
|
38
|
+
* 到库会被当成「这个用户没登记过」。
|
|
39
|
+
*
|
|
40
|
+
* @param {{ subscription?: unknown, updated_at?: unknown }|null|undefined} row - `getPushSubscription()` 读回的行
|
|
41
|
+
* @param {string} userKey
|
|
42
|
+
* @returns {Promise<{ subscription: Object, updatedAt: number|null }|null>}
|
|
43
|
+
*/
|
|
44
|
+
export function decodePushSubscriptionRow(row: {
|
|
45
|
+
subscription?: unknown;
|
|
46
|
+
updated_at?: unknown;
|
|
47
|
+
} | null | undefined, userKey: string): Promise<{
|
|
48
|
+
subscription: any;
|
|
49
|
+
updatedAt: number | null;
|
|
50
|
+
} | null>;
|
|
31
51
|
/**
|
|
32
52
|
* 读回这个用户的订阅(解密后的明文对象)。没有登记过 → null。
|
|
33
53
|
*
|
|
54
|
+
* 读库失败和解密失败都原样抛出去:投递链路上这两种都得让任务失败,分不分得
|
|
55
|
+
* 开无所谓;要分开处理的调用方(GET /push-subscription)自己走上面两步。
|
|
56
|
+
*
|
|
34
57
|
* @param {Object} args
|
|
35
58
|
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
36
59
|
* @param {string} args.userId
|
package/dist/lib/request.d.ts
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把请求正文读成字符串,`Content-Encoding: gzip` 的在这一步还原。
|
|
3
|
+
*
|
|
4
|
+
* 客户端把大 body(一次 fire_pack、一整批 client_state)压了再传能省下几倍
|
|
5
|
+
* 传输量,代价只有服务端这一次解压。所有带 body 的端点走同一个入口,谁都不用
|
|
6
|
+
* 各自判一遍这个头。
|
|
7
|
+
*
|
|
8
|
+
* 说是 gzip、字节却不是 gzip 开头时按明文处理:有些边缘网关会替你把请求体解
|
|
9
|
+
* 开却留着原来的 `Content-Encoding` 头,再解一次只会解出乱码。魔数判断两种情
|
|
10
|
+
* 况都接得住。
|
|
11
|
+
*
|
|
12
|
+
* 只认 gzip。deflate / br 回 415 而不是猜着解——猜错解出来的是一段乱码,会变
|
|
13
|
+
* 成一句让人找不着北的「请求体不是有效的 JSON」。
|
|
14
|
+
*
|
|
15
|
+
* @param {{ headers?: { get?: (name: string) => string|null }, text: () => Promise<string>, arrayBuffer: () => Promise<ArrayBuffer> }} request
|
|
16
|
+
* Fetch API 的 Request(或形状相同的对象)。
|
|
17
|
+
* @param {{ maxBytes?: number }} [options] - `maxBytes`:解压后的字节上限,
|
|
18
|
+
* 默认 {@link DEFAULT_MAX_REQUEST_BODY_BYTES}。
|
|
19
|
+
* @returns {Promise<{ ok: true, body: string } | { ok: false, error: { status: number, body: Object } }>}
|
|
20
|
+
* `error` 就是可以直接回给调用方的那个信封(与 handler 的返回同构)。
|
|
21
|
+
*/
|
|
22
|
+
export function readRequestBody(request: {
|
|
23
|
+
headers?: {
|
|
24
|
+
get?: (name: string) => string | null;
|
|
25
|
+
};
|
|
26
|
+
text: () => Promise<string>;
|
|
27
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
28
|
+
}, options?: {
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
ok: true;
|
|
32
|
+
body: string;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
error: {
|
|
36
|
+
status: number;
|
|
37
|
+
body: any;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
1
40
|
/**
|
|
2
41
|
* @typedef {{ code: string, message: string }} ValidationError
|
|
3
42
|
*/
|
|
@@ -118,6 +157,14 @@ export namespace REQUEST_ERRORS {
|
|
|
118
157
|
export { message_2 as message };
|
|
119
158
|
}
|
|
120
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* 解压后请求体的字节上限(默认 32MB)。
|
|
162
|
+
*
|
|
163
|
+
* 只管 gzip 那条路:压缩数据能用很小的体积展开成极大的正文(几百 KB 换几个
|
|
164
|
+
* GB),不设上限等于把内存交给调用方决定。不压缩的请求体不受这个数约束,行为
|
|
165
|
+
* 与以前一致——那条路的体积本来就摆在传输量上,平台自己的请求大小限制盖得住。
|
|
166
|
+
*/
|
|
167
|
+
export const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
|
|
121
168
|
export type ValidationError = {
|
|
122
169
|
code: string;
|
|
123
170
|
message: string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ResultEmitter
|
|
3
|
+
* @property {(payload: Object) => Promise<{ messageId: string, pushed: boolean }>} emitResult
|
|
4
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
5
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
6
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
7
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 造一份作用于某条任务的结果出口。
|
|
11
|
+
*
|
|
12
|
+
* 身份字段(`taskId` / `taskUuid` / `recurrenceType` / `occurrenceMs` /
|
|
13
|
+
* `messageKind`)由库覆盖写:它们描述的是这条任务行的事实,不是内容。
|
|
14
|
+
* `messageId` / `sessionId` / `timestamp` 宿主没给才补——`messageId` 的缺省
|
|
15
|
+
* 值掺了任务 id 与本次名义触发时刻,同一次触发重试时组出同一个 id,收件箱靠
|
|
16
|
+
* `(user_id, message_id)` 唯一约束天然去重,不会补出第二条。
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} args
|
|
19
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
20
|
+
* @param {Object} args.task - 数据库任务行
|
|
21
|
+
* @param {string} args.userKey - per-user 存储密钥
|
|
22
|
+
* @param {Object} args.decryptedPayload - 解密后的任务 payload(取 messageType / recurrenceType / 老任务里的订阅)
|
|
23
|
+
* @param {string} args.messageIdBase - messageId 的前缀(与推送链路同一份)
|
|
24
|
+
* @param {string} args.sessionId
|
|
25
|
+
* @param {number|null} args.occurrenceMs
|
|
26
|
+
* @param {{ sendNotification: Function }|null} args.webpush
|
|
27
|
+
* @param {() => number} [args.now] - 取当前时刻(测试可注入假时钟)
|
|
28
|
+
* @returns {ResultEmitter}
|
|
29
|
+
*/
|
|
30
|
+
export function createResultEmitter({ db, task, userKey, decryptedPayload, messageIdBase, sessionId, occurrenceMs, webpush, now, }: {
|
|
31
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
32
|
+
task: any;
|
|
33
|
+
userKey: string;
|
|
34
|
+
decryptedPayload: any;
|
|
35
|
+
messageIdBase: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
occurrenceMs: number | null;
|
|
38
|
+
webpush: {
|
|
39
|
+
sendNotification: Function;
|
|
40
|
+
} | null;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}): ResultEmitter;
|
|
43
|
+
export type ResultEmitter = {
|
|
44
|
+
/**
|
|
45
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
46
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
47
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
48
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
49
|
+
*/
|
|
50
|
+
emitResult: (payload: any) => Promise<{
|
|
51
|
+
messageId: string;
|
|
52
|
+
pushed: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
package/dist/lib/run-tick.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export function runScheduledTick(ctx: any): Promise<{
|
|
|
24
24
|
deletedOnceOffTasks: number;
|
|
25
25
|
updatedRecurringTasks: number;
|
|
26
26
|
staleTasks: any[];
|
|
27
|
+
cancelledTasks: any[];
|
|
28
|
+
reasoningSkippedTasks: any[];
|
|
27
29
|
failedTasks: any[];
|
|
28
30
|
};
|
|
29
31
|
}>;
|
|
@@ -102,5 +104,12 @@ export type StaleSkipInfo = {
|
|
|
102
104
|
nextSendAt: string | null;
|
|
103
105
|
readState: (namespace: string) => Promise<any[]>;
|
|
104
106
|
writeState: (namespace: string, entries: any[]) => Promise<any>;
|
|
107
|
+
/**
|
|
108
|
+
* 往客户端补一条自定义结果(落收件箱 + 推送,见 lib/result-emitter.js)。
|
|
109
|
+
*/
|
|
110
|
+
emitResult: (payload: any) => Promise<{
|
|
111
|
+
messageId: string;
|
|
112
|
+
pushed: boolean;
|
|
113
|
+
}>;
|
|
105
114
|
};
|
|
106
115
|
import { sanitizeErrorSummary } from './errors.js';
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* @property {string|null} clientTaskId - 取自 metadata.amsgClientTaskId
|
|
27
27
|
* @property {Object|null} credRefs - 凭据引用({ <purpose>: <cred_id> });引用不是机密,客户端对账要看。没带 → null
|
|
28
28
|
* @property {Object|null} [metadata] - 只有 includeMetadata 时才有,见下
|
|
29
|
-
* @property {{ at: string, occurrence: string, reason: string, pushStatus?: number }|null} lastError
|
|
30
|
-
* `
|
|
29
|
+
* @property {{ at: string, occurrence: string, reason: string, errorCode?: string, pushStatus?: number }|null} lastError
|
|
30
|
+
* `errorCode` 是底层错误的稳定 code(如 `PUSH_PAYLOAD_TOO_LARGE`),拿得到就
|
|
31
|
+
* 带上;`pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP
|
|
32
|
+
* 状态码。判断该怎么处置读这两个字段,别去正则匹配 `reason`。
|
|
31
33
|
*/
|
|
32
34
|
/**
|
|
33
35
|
* @param {Object} row - 数据库任务行
|
|
@@ -76,12 +78,15 @@ export type TaskProjection = {
|
|
|
76
78
|
*/
|
|
77
79
|
metadata?: any | null;
|
|
78
80
|
/**
|
|
79
|
-
* `
|
|
81
|
+
* `errorCode` 是底层错误的稳定 code(如 `PUSH_PAYLOAD_TOO_LARGE`),拿得到就
|
|
82
|
+
* 带上;`pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP
|
|
83
|
+
* 状态码。判断该怎么处置读这两个字段,别去正则匹配 `reason`。
|
|
80
84
|
*/
|
|
81
85
|
lastError: {
|
|
82
86
|
at: string;
|
|
83
87
|
occurrence: string;
|
|
84
88
|
reason: string;
|
|
89
|
+
errorCode?: string;
|
|
85
90
|
pushStatus?: number;
|
|
86
91
|
} | null;
|
|
87
92
|
};
|
package/dist/lib/validation.d.ts
CHANGED
|
@@ -46,6 +46,36 @@ export function validateSplitPattern(value: unknown): string | null;
|
|
|
46
46
|
* @returns {string | null} Error message, or null if valid.
|
|
47
47
|
*/
|
|
48
48
|
export function validateLlmMessagesArray(messages: unknown): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* 落库前量一次任务正文。合法返回 null,超限返回可以直接放进 400 响应体的
|
|
51
|
+
* error 对象——`POST /schedule-message` 与 `PUT /update-message` 共用同一个错
|
|
52
|
+
* 误码和同一套 details,下游判断读 `details.bytes` / `details.maxBytes`,不用
|
|
53
|
+
* 去解析那句人话。
|
|
54
|
+
*
|
|
55
|
+
* 按 UTF-8 字节算而不是字符数:D1 限的是字节,加密前也是先转 UTF-8,一段全中
|
|
56
|
+
* 文的正文字符数只有字节数的三分之一,用 `.length` 量等于把上限放大三倍。
|
|
57
|
+
*
|
|
58
|
+
* @param {string} serializedPayload - 将要交给 encryptForStorage 的那个字符串。
|
|
59
|
+
* @returns {{ code: string, message: string, details: { bytes: number, maxBytes: number } } | null}
|
|
60
|
+
*/
|
|
61
|
+
export function validateTaskPayloadSize(serializedPayload: string): {
|
|
62
|
+
code: string;
|
|
63
|
+
message: string;
|
|
64
|
+
details: {
|
|
65
|
+
bytes: number;
|
|
66
|
+
maxBytes: number;
|
|
67
|
+
};
|
|
68
|
+
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* 任务正文的 UTF-8 字节数(与 {@link validateTaskPayloadSize} 同一把尺子)。
|
|
71
|
+
*
|
|
72
|
+
* `PUT /update-message` 拿它跟改动前的正文比大小:上限是后加的,比它更早建出来
|
|
73
|
+
* 的大任务不该因此连排期都改不了,只要这次改动没把它变得更大就放行。
|
|
74
|
+
*
|
|
75
|
+
* @param {string} serializedPayload
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
export function taskPayloadByteLength(serializedPayload: string): number;
|
|
49
79
|
/**
|
|
50
80
|
* Validate the schedule-message request payload.
|
|
51
81
|
*
|
|
@@ -58,5 +88,10 @@ export function validateScheduleMessagePayload(payload: any): {
|
|
|
58
88
|
errorMessage?: string;
|
|
59
89
|
details?: any;
|
|
60
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* 一条任务正文(`JSON.stringify` 的结果)的明文上限,UTF-8 字节数。
|
|
93
|
+
* 客户端想在提交前自己预算就读这个数(从 `@rei-standard/amsg-server` 导出)。
|
|
94
|
+
*/
|
|
95
|
+
export const MAX_TASK_PAYLOAD_BYTES: number;
|
|
61
96
|
import { isValidTimeZoneId } from './recurrence.js';
|
|
62
97
|
export { isValidTimeZoneId, isValidUrl, validateAvatarUrl };
|
|
@@ -19,7 +19,9 @@ var _chunkE7OWP3VLcjs = require('./chunk-E7OWP3VL.cjs');
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var _chunk7NMQFTDJcjs = require('./chunk-7NMQFTDJ.cjs');
|
|
23
25
|
|
|
24
26
|
// src/server/adapters/neon.js
|
|
25
27
|
var _serverless = require('@neondatabase/serverless');
|
|
@@ -29,7 +31,13 @@ var NeonAdapter = class {
|
|
|
29
31
|
this._connectionString = connectionString;
|
|
30
32
|
this._sql = null;
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
36
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
37
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
38
|
+
*
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
33
41
|
_getSql() {
|
|
34
42
|
if (!this._sql) {
|
|
35
43
|
this._sql = _serverless.neon.call(void 0, this._connectionString);
|
|
@@ -38,14 +46,14 @@ var NeonAdapter = class {
|
|
|
38
46
|
}
|
|
39
47
|
async initSchema() {
|
|
40
48
|
const sql = this._getSql();
|
|
41
|
-
await sql.query(
|
|
42
|
-
await sql.query(
|
|
43
|
-
await sql.query(
|
|
44
|
-
for (const migration of
|
|
49
|
+
await sql.query(_chunk7NMQFTDJcjs.TABLE_SQL);
|
|
50
|
+
await sql.query(_chunk7NMQFTDJcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
51
|
+
await sql.query(_chunk7NMQFTDJcjs.LLM_CREDENTIALS_TABLE_SQL);
|
|
52
|
+
for (const migration of _chunk7NMQFTDJcjs.MIGRATIONS) {
|
|
45
53
|
await sql.query(migration.sql);
|
|
46
54
|
}
|
|
47
55
|
const indexResults = [];
|
|
48
|
-
for (const index of
|
|
56
|
+
for (const index of _chunk7NMQFTDJcjs.INDEXES) {
|
|
49
57
|
try {
|
|
50
58
|
await sql.query(index.sql);
|
|
51
59
|
indexResults.push({
|
|
@@ -71,11 +79,11 @@ var NeonAdapter = class {
|
|
|
71
79
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
72
80
|
);
|
|
73
81
|
}
|
|
74
|
-
const tableCheck = await sql.query(
|
|
82
|
+
const tableCheck = await sql.query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
|
|
75
83
|
if (tableCheck.length === 0) {
|
|
76
84
|
throw new Error("Table creation verification failed");
|
|
77
85
|
}
|
|
78
|
-
const columns = await sql.query(
|
|
86
|
+
const columns = await sql.query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
|
|
79
87
|
return {
|
|
80
88
|
columnsCreated: columns.length,
|
|
81
89
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -106,7 +114,7 @@ var NeonAdapter = class {
|
|
|
106
114
|
async getTaskByUuid(uuid, userId) {
|
|
107
115
|
const sql = this._getSql();
|
|
108
116
|
const rows = await sql.query(
|
|
109
|
-
`SELECT
|
|
117
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
110
118
|
FROM scheduled_messages
|
|
111
119
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
112
120
|
LIMIT 1`,
|
|
@@ -117,7 +125,7 @@ var NeonAdapter = class {
|
|
|
117
125
|
async getTaskByUuidOnly(uuid) {
|
|
118
126
|
const sql = this._getSql();
|
|
119
127
|
const rows = await sql.query(
|
|
120
|
-
`SELECT
|
|
128
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
121
129
|
FROM scheduled_messages
|
|
122
130
|
WHERE uuid = $1 AND status = 'pending'
|
|
123
131
|
LIMIT 1`,
|
|
@@ -146,7 +154,7 @@ var NeonAdapter = class {
|
|
|
146
154
|
const values = [];
|
|
147
155
|
let idx = 1;
|
|
148
156
|
for (const [key, value] of Object.entries(updates)) {
|
|
149
|
-
if (!
|
|
157
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
150
158
|
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
151
159
|
}
|
|
152
160
|
sets.push(`${key} = $${idx}`);
|
|
@@ -168,7 +176,7 @@ var NeonAdapter = class {
|
|
|
168
176
|
let idx = 2;
|
|
169
177
|
if (extraFields) {
|
|
170
178
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
171
|
-
if (!
|
|
179
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
172
180
|
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
173
181
|
}
|
|
174
182
|
sets.push(`${key} = $${idx}`);
|
|
@@ -201,7 +209,7 @@ var NeonAdapter = class {
|
|
|
201
209
|
async getPendingTasks(limit = 50) {
|
|
202
210
|
const sql = this._getSql();
|
|
203
211
|
return sql.query(
|
|
204
|
-
`SELECT
|
|
212
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
205
213
|
FROM scheduled_messages
|
|
206
214
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
207
215
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -241,7 +249,7 @@ var NeonAdapter = class {
|
|
|
241
249
|
const total = parseInt(countRows[0].count, 10);
|
|
242
250
|
const taskParams = [...params, limit, offset];
|
|
243
251
|
const tasks = await sql.query(
|
|
244
|
-
`SELECT
|
|
252
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
245
253
|
FROM scheduled_messages
|
|
246
254
|
WHERE ${where}
|
|
247
255
|
ORDER BY next_send_at ASC
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
MIGRATIONS,
|
|
18
18
|
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
19
19
|
TABLE_SQL,
|
|
20
|
+
TASK_DELIVERY_COLUMNS,
|
|
21
|
+
TASK_DETAIL_COLUMNS,
|
|
20
22
|
UPDATABLE_COLUMNS,
|
|
21
23
|
VERIFY_TABLE_SQL
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GN44PST5.mjs";
|
|
23
25
|
|
|
24
26
|
// src/server/adapters/neon.js
|
|
25
27
|
import { neon } from "@neondatabase/serverless";
|
|
@@ -29,7 +31,13 @@ var NeonAdapter = class {
|
|
|
29
31
|
this._connectionString = connectionString;
|
|
30
32
|
this._sql = null;
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
36
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
37
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
38
|
+
*
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
33
41
|
_getSql() {
|
|
34
42
|
if (!this._sql) {
|
|
35
43
|
this._sql = neon(this._connectionString);
|
|
@@ -106,7 +114,7 @@ var NeonAdapter = class {
|
|
|
106
114
|
async getTaskByUuid(uuid, userId) {
|
|
107
115
|
const sql = this._getSql();
|
|
108
116
|
const rows = await sql.query(
|
|
109
|
-
`SELECT
|
|
117
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
110
118
|
FROM scheduled_messages
|
|
111
119
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
112
120
|
LIMIT 1`,
|
|
@@ -117,7 +125,7 @@ var NeonAdapter = class {
|
|
|
117
125
|
async getTaskByUuidOnly(uuid) {
|
|
118
126
|
const sql = this._getSql();
|
|
119
127
|
const rows = await sql.query(
|
|
120
|
-
`SELECT
|
|
128
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
121
129
|
FROM scheduled_messages
|
|
122
130
|
WHERE uuid = $1 AND status = 'pending'
|
|
123
131
|
LIMIT 1`,
|
|
@@ -201,7 +209,7 @@ var NeonAdapter = class {
|
|
|
201
209
|
async getPendingTasks(limit = 50) {
|
|
202
210
|
const sql = this._getSql();
|
|
203
211
|
return sql.query(
|
|
204
|
-
`SELECT
|
|
212
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
205
213
|
FROM scheduled_messages
|
|
206
214
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
207
215
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -241,7 +249,7 @@ var NeonAdapter = class {
|
|
|
241
249
|
const total = parseInt(countRows[0].count, 10);
|
|
242
250
|
const taskParams = [...params, limit, offset];
|
|
243
251
|
const tasks = await sql.query(
|
|
244
|
-
`SELECT
|
|
252
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
245
253
|
FROM scheduled_messages
|
|
246
254
|
WHERE ${where}
|
|
247
255
|
ORDER BY next_send_at ASC
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
MIGRATIONS,
|
|
18
18
|
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
19
19
|
TABLE_SQL,
|
|
20
|
+
TASK_DELIVERY_COLUMNS,
|
|
21
|
+
TASK_DETAIL_COLUMNS,
|
|
20
22
|
UPDATABLE_COLUMNS,
|
|
21
23
|
VERIFY_TABLE_SQL
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GN44PST5.mjs";
|
|
23
25
|
|
|
24
26
|
// src/server/adapters/pg.js
|
|
25
27
|
import { Pool } from "pg";
|
|
@@ -33,6 +35,12 @@ var PgAdapter = class {
|
|
|
33
35
|
_getPool() {
|
|
34
36
|
if (!this._pool) {
|
|
35
37
|
this._pool = new Pool({ connectionString: this._connectionString });
|
|
38
|
+
this._pool.on("error", (error) => {
|
|
39
|
+
console.error(
|
|
40
|
+
"[amsg-server pg] \u8FDE\u63A5\u6C60\u91CC\u7684\u7A7A\u95F2\u8FDE\u63A5\u51FA\u9519\uFF08\u8BE5\u8FDE\u63A5\u5DF2\u88AB\u6458\u9664\uFF0C\u4E0B\u6B21\u67E5\u8BE2\u4F1A\u91CD\u8FDE\uFF09:",
|
|
41
|
+
error && error.message
|
|
42
|
+
);
|
|
43
|
+
});
|
|
36
44
|
}
|
|
37
45
|
return this._pool;
|
|
38
46
|
}
|
|
@@ -108,7 +116,7 @@ var PgAdapter = class {
|
|
|
108
116
|
}
|
|
109
117
|
async getTaskByUuid(uuid, userId) {
|
|
110
118
|
const rows = await this._query(
|
|
111
|
-
`SELECT
|
|
119
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
112
120
|
FROM scheduled_messages
|
|
113
121
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
114
122
|
LIMIT 1`,
|
|
@@ -118,7 +126,7 @@ var PgAdapter = class {
|
|
|
118
126
|
}
|
|
119
127
|
async getTaskByUuidOnly(uuid) {
|
|
120
128
|
const rows = await this._query(
|
|
121
|
-
`SELECT
|
|
129
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
122
130
|
FROM scheduled_messages
|
|
123
131
|
WHERE uuid = $1 AND status = 'pending'
|
|
124
132
|
LIMIT 1`,
|
|
@@ -196,7 +204,7 @@ var PgAdapter = class {
|
|
|
196
204
|
}
|
|
197
205
|
async getPendingTasks(limit = 50) {
|
|
198
206
|
return this._query(
|
|
199
|
-
`SELECT
|
|
207
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
200
208
|
FROM scheduled_messages
|
|
201
209
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
202
210
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -233,7 +241,7 @@ var PgAdapter = class {
|
|
|
233
241
|
const total = parseInt(countRows[0].count, 10);
|
|
234
242
|
const taskParams = [...params, limit, offset];
|
|
235
243
|
const tasks = await this._query(
|
|
236
|
-
`SELECT
|
|
244
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
237
245
|
FROM scheduled_messages
|
|
238
246
|
WHERE ${where}
|
|
239
247
|
ORDER BY next_send_at ASC
|