@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +599 -24
- package/dist/adapters/d1.d.ts +416 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +306 -0
- package/dist/adapters/neon.d.ts +88 -0
- package/dist/adapters/pg-shared.d.ts +161 -0
- package/dist/adapters/pg.d.ts +84 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +49 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/chunk-E7OWP3VL.cjs +135 -0
- package/dist/chunk-GN44PST5.mjs +148 -0
- package/dist/chunk-KOJQVOYS.mjs +5847 -0
- package/dist/chunk-QHVID3V2.cjs +5847 -0
- package/dist/chunk-ZGGF4GMA.mjs +135 -0
- package/dist/cloudflare/single-user-worker.d.ts +37 -0
- package/dist/cloudflare.cjs +39 -2
- package/dist/cloudflare.d.ts +10 -2
- package/dist/cloudflare.mjs +40 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +7 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +133 -44
- package/dist/index.d.cts +131 -766
- package/dist/index.d.ts +131 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +108 -0
- package/dist/lib/client-state-store.d.ts +80 -0
- package/dist/lib/constant-time.d.ts +14 -0
- package/dist/lib/db-errors.d.ts +10 -0
- package/dist/lib/encryption.d.ts +48 -0
- package/dist/lib/errors.d.ts +215 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +1 -0
- package/dist/lib/message-processor.d.ts +73 -0
- package/dist/lib/outbox-store.d.ts +95 -0
- package/dist/lib/push-subscription-store.d.ts +94 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +182 -0
- package/dist/lib/result-emitter.d.ts +54 -0
- package/dist/lib/run-tick.d.ts +115 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +55 -0
- package/dist/lib/task-projection.d.ts +92 -0
- package/dist/lib/validation.d.ts +97 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
- package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
- package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
- package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
- package/dist/single-user.d.ts +95 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-5ENJGVZX.mjs +0 -72
- package/dist/chunk-GN5QTMCL.cjs +0 -1870
- package/dist/chunk-PVNLB6TF.mjs +0 -1870
- package/dist/chunk-RGECD4OH.cjs +0 -72
- package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
- package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-CJl66EGy.d.cts +0 -251
- package/dist/neon-DYvGnCzx.d.ts +0 -251
- package/dist/pg-07S-u_H4.d.cts +0 -246
- package/dist/pg-Du-pN_UT.d.ts +0 -246
- package/dist/schema-C8OnYk6j.d.cts +0 -74
- package/dist/schema-C8OnYk6j.d.ts +0 -74
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate ISO 8601 date string.
|
|
3
|
+
* @param {string} dateString
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isValidISO8601(dateString: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Validate UUID format.
|
|
9
|
+
* @param {string} uuid
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isValidUUID(uuid: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Validate UUID v4 format.
|
|
15
|
+
* @param {string} uuid
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function isValidUUIDv4(uuid: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Validate the optional `splitPattern` field (amsg-server scheduled tasks
|
|
21
|
+
* only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
|
|
22
|
+
* Accepts `string`, `string[]`, or absent/null. Returns an error message
|
|
23
|
+
* string, or null when valid.
|
|
24
|
+
*
|
|
25
|
+
* Limits (per-item length ≤ 200, array ≤ 10 items, must compile via
|
|
26
|
+
* `new RegExp(item)`) are an **input-size guard**, NOT a ReDoS defense —
|
|
27
|
+
* a 6-character pattern like `(a+)+$` is enough to trigger catastrophic
|
|
28
|
+
* backtracking. The real backstop is Worker / runtime CPU limits + the
|
|
29
|
+
* fact that splitPattern is stored under the user's own encrypted task
|
|
30
|
+
* and matched against output from the user's own LLM API key, so the
|
|
31
|
+
* blast radius is self-inflicted only (no cross-tenant attack surface).
|
|
32
|
+
*
|
|
33
|
+
* @param {unknown} value
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
export function validateSplitPattern(value: unknown): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Validate an OpenAI-style messages array. 形状规则统一在
|
|
39
|
+
* `@rei-standard/amsg-shared` 的 `validateLlmMessagesShape`(amsg-instant
|
|
40
|
+
* 的 `validateMessagesArray` 走同一实现,两包接受的形状不会漂移)——
|
|
41
|
+
* 因此 agentic 会话回放(assistant 带 `tool_calls` 时 content 可空、
|
|
42
|
+
* `role:'tool'` 消息要求 `tool_call_id`)在这里同样合法。本函数只把
|
|
43
|
+
* 结构化错误码映射成本包既有的英文错误文案,返回形状不变。
|
|
44
|
+
*
|
|
45
|
+
* @param {unknown} messages
|
|
46
|
+
* @returns {string | null} Error message, or null if valid.
|
|
47
|
+
*/
|
|
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;
|
|
79
|
+
/**
|
|
80
|
+
* Validate the schedule-message request payload.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} payload
|
|
83
|
+
* @returns {{ valid: boolean, errorCode?: string, errorMessage?: string, details?: Object }}
|
|
84
|
+
*/
|
|
85
|
+
export function validateScheduleMessagePayload(payload: any): {
|
|
86
|
+
valid: boolean;
|
|
87
|
+
errorCode?: string;
|
|
88
|
+
errorMessage?: string;
|
|
89
|
+
details?: any;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 一条任务正文(`JSON.stringify` 的结果)的明文上限,UTF-8 字节数。
|
|
93
|
+
* 客户端想在提交前自己预算就读这个数(从 `@rei-standard/amsg-server` 导出)。
|
|
94
|
+
*/
|
|
95
|
+
export const MAX_TASK_PAYLOAD_BYTES: number;
|
|
96
|
+
import { isValidTimeZoneId } from './recurrence.js';
|
|
97
|
+
export { isValidTimeZoneId, isValidUrl, validateAvatarUrl };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 组 payload 前做预算用:先量骨架有多大,剩下多少字节才是能塞附加数据的额度。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // hook 里组 push:把库要补的信封字段先扣掉
|
|
6
|
+
* const { remainingBytes } = measurePushPayload(
|
|
7
|
+
* JSON.stringify({ ...basePush, message: '' }),
|
|
8
|
+
* { reserveEnvelope: true }
|
|
9
|
+
* );
|
|
10
|
+
* const excerpt = remainingBytes > 0 ? text.slice(0, remainingBytes) : '';
|
|
11
|
+
*
|
|
12
|
+
* @param {string} payload - 待发送的 payload 字符串(通常是 JSON.stringify 的结果)。
|
|
13
|
+
* @param {{ reserveEnvelope?: boolean }} [options]
|
|
14
|
+
* `reserveEnvelope: true` → 额度里扣掉 {@link PUSH_ENVELOPE_RESERVED_BYTES},
|
|
15
|
+
* 也就是「库补完字段之后还装得下」的口径。fire-time hook 组 payload 时用这个。
|
|
16
|
+
* 默认 false = 量这个字符串本身对上限的占用。
|
|
17
|
+
* @returns {{ bytes: number, maxBytes: number, remainingBytes: number, withinLimit: boolean, envelopeReservedBytes: number }}
|
|
18
|
+
* `bytes` 是 payload 的 UTF-8 字节数;`maxBytes` 是本次口径下的上限;
|
|
19
|
+
* `remainingBytes` 是还能加多少字节(已超限时为负);`withinLimit` 为 false
|
|
20
|
+
* 时(不留信封的口径下)sendWebPush 会抛 `PUSH_PAYLOAD_TOO_LARGE`。
|
|
21
|
+
*/
|
|
22
|
+
export function measurePushPayload(payload: string, options?: {
|
|
23
|
+
reserveEnvelope?: boolean;
|
|
24
|
+
}): {
|
|
25
|
+
bytes: number;
|
|
26
|
+
maxBytes: number;
|
|
27
|
+
remainingBytes: number;
|
|
28
|
+
withinLimit: boolean;
|
|
29
|
+
envelopeReservedBytes: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Send a single Web Push notification. 委托 shared 的 `sendWebPush`(参数、
|
|
33
|
+
* 返回值与之完全一致),只在加密前多做一步 payload 大小护栏——超限的
|
|
34
|
+
* payload 发出去只会被推送服务 413 掉,用户什么也收不到。加密之前就拦下
|
|
35
|
+
* 来,让调用方拿到一个说得清的错误(宿主可以退回一条短消息 + 引用键)。
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} args - 同 shared `sendWebPush`:{ subscription, payload, vapid, ttl, fetch }。
|
|
38
|
+
* @returns {Promise<{ statusCode: number, body: string, headers: Headers }>}
|
|
39
|
+
* @throws {Error} err.code = 'PUSH_PAYLOAD_TOO_LARGE' when the payload
|
|
40
|
+
* exceeds MAX_PUSH_PAYLOAD_BYTES (nothing is sent);
|
|
41
|
+
* err.code = 'PUSH_SEND_FAILED' on push-service error.
|
|
42
|
+
*/
|
|
43
|
+
export function sendWebPush(args: any): Promise<{
|
|
44
|
+
statusCode: number;
|
|
45
|
+
body: string;
|
|
46
|
+
headers: Headers;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* web-push-compatible sender backed by the shared Web Crypto implementation.
|
|
50
|
+
* message-processor calls `ctx.webpush.sendNotification(subscription, payloadString)`,
|
|
51
|
+
* so we only need that one method. VAPID keys are baked in at construction.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ email: string, publicKey: string, privateKey: string }} [vapid]
|
|
54
|
+
* @param {{ ttl?: number }} [options] - Push TTL in seconds; defaults to 4 weeks
|
|
55
|
+
* (matches the web-push backend) so scheduled pushes outlive an offline device.
|
|
56
|
+
* @returns {{ sendNotification: (subscription: Object, payload: string) => Promise<any> }}
|
|
57
|
+
*/
|
|
58
|
+
export function createWebCryptoWebPush(vapid?: {
|
|
59
|
+
email: string;
|
|
60
|
+
publicKey: string;
|
|
61
|
+
privateKey: string;
|
|
62
|
+
}, { ttl }?: {
|
|
63
|
+
ttl?: number;
|
|
64
|
+
}): {
|
|
65
|
+
sendNotification: (subscription: any, payload: string) => Promise<any>;
|
|
66
|
+
};
|
|
67
|
+
/** 推送服务对加密后 body 的上限(字节)。 */
|
|
68
|
+
export const WEB_PUSH_MAX_BODY_BYTES: 4096;
|
|
69
|
+
/** aes128gcm 固定开销:header 86 + 填充分隔符 1 + GCM tag 16。 */
|
|
70
|
+
export const WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES: number;
|
|
71
|
+
/** 一条 push 的明文(JSON 字符串)上限,UTF-8 字节数。 */
|
|
72
|
+
export const MAX_PUSH_PAYLOAD_BYTES: number;
|
|
73
|
+
/**
|
|
74
|
+
* 库在 hook 交还 payload 之后还要补的那批字段占的字节数上界。
|
|
75
|
+
* 组 payload 时按 `MAX_PUSH_PAYLOAD_BYTES - PUSH_ENVELOPE_RESERVED_BYTES`
|
|
76
|
+
* 预算,或者直接用 `measurePushPayload(s, { reserveEnvelope: true })`。
|
|
77
|
+
*/
|
|
78
|
+
export const PUSH_ENVELOPE_RESERVED_BYTES: 384;
|
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
var _chunkE7OWP3VLcjs = require('./chunk-E7OWP3VL.cjs');
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var _chunk7NMQFTDJcjs = require('./chunk-7NMQFTDJ.cjs');
|
|
7
25
|
|
|
8
26
|
// src/server/adapters/neon.js
|
|
9
27
|
var _serverless = require('@neondatabase/serverless');
|
|
@@ -13,7 +31,13 @@ var NeonAdapter = class {
|
|
|
13
31
|
this._connectionString = connectionString;
|
|
14
32
|
this._sql = null;
|
|
15
33
|
}
|
|
16
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
36
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
37
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
38
|
+
*
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
17
41
|
_getSql() {
|
|
18
42
|
if (!this._sql) {
|
|
19
43
|
this._sql = _serverless.neon.call(void 0, this._connectionString);
|
|
@@ -22,9 +46,14 @@ var NeonAdapter = class {
|
|
|
22
46
|
}
|
|
23
47
|
async initSchema() {
|
|
24
48
|
const sql = this._getSql();
|
|
25
|
-
await sql.query(
|
|
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) {
|
|
53
|
+
await sql.query(migration.sql);
|
|
54
|
+
}
|
|
26
55
|
const indexResults = [];
|
|
27
|
-
for (const index of
|
|
56
|
+
for (const index of _chunk7NMQFTDJcjs.INDEXES) {
|
|
28
57
|
try {
|
|
29
58
|
await sql.query(index.sql);
|
|
30
59
|
indexResults.push({
|
|
@@ -50,11 +79,11 @@ var NeonAdapter = class {
|
|
|
50
79
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
51
80
|
);
|
|
52
81
|
}
|
|
53
|
-
const tableCheck = await sql.query(
|
|
82
|
+
const tableCheck = await sql.query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
|
|
54
83
|
if (tableCheck.length === 0) {
|
|
55
84
|
throw new Error("Table creation verification failed");
|
|
56
85
|
}
|
|
57
|
-
const columns = await sql.query(
|
|
86
|
+
const columns = await sql.query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
|
|
58
87
|
return {
|
|
59
88
|
columnsCreated: columns.length,
|
|
60
89
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -68,6 +97,8 @@ var NeonAdapter = class {
|
|
|
68
97
|
async dropSchema() {
|
|
69
98
|
const sql = this._getSql();
|
|
70
99
|
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
100
|
+
await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
101
|
+
await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
71
102
|
}
|
|
72
103
|
async createTask(params) {
|
|
73
104
|
const sql = this._getSql();
|
|
@@ -83,7 +114,7 @@ var NeonAdapter = class {
|
|
|
83
114
|
async getTaskByUuid(uuid, userId) {
|
|
84
115
|
const sql = this._getSql();
|
|
85
116
|
const rows = await sql.query(
|
|
86
|
-
`SELECT
|
|
117
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
87
118
|
FROM scheduled_messages
|
|
88
119
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
89
120
|
LIMIT 1`,
|
|
@@ -94,7 +125,7 @@ var NeonAdapter = class {
|
|
|
94
125
|
async getTaskByUuidOnly(uuid) {
|
|
95
126
|
const sql = this._getSql();
|
|
96
127
|
const rows = await sql.query(
|
|
97
|
-
`SELECT
|
|
128
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
98
129
|
FROM scheduled_messages
|
|
99
130
|
WHERE uuid = $1 AND status = 'pending'
|
|
100
131
|
LIMIT 1`,
|
|
@@ -102,12 +133,30 @@ var NeonAdapter = class {
|
|
|
102
133
|
);
|
|
103
134
|
return rows[0] || null;
|
|
104
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
138
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
139
|
+
*
|
|
140
|
+
* @param {string} uuid
|
|
141
|
+
* @returns {Promise<{ status: string }|null>}
|
|
142
|
+
*/
|
|
143
|
+
async getTaskStatusByUuidOnly(uuid) {
|
|
144
|
+
const sql = this._getSql();
|
|
145
|
+
const rows = await sql.query(
|
|
146
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
147
|
+
[uuid]
|
|
148
|
+
);
|
|
149
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
150
|
+
}
|
|
105
151
|
async updateTaskById(taskId, updates) {
|
|
106
152
|
const sql = this._getSql();
|
|
107
153
|
const sets = [];
|
|
108
154
|
const values = [];
|
|
109
155
|
let idx = 1;
|
|
110
156
|
for (const [key, value] of Object.entries(updates)) {
|
|
157
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
158
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
159
|
+
}
|
|
111
160
|
sets.push(`${key} = $${idx}`);
|
|
112
161
|
values.push(value);
|
|
113
162
|
idx++;
|
|
@@ -127,6 +176,9 @@ var NeonAdapter = class {
|
|
|
127
176
|
let idx = 2;
|
|
128
177
|
if (extraFields) {
|
|
129
178
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
179
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
180
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
181
|
+
}
|
|
130
182
|
sets.push(`${key} = $${idx}`);
|
|
131
183
|
values.push(value);
|
|
132
184
|
idx++;
|
|
@@ -157,14 +209,27 @@ var NeonAdapter = class {
|
|
|
157
209
|
async getPendingTasks(limit = 50) {
|
|
158
210
|
const sql = this._getSql();
|
|
159
211
|
return sql.query(
|
|
160
|
-
`SELECT
|
|
212
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
161
213
|
FROM scheduled_messages
|
|
162
214
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
215
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
216
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
163
217
|
ORDER BY next_send_at ASC
|
|
164
218
|
LIMIT $1`,
|
|
165
219
|
[limit]
|
|
166
220
|
);
|
|
167
221
|
}
|
|
222
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
223
|
+
// 语义说明也在那里)。
|
|
224
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
225
|
+
const sql = this._getSql();
|
|
226
|
+
return _chunkE7OWP3VLcjs.claimTask.call(void 0, (text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
227
|
+
}
|
|
228
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
229
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
230
|
+
const sql = this._getSql();
|
|
231
|
+
return _chunkE7OWP3VLcjs.renewTaskLease.call(void 0, (text, params) => sql.query(text, params), taskId, leaseUntil);
|
|
232
|
+
}
|
|
168
233
|
async listTasks(userId, opts = {}) {
|
|
169
234
|
const sql = this._getSql();
|
|
170
235
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -184,7 +249,7 @@ var NeonAdapter = class {
|
|
|
184
249
|
const total = parseInt(countRows[0].count, 10);
|
|
185
250
|
const taskParams = [...params, limit, offset];
|
|
186
251
|
const tasks = await sql.query(
|
|
187
|
-
`SELECT
|
|
252
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
188
253
|
FROM scheduled_messages
|
|
189
254
|
WHERE ${where}
|
|
190
255
|
ORDER BY next_send_at ASC
|
|
@@ -213,6 +278,41 @@ var NeonAdapter = class {
|
|
|
213
278
|
);
|
|
214
279
|
return rows.length > 0 ? rows[0].status : null;
|
|
215
280
|
}
|
|
281
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
282
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
283
|
+
const sql = this._getSql();
|
|
284
|
+
return _chunkE7OWP3VLcjs.getTaskStatusInfo.call(void 0, (text, params) => sql.query(text, params), uuid, userId);
|
|
285
|
+
}
|
|
286
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
287
|
+
async getPushSubscription(userId) {
|
|
288
|
+
const sql = this._getSql();
|
|
289
|
+
return _chunkE7OWP3VLcjs.getPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
290
|
+
}
|
|
291
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
292
|
+
const sql = this._getSql();
|
|
293
|
+
return _chunkE7OWP3VLcjs.upsertPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
|
|
294
|
+
}
|
|
295
|
+
async deletePushSubscription(userId) {
|
|
296
|
+
const sql = this._getSql();
|
|
297
|
+
return _chunkE7OWP3VLcjs.deletePushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
298
|
+
}
|
|
299
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
300
|
+
async upsertLlmCredentials(userId, entries) {
|
|
301
|
+
const sql = this._getSql();
|
|
302
|
+
return _chunkE7OWP3VLcjs.upsertLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
303
|
+
}
|
|
304
|
+
async getLlmCredentials(userId, credIds) {
|
|
305
|
+
const sql = this._getSql();
|
|
306
|
+
return _chunkE7OWP3VLcjs.getLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
|
|
307
|
+
}
|
|
308
|
+
async listLlmCredentials(userId) {
|
|
309
|
+
const sql = this._getSql();
|
|
310
|
+
return _chunkE7OWP3VLcjs.listLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
311
|
+
}
|
|
312
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
313
|
+
const sql = this._getSql();
|
|
314
|
+
return _chunkE7OWP3VLcjs.deleteLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
|
|
315
|
+
}
|
|
216
316
|
};
|
|
217
317
|
|
|
218
318
|
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claimTask,
|
|
3
|
+
deleteLlmCredentials,
|
|
4
|
+
deletePushSubscription,
|
|
5
|
+
getLlmCredentials,
|
|
6
|
+
getPushSubscription,
|
|
7
|
+
getTaskStatusInfo,
|
|
8
|
+
listLlmCredentials,
|
|
9
|
+
renewTaskLease,
|
|
10
|
+
upsertLlmCredentials,
|
|
11
|
+
upsertPushSubscription
|
|
12
|
+
} from "./chunk-ZGGF4GMA.mjs";
|
|
1
13
|
import {
|
|
2
14
|
COLUMNS_SQL,
|
|
3
15
|
INDEXES,
|
|
16
|
+
LLM_CREDENTIALS_TABLE_SQL,
|
|
17
|
+
MIGRATIONS,
|
|
18
|
+
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
4
19
|
TABLE_SQL,
|
|
20
|
+
TASK_DELIVERY_COLUMNS,
|
|
21
|
+
TASK_DETAIL_COLUMNS,
|
|
22
|
+
UPDATABLE_COLUMNS,
|
|
5
23
|
VERIFY_TABLE_SQL
|
|
6
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GN44PST5.mjs";
|
|
7
25
|
|
|
8
26
|
// src/server/adapters/neon.js
|
|
9
27
|
import { neon } from "@neondatabase/serverless";
|
|
@@ -13,7 +31,13 @@ var NeonAdapter = class {
|
|
|
13
31
|
this._connectionString = connectionString;
|
|
14
32
|
this._sql = null;
|
|
15
33
|
}
|
|
16
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
36
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
37
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
38
|
+
*
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
17
41
|
_getSql() {
|
|
18
42
|
if (!this._sql) {
|
|
19
43
|
this._sql = neon(this._connectionString);
|
|
@@ -23,6 +47,11 @@ var NeonAdapter = class {
|
|
|
23
47
|
async initSchema() {
|
|
24
48
|
const sql = this._getSql();
|
|
25
49
|
await sql.query(TABLE_SQL);
|
|
50
|
+
await sql.query(PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
51
|
+
await sql.query(LLM_CREDENTIALS_TABLE_SQL);
|
|
52
|
+
for (const migration of MIGRATIONS) {
|
|
53
|
+
await sql.query(migration.sql);
|
|
54
|
+
}
|
|
26
55
|
const indexResults = [];
|
|
27
56
|
for (const index of INDEXES) {
|
|
28
57
|
try {
|
|
@@ -68,6 +97,8 @@ var NeonAdapter = class {
|
|
|
68
97
|
async dropSchema() {
|
|
69
98
|
const sql = this._getSql();
|
|
70
99
|
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
100
|
+
await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
101
|
+
await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
71
102
|
}
|
|
72
103
|
async createTask(params) {
|
|
73
104
|
const sql = this._getSql();
|
|
@@ -83,7 +114,7 @@ var NeonAdapter = class {
|
|
|
83
114
|
async getTaskByUuid(uuid, userId) {
|
|
84
115
|
const sql = this._getSql();
|
|
85
116
|
const rows = await sql.query(
|
|
86
|
-
`SELECT
|
|
117
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
87
118
|
FROM scheduled_messages
|
|
88
119
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
89
120
|
LIMIT 1`,
|
|
@@ -94,7 +125,7 @@ var NeonAdapter = class {
|
|
|
94
125
|
async getTaskByUuidOnly(uuid) {
|
|
95
126
|
const sql = this._getSql();
|
|
96
127
|
const rows = await sql.query(
|
|
97
|
-
`SELECT
|
|
128
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
98
129
|
FROM scheduled_messages
|
|
99
130
|
WHERE uuid = $1 AND status = 'pending'
|
|
100
131
|
LIMIT 1`,
|
|
@@ -102,12 +133,30 @@ var NeonAdapter = class {
|
|
|
102
133
|
);
|
|
103
134
|
return rows[0] || null;
|
|
104
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
138
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
139
|
+
*
|
|
140
|
+
* @param {string} uuid
|
|
141
|
+
* @returns {Promise<{ status: string }|null>}
|
|
142
|
+
*/
|
|
143
|
+
async getTaskStatusByUuidOnly(uuid) {
|
|
144
|
+
const sql = this._getSql();
|
|
145
|
+
const rows = await sql.query(
|
|
146
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
147
|
+
[uuid]
|
|
148
|
+
);
|
|
149
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
150
|
+
}
|
|
105
151
|
async updateTaskById(taskId, updates) {
|
|
106
152
|
const sql = this._getSql();
|
|
107
153
|
const sets = [];
|
|
108
154
|
const values = [];
|
|
109
155
|
let idx = 1;
|
|
110
156
|
for (const [key, value] of Object.entries(updates)) {
|
|
157
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
158
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
159
|
+
}
|
|
111
160
|
sets.push(`${key} = $${idx}`);
|
|
112
161
|
values.push(value);
|
|
113
162
|
idx++;
|
|
@@ -127,6 +176,9 @@ var NeonAdapter = class {
|
|
|
127
176
|
let idx = 2;
|
|
128
177
|
if (extraFields) {
|
|
129
178
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
179
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
180
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
181
|
+
}
|
|
130
182
|
sets.push(`${key} = $${idx}`);
|
|
131
183
|
values.push(value);
|
|
132
184
|
idx++;
|
|
@@ -157,14 +209,27 @@ var NeonAdapter = class {
|
|
|
157
209
|
async getPendingTasks(limit = 50) {
|
|
158
210
|
const sql = this._getSql();
|
|
159
211
|
return sql.query(
|
|
160
|
-
`SELECT
|
|
212
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
161
213
|
FROM scheduled_messages
|
|
162
214
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
215
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
216
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
163
217
|
ORDER BY next_send_at ASC
|
|
164
218
|
LIMIT $1`,
|
|
165
219
|
[limit]
|
|
166
220
|
);
|
|
167
221
|
}
|
|
222
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
223
|
+
// 语义说明也在那里)。
|
|
224
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
225
|
+
const sql = this._getSql();
|
|
226
|
+
return claimTask((text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
227
|
+
}
|
|
228
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
229
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
230
|
+
const sql = this._getSql();
|
|
231
|
+
return renewTaskLease((text, params) => sql.query(text, params), taskId, leaseUntil);
|
|
232
|
+
}
|
|
168
233
|
async listTasks(userId, opts = {}) {
|
|
169
234
|
const sql = this._getSql();
|
|
170
235
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -184,7 +249,7 @@ var NeonAdapter = class {
|
|
|
184
249
|
const total = parseInt(countRows[0].count, 10);
|
|
185
250
|
const taskParams = [...params, limit, offset];
|
|
186
251
|
const tasks = await sql.query(
|
|
187
|
-
`SELECT
|
|
252
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
188
253
|
FROM scheduled_messages
|
|
189
254
|
WHERE ${where}
|
|
190
255
|
ORDER BY next_send_at ASC
|
|
@@ -213,6 +278,41 @@ var NeonAdapter = class {
|
|
|
213
278
|
);
|
|
214
279
|
return rows.length > 0 ? rows[0].status : null;
|
|
215
280
|
}
|
|
281
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
282
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
283
|
+
const sql = this._getSql();
|
|
284
|
+
return getTaskStatusInfo((text, params) => sql.query(text, params), uuid, userId);
|
|
285
|
+
}
|
|
286
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
287
|
+
async getPushSubscription(userId) {
|
|
288
|
+
const sql = this._getSql();
|
|
289
|
+
return getPushSubscription((text, params) => sql.query(text, params), userId);
|
|
290
|
+
}
|
|
291
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
292
|
+
const sql = this._getSql();
|
|
293
|
+
return upsertPushSubscription((text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
|
|
294
|
+
}
|
|
295
|
+
async deletePushSubscription(userId) {
|
|
296
|
+
const sql = this._getSql();
|
|
297
|
+
return deletePushSubscription((text, params) => sql.query(text, params), userId);
|
|
298
|
+
}
|
|
299
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
300
|
+
async upsertLlmCredentials(userId, entries) {
|
|
301
|
+
const sql = this._getSql();
|
|
302
|
+
return upsertLlmCredentials((text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
303
|
+
}
|
|
304
|
+
async getLlmCredentials(userId, credIds) {
|
|
305
|
+
const sql = this._getSql();
|
|
306
|
+
return getLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
|
|
307
|
+
}
|
|
308
|
+
async listLlmCredentials(userId) {
|
|
309
|
+
const sql = this._getSql();
|
|
310
|
+
return listLlmCredentials((text, params) => sql.query(text, params), userId);
|
|
311
|
+
}
|
|
312
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
313
|
+
const sql = this._getSql();
|
|
314
|
+
return deleteLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
|
|
315
|
+
}
|
|
216
316
|
};
|
|
217
317
|
export {
|
|
218
318
|
NeonAdapter
|