@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
|
@@ -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/pg.js
|
|
10
28
|
var _pg = require('pg');
|
|
@@ -18,6 +36,12 @@ var PgAdapter = class {
|
|
|
18
36
|
_getPool() {
|
|
19
37
|
if (!this._pool) {
|
|
20
38
|
this._pool = new (0, _pg.Pool)({ connectionString: this._connectionString });
|
|
39
|
+
this._pool.on("error", (error) => {
|
|
40
|
+
console.error(
|
|
41
|
+
"[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:",
|
|
42
|
+
error && error.message
|
|
43
|
+
);
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
return this._pool;
|
|
23
47
|
}
|
|
@@ -28,12 +52,14 @@ var PgAdapter = class {
|
|
|
28
52
|
return result.rows;
|
|
29
53
|
}
|
|
30
54
|
async initSchema() {
|
|
31
|
-
await this._query(
|
|
32
|
-
|
|
55
|
+
await this._query(_chunk7NMQFTDJcjs.TABLE_SQL);
|
|
56
|
+
await this._query(_chunk7NMQFTDJcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
57
|
+
await this._query(_chunk7NMQFTDJcjs.LLM_CREDENTIALS_TABLE_SQL);
|
|
58
|
+
for (const migration of _chunk7NMQFTDJcjs.MIGRATIONS) {
|
|
33
59
|
await this._query(migration.sql);
|
|
34
60
|
}
|
|
35
61
|
const indexResults = [];
|
|
36
|
-
for (const index of
|
|
62
|
+
for (const index of _chunk7NMQFTDJcjs.INDEXES) {
|
|
37
63
|
try {
|
|
38
64
|
await this._query(index.sql);
|
|
39
65
|
indexResults.push({
|
|
@@ -59,11 +85,11 @@ var PgAdapter = class {
|
|
|
59
85
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
60
86
|
);
|
|
61
87
|
}
|
|
62
|
-
const tableCheck = await this._query(
|
|
88
|
+
const tableCheck = await this._query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
|
|
63
89
|
if (tableCheck.length === 0) {
|
|
64
90
|
throw new Error("Table creation verification failed");
|
|
65
91
|
}
|
|
66
|
-
const columns = await this._query(
|
|
92
|
+
const columns = await this._query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
|
|
67
93
|
return {
|
|
68
94
|
columnsCreated: columns.length,
|
|
69
95
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -76,6 +102,8 @@ var PgAdapter = class {
|
|
|
76
102
|
}
|
|
77
103
|
async dropSchema() {
|
|
78
104
|
await this._query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
105
|
+
await this._query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
106
|
+
await this._query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
79
107
|
}
|
|
80
108
|
async createTask(params) {
|
|
81
109
|
const rows = await this._query(
|
|
@@ -87,9 +115,13 @@ var PgAdapter = class {
|
|
|
87
115
|
);
|
|
88
116
|
return rows[0] || null;
|
|
89
117
|
}
|
|
118
|
+
// 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
|
|
119
|
+
async createTaskSuperseding(params, supersedesUuid) {
|
|
120
|
+
return _chunkTKDISKCFcjs.createTaskSuperseding.call(void 0, (text, args) => this._query(text, args), params, supersedesUuid);
|
|
121
|
+
}
|
|
90
122
|
async getTaskByUuid(uuid, userId) {
|
|
91
123
|
const rows = await this._query(
|
|
92
|
-
`SELECT
|
|
124
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
93
125
|
FROM scheduled_messages
|
|
94
126
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
95
127
|
LIMIT 1`,
|
|
@@ -99,7 +131,7 @@ var PgAdapter = class {
|
|
|
99
131
|
}
|
|
100
132
|
async getTaskByUuidOnly(uuid) {
|
|
101
133
|
const rows = await this._query(
|
|
102
|
-
`SELECT
|
|
134
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
103
135
|
FROM scheduled_messages
|
|
104
136
|
WHERE uuid = $1 AND status = 'pending'
|
|
105
137
|
LIMIT 1`,
|
|
@@ -107,11 +139,28 @@ var PgAdapter = class {
|
|
|
107
139
|
);
|
|
108
140
|
return rows[0] || null;
|
|
109
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 rows = await this._query(
|
|
151
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
152
|
+
[uuid]
|
|
153
|
+
);
|
|
154
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
155
|
+
}
|
|
110
156
|
async updateTaskById(taskId, updates) {
|
|
111
157
|
const sets = [];
|
|
112
158
|
const values = [];
|
|
113
159
|
let idx = 1;
|
|
114
160
|
for (const [key, value] of Object.entries(updates)) {
|
|
161
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
162
|
+
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
163
|
+
}
|
|
115
164
|
sets.push(`${key} = $${idx}`);
|
|
116
165
|
values.push(value);
|
|
117
166
|
idx++;
|
|
@@ -124,21 +173,34 @@ var PgAdapter = class {
|
|
|
124
173
|
);
|
|
125
174
|
return rows[0] || null;
|
|
126
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
|
|
178
|
+
*
|
|
179
|
+
* 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
|
|
180
|
+
* 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
|
|
181
|
+
* 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
|
|
182
|
+
* ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
|
|
183
|
+
* 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
|
|
184
|
+
*/
|
|
127
185
|
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
128
186
|
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
129
187
|
const values = [encryptedPayload];
|
|
130
188
|
let idx = 2;
|
|
131
189
|
if (extraFields) {
|
|
132
190
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
191
|
+
if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
192
|
+
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
193
|
+
}
|
|
133
194
|
sets.push(`${key} = $${idx}`);
|
|
134
195
|
values.push(value);
|
|
135
196
|
idx++;
|
|
136
197
|
}
|
|
137
198
|
}
|
|
138
199
|
values.push(uuid, userId);
|
|
200
|
+
const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
|
|
139
201
|
const rows = await this._query(
|
|
140
202
|
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
141
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
203
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
|
|
142
204
|
RETURNING uuid, updated_at`,
|
|
143
205
|
values
|
|
144
206
|
);
|
|
@@ -157,52 +219,24 @@ var PgAdapter = class {
|
|
|
157
219
|
}
|
|
158
220
|
async getPendingTasks(limit = 50) {
|
|
159
221
|
return this._query(
|
|
160
|
-
`SELECT
|
|
222
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
|
|
161
223
|
FROM scheduled_messages
|
|
162
224
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
163
225
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
226
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
164
227
|
ORDER BY next_send_at ASC
|
|
165
228
|
LIMIT $1`,
|
|
166
229
|
[limit]
|
|
167
230
|
);
|
|
168
231
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
178
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
179
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
180
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
181
|
-
* 这一跳就不该再按旧时刻发。
|
|
182
|
-
*
|
|
183
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
184
|
-
* 要改表。
|
|
185
|
-
*
|
|
186
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
187
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
188
|
-
*
|
|
189
|
-
* @param {number} taskId
|
|
190
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
191
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
192
|
-
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
193
|
-
*/
|
|
194
|
-
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
195
|
-
const rows = await this._query(
|
|
196
|
-
`UPDATE scheduled_messages
|
|
197
|
-
SET lease_until = $1, updated_at = NOW()
|
|
198
|
-
WHERE id = $2 AND status = 'pending'
|
|
199
|
-
AND date_trunc('milliseconds', next_send_at)
|
|
200
|
-
= date_trunc('milliseconds', $3::timestamptz)
|
|
201
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
202
|
-
RETURNING id`,
|
|
203
|
-
[leaseUntil, taskId, expectedNextSendAt]
|
|
204
|
-
);
|
|
205
|
-
return rows.length > 0;
|
|
232
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
233
|
+
// 语义说明也在那里)。
|
|
234
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
235
|
+
return _chunkTKDISKCFcjs.claimTask.call(void 0, (text, params) => this._query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
236
|
+
}
|
|
237
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
238
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
239
|
+
return _chunkTKDISKCFcjs.renewTaskLease.call(void 0, (text, params) => this._query(text, params), taskId, leaseUntil);
|
|
206
240
|
}
|
|
207
241
|
async listTasks(userId, opts = {}) {
|
|
208
242
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -222,7 +256,7 @@ var PgAdapter = class {
|
|
|
222
256
|
const total = parseInt(countRows[0].count, 10);
|
|
223
257
|
const taskParams = [...params, limit, offset];
|
|
224
258
|
const tasks = await this._query(
|
|
225
|
-
`SELECT
|
|
259
|
+
`SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
|
|
226
260
|
FROM scheduled_messages
|
|
227
261
|
WHERE ${where}
|
|
228
262
|
ORDER BY next_send_at ASC
|
|
@@ -249,6 +283,33 @@ var PgAdapter = class {
|
|
|
249
283
|
);
|
|
250
284
|
return rows.length > 0 ? rows[0].status : null;
|
|
251
285
|
}
|
|
286
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
287
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
288
|
+
return _chunkTKDISKCFcjs.getTaskStatusInfo.call(void 0, (text, params) => this._query(text, params), uuid, userId);
|
|
289
|
+
}
|
|
290
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
291
|
+
async getPushSubscription(userId) {
|
|
292
|
+
return _chunkTKDISKCFcjs.getPushSubscription.call(void 0, (text, params) => this._query(text, params), userId);
|
|
293
|
+
}
|
|
294
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
295
|
+
return _chunkTKDISKCFcjs.upsertPushSubscription.call(void 0, (text, params) => this._query(text, params), userId, encryptedSubscription, updatedAt);
|
|
296
|
+
}
|
|
297
|
+
async deletePushSubscription(userId) {
|
|
298
|
+
return _chunkTKDISKCFcjs.deletePushSubscription.call(void 0, (text, params) => this._query(text, params), userId);
|
|
299
|
+
}
|
|
300
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
301
|
+
async upsertLlmCredentials(userId, entries) {
|
|
302
|
+
return _chunkTKDISKCFcjs.upsertLlmCredentials.call(void 0, (text, params) => this._query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
303
|
+
}
|
|
304
|
+
async getLlmCredentials(userId, credIds) {
|
|
305
|
+
return _chunkTKDISKCFcjs.getLlmCredentials.call(void 0, (text, params) => this._query(text, params), userId, credIds);
|
|
306
|
+
}
|
|
307
|
+
async listLlmCredentials(userId) {
|
|
308
|
+
return _chunkTKDISKCFcjs.listLlmCredentials.call(void 0, (text, params) => this._query(text, params), userId);
|
|
309
|
+
}
|
|
310
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
311
|
+
return _chunkTKDISKCFcjs.deleteLlmCredentials.call(void 0, (text, params) => this._query(text, params), userId, credIds);
|
|
312
|
+
}
|
|
252
313
|
};
|
|
253
314
|
|
|
254
315
|
|
|
@@ -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/pg.js
|
|
10
28
|
import { Pool } from "pg";
|
|
@@ -18,6 +36,12 @@ var PgAdapter = class {
|
|
|
18
36
|
_getPool() {
|
|
19
37
|
if (!this._pool) {
|
|
20
38
|
this._pool = new Pool({ connectionString: this._connectionString });
|
|
39
|
+
this._pool.on("error", (error) => {
|
|
40
|
+
console.error(
|
|
41
|
+
"[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:",
|
|
42
|
+
error && error.message
|
|
43
|
+
);
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
return this._pool;
|
|
23
47
|
}
|
|
@@ -29,6 +53,8 @@ var PgAdapter = class {
|
|
|
29
53
|
}
|
|
30
54
|
async initSchema() {
|
|
31
55
|
await this._query(TABLE_SQL);
|
|
56
|
+
await this._query(PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
57
|
+
await this._query(LLM_CREDENTIALS_TABLE_SQL);
|
|
32
58
|
for (const migration of MIGRATIONS) {
|
|
33
59
|
await this._query(migration.sql);
|
|
34
60
|
}
|
|
@@ -76,6 +102,8 @@ var PgAdapter = class {
|
|
|
76
102
|
}
|
|
77
103
|
async dropSchema() {
|
|
78
104
|
await this._query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
105
|
+
await this._query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
|
|
106
|
+
await this._query("DROP TABLE IF EXISTS llm_credentials CASCADE");
|
|
79
107
|
}
|
|
80
108
|
async createTask(params) {
|
|
81
109
|
const rows = await this._query(
|
|
@@ -87,9 +115,13 @@ var PgAdapter = class {
|
|
|
87
115
|
);
|
|
88
116
|
return rows[0] || null;
|
|
89
117
|
}
|
|
118
|
+
// 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
|
|
119
|
+
async createTaskSuperseding(params, supersedesUuid) {
|
|
120
|
+
return createTaskSuperseding((text, args) => this._query(text, args), params, supersedesUuid);
|
|
121
|
+
}
|
|
90
122
|
async getTaskByUuid(uuid, userId) {
|
|
91
123
|
const rows = await this._query(
|
|
92
|
-
`SELECT
|
|
124
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
93
125
|
FROM scheduled_messages
|
|
94
126
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
95
127
|
LIMIT 1`,
|
|
@@ -99,7 +131,7 @@ var PgAdapter = class {
|
|
|
99
131
|
}
|
|
100
132
|
async getTaskByUuidOnly(uuid) {
|
|
101
133
|
const rows = await this._query(
|
|
102
|
-
`SELECT
|
|
134
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
103
135
|
FROM scheduled_messages
|
|
104
136
|
WHERE uuid = $1 AND status = 'pending'
|
|
105
137
|
LIMIT 1`,
|
|
@@ -107,11 +139,28 @@ var PgAdapter = class {
|
|
|
107
139
|
);
|
|
108
140
|
return rows[0] || null;
|
|
109
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 rows = await this._query(
|
|
151
|
+
`SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
|
|
152
|
+
[uuid]
|
|
153
|
+
);
|
|
154
|
+
return rows[0] ? { status: rows[0].status } : null;
|
|
155
|
+
}
|
|
110
156
|
async updateTaskById(taskId, updates) {
|
|
111
157
|
const sets = [];
|
|
112
158
|
const values = [];
|
|
113
159
|
let idx = 1;
|
|
114
160
|
for (const [key, value] of Object.entries(updates)) {
|
|
161
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
162
|
+
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
163
|
+
}
|
|
115
164
|
sets.push(`${key} = $${idx}`);
|
|
116
165
|
values.push(value);
|
|
117
166
|
idx++;
|
|
@@ -124,21 +173,34 @@ var PgAdapter = class {
|
|
|
124
173
|
);
|
|
125
174
|
return rows[0] || null;
|
|
126
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
|
|
178
|
+
*
|
|
179
|
+
* 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
|
|
180
|
+
* 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
|
|
181
|
+
* 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
|
|
182
|
+
* ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
|
|
183
|
+
* 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
|
|
184
|
+
*/
|
|
127
185
|
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
128
186
|
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
129
187
|
const values = [encryptedPayload];
|
|
130
188
|
let idx = 2;
|
|
131
189
|
if (extraFields) {
|
|
132
190
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
191
|
+
if (!UPDATABLE_COLUMNS.has(key)) {
|
|
192
|
+
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
193
|
+
}
|
|
133
194
|
sets.push(`${key} = $${idx}`);
|
|
134
195
|
values.push(value);
|
|
135
196
|
idx++;
|
|
136
197
|
}
|
|
137
198
|
}
|
|
138
199
|
values.push(uuid, userId);
|
|
200
|
+
const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
|
|
139
201
|
const rows = await this._query(
|
|
140
202
|
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
141
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
203
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
|
|
142
204
|
RETURNING uuid, updated_at`,
|
|
143
205
|
values
|
|
144
206
|
);
|
|
@@ -157,52 +219,24 @@ var PgAdapter = class {
|
|
|
157
219
|
}
|
|
158
220
|
async getPendingTasks(limit = 50) {
|
|
159
221
|
return this._query(
|
|
160
|
-
`SELECT
|
|
222
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
161
223
|
FROM scheduled_messages
|
|
162
224
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
163
225
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
226
|
+
AND (retry_after IS NULL OR retry_after <= NOW())
|
|
164
227
|
ORDER BY next_send_at ASC
|
|
165
228
|
LIMIT $1`,
|
|
166
229
|
[limit]
|
|
167
230
|
);
|
|
168
231
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
178
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
179
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
180
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
181
|
-
* 这一跳就不该再按旧时刻发。
|
|
182
|
-
*
|
|
183
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
184
|
-
* 要改表。
|
|
185
|
-
*
|
|
186
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
187
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
188
|
-
*
|
|
189
|
-
* @param {number} taskId
|
|
190
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
191
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
192
|
-
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
193
|
-
*/
|
|
194
|
-
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
195
|
-
const rows = await this._query(
|
|
196
|
-
`UPDATE scheduled_messages
|
|
197
|
-
SET lease_until = $1, updated_at = NOW()
|
|
198
|
-
WHERE id = $2 AND status = 'pending'
|
|
199
|
-
AND date_trunc('milliseconds', next_send_at)
|
|
200
|
-
= date_trunc('milliseconds', $3::timestamptz)
|
|
201
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
202
|
-
RETURNING id`,
|
|
203
|
-
[leaseUntil, taskId, expectedNextSendAt]
|
|
204
|
-
);
|
|
205
|
-
return rows.length > 0;
|
|
232
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
233
|
+
// 语义说明也在那里)。
|
|
234
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
235
|
+
return claimTask((text, params) => this._query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
236
|
+
}
|
|
237
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
238
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
239
|
+
return renewTaskLease((text, params) => this._query(text, params), taskId, leaseUntil);
|
|
206
240
|
}
|
|
207
241
|
async listTasks(userId, opts = {}) {
|
|
208
242
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -222,7 +256,7 @@ var PgAdapter = class {
|
|
|
222
256
|
const total = parseInt(countRows[0].count, 10);
|
|
223
257
|
const taskParams = [...params, limit, offset];
|
|
224
258
|
const tasks = await this._query(
|
|
225
|
-
`SELECT
|
|
259
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
226
260
|
FROM scheduled_messages
|
|
227
261
|
WHERE ${where}
|
|
228
262
|
ORDER BY next_send_at ASC
|
|
@@ -249,6 +283,33 @@ var PgAdapter = class {
|
|
|
249
283
|
);
|
|
250
284
|
return rows.length > 0 ? rows[0].status : null;
|
|
251
285
|
}
|
|
286
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
287
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
288
|
+
return getTaskStatusInfo((text, params) => this._query(text, params), uuid, userId);
|
|
289
|
+
}
|
|
290
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
291
|
+
async getPushSubscription(userId) {
|
|
292
|
+
return getPushSubscription((text, params) => this._query(text, params), userId);
|
|
293
|
+
}
|
|
294
|
+
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
295
|
+
return upsertPushSubscription((text, params) => this._query(text, params), userId, encryptedSubscription, updatedAt);
|
|
296
|
+
}
|
|
297
|
+
async deletePushSubscription(userId) {
|
|
298
|
+
return deletePushSubscription((text, params) => this._query(text, params), userId);
|
|
299
|
+
}
|
|
300
|
+
// ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
|
|
301
|
+
async upsertLlmCredentials(userId, entries) {
|
|
302
|
+
return upsertLlmCredentials((text, params) => this._query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
|
|
303
|
+
}
|
|
304
|
+
async getLlmCredentials(userId, credIds) {
|
|
305
|
+
return getLlmCredentials((text, params) => this._query(text, params), userId, credIds);
|
|
306
|
+
}
|
|
307
|
+
async listLlmCredentials(userId) {
|
|
308
|
+
return listLlmCredentials((text, params) => this._query(text, params), userId);
|
|
309
|
+
}
|
|
310
|
+
async deleteLlmCredentials(userId, credIds = null) {
|
|
311
|
+
return deleteLlmCredentials((text, params) => this._query(text, params), userId, credIds);
|
|
312
|
+
}
|
|
252
313
|
};
|
|
253
314
|
export {
|
|
254
315
|
PgAdapter
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export function createSingleUserServer(config: any): {
|
|
2
|
+
ctx: {
|
|
3
|
+
vapid: {
|
|
4
|
+
email: any;
|
|
5
|
+
publicKey: any;
|
|
6
|
+
privateKey: any;
|
|
7
|
+
};
|
|
8
|
+
webpush: any;
|
|
9
|
+
multipart: any;
|
|
10
|
+
tenantManager: {
|
|
11
|
+
resolveTenant: (headers: any) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
error: {
|
|
14
|
+
status: number;
|
|
15
|
+
body: {
|
|
16
|
+
success: boolean;
|
|
17
|
+
error: {
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
context?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
context: {
|
|
27
|
+
tenantId: string;
|
|
28
|
+
tokenType: string;
|
|
29
|
+
db: any;
|
|
30
|
+
masterKey: any;
|
|
31
|
+
};
|
|
32
|
+
error?: undefined;
|
|
33
|
+
}>;
|
|
34
|
+
initializeTenant: () => Promise<{
|
|
35
|
+
tenantId: string;
|
|
36
|
+
schema: any;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
hooks: any;
|
|
40
|
+
maxToolIterations: any;
|
|
41
|
+
totalTimeoutMs: any;
|
|
42
|
+
maxStateValueBytes: any;
|
|
43
|
+
onAfterSend: any;
|
|
44
|
+
onFireSettled: any;
|
|
45
|
+
maxScheduledTasksPerFire: any;
|
|
46
|
+
maxGenerationRetries: any;
|
|
47
|
+
maxDeliveryRetries: any;
|
|
48
|
+
cloudData: any;
|
|
49
|
+
};
|
|
50
|
+
handlers: {
|
|
51
|
+
init: {
|
|
52
|
+
POST: Function;
|
|
53
|
+
};
|
|
54
|
+
getUserKey: {
|
|
55
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
56
|
+
};
|
|
57
|
+
scheduleMessage: {
|
|
58
|
+
POST: (headers: any, body: any) => Promise<any>;
|
|
59
|
+
};
|
|
60
|
+
updateMessage: {
|
|
61
|
+
PUT: (url: any, headers: any, body: any) => Promise<any>;
|
|
62
|
+
};
|
|
63
|
+
cancelMessage: {
|
|
64
|
+
DELETE: (url: any, headers: any) => Promise<any>;
|
|
65
|
+
};
|
|
66
|
+
messages: {
|
|
67
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
68
|
+
};
|
|
69
|
+
getMessage: {
|
|
70
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
71
|
+
};
|
|
72
|
+
vapidPublicKey: {
|
|
73
|
+
GET: Function;
|
|
74
|
+
};
|
|
75
|
+
clientState: {
|
|
76
|
+
PUT: (headers: any, body: any) => Promise<any>;
|
|
77
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
78
|
+
DELETE: (url: any, headers: any) => Promise<any>;
|
|
79
|
+
};
|
|
80
|
+
clientStateNamespaces: {
|
|
81
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
82
|
+
};
|
|
83
|
+
pushSubscription: {
|
|
84
|
+
PUT: (headers: any, body: any) => Promise<any>;
|
|
85
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
86
|
+
DELETE: (url: any, headers: any) => Promise<any>;
|
|
87
|
+
};
|
|
88
|
+
llmCredentials: {
|
|
89
|
+
PUT: (headers: any, body: any) => Promise<any>;
|
|
90
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
91
|
+
DELETE: (url: any, headers: any, body: any) => Promise<any>;
|
|
92
|
+
};
|
|
93
|
+
capabilities: {
|
|
94
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
95
|
+
};
|
|
96
|
+
outbox: {
|
|
97
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
98
|
+
POST: (headers: any, body: any) => Promise<any>;
|
|
99
|
+
DELETE: (url: any, headers: any, body: any) => Promise<any>;
|
|
100
|
+
};
|
|
101
|
+
cloudData: {
|
|
102
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
103
|
+
POST: (url: any, headers: any, body: any) => Promise<any>;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* tenantId: string,
|
|
4
|
+
* db: { driver: 'neon'|'pg', connectionString: string },
|
|
5
|
+
* masterKey: string,
|
|
6
|
+
* createdAt: string,
|
|
7
|
+
* updatedAt: string
|
|
8
|
+
* }} TenantConfig
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ namespace?: string, kek: string }} options
|
|
12
|
+
*/
|
|
13
|
+
export function createTenantBlobStore(options: {
|
|
14
|
+
namespace?: string;
|
|
15
|
+
kek: string;
|
|
16
|
+
}): {
|
|
17
|
+
getTenantConfig(tenantId: any): Promise<any>;
|
|
18
|
+
setTenantConfig(config: any): Promise<void>;
|
|
19
|
+
deleteTenantConfig(tenantId: any): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export type TenantConfig = {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
db: {
|
|
24
|
+
driver: "neon" | "pg";
|
|
25
|
+
connectionString: string;
|
|
26
|
+
};
|
|
27
|
+
masterKey: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
};
|