@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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +599 -24
  3. package/dist/adapters/d1.d.ts +416 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +306 -0
  6. package/dist/adapters/neon.d.ts +88 -0
  7. package/dist/adapters/pg-shared.d.ts +161 -0
  8. package/dist/adapters/pg.d.ts +84 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +49 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/chunk-E7OWP3VL.cjs +135 -0
  13. package/dist/chunk-GN44PST5.mjs +148 -0
  14. package/dist/chunk-KOJQVOYS.mjs +5847 -0
  15. package/dist/chunk-QHVID3V2.cjs +5847 -0
  16. package/dist/chunk-ZGGF4GMA.mjs +135 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +37 -0
  18. package/dist/cloudflare.cjs +39 -2
  19. package/dist/cloudflare.d.ts +10 -2
  20. package/dist/cloudflare.mjs +40 -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.d.ts +7 -0
  24. package/dist/handlers/get-message.d.ts +3 -0
  25. package/dist/handlers/get-user-key.d.ts +3 -0
  26. package/dist/handlers/init-tenant.d.ts +23 -0
  27. package/dist/handlers/llm-credentials.d.ts +5 -0
  28. package/dist/handlers/messages.d.ts +3 -0
  29. package/dist/handlers/outbox.d.ts +7 -0
  30. package/dist/handlers/push-subscription.d.ts +5 -0
  31. package/dist/handlers/schedule-message.d.ts +3 -0
  32. package/dist/handlers/send-notifications.d.ts +3 -0
  33. package/dist/handlers/single-user-init.d.ts +13 -0
  34. package/dist/handlers/update-message.d.ts +3 -0
  35. package/dist/handlers/vapid-public-key.d.ts +19 -0
  36. package/dist/index.cjs +133 -44
  37. package/dist/index.d.cts +131 -766
  38. package/dist/index.d.ts +131 -766
  39. package/dist/index.mjs +125 -36
  40. package/dist/lib/agentic-fire.d.ts +108 -0
  41. package/dist/lib/client-state-store.d.ts +80 -0
  42. package/dist/lib/constant-time.d.ts +14 -0
  43. package/dist/lib/db-errors.d.ts +10 -0
  44. package/dist/lib/encryption.d.ts +48 -0
  45. package/dist/lib/errors.d.ts +215 -0
  46. package/dist/lib/llm-credentials-store.d.ts +147 -0
  47. package/dist/lib/llm.d.ts +1 -0
  48. package/dist/lib/message-processor.d.ts +73 -0
  49. package/dist/lib/outbox-store.d.ts +95 -0
  50. package/dist/lib/push-subscription-store.d.ts +94 -0
  51. package/dist/lib/recurrence.d.ts +51 -0
  52. package/dist/lib/request.d.ts +182 -0
  53. package/dist/lib/result-emitter.d.ts +54 -0
  54. package/dist/lib/run-tick.d.ts +115 -0
  55. package/dist/lib/schema-version.d.ts +50 -0
  56. package/dist/lib/state-accessors.d.ts +70 -0
  57. package/dist/lib/state-chunks.d.ts +55 -0
  58. package/dist/lib/task-projection.d.ts +92 -0
  59. package/dist/lib/validation.d.ts +97 -0
  60. package/dist/lib/version.d.ts +7 -0
  61. package/dist/lib/webcrypto-utils.d.ts +1 -0
  62. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  63. package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
  64. package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
  65. package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
  66. package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
  67. package/dist/single-user.d.ts +95 -0
  68. package/dist/tenant/blob-store.d.ts +30 -0
  69. package/dist/tenant/context.d.ts +47 -0
  70. package/dist/tenant/single-user-context.d.ts +29 -0
  71. package/dist/tenant/token.d.ts +26 -0
  72. package/package.json +3 -3
  73. package/dist/chunk-5ENJGVZX.mjs +0 -72
  74. package/dist/chunk-GN5QTMCL.cjs +0 -1870
  75. package/dist/chunk-PVNLB6TF.mjs +0 -1870
  76. package/dist/chunk-RGECD4OH.cjs +0 -72
  77. package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
  78. package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
  79. package/dist/cloudflare.d.cts +0 -2
  80. package/dist/neon-CJl66EGy.d.cts +0 -251
  81. package/dist/neon-DYvGnCzx.d.ts +0 -251
  82. package/dist/pg-07S-u_H4.d.cts +0 -246
  83. package/dist/pg-Du-pN_UT.d.ts +0 -246
  84. package/dist/schema-C8OnYk6j.d.cts +0 -74
  85. package/dist/schema-C8OnYk6j.d.ts +0 -74
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared SQL schema constants
3
+ *
4
+ * 定时触发用的三列各管一件事,别把它们混着读(见 lib/run-tick.js):
5
+ * - `lease_until`:**这条正在跑**。某个 tick 占位时写「归我管到什么时候」,
6
+ * 投递收尾时放掉。捞取待发任务时跳过租约未到期的行。
7
+ * - `retry_after`:**这条没在跑,等着重试**。投递失败的退避时刻,到点之前
8
+ * 捞不到这行。跟租约分开写,是因为「正在跑」会挡住同分组的其他任务,而
9
+ * 一条正在等重试的任务其实闲着,不该连累别人。
10
+ * - `serialize_group`:这条属于哪个串行分组(`runScheduledTick` 的
11
+ * `serializeBy` 算出来的,占位时一起写)。同一分组同时只放行一条。存的是
12
+ * 派生值而不是宿主给的原始 key —— 原始 key 往往是角色 id 之类的宿主数据,
13
+ * 任务内容都是密文落库的,这一列不该成为它的明文出口。
14
+ */
15
+ export const TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id SERIAL PRIMARY KEY,\n user_id VARCHAR(255) NOT NULL,\n uuid VARCHAR(36),\n encrypted_payload TEXT NOT NULL,\n message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,\n lease_until TIMESTAMP WITH TIME ZONE,\n retry_after TIMESTAMP WITH TIME ZONE,\n serialize_group VARCHAR(64),\n status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER DEFAULT 0,\n last_error TEXT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n";
16
+ /**
17
+ * 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
18
+ * 后加的列要单独补。initSchema 每次都会跑一遍,Postgres 的 IF NOT EXISTS
19
+ * 让重复执行也没事。
20
+ */
21
+ export const MIGRATIONS: {
22
+ name: string;
23
+ sql: string;
24
+ description: string;
25
+ }[];
26
+ export const INDEXES: ({
27
+ name: string;
28
+ sql: string;
29
+ description: string;
30
+ critical?: undefined;
31
+ } | {
32
+ name: string;
33
+ sql: string;
34
+ description: string;
35
+ critical: boolean;
36
+ })[];
37
+ export const PUSH_SUBSCRIPTION_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS push_subscriptions (\n user_id VARCHAR(255) PRIMARY KEY,\n subscription TEXT NOT NULL,\n updated_at BIGINT NOT NULL\n )\n";
38
+ export const LLM_CREDENTIALS_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS llm_credentials (\n user_id VARCHAR(255) NOT NULL,\n cred_id VARCHAR(128) NOT NULL,\n encrypted_value TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, cred_id)\n )\n";
39
+ export const VERIFY_TABLE_SQL: "\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public'\n AND table_name = 'scheduled_messages'\n";
40
+ export const COLUMNS_SQL: "\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'scheduled_messages'\n ORDER BY ordinal_position\n";
41
+ export const UPDATABLE_COLUMNS: Set<string>;
42
+ /**
43
+ * 任务行的 SELECT 列集,同样是三个适配器共用一份(列名不分方言,加列只改这
44
+ * 里)。分成两份,是因为两条链路要的东西本来就不一样:
45
+ *
46
+ * - `TASK_DELIVERY_COLUMNS`:投递链路读的行(`getPendingTasks` /
47
+ * `getTaskByUuidOnly`)。除了发消息本身要用的字段,还必须带 `retry_after`
48
+ * —— run-tick 的退避守卫就是拿这一列判断「这条还在等重试,现在别跑」,列
49
+ * 不在行里,守卫读到的永远是 undefined,等于没有守卫。
50
+ * - `TASK_DETAIL_COLUMNS`:读接口返回的行(`getTaskByUuid` / `listTasks`),
51
+ * 多了 `last_error` 和两个时间戳,不带 `lease_until` 这类内部调度列。
52
+ *
53
+ * 收进来之前每个适配器各写各的 SELECT 列表,加列时漏掉其中一个不会有任何报
54
+ * 错,只会在那种部署上静默少一列。
55
+ */
56
+ export const TASK_DELIVERY_COLUMNS: "id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count";
57
+ export const TASK_DETAIL_COLUMNS: "id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * SQLite (Cloudflare D1) dialect schema for scheduled_messages.
3
+ *
4
+ * 定时触发用的 `lease_until` / `retry_after` / `serialize_group` 三列分别是什么
5
+ * 意思,写在 Postgres 那份 schema 的文件头(adapters/schema.js)。
6
+ *
7
+ * Differences from the Postgres schema (adapters/schema.js):
8
+ * - id: INTEGER PRIMARY KEY AUTOINCREMENT (vs SERIAL)
9
+ * - timestamps stored as TEXT ISO8601 UTC (vs TIMESTAMP WITH TIME ZONE)
10
+ * - no NOW()/DEFAULT; the adapter always writes timestamps explicitly
11
+ * - retry_count is NOT NULL here (Postgres omits NOT NULL); every write path
12
+ * sets it explicitly, so the tighter constraint just documents that intent
13
+ * Partial indexes and CHECK constraints are native to SQLite, so they carry over.
14
+ * Index entries mirror the Postgres INDEXES shape ({ name, sql, description,
15
+ * critical }) so both adapters' initSchema() return the same index metadata.
16
+ */
17
+ export const SQLITE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n uuid TEXT,\n encrypted_payload TEXT NOT NULL,\n message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TEXT NOT NULL,\n lease_until TEXT,\n retry_after TEXT,\n serialize_group TEXT,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n";
18
+ /**
19
+ * 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
20
+ * 后加的列要单独补。initSchema 每次都会跑一遍,列已经在了就跳过。
21
+ */
22
+ export const SQLITE_MIGRATIONS: {
23
+ name: string;
24
+ sql: string;
25
+ description: string;
26
+ }[];
27
+ export const SQLITE_INDEXES: {
28
+ name: string;
29
+ sql: string;
30
+ description: string;
31
+ critical: boolean;
32
+ }[];
33
+ export const CLIENT_STATE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS client_state (\n user_id TEXT NOT NULL,\n namespace TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (user_id, namespace, key)\n )\n";
34
+ export const PUSH_SUBSCRIPTION_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS push_subscriptions (\n user_id TEXT PRIMARY KEY,\n subscription TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )\n";
35
+ export const LLM_CREDENTIALS_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS llm_credentials (\n user_id TEXT NOT NULL,\n cred_id TEXT NOT NULL,\n encrypted_value TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, cred_id)\n )\n";
36
+ export const MESSAGE_OUTBOX_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS message_outbox (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n task_uuid TEXT,\n session_id TEXT,\n message_index INTEGER,\n total_messages INTEGER,\n payload TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n delivered_at INTEGER,\n acked_at INTEGER,\n UNIQUE (user_id, message_id)\n )\n";
37
+ export const MESSAGE_OUTBOX_INDEX_SQL: "\n CREATE INDEX IF NOT EXISTS idx_outbox_unacked\n ON message_outbox (user_id, id)\n WHERE acked_at IS NULL\n";
38
+ /**
39
+ * 这一版代码跑起来需要的表 / 列 / 索引。
40
+ *
41
+ * 索引只列 critical 的那几个(uidx_uuid 之类):其余索引缺了只是慢,缺了它则
42
+ * 是正确性问题。
43
+ *
44
+ * @type {{ tables: Record<string, string[]>, indexes: string[] }}
45
+ */
46
+ export const SQLITE_REQUIRED_SCHEMA: {
47
+ tables: Record<string, string[]>;
48
+ indexes: string[];
49
+ };
@@ -0,0 +1,148 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/server/adapters/schema.js
2
+ var TABLE_SQL = `
3
+ CREATE TABLE IF NOT EXISTS scheduled_messages (
4
+ id SERIAL PRIMARY KEY,
5
+ user_id VARCHAR(255) NOT NULL,
6
+ uuid VARCHAR(36),
7
+ encrypted_payload TEXT NOT NULL,
8
+ message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
9
+ next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
10
+ lease_until TIMESTAMP WITH TIME ZONE,
11
+ retry_after TIMESTAMP WITH TIME ZONE,
12
+ serialize_group VARCHAR(64),
13
+ status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
14
+ retry_count INTEGER DEFAULT 0,
15
+ last_error TEXT,
16
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
17
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
18
+ )
19
+ `;
20
+ var MIGRATIONS = [
21
+ {
22
+ name: "add_lease_until",
23
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS lease_until TIMESTAMP WITH TIME ZONE",
24
+ description: "Task claim lease (2.6.0)"
25
+ },
26
+ {
27
+ name: "add_retry_after",
28
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS retry_after TIMESTAMP WITH TIME ZONE",
29
+ description: "Retry backoff, held apart from the claim lease (2.6.0)"
30
+ },
31
+ {
32
+ name: "add_serialize_group",
33
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS serialize_group VARCHAR(64)",
34
+ description: "Serialization group for runScheduledTick serializeBy (2.6.0)"
35
+ },
36
+ {
37
+ name: "add_last_error",
38
+ // 上一次投递失败的脱敏摘要(JSON:{ at, occurrence, reason })。payload 里
39
+ // 的 lastError 是密文里的完整记录,这一列是它的明文出口——payload 本身
40
+ // 解密失败时也写得进去,GET /message 对已失败的行也读得出来。
41
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS last_error TEXT",
42
+ description: "Sanitized last delivery-failure summary (2.6.0)"
43
+ }
44
+ ];
45
+ var INDEXES = [
46
+ {
47
+ name: "idx_pending_tasks_optimized",
48
+ sql: `CREATE INDEX IF NOT EXISTS idx_pending_tasks_optimized
49
+ ON scheduled_messages (status, next_send_at, id, retry_count)
50
+ WHERE status = 'pending'`,
51
+ description: "Main query index (Cron Job finds pending tasks)"
52
+ },
53
+ {
54
+ name: "idx_cleanup_completed",
55
+ sql: `CREATE INDEX IF NOT EXISTS idx_cleanup_completed
56
+ ON scheduled_messages (status, updated_at)
57
+ WHERE status IN ('sent', 'failed')`,
58
+ description: "Cleanup query index"
59
+ },
60
+ {
61
+ name: "idx_failed_retry",
62
+ sql: `CREATE INDEX IF NOT EXISTS idx_failed_retry
63
+ ON scheduled_messages (status, retry_count, next_send_at)
64
+ WHERE status = 'failed' AND retry_count < 3`,
65
+ description: "Failed retry index"
66
+ },
67
+ {
68
+ name: "idx_user_id",
69
+ sql: `CREATE INDEX IF NOT EXISTS idx_user_id
70
+ ON scheduled_messages (user_id)`,
71
+ description: "User task query index"
72
+ },
73
+ {
74
+ name: "uidx_uuid",
75
+ sql: `CREATE UNIQUE INDEX IF NOT EXISTS uidx_uuid
76
+ ON scheduled_messages (uuid)
77
+ WHERE uuid IS NOT NULL`,
78
+ description: "UUID uniqueness guard",
79
+ critical: true
80
+ },
81
+ {
82
+ name: "idx_serialize_group_lease",
83
+ sql: `CREATE INDEX IF NOT EXISTS idx_serialize_group_lease
84
+ ON scheduled_messages (serialize_group, lease_until)
85
+ WHERE serialize_group IS NOT NULL AND status = 'pending'`,
86
+ description: "Serialization group busy-check index (claimTask)"
87
+ }
88
+ ];
89
+ var PUSH_SUBSCRIPTION_TABLE_SQL = `
90
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
91
+ user_id VARCHAR(255) PRIMARY KEY,
92
+ subscription TEXT NOT NULL,
93
+ updated_at BIGINT NOT NULL
94
+ )
95
+ `;
96
+ var LLM_CREDENTIALS_TABLE_SQL = `
97
+ CREATE TABLE IF NOT EXISTS llm_credentials (
98
+ user_id VARCHAR(255) NOT NULL,
99
+ cred_id VARCHAR(128) NOT NULL,
100
+ encrypted_value TEXT NOT NULL,
101
+ created_at TEXT NOT NULL,
102
+ updated_at TEXT NOT NULL,
103
+ PRIMARY KEY (user_id, cred_id)
104
+ )
105
+ `;
106
+ var VERIFY_TABLE_SQL = `
107
+ SELECT table_name
108
+ FROM information_schema.tables
109
+ WHERE table_schema = 'public'
110
+ AND table_name = 'scheduled_messages'
111
+ `;
112
+ var COLUMNS_SQL = `
113
+ SELECT column_name, data_type, is_nullable
114
+ FROM information_schema.columns
115
+ WHERE table_schema = 'public'
116
+ AND table_name = 'scheduled_messages'
117
+ ORDER BY ordinal_position
118
+ `;
119
+ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
120
+ "user_id",
121
+ "uuid",
122
+ "encrypted_payload",
123
+ "message_type",
124
+ "next_send_at",
125
+ "lease_until",
126
+ "retry_after",
127
+ "serialize_group",
128
+ "status",
129
+ "retry_count",
130
+ "last_error",
131
+ "created_at",
132
+ "updated_at"
133
+ ]);
134
+ var TASK_DELIVERY_COLUMNS = "id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count";
135
+ var TASK_DETAIL_COLUMNS = "id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at";
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+ exports.TABLE_SQL = TABLE_SQL; exports.MIGRATIONS = MIGRATIONS; exports.INDEXES = INDEXES; exports.PUSH_SUBSCRIPTION_TABLE_SQL = PUSH_SUBSCRIPTION_TABLE_SQL; exports.LLM_CREDENTIALS_TABLE_SQL = LLM_CREDENTIALS_TABLE_SQL; exports.VERIFY_TABLE_SQL = VERIFY_TABLE_SQL; exports.COLUMNS_SQL = COLUMNS_SQL; exports.UPDATABLE_COLUMNS = UPDATABLE_COLUMNS; exports.TASK_DELIVERY_COLUMNS = TASK_DELIVERY_COLUMNS; exports.TASK_DETAIL_COLUMNS = TASK_DETAIL_COLUMNS;
@@ -0,0 +1,135 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/server/adapters/pg-shared.js
2
+ async function claimTask(query, taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
3
+ const grouped = typeof serializeGroup === "string" && serializeGroup.length > 0;
4
+ const params = [leaseUntil, taskId, expectedNextSendAt];
5
+ let setClause = "lease_until = $1, updated_at = NOW()";
6
+ let groupGuard = "";
7
+ if (grouped) {
8
+ params.push(serializeGroup);
9
+ setClause = "lease_until = $1, serialize_group = $4, updated_at = NOW()";
10
+ groupGuard = `
11
+ AND NOT EXISTS (
12
+ SELECT 1 FROM scheduled_messages busy
13
+ WHERE busy.serialize_group = $4 AND busy.id <> $2
14
+ AND busy.status = 'pending' AND busy.lease_until > NOW()
15
+ )`;
16
+ }
17
+ const rows = await query(
18
+ `UPDATE scheduled_messages
19
+ SET ${setClause}
20
+ WHERE id = $2 AND status = 'pending'
21
+ AND date_trunc('milliseconds', next_send_at)
22
+ = date_trunc('milliseconds', $3::timestamptz)
23
+ AND (lease_until IS NULL OR lease_until <= NOW())${groupGuard}
24
+ RETURNING id`,
25
+ params
26
+ );
27
+ if (rows.length === 0) return false;
28
+ if (!grouped) return true;
29
+ const conflict = await query(
30
+ `SELECT 1 FROM scheduled_messages busy
31
+ WHERE busy.serialize_group = $1 AND busy.id <> $2
32
+ AND busy.status = 'pending' AND busy.lease_until > NOW()
33
+ LIMIT 1`,
34
+ [serializeGroup, taskId]
35
+ );
36
+ if (conflict.length === 0) return true;
37
+ await query(
38
+ "UPDATE scheduled_messages SET lease_until = NULL, updated_at = NOW() WHERE id = $1",
39
+ [taskId]
40
+ );
41
+ return false;
42
+ }
43
+ async function renewTaskLease(query, taskId, leaseUntil) {
44
+ const rows = await query(
45
+ `UPDATE scheduled_messages SET lease_until = $1
46
+ WHERE id = $2 AND status = 'pending' AND lease_until IS NOT NULL
47
+ RETURNING id`,
48
+ [leaseUntil, taskId]
49
+ );
50
+ return rows.length > 0;
51
+ }
52
+ async function getTaskStatusInfo(query, uuid, userId) {
53
+ const rows = await query(
54
+ "SELECT status, last_error FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1",
55
+ [uuid, userId]
56
+ );
57
+ if (rows.length === 0) return null;
58
+ return { status: rows[0].status, last_error: _nullishCoalesce(rows[0].last_error, () => ( null)) };
59
+ }
60
+ async function getPushSubscription(query, userId) {
61
+ const rows = await query(
62
+ "SELECT subscription, updated_at FROM push_subscriptions WHERE user_id = $1 LIMIT 1",
63
+ [userId]
64
+ );
65
+ if (rows.length === 0) return null;
66
+ return { subscription: rows[0].subscription, updated_at: Number(rows[0].updated_at) };
67
+ }
68
+ async function upsertPushSubscription(query, userId, encryptedSubscription, updatedAt) {
69
+ const rows = await query(
70
+ `INSERT INTO push_subscriptions (user_id, subscription, updated_at)
71
+ VALUES ($1, $2, $3)
72
+ ON CONFLICT (user_id) DO UPDATE SET
73
+ subscription = EXCLUDED.subscription,
74
+ updated_at = EXCLUDED.updated_at
75
+ RETURNING user_id`,
76
+ [userId, encryptedSubscription, updatedAt]
77
+ );
78
+ return rows.length > 0;
79
+ }
80
+ async function deletePushSubscription(query, userId) {
81
+ const rows = await query(
82
+ "DELETE FROM push_subscriptions WHERE user_id = $1 RETURNING user_id",
83
+ [userId]
84
+ );
85
+ return rows.length > 0;
86
+ }
87
+ async function upsertLlmCredentials(query, userId, entries, now) {
88
+ let upserted = 0;
89
+ for (const entry of entries) {
90
+ const rows = await query(
91
+ `INSERT INTO llm_credentials (user_id, cred_id, encrypted_value, created_at, updated_at)
92
+ VALUES ($1, $2, $3, $4, $4)
93
+ ON CONFLICT (user_id, cred_id) DO UPDATE SET
94
+ encrypted_value = EXCLUDED.encrypted_value,
95
+ updated_at = EXCLUDED.updated_at
96
+ RETURNING cred_id`,
97
+ [userId, entry.credId, entry.encryptedValue, now]
98
+ );
99
+ if (rows.length > 0) upserted++;
100
+ }
101
+ return upserted;
102
+ }
103
+ async function getLlmCredentials(query, userId, credIds) {
104
+ if (!credIds || credIds.length === 0) return [];
105
+ return query(
106
+ `SELECT cred_id, encrypted_value, updated_at
107
+ FROM llm_credentials
108
+ WHERE user_id = $1 AND cred_id = ANY($2)`,
109
+ [userId, credIds]
110
+ );
111
+ }
112
+ async function listLlmCredentials(query, userId) {
113
+ return query(
114
+ "SELECT cred_id, updated_at FROM llm_credentials WHERE user_id = $1 ORDER BY cred_id ASC",
115
+ [userId]
116
+ );
117
+ }
118
+ async function deleteLlmCredentials(query, userId, credIds = null) {
119
+ if (credIds !== null && credIds.length === 0) return 0;
120
+ const rows = credIds === null ? await query("DELETE FROM llm_credentials WHERE user_id = $1 RETURNING cred_id", [userId]) : await query("DELETE FROM llm_credentials WHERE user_id = $1 AND cred_id = ANY($2) RETURNING cred_id", [userId, credIds]);
121
+ return rows.length;
122
+ }
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+ exports.claimTask = claimTask; exports.renewTaskLease = renewTaskLease; exports.getTaskStatusInfo = getTaskStatusInfo; exports.getPushSubscription = getPushSubscription; exports.upsertPushSubscription = upsertPushSubscription; exports.deletePushSubscription = deletePushSubscription; exports.upsertLlmCredentials = upsertLlmCredentials; exports.getLlmCredentials = getLlmCredentials; exports.listLlmCredentials = listLlmCredentials; exports.deleteLlmCredentials = deleteLlmCredentials;
@@ -0,0 +1,148 @@
1
+ // src/server/adapters/schema.js
2
+ var TABLE_SQL = `
3
+ CREATE TABLE IF NOT EXISTS scheduled_messages (
4
+ id SERIAL PRIMARY KEY,
5
+ user_id VARCHAR(255) NOT NULL,
6
+ uuid VARCHAR(36),
7
+ encrypted_payload TEXT NOT NULL,
8
+ message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
9
+ next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
10
+ lease_until TIMESTAMP WITH TIME ZONE,
11
+ retry_after TIMESTAMP WITH TIME ZONE,
12
+ serialize_group VARCHAR(64),
13
+ status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
14
+ retry_count INTEGER DEFAULT 0,
15
+ last_error TEXT,
16
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
17
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
18
+ )
19
+ `;
20
+ var MIGRATIONS = [
21
+ {
22
+ name: "add_lease_until",
23
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS lease_until TIMESTAMP WITH TIME ZONE",
24
+ description: "Task claim lease (2.6.0)"
25
+ },
26
+ {
27
+ name: "add_retry_after",
28
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS retry_after TIMESTAMP WITH TIME ZONE",
29
+ description: "Retry backoff, held apart from the claim lease (2.6.0)"
30
+ },
31
+ {
32
+ name: "add_serialize_group",
33
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS serialize_group VARCHAR(64)",
34
+ description: "Serialization group for runScheduledTick serializeBy (2.6.0)"
35
+ },
36
+ {
37
+ name: "add_last_error",
38
+ // 上一次投递失败的脱敏摘要(JSON:{ at, occurrence, reason })。payload 里
39
+ // 的 lastError 是密文里的完整记录,这一列是它的明文出口——payload 本身
40
+ // 解密失败时也写得进去,GET /message 对已失败的行也读得出来。
41
+ sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS last_error TEXT",
42
+ description: "Sanitized last delivery-failure summary (2.6.0)"
43
+ }
44
+ ];
45
+ var INDEXES = [
46
+ {
47
+ name: "idx_pending_tasks_optimized",
48
+ sql: `CREATE INDEX IF NOT EXISTS idx_pending_tasks_optimized
49
+ ON scheduled_messages (status, next_send_at, id, retry_count)
50
+ WHERE status = 'pending'`,
51
+ description: "Main query index (Cron Job finds pending tasks)"
52
+ },
53
+ {
54
+ name: "idx_cleanup_completed",
55
+ sql: `CREATE INDEX IF NOT EXISTS idx_cleanup_completed
56
+ ON scheduled_messages (status, updated_at)
57
+ WHERE status IN ('sent', 'failed')`,
58
+ description: "Cleanup query index"
59
+ },
60
+ {
61
+ name: "idx_failed_retry",
62
+ sql: `CREATE INDEX IF NOT EXISTS idx_failed_retry
63
+ ON scheduled_messages (status, retry_count, next_send_at)
64
+ WHERE status = 'failed' AND retry_count < 3`,
65
+ description: "Failed retry index"
66
+ },
67
+ {
68
+ name: "idx_user_id",
69
+ sql: `CREATE INDEX IF NOT EXISTS idx_user_id
70
+ ON scheduled_messages (user_id)`,
71
+ description: "User task query index"
72
+ },
73
+ {
74
+ name: "uidx_uuid",
75
+ sql: `CREATE UNIQUE INDEX IF NOT EXISTS uidx_uuid
76
+ ON scheduled_messages (uuid)
77
+ WHERE uuid IS NOT NULL`,
78
+ description: "UUID uniqueness guard",
79
+ critical: true
80
+ },
81
+ {
82
+ name: "idx_serialize_group_lease",
83
+ sql: `CREATE INDEX IF NOT EXISTS idx_serialize_group_lease
84
+ ON scheduled_messages (serialize_group, lease_until)
85
+ WHERE serialize_group IS NOT NULL AND status = 'pending'`,
86
+ description: "Serialization group busy-check index (claimTask)"
87
+ }
88
+ ];
89
+ var PUSH_SUBSCRIPTION_TABLE_SQL = `
90
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
91
+ user_id VARCHAR(255) PRIMARY KEY,
92
+ subscription TEXT NOT NULL,
93
+ updated_at BIGINT NOT NULL
94
+ )
95
+ `;
96
+ var LLM_CREDENTIALS_TABLE_SQL = `
97
+ CREATE TABLE IF NOT EXISTS llm_credentials (
98
+ user_id VARCHAR(255) NOT NULL,
99
+ cred_id VARCHAR(128) NOT NULL,
100
+ encrypted_value TEXT NOT NULL,
101
+ created_at TEXT NOT NULL,
102
+ updated_at TEXT NOT NULL,
103
+ PRIMARY KEY (user_id, cred_id)
104
+ )
105
+ `;
106
+ var VERIFY_TABLE_SQL = `
107
+ SELECT table_name
108
+ FROM information_schema.tables
109
+ WHERE table_schema = 'public'
110
+ AND table_name = 'scheduled_messages'
111
+ `;
112
+ var COLUMNS_SQL = `
113
+ SELECT column_name, data_type, is_nullable
114
+ FROM information_schema.columns
115
+ WHERE table_schema = 'public'
116
+ AND table_name = 'scheduled_messages'
117
+ ORDER BY ordinal_position
118
+ `;
119
+ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
120
+ "user_id",
121
+ "uuid",
122
+ "encrypted_payload",
123
+ "message_type",
124
+ "next_send_at",
125
+ "lease_until",
126
+ "retry_after",
127
+ "serialize_group",
128
+ "status",
129
+ "retry_count",
130
+ "last_error",
131
+ "created_at",
132
+ "updated_at"
133
+ ]);
134
+ var TASK_DELIVERY_COLUMNS = "id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count";
135
+ var TASK_DETAIL_COLUMNS = "id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at";
136
+
137
+ export {
138
+ TABLE_SQL,
139
+ MIGRATIONS,
140
+ INDEXES,
141
+ PUSH_SUBSCRIPTION_TABLE_SQL,
142
+ LLM_CREDENTIALS_TABLE_SQL,
143
+ VERIFY_TABLE_SQL,
144
+ COLUMNS_SQL,
145
+ UPDATABLE_COLUMNS,
146
+ TASK_DELIVERY_COLUMNS,
147
+ TASK_DETAIL_COLUMNS
148
+ };