@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +861 -35
  3. package/dist/adapters/d1.d.ts +693 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +384 -0
  6. package/dist/adapters/neon.d.ts +105 -0
  7. package/dist/adapters/pg-shared.d.ts +194 -0
  8. package/dist/adapters/pg.d.ts +101 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +80 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
  13. package/dist/chunk-RL7V7VSS.mjs +8784 -0
  14. package/dist/chunk-TKDISKCF.cjs +163 -0
  15. package/dist/chunk-XKUWMYVF.mjs +163 -0
  16. package/dist/chunk-YBDQJVFQ.cjs +8784 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +39 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +11 -2
  20. package/dist/cloudflare.mjs +36 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state-namespaces.d.ts +5 -0
  24. package/dist/handlers/client-state.d.ts +7 -0
  25. package/dist/handlers/cloud-data.d.ts +4 -0
  26. package/dist/handlers/get-message.d.ts +3 -0
  27. package/dist/handlers/get-user-key.d.ts +3 -0
  28. package/dist/handlers/init-tenant.d.ts +23 -0
  29. package/dist/handlers/llm-credentials.d.ts +5 -0
  30. package/dist/handlers/messages.d.ts +3 -0
  31. package/dist/handlers/outbox.d.ts +9 -0
  32. package/dist/handlers/push-subscription.d.ts +5 -0
  33. package/dist/handlers/schedule-message.d.ts +3 -0
  34. package/dist/handlers/send-notifications.d.ts +3 -0
  35. package/dist/handlers/single-user-init.d.ts +13 -0
  36. package/dist/handlers/update-message.d.ts +3 -0
  37. package/dist/handlers/vapid-public-key.d.ts +19 -0
  38. package/dist/index.cjs +133 -44
  39. package/dist/index.d.cts +145 -766
  40. package/dist/index.d.ts +145 -766
  41. package/dist/index.mjs +125 -36
  42. package/dist/lib/agentic-fire.d.ts +177 -0
  43. package/dist/lib/client-state-store.d.ts +88 -0
  44. package/dist/lib/cloud-data-cleanup.d.ts +36 -0
  45. package/dist/lib/cloud-data-guard.d.ts +14 -0
  46. package/dist/lib/cloud-data-inventory.d.ts +15 -0
  47. package/dist/lib/constant-time.d.ts +14 -0
  48. package/dist/lib/db-errors.d.ts +10 -0
  49. package/dist/lib/encryption.d.ts +48 -0
  50. package/dist/lib/errors.d.ts +273 -0
  51. package/dist/lib/llm-credentials-store.d.ts +147 -0
  52. package/dist/lib/llm.d.ts +12 -0
  53. package/dist/lib/message-processor.d.ts +82 -0
  54. package/dist/lib/outbox-store.d.ts +162 -0
  55. package/dist/lib/push-policy.d.ts +11 -0
  56. package/dist/lib/push-subscription-store.d.ts +94 -0
  57. package/dist/lib/recurrence.d.ts +51 -0
  58. package/dist/lib/request.d.ts +182 -0
  59. package/dist/lib/result-emitter.d.ts +60 -0
  60. package/dist/lib/retry-policy.d.ts +11 -0
  61. package/dist/lib/run-tick.d.ts +121 -0
  62. package/dist/lib/schema-version.d.ts +50 -0
  63. package/dist/lib/state-accessors.d.ts +70 -0
  64. package/dist/lib/state-chunks.d.ts +65 -0
  65. package/dist/lib/task-projection.d.ts +92 -0
  66. package/dist/lib/validation.d.ts +97 -0
  67. package/dist/lib/version.d.ts +7 -0
  68. package/dist/lib/webcrypto-utils.d.ts +1 -0
  69. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  70. package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
  71. package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
  72. package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
  73. package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
  74. package/dist/single-user.d.ts +106 -0
  75. package/dist/tenant/blob-store.d.ts +30 -0
  76. package/dist/tenant/context.d.ts +47 -0
  77. package/dist/tenant/single-user-context.d.ts +29 -0
  78. package/dist/tenant/token.d.ts +26 -0
  79. package/package.json +3 -3
  80. package/dist/chunk-DCKCAGFV.cjs +0 -81
  81. package/dist/chunk-HQHRXSQA.cjs +0 -2710
  82. package/dist/chunk-LEE5KQ7Y.mjs +0 -2710
  83. package/dist/cloudflare-CH3dBl5b.d.cts +0 -4235
  84. package/dist/cloudflare-CH3dBl5b.d.ts +0 -4235
  85. package/dist/cloudflare.d.cts +0 -2
  86. package/dist/neon-D8Z2Nn97.d.cts +0 -295
  87. package/dist/neon-PqNFJIz2.d.ts +0 -295
  88. package/dist/pg-C48FwCWW.d.cts +0 -289
  89. package/dist/pg-C5gtWUfU.d.ts +0 -289
  90. package/dist/schema-BHuyH_6Z.d.cts +0 -88
  91. package/dist/schema-BHuyH_6Z.d.ts +0 -88
@@ -4,7 +4,25 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkDCKCAGFVcjs = require('./chunk-DCKCAGFV.cjs');
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(_chunkDCKCAGFVcjs.TABLE_SQL);
32
- for (const migration of _chunkDCKCAGFVcjs.MIGRATIONS) {
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 _chunkDCKCAGFVcjs.INDEXES) {
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(_chunkDCKCAGFVcjs.VERIFY_TABLE_SQL);
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(_chunkDCKCAGFVcjs.COLUMNS_SQL);
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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
- * 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
171
- * 本次投递期间别的 tick 领不走它。
172
- *
173
- * 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
174
- * 任务列表要读它、循环任务推进下一次也要拿它当基准。
175
- *
176
- * 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
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-GOBCWCJP.mjs";
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
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
- * 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
171
- * 本次投递期间别的 tick 领不走它。
172
- *
173
- * 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
174
- * 任务列表要读它、循环任务推进下一次也要拿它当基准。
175
- *
176
- * 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
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 id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
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
+ };