@rei-standard/amsg-server 2.6.0-next.8 → 3.0.0
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 +861 -35
- package/dist/adapters/d1.d.ts +693 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +384 -0
- package/dist/adapters/neon.d.ts +105 -0
- package/dist/adapters/pg-shared.d.ts +194 -0
- package/dist/adapters/pg.d.ts +101 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +80 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
- package/dist/chunk-RL7V7VSS.mjs +8784 -0
- package/dist/chunk-TKDISKCF.cjs +163 -0
- package/dist/chunk-XKUWMYVF.mjs +163 -0
- package/dist/chunk-YBDQJVFQ.cjs +8784 -0
- package/dist/cloudflare/single-user-worker.d.ts +39 -0
- package/dist/cloudflare.cjs +35 -2
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.mjs +36 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state-namespaces.d.ts +5 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/cloud-data.d.ts +4 -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 +9 -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 +145 -766
- package/dist/index.d.ts +145 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +177 -0
- package/dist/lib/client-state-store.d.ts +88 -0
- package/dist/lib/cloud-data-cleanup.d.ts +36 -0
- package/dist/lib/cloud-data-guard.d.ts +14 -0
- package/dist/lib/cloud-data-inventory.d.ts +15 -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 +273 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +12 -0
- package/dist/lib/message-processor.d.ts +82 -0
- package/dist/lib/outbox-store.d.ts +162 -0
- package/dist/lib/push-policy.d.ts +11 -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 +60 -0
- package/dist/lib/retry-policy.d.ts +11 -0
- package/dist/lib/run-tick.d.ts +121 -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 +65 -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-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
- package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
- package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
- package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
- package/dist/single-user.d.ts +106 -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-DCKCAGFV.cjs +0 -81
- package/dist/chunk-HQHRXSQA.cjs +0 -2710
- package/dist/chunk-LEE5KQ7Y.mjs +0 -2710
- package/dist/cloudflare-CH3dBl5b.d.cts +0 -4235
- package/dist/cloudflare-CH3dBl5b.d.ts +0 -4235
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-D8Z2Nn97.d.cts +0 -295
- package/dist/neon-PqNFJIz2.d.ts +0 -295
- package/dist/pg-C48FwCWW.d.cts +0 -289
- package/dist/pg-C5gtWUfU.d.ts +0 -289
- package/dist/schema-BHuyH_6Z.d.cts +0 -88
- package/dist/schema-BHuyH_6Z.d.ts +0 -88
|
@@ -1,10 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claimTask,
|
|
3
|
+
createTaskSuperseding,
|
|
4
|
+
deleteLlmCredentials,
|
|
5
|
+
deletePushSubscription,
|
|
6
|
+
getLlmCredentials,
|
|
7
|
+
getPushSubscription,
|
|
8
|
+
getTaskStatusInfo,
|
|
9
|
+
listLlmCredentials,
|
|
10
|
+
renewTaskLease,
|
|
11
|
+
upsertLlmCredentials,
|
|
12
|
+
upsertPushSubscription
|
|
13
|
+
} from "./chunk-XKUWMYVF.mjs";
|
|
1
14
|
import {
|
|
2
15
|
COLUMNS_SQL,
|
|
3
16
|
INDEXES,
|
|
17
|
+
LLM_CREDENTIALS_TABLE_SQL,
|
|
4
18
|
MIGRATIONS,
|
|
19
|
+
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
5
20
|
TABLE_SQL,
|
|
21
|
+
TASK_DELIVERY_COLUMNS,
|
|
22
|
+
TASK_DETAIL_COLUMNS,
|
|
23
|
+
UPDATABLE_COLUMNS,
|
|
6
24
|
VERIFY_TABLE_SQL
|
|
7
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-GN44PST5.mjs";
|
|
8
26
|
|
|
9
27
|
// src/server/adapters/neon.js
|
|
10
28
|
import { neon } from "@neondatabase/serverless";
|
|
@@ -14,7 +32,13 @@ var NeonAdapter = class {
|
|
|
14
32
|
this._connectionString = connectionString;
|
|
15
33
|
this._sql = null;
|
|
16
34
|
}
|
|
17
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
37
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
38
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
39
|
+
*
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
18
42
|
_getSql() {
|
|
19
43
|
if (!this._sql) {
|
|
20
44
|
this._sql = neon(this._connectionString);
|
|
@@ -24,6 +48,8 @@ var NeonAdapter = class {
|
|
|
24
48
|
async initSchema() {
|
|
25
49
|
const sql = this._getSql();
|
|
26
50
|
await sql.query(TABLE_SQL);
|
|
51
|
+
await sql.query(PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
52
|
+
await sql.query(LLM_CREDENTIALS_TABLE_SQL);
|
|
27
53
|
for (const migration of MIGRATIONS) {
|
|
28
54
|
await sql.query(migration.sql);
|
|
29
55
|
}
|
|
@@ -72,6 +98,8 @@ var NeonAdapter = class {
|
|
|
72
98
|
async dropSchema() {
|
|
73
99
|
const sql = this._getSql();
|
|
74
100
|
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
101
|
+
await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
102
|
+
await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
75
103
|
}
|
|
76
104
|
async createTask(params) {
|
|
77
105
|
const sql = this._getSql();
|
|
@@ -84,10 +112,15 @@ var NeonAdapter = class {
|
|
|
84
112
|
);
|
|
85
113
|
return rows[0] || null;
|
|
86
114
|
}
|
|
115
|
+
// 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
|
|
116
|
+
async createTaskSuperseding(params, supersedesUuid) {
|
|
117
|
+
const sql = this._getSql();
|
|
118
|
+
return createTaskSuperseding((text, args) => sql.query(text, args), params, supersedesUuid);
|
|
119
|
+
}
|
|
87
120
|
async getTaskByUuid(uuid, userId) {
|
|
88
121
|
const sql = this._getSql();
|
|
89
122
|
const rows = await sql.query(
|
|
90
|
-
`SELECT
|
|
123
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
91
124
|
FROM scheduled_messages
|
|
92
125
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
93
126
|
LIMIT 1`,
|
|
@@ -98,7 +131,7 @@ var NeonAdapter = class {
|
|
|
98
131
|
async getTaskByUuidOnly(uuid) {
|
|
99
132
|
const sql = this._getSql();
|
|
100
133
|
const rows = await sql.query(
|
|
101
|
-
`SELECT
|
|
134
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
102
135
|
FROM scheduled_messages
|
|
103
136
|
WHERE uuid = $1 AND status = 'pending'
|
|
104
137
|
LIMIT 1`,
|
|
@@ -106,12 +139,30 @@ var NeonAdapter = class {
|
|
|
106
139
|
);
|
|
107
140
|
return rows[0] || null;
|
|
108
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
144
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
145
|
+
*
|
|
146
|
+
* @param {string} uuid
|
|
147
|
+
* @returns {Promise<{ status: string }|null>}
|
|
148
|
+
*/
|
|
149
|
+
async getTaskStatusByUuidOnly(uuid) {
|
|
150
|
+
const sql = this._getSql();
|
|
151
|
+
const rows = await sql.query(
|
|
152
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
153
|
+
[uuid]
|
|
154
|
+
);
|
|
155
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
156
|
+
}
|
|
109
157
|
async updateTaskById(taskId, updates) {
|
|
110
158
|
const sql = this._getSql();
|
|
111
159
|
const sets = [];
|
|
112
160
|
const values = [];
|
|
113
161
|
let idx = 1;
|
|
114
162
|
for (const [key, value] of Object.entries(updates)) {
|
|
163
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
164
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
165
|
+
}
|
|
115
166
|
sets.push(`${key} = $${idx}`);
|
|
116
167
|
values.push(value);
|
|
117
168
|
idx++;
|
|
@@ -124,6 +175,15 @@ var NeonAdapter = class {
|
|
|
124
175
|
);
|
|
125
176
|
return rows[0] || null;
|
|
126
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
|
|
180
|
+
*
|
|
181
|
+
* 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
|
|
182
|
+
* 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
|
|
183
|
+
* 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
|
|
184
|
+
* ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
|
|
185
|
+
* 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
|
|
186
|
+
*/
|
|
127
187
|
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
128
188
|
const sql = this._getSql();
|
|
129
189
|
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
@@ -131,15 +191,19 @@ var NeonAdapter = class {
|
|
|
131
191
|
let idx = 2;
|
|
132
192
|
if (extraFields) {
|
|
133
193
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
194
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
195
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
196
|
+
}
|
|
134
197
|
sets.push(`${key} = $${idx}`);
|
|
135
198
|
values.push(value);
|
|
136
199
|
idx++;
|
|
137
200
|
}
|
|
138
201
|
}
|
|
139
202
|
values.push(uuid, userId);
|
|
203
|
+
const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
|
|
140
204
|
const rows = await sql.query(
|
|
141
205
|
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
142
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
206
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
|
|
143
207
|
RETURNING uuid, updated_at`,
|
|
144
208
|
values
|
|
145
209
|
);
|
|
@@ -161,53 +225,26 @@ var NeonAdapter = class {
|
|
|
161
225
|
async getPendingTasks(limit = 50) {
|
|
162
226
|
const sql = this._getSql();
|
|
163
227
|
return sql.query(
|
|
164
|
-
`SELECT
|
|
228
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
165
229
|
FROM scheduled_messages
|
|
166
230
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
167
231
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
232
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
168
233
|
ORDER BY next_send_at ASC
|
|
169
234
|
LIMIT $1`,
|
|
170
235
|
[limit]
|
|
171
236
|
);
|
|
172
237
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
178
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
179
|
-
*
|
|
180
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
181
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
182
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
183
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
184
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
185
|
-
* 这一跳就不该再按旧时刻发。
|
|
186
|
-
*
|
|
187
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
188
|
-
* 要改表。
|
|
189
|
-
*
|
|
190
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
191
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
192
|
-
*
|
|
193
|
-
* @param {number} taskId
|
|
194
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
195
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
196
|
-
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
197
|
-
*/
|
|
198
|
-
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
238
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
239
|
+
// 语义说明也在那里)。
|
|
240
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
199
241
|
const sql = this._getSql();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
207
|
-
RETURNING id`,
|
|
208
|
-
[leaseUntil, taskId, expectedNextSendAt]
|
|
209
|
-
);
|
|
210
|
-
return rows.length > 0;
|
|
242
|
+
return claimTask((text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
243
|
+
}
|
|
244
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
245
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
246
|
+
const sql = this._getSql();
|
|
247
|
+
return renewTaskLease((text, params) => sql.query(text, params), taskId, leaseUntil);
|
|
211
248
|
}
|
|
212
249
|
async listTasks(userId, opts = {}) {
|
|
213
250
|
const sql = this._getSql();
|
|
@@ -228,7 +265,7 @@ var NeonAdapter = class {
|
|
|
228
265
|
const total = parseInt(countRows[0].count, 10);
|
|
229
266
|
const taskParams = [...params, limit, offset];
|
|
230
267
|
const tasks = await sql.query(
|
|
231
|
-
`SELECT
|
|
268
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
232
269
|
FROM scheduled_messages
|
|
233
270
|
WHERE ${where}
|
|
234
271
|
ORDER BY next_send_at ASC
|
|
@@ -257,6 +294,41 @@ var NeonAdapter = class {
|
|
|
257
294
|
);
|
|
258
295
|
return rows.length > 0 ? rows[0].status : null;
|
|
259
296
|
}
|
|
297
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
298
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
299
|
+
const sql = this._getSql();
|
|
300
|
+
return getTaskStatusInfo((text, params) => sql.query(text, params), uuid, userId);
|
|
301
|
+
}
|
|
302
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
303
|
+
async getPushSubscription(userId) {
|
|
304
|
+
const sql = this._getSql();
|
|
305
|
+
return getPushSubscription((text, params) => sql.query(text, params), userId);
|
|
306
|
+
}
|
|
307
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
308
|
+
const sql = this._getSql();
|
|
309
|
+
return upsertPushSubscription((text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
|
|
310
|
+
}
|
|
311
|
+
async deletePushSubscription(userId) {
|
|
312
|
+
const sql = this._getSql();
|
|
313
|
+
return deletePushSubscription((text, params) => sql.query(text, params), userId);
|
|
314
|
+
}
|
|
315
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
316
|
+
async upsertLlmCredentials(userId, entries) {
|
|
317
|
+
const sql = this._getSql();
|
|
318
|
+
return upsertLlmCredentials((text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
319
|
+
}
|
|
320
|
+
async getLlmCredentials(userId, credIds) {
|
|
321
|
+
const sql = this._getSql();
|
|
322
|
+
return getLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
|
|
323
|
+
}
|
|
324
|
+
async listLlmCredentials(userId) {
|
|
325
|
+
const sql = this._getSql();
|
|
326
|
+
return listLlmCredentials((text, params) => sql.query(text, params), userId);
|
|
327
|
+
}
|
|
328
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
329
|
+
const sql = this._getSql();
|
|
330
|
+
return deleteLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
|
|
331
|
+
}
|
|
260
332
|
};
|
|
261
333
|
export {
|
|
262
334
|
NeonAdapter
|
|
@@ -4,7 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
var _chunkTKDISKCFcjs = require('./chunk-TKDISKCF.cjs');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
var _chunk7NMQFTDJcjs = require('./chunk-7NMQFTDJ.cjs');
|
|
8
26
|
|
|
9
27
|
// src/server/adapters/neon.js
|
|
10
28
|
var _serverless = require('@neondatabase/serverless');
|
|
@@ -14,7 +32,13 @@ var NeonAdapter = class {
|
|
|
14
32
|
this._connectionString = connectionString;
|
|
15
33
|
this._sql = null;
|
|
16
34
|
}
|
|
17
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
|
|
37
|
+
* pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
|
|
38
|
+
* Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
|
|
39
|
+
*
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
18
42
|
_getSql() {
|
|
19
43
|
if (!this._sql) {
|
|
20
44
|
this._sql = _serverless.neon.call(void 0, this._connectionString);
|
|
@@ -23,12 +47,14 @@ var NeonAdapter = class {
|
|
|
23
47
|
}
|
|
24
48
|
async initSchema() {
|
|
25
49
|
const sql = this._getSql();
|
|
26
|
-
await sql.query(
|
|
27
|
-
|
|
50
|
+
await sql.query(_chunk7NMQFTDJcjs.TABLE_SQL);
|
|
51
|
+
await sql.query(_chunk7NMQFTDJcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
52
|
+
await sql.query(_chunk7NMQFTDJcjs.LLM_CREDENTIALS_TABLE_SQL);
|
|
53
|
+
for (const migration of _chunk7NMQFTDJcjs.MIGRATIONS) {
|
|
28
54
|
await sql.query(migration.sql);
|
|
29
55
|
}
|
|
30
56
|
const indexResults = [];
|
|
31
|
-
for (const index of
|
|
57
|
+
for (const index of _chunk7NMQFTDJcjs.INDEXES) {
|
|
32
58
|
try {
|
|
33
59
|
await sql.query(index.sql);
|
|
34
60
|
indexResults.push({
|
|
@@ -54,11 +80,11 @@ var NeonAdapter = class {
|
|
|
54
80
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
55
81
|
);
|
|
56
82
|
}
|
|
57
|
-
const tableCheck = await sql.query(
|
|
83
|
+
const tableCheck = await sql.query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
|
|
58
84
|
if (tableCheck.length === 0) {
|
|
59
85
|
throw new Error("Table creation verification failed");
|
|
60
86
|
}
|
|
61
|
-
const columns = await sql.query(
|
|
87
|
+
const columns = await sql.query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
|
|
62
88
|
return {
|
|
63
89
|
columnsCreated: columns.length,
|
|
64
90
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -72,6 +98,8 @@ var NeonAdapter = class {
|
|
|
72
98
|
async dropSchema() {
|
|
73
99
|
const sql = this._getSql();
|
|
74
100
|
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
101
|
+
await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
102
|
+
await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
75
103
|
}
|
|
76
104
|
async createTask(params) {
|
|
77
105
|
const sql = this._getSql();
|
|
@@ -84,10 +112,15 @@ var NeonAdapter = class {
|
|
|
84
112
|
);
|
|
85
113
|
return rows[0] || null;
|
|
86
114
|
}
|
|
115
|
+
// 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
|
|
116
|
+
async createTaskSuperseding(params, supersedesUuid) {
|
|
117
|
+
const sql = this._getSql();
|
|
118
|
+
return _chunkTKDISKCFcjs.createTaskSuperseding.call(void 0, (text, args) => sql.query(text, args), params, supersedesUuid);
|
|
119
|
+
}
|
|
87
120
|
async getTaskByUuid(uuid, userId) {
|
|
88
121
|
const sql = this._getSql();
|
|
89
122
|
const rows = await sql.query(
|
|
90
|
-
`SELECT
|
|
123
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
91
124
|
FROM scheduled_messages
|
|
92
125
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
93
126
|
LIMIT 1`,
|
|
@@ -98,7 +131,7 @@ var NeonAdapter = class {
|
|
|
98
131
|
async getTaskByUuidOnly(uuid) {
|
|
99
132
|
const sql = this._getSql();
|
|
100
133
|
const rows = await sql.query(
|
|
101
|
-
`SELECT
|
|
134
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
102
135
|
FROM scheduled_messages
|
|
103
136
|
WHERE uuid = $1 AND status = 'pending'
|
|
104
137
|
LIMIT 1`,
|
|
@@ -106,12 +139,30 @@ var NeonAdapter = class {
|
|
|
106
139
|
);
|
|
107
140
|
return rows[0] || null;
|
|
108
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
144
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
145
|
+
*
|
|
146
|
+
* @param {string} uuid
|
|
147
|
+
* @returns {Promise<{ status: string }|null>}
|
|
148
|
+
*/
|
|
149
|
+
async getTaskStatusByUuidOnly(uuid) {
|
|
150
|
+
const sql = this._getSql();
|
|
151
|
+
const rows = await sql.query(
|
|
152
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
153
|
+
[uuid]
|
|
154
|
+
);
|
|
155
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
156
|
+
}
|
|
109
157
|
async updateTaskById(taskId, updates) {
|
|
110
158
|
const sql = this._getSql();
|
|
111
159
|
const sets = [];
|
|
112
160
|
const values = [];
|
|
113
161
|
let idx = 1;
|
|
114
162
|
for (const [key, value] of Object.entries(updates)) {
|
|
163
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
164
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
165
|
+
}
|
|
115
166
|
sets.push(`${key} = $${idx}`);
|
|
116
167
|
values.push(value);
|
|
117
168
|
idx++;
|
|
@@ -124,6 +175,15 @@ var NeonAdapter = class {
|
|
|
124
175
|
);
|
|
125
176
|
return rows[0] || null;
|
|
126
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
|
|
180
|
+
*
|
|
181
|
+
* 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
|
|
182
|
+
* 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
|
|
183
|
+
* 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
|
|
184
|
+
* ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
|
|
185
|
+
* 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
|
|
186
|
+
*/
|
|
127
187
|
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
128
188
|
const sql = this._getSql();
|
|
129
189
|
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
@@ -131,15 +191,19 @@ var NeonAdapter = class {
|
|
|
131
191
|
let idx = 2;
|
|
132
192
|
if (extraFields) {
|
|
133
193
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
194
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
195
|
+
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
196
|
+
}
|
|
134
197
|
sets.push(`${key} = $${idx}`);
|
|
135
198
|
values.push(value);
|
|
136
199
|
idx++;
|
|
137
200
|
}
|
|
138
201
|
}
|
|
139
202
|
values.push(uuid, userId);
|
|
203
|
+
const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
|
|
140
204
|
const rows = await sql.query(
|
|
141
205
|
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
142
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
206
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
|
|
143
207
|
RETURNING uuid, updated_at`,
|
|
144
208
|
values
|
|
145
209
|
);
|
|
@@ -161,53 +225,26 @@ var NeonAdapter = class {
|
|
|
161
225
|
async getPendingTasks(limit = 50) {
|
|
162
226
|
const sql = this._getSql();
|
|
163
227
|
return sql.query(
|
|
164
|
-
`SELECT
|
|
228
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
165
229
|
FROM scheduled_messages
|
|
166
230
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
167
231
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
232
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
168
233
|
ORDER BY next_send_at ASC
|
|
169
234
|
LIMIT $1`,
|
|
170
235
|
[limit]
|
|
171
236
|
);
|
|
172
237
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
178
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
179
|
-
*
|
|
180
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
181
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
182
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
183
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
184
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
185
|
-
* 这一跳就不该再按旧时刻发。
|
|
186
|
-
*
|
|
187
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
188
|
-
* 要改表。
|
|
189
|
-
*
|
|
190
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
191
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
192
|
-
*
|
|
193
|
-
* @param {number} taskId
|
|
194
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
195
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
196
|
-
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
197
|
-
*/
|
|
198
|
-
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
238
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
239
|
+
// 语义说明也在那里)。
|
|
240
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
199
241
|
const sql = this._getSql();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
207
|
-
RETURNING id`,
|
|
208
|
-
[leaseUntil, taskId, expectedNextSendAt]
|
|
209
|
-
);
|
|
210
|
-
return rows.length > 0;
|
|
242
|
+
return _chunkTKDISKCFcjs.claimTask.call(void 0, (text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
243
|
+
}
|
|
244
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
245
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
246
|
+
const sql = this._getSql();
|
|
247
|
+
return _chunkTKDISKCFcjs.renewTaskLease.call(void 0, (text, params) => sql.query(text, params), taskId, leaseUntil);
|
|
211
248
|
}
|
|
212
249
|
async listTasks(userId, opts = {}) {
|
|
213
250
|
const sql = this._getSql();
|
|
@@ -228,7 +265,7 @@ var NeonAdapter = class {
|
|
|
228
265
|
const total = parseInt(countRows[0].count, 10);
|
|
229
266
|
const taskParams = [...params, limit, offset];
|
|
230
267
|
const tasks = await sql.query(
|
|
231
|
-
`SELECT
|
|
268
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
232
269
|
FROM scheduled_messages
|
|
233
270
|
WHERE ${where}
|
|
234
271
|
ORDER BY next_send_at ASC
|
|
@@ -257,6 +294,41 @@ var NeonAdapter = class {
|
|
|
257
294
|
);
|
|
258
295
|
return rows.length > 0 ? rows[0].status : null;
|
|
259
296
|
}
|
|
297
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
298
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
299
|
+
const sql = this._getSql();
|
|
300
|
+
return _chunkTKDISKCFcjs.getTaskStatusInfo.call(void 0, (text, params) => sql.query(text, params), uuid, userId);
|
|
301
|
+
}
|
|
302
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
303
|
+
async getPushSubscription(userId) {
|
|
304
|
+
const sql = this._getSql();
|
|
305
|
+
return _chunkTKDISKCFcjs.getPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
306
|
+
}
|
|
307
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
308
|
+
const sql = this._getSql();
|
|
309
|
+
return _chunkTKDISKCFcjs.upsertPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
|
|
310
|
+
}
|
|
311
|
+
async deletePushSubscription(userId) {
|
|
312
|
+
const sql = this._getSql();
|
|
313
|
+
return _chunkTKDISKCFcjs.deletePushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
314
|
+
}
|
|
315
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
316
|
+
async upsertLlmCredentials(userId, entries) {
|
|
317
|
+
const sql = this._getSql();
|
|
318
|
+
return _chunkTKDISKCFcjs.upsertLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
319
|
+
}
|
|
320
|
+
async getLlmCredentials(userId, credIds) {
|
|
321
|
+
const sql = this._getSql();
|
|
322
|
+
return _chunkTKDISKCFcjs.getLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
|
|
323
|
+
}
|
|
324
|
+
async listLlmCredentials(userId) {
|
|
325
|
+
const sql = this._getSql();
|
|
326
|
+
return _chunkTKDISKCFcjs.listLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
327
|
+
}
|
|
328
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
329
|
+
const sql = this._getSql();
|
|
330
|
+
return _chunkTKDISKCFcjs.deleteLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
|
|
331
|
+
}
|
|
260
332
|
};
|
|
261
333
|
|
|
262
334
|
|