@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.20
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 +452 -24
- package/dist/adapters/d1.d.ts +388 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +265 -0
- package/dist/adapters/neon.d.ts +82 -0
- package/dist/adapters/pg-shared.d.ts +161 -0
- package/dist/adapters/pg.d.ts +84 -0
- package/dist/adapters/schema.d.ts +41 -0
- package/dist/adapters/schema.sqlite.d.ts +49 -0
- package/dist/chunk-5FXVSC5O.mjs +144 -0
- package/dist/chunk-E7OWP3VL.cjs +135 -0
- package/dist/chunk-HTGYGGUH.mjs +4998 -0
- package/dist/chunk-V6NZQ22S.cjs +144 -0
- package/dist/chunk-WOXSQBAM.cjs +4998 -0
- package/dist/chunk-ZGGF4GMA.mjs +135 -0
- package/dist/cloudflare/single-user-worker.d.ts +37 -0
- package/dist/cloudflare.cjs +35 -2
- package/dist/cloudflare.d.ts +9 -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.d.ts +7 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +7 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +121 -44
- package/dist/index.d.cts +113 -766
- package/dist/index.d.ts +113 -766
- package/dist/index.mjs +113 -36
- package/dist/lib/agentic-fire.d.ts +108 -0
- package/dist/lib/client-state-store.d.ts +61 -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 +93 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +1 -0
- package/dist/lib/message-processor.d.ts +56 -0
- package/dist/lib/outbox-store.d.ts +33 -0
- package/dist/lib/push-subscription-store.d.ts +71 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +135 -0
- package/dist/lib/run-tick.d.ts +106 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +55 -0
- package/dist/lib/task-projection.d.ts +87 -0
- package/dist/lib/validation.d.ts +62 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-BFUS25UX.cjs → neon-U6TQFKMJ.cjs} +100 -8
- package/dist/{neon-CU5N3CSW.mjs → neon-Y4KEZO5C.mjs} +96 -4
- package/dist/{pg-IIH3M4OM.cjs → pg-B324Q2R4.cjs} +89 -8
- package/dist/{pg-QO6NKTGL.mjs → pg-GWJZBWAM.mjs} +85 -4
- package/dist/single-user.d.ts +94 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-5ENJGVZX.mjs +0 -72
- package/dist/chunk-GN5QTMCL.cjs +0 -1870
- package/dist/chunk-PVNLB6TF.mjs +0 -1870
- package/dist/chunk-RGECD4OH.cjs +0 -72
- package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
- package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-CJl66EGy.d.cts +0 -251
- package/dist/neon-DYvGnCzx.d.ts +0 -251
- package/dist/pg-07S-u_H4.d.cts +0 -246
- package/dist/pg-Du-pN_UT.d.ts +0 -246
- package/dist/schema-C8OnYk6j.d.cts +0 -74
- package/dist/schema-C8OnYk6j.d.ts +0 -74
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export class NeonAdapter {
|
|
2
|
+
/** @param {string} connectionString */
|
|
3
|
+
constructor(connectionString: string);
|
|
4
|
+
/** @private */
|
|
5
|
+
private _connectionString;
|
|
6
|
+
/** @private */
|
|
7
|
+
private _sql;
|
|
8
|
+
/** @private */
|
|
9
|
+
private _getSql;
|
|
10
|
+
initSchema(): Promise<{
|
|
11
|
+
columnsCreated: number;
|
|
12
|
+
indexesCreated: number;
|
|
13
|
+
indexesFailed: number;
|
|
14
|
+
columns: {
|
|
15
|
+
table: string;
|
|
16
|
+
name: any;
|
|
17
|
+
type: any;
|
|
18
|
+
nullable: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
indexes: ({
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
description: string;
|
|
24
|
+
critical: boolean;
|
|
25
|
+
error?: undefined;
|
|
26
|
+
} | {
|
|
27
|
+
name: string;
|
|
28
|
+
status: string;
|
|
29
|
+
description: string;
|
|
30
|
+
critical: boolean;
|
|
31
|
+
error: any;
|
|
32
|
+
})[];
|
|
33
|
+
}>;
|
|
34
|
+
dropSchema(): Promise<void>;
|
|
35
|
+
createTask(params: any): Promise<Record<string, any>>;
|
|
36
|
+
getTaskByUuid(uuid: any, userId: any): Promise<Record<string, any>>;
|
|
37
|
+
getTaskByUuidOnly(uuid: any): Promise<Record<string, any>>;
|
|
38
|
+
/**
|
|
39
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
40
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
41
|
+
*
|
|
42
|
+
* @param {string} uuid
|
|
43
|
+
* @returns {Promise<{ status: string }|null>}
|
|
44
|
+
*/
|
|
45
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
46
|
+
status: string;
|
|
47
|
+
} | null>;
|
|
48
|
+
updateTaskById(taskId: any, updates: any): Promise<Record<string, any>>;
|
|
49
|
+
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<Record<string, any>>;
|
|
50
|
+
deleteTaskById(taskId: any): Promise<boolean>;
|
|
51
|
+
deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
|
|
52
|
+
getPendingTasks(limit?: number): Promise<Record<string, any>[]>;
|
|
53
|
+
claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
|
|
54
|
+
renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
|
|
55
|
+
listTasks(userId: any, opts?: {}): Promise<{
|
|
56
|
+
tasks: Record<string, any>[];
|
|
57
|
+
total: number;
|
|
58
|
+
}>;
|
|
59
|
+
cleanupOldTasks(days?: number): Promise<number>;
|
|
60
|
+
getTaskStatus(uuid: any, userId: any): Promise<any>;
|
|
61
|
+
getTaskStatusInfo(uuid: any, userId: any): Promise<{
|
|
62
|
+
status: string;
|
|
63
|
+
last_error: string | null;
|
|
64
|
+
}>;
|
|
65
|
+
getPushSubscription(userId: any): Promise<{
|
|
66
|
+
subscription: string;
|
|
67
|
+
updated_at: number;
|
|
68
|
+
}>;
|
|
69
|
+
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
70
|
+
deletePushSubscription(userId: any): Promise<boolean>;
|
|
71
|
+
upsertLlmCredentials(userId: any, entries: any): Promise<number>;
|
|
72
|
+
getLlmCredentials(userId: any, credIds: any): Promise<{
|
|
73
|
+
cred_id: string;
|
|
74
|
+
encrypted_value: string;
|
|
75
|
+
updated_at: string;
|
|
76
|
+
}[]>;
|
|
77
|
+
listLlmCredentials(userId: any): Promise<{
|
|
78
|
+
cred_id: string;
|
|
79
|
+
updated_at: string;
|
|
80
|
+
}[]>;
|
|
81
|
+
deleteLlmCredentials(userId: any, credIds?: any): Promise<number>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg / neon 共用的 Postgres 查询实现。
|
|
3
|
+
*
|
|
4
|
+
* 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
|
|
5
|
+
* 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
|
|
6
|
+
* 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
|
|
7
|
+
* 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
|
|
8
|
+
*
|
|
9
|
+
* D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
|
|
10
|
+
*
|
|
11
|
+
* @typedef {(text: string, params?: any[]) => Promise<any[]>} PgQuery
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
15
|
+
* 本次投递期间别的 tick 领不走它。
|
|
16
|
+
*
|
|
17
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
18
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
19
|
+
*
|
|
20
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
21
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
22
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
23
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
24
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
25
|
+
* 这一跳就不该再按旧时刻发。
|
|
26
|
+
*
|
|
27
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
28
|
+
* 要改表。
|
|
29
|
+
*
|
|
30
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
31
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
32
|
+
*
|
|
33
|
+
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
34
|
+
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
35
|
+
* 里完成——但那只对「抢同一行」成立;READ COMMITTED 下两个并发 tick 各领
|
|
36
|
+
* 同组的**不同**行时,各自的 NOT EXISTS 子查询都看不到对方尚未提交的租约,
|
|
37
|
+
* 也没有行锁冲突逼它重查(写偏斜)。所以占位成功后再回头查一次:真撞上了
|
|
38
|
+
* 就把自己刚写的租约放掉、这一跳不跑——两边都让也没事,行保持 pending,
|
|
39
|
+
* 下一跳重试。分组门只看租约,不看 `retry_after`:等着重试的任务其实闲着,
|
|
40
|
+
* 不该把同分组的其他任务一起堵住。
|
|
41
|
+
*
|
|
42
|
+
* @param {PgQuery} query
|
|
43
|
+
* @param {number} taskId
|
|
44
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
45
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
46
|
+
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
47
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
|
|
48
|
+
* 任务正在跑、排期被改过、或行已不是 pending
|
|
49
|
+
*/
|
|
50
|
+
export function claimTask(query: PgQuery, taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* 投递期间的租约续期(run-tick 的心跳用)。只在行仍是 pending 且确实持有
|
|
53
|
+
* 租约(lease_until 非空)时生效——收尾把租约放掉之后,迟到的心跳不会把
|
|
54
|
+
* 租约复活。
|
|
55
|
+
*
|
|
56
|
+
* @param {PgQuery} query
|
|
57
|
+
* @param {number} taskId
|
|
58
|
+
* @param {string|Date} leaseUntil - 新的租期末尾
|
|
59
|
+
* @returns {Promise<boolean>} true = 续上了
|
|
60
|
+
*/
|
|
61
|
+
export function renewTaskLease(query: PgQuery, taskId: number, leaseUntil: string | Date): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* 状态 + 失败摘要(GET /message 用它把「为什么失败」透给已失败的行)。
|
|
64
|
+
*
|
|
65
|
+
* @param {PgQuery} query
|
|
66
|
+
* @param {string} uuid
|
|
67
|
+
* @param {string} userId
|
|
68
|
+
* @returns {Promise<{ status: string, last_error: string|null }|null>}
|
|
69
|
+
*/
|
|
70
|
+
export function getTaskStatusInfo(query: PgQuery, uuid: string, userId: string): Promise<{
|
|
71
|
+
status: string;
|
|
72
|
+
last_error: string | null;
|
|
73
|
+
} | null>;
|
|
74
|
+
/**
|
|
75
|
+
* 这个用户当前登记的推送订阅(密文原样返回,解密在上层)。
|
|
76
|
+
*
|
|
77
|
+
* @param {PgQuery} query
|
|
78
|
+
* @param {string} userId
|
|
79
|
+
* @returns {Promise<{ subscription: string, updated_at: number }|null>}
|
|
80
|
+
*/
|
|
81
|
+
export function getPushSubscription(query: PgQuery, userId: string): Promise<{
|
|
82
|
+
subscription: string;
|
|
83
|
+
updated_at: number;
|
|
84
|
+
} | null>;
|
|
85
|
+
/**
|
|
86
|
+
* 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
|
|
87
|
+
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
88
|
+
*
|
|
89
|
+
* @param {PgQuery} query
|
|
90
|
+
* @param {string} userId
|
|
91
|
+
* @param {string} encryptedSubscription
|
|
92
|
+
* @param {number} updatedAt - epoch 毫秒
|
|
93
|
+
* @returns {Promise<boolean>}
|
|
94
|
+
*/
|
|
95
|
+
export function upsertPushSubscription(query: PgQuery, userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
98
|
+
*
|
|
99
|
+
* @param {PgQuery} query
|
|
100
|
+
* @param {string} userId
|
|
101
|
+
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
102
|
+
*/
|
|
103
|
+
export function deletePushSubscription(query: PgQuery, userId: string): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* 批量 upsert 这个用户的凭据(value 已是密文,加密在上层)。已存在的行覆盖
|
|
106
|
+
* encrypted_value 并刷 updated_at,created_at 保留首次写入的时刻。
|
|
107
|
+
*
|
|
108
|
+
* @param {PgQuery} query
|
|
109
|
+
* @param {string} userId
|
|
110
|
+
* @param {Array<{ credId: string, encryptedValue: string }>} entries
|
|
111
|
+
* @param {string} now - ISO8601 时间戳(与 SQLite 侧同口径的 TEXT)
|
|
112
|
+
* @returns {Promise<number>} 实际写入/覆盖的行数
|
|
113
|
+
*/
|
|
114
|
+
export function upsertLlmCredentials(query: PgQuery, userId: string, entries: Array<{
|
|
115
|
+
credId: string;
|
|
116
|
+
encryptedValue: string;
|
|
117
|
+
}>, now: string): Promise<number>;
|
|
118
|
+
/**
|
|
119
|
+
* 按 cred_id 批量读这个用户的凭据行(密文原样返回,解密在上层)。
|
|
120
|
+
*
|
|
121
|
+
* @param {PgQuery} query
|
|
122
|
+
* @param {string} userId
|
|
123
|
+
* @param {string[]} credIds
|
|
124
|
+
* @returns {Promise<Array<{ cred_id: string, encrypted_value: string, updated_at: string }>>}
|
|
125
|
+
*/
|
|
126
|
+
export function getLlmCredentials(query: PgQuery, userId: string, credIds: string[]): Promise<Array<{
|
|
127
|
+
cred_id: string;
|
|
128
|
+
encrypted_value: string;
|
|
129
|
+
updated_at: string;
|
|
130
|
+
}>>;
|
|
131
|
+
/**
|
|
132
|
+
* 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at)。
|
|
133
|
+
*
|
|
134
|
+
* @param {PgQuery} query
|
|
135
|
+
* @param {string} userId
|
|
136
|
+
* @returns {Promise<Array<{ cred_id: string, updated_at: string }>>}
|
|
137
|
+
*/
|
|
138
|
+
export function listLlmCredentials(query: PgQuery, userId: string): Promise<Array<{
|
|
139
|
+
cred_id: string;
|
|
140
|
+
updated_at: string;
|
|
141
|
+
}>>;
|
|
142
|
+
/**
|
|
143
|
+
* 删凭据。`credIds` 传数组删指定那几行;传 null 删这个用户的全部。
|
|
144
|
+
*
|
|
145
|
+
* @param {PgQuery} query
|
|
146
|
+
* @param {string} userId
|
|
147
|
+
* @param {string[]|null} credIds
|
|
148
|
+
* @returns {Promise<number>} 删掉的行数
|
|
149
|
+
*/
|
|
150
|
+
export function deleteLlmCredentials(query: PgQuery, userId: string, credIds?: string[] | null): Promise<number>;
|
|
151
|
+
/**
|
|
152
|
+
* pg / neon 共用的 Postgres 查询实现。
|
|
153
|
+
*
|
|
154
|
+
* 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
|
|
155
|
+
* 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
|
|
156
|
+
* 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
|
|
157
|
+
* 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
|
|
158
|
+
*
|
|
159
|
+
* D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
|
|
160
|
+
*/
|
|
161
|
+
export type PgQuery = (text: string, params?: any[]) => Promise<any[]>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export class PgAdapter {
|
|
2
|
+
/** @param {string} connectionString */
|
|
3
|
+
constructor(connectionString: string);
|
|
4
|
+
/** @private */
|
|
5
|
+
private _connectionString;
|
|
6
|
+
/** @private */
|
|
7
|
+
private _pool;
|
|
8
|
+
/** @private */
|
|
9
|
+
private _getPool;
|
|
10
|
+
/** @private */
|
|
11
|
+
private _query;
|
|
12
|
+
initSchema(): Promise<{
|
|
13
|
+
columnsCreated: number;
|
|
14
|
+
indexesCreated: number;
|
|
15
|
+
indexesFailed: number;
|
|
16
|
+
columns: {
|
|
17
|
+
table: string;
|
|
18
|
+
name: any;
|
|
19
|
+
type: any;
|
|
20
|
+
nullable: boolean;
|
|
21
|
+
}[];
|
|
22
|
+
indexes: ({
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
description: string;
|
|
26
|
+
critical: boolean;
|
|
27
|
+
error?: undefined;
|
|
28
|
+
} | {
|
|
29
|
+
name: string;
|
|
30
|
+
status: string;
|
|
31
|
+
description: string;
|
|
32
|
+
critical: boolean;
|
|
33
|
+
error: any;
|
|
34
|
+
})[];
|
|
35
|
+
}>;
|
|
36
|
+
dropSchema(): Promise<void>;
|
|
37
|
+
createTask(params: any): Promise<any[]>;
|
|
38
|
+
getTaskByUuid(uuid: any, userId: any): Promise<any[]>;
|
|
39
|
+
getTaskByUuidOnly(uuid: any): Promise<any[]>;
|
|
40
|
+
/**
|
|
41
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
42
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
43
|
+
*
|
|
44
|
+
* @param {string} uuid
|
|
45
|
+
* @returns {Promise<{ status: string }|null>}
|
|
46
|
+
*/
|
|
47
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
48
|
+
status: string;
|
|
49
|
+
} | null>;
|
|
50
|
+
updateTaskById(taskId: any, updates: any): Promise<any[]>;
|
|
51
|
+
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<any[]>;
|
|
52
|
+
deleteTaskById(taskId: any): Promise<boolean>;
|
|
53
|
+
deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
|
|
54
|
+
getPendingTasks(limit?: number): Promise<any[][]>;
|
|
55
|
+
claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
|
|
56
|
+
renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
|
|
57
|
+
listTasks(userId: any, opts?: {}): Promise<{
|
|
58
|
+
tasks: any[][];
|
|
59
|
+
total: number;
|
|
60
|
+
}>;
|
|
61
|
+
cleanupOldTasks(days?: number): Promise<number>;
|
|
62
|
+
getTaskStatus(uuid: any, userId: any): Promise<any>;
|
|
63
|
+
getTaskStatusInfo(uuid: any, userId: any): Promise<{
|
|
64
|
+
status: string;
|
|
65
|
+
last_error: string | null;
|
|
66
|
+
}>;
|
|
67
|
+
getPushSubscription(userId: any): Promise<{
|
|
68
|
+
subscription: string;
|
|
69
|
+
updated_at: number;
|
|
70
|
+
}>;
|
|
71
|
+
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
72
|
+
deletePushSubscription(userId: any): Promise<boolean>;
|
|
73
|
+
upsertLlmCredentials(userId: any, entries: any): Promise<number>;
|
|
74
|
+
getLlmCredentials(userId: any, credIds: any): Promise<{
|
|
75
|
+
cred_id: string;
|
|
76
|
+
encrypted_value: string;
|
|
77
|
+
updated_at: string;
|
|
78
|
+
}[]>;
|
|
79
|
+
listLlmCredentials(userId: any): Promise<{
|
|
80
|
+
cred_id: string;
|
|
81
|
+
updated_at: string;
|
|
82
|
+
}[]>;
|
|
83
|
+
deleteLlmCredentials(userId: any, credIds?: any): Promise<number>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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>;
|
|
@@ -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,144 @@
|
|
|
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
|
+
|
|
135
|
+
export {
|
|
136
|
+
TABLE_SQL,
|
|
137
|
+
MIGRATIONS,
|
|
138
|
+
INDEXES,
|
|
139
|
+
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
140
|
+
LLM_CREDENTIALS_TABLE_SQL,
|
|
141
|
+
VERIFY_TABLE_SQL,
|
|
142
|
+
COLUMNS_SQL,
|
|
143
|
+
UPDATABLE_COLUMNS
|
|
144
|
+
};
|