@rei-standard/amsg-server 2.6.0-next.15 → 2.6.0-next.17
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/README.md +159 -2
- package/dist/adapters/d1.d.ts +69 -0
- package/dist/adapters/interface.d.ts +50 -0
- package/dist/adapters/neon.d.ts +21 -0
- package/dist/adapters/pg-shared.d.ts +47 -0
- package/dist/adapters/pg.d.ts +21 -0
- package/dist/adapters/schema.d.ts +1 -0
- package/dist/adapters/schema.sqlite.d.ts +13 -0
- package/dist/{chunk-34I2YSWE.mjs → chunk-5FXVSC5O.mjs} +11 -0
- package/dist/{chunk-TFFBMYA2.cjs → chunk-E7OWP3VL.cjs} +41 -1
- package/dist/{chunk-TCKPLM3P.cjs → chunk-QHWGWKB4.cjs} +877 -202
- package/dist/{chunk-D3DQF647.mjs → chunk-QL6PMTIN.mjs} +882 -207
- package/dist/{chunk-OUPM4HAV.cjs → chunk-V6NZQ22S.cjs} +12 -1
- package/dist/{chunk-UYROU4L3.mjs → chunk-ZGGF4GMA.mjs} +41 -1
- package/dist/cloudflare/single-user-worker.d.ts +33 -2
- package/dist/cloudflare.cjs +17 -3
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.mjs +18 -4
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/index.cjs +57 -27
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.mjs +35 -5
- package/dist/lib/errors.d.ts +43 -0
- package/dist/lib/llm-credentials-store.d.ts +139 -0
- package/dist/lib/run-tick.d.ts +12 -2
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/task-projection.d.ts +5 -0
- package/dist/{neon-ZWA2U7IZ.cjs → neon-U6TQFKMJ.cjs} +55 -16
- package/dist/{neon-XLWK27RL.mjs → neon-Y4KEZO5C.mjs} +41 -2
- package/dist/{pg-Q4VU6D75.cjs → pg-B324Q2R4.cjs} +50 -16
- package/dist/{pg-X63HFEV7.mjs → pg-GWJZBWAM.mjs} +36 -2
- package/dist/single-user.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ const rei = await createReiServer({
|
|
|
47
47
|
// PUT /api/v1/push-subscription -> rei.handlers.pushSubscription.PUT
|
|
48
48
|
// GET /api/v1/push-subscription -> rei.handlers.pushSubscription.GET
|
|
49
49
|
// DELETE /api/v1/push-subscription -> rei.handlers.pushSubscription.DELETE
|
|
50
|
+
// PUT /api/v1/llm-credentials -> rei.handlers.llmCredentials.PUT
|
|
51
|
+
// GET /api/v1/llm-credentials -> rei.handlers.llmCredentials.GET
|
|
52
|
+
// DELETE /api/v1/llm-credentials -> rei.handlers.llmCredentials.DELETE
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
## 关于 `messageType: 'instant'`
|
|
@@ -160,6 +163,51 @@ const message = body.length <= remainingBytes ? body : body.slice(0, remainingBy
|
|
|
160
163
|
|
|
161
164
|
装不下的内容(长文、附件详情)建议走旁路:正文存进 `client_state`,push 里只带一个引用键,客户端上线后用 `GET /client-state` 取回。单用户 Worker 的 fire-time hook 用 `ctx.writeState()` 写,见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md)。
|
|
162
165
|
|
|
166
|
+
## LLM 凭据(用户级)与 credRefs
|
|
167
|
+
|
|
168
|
+
LLM API 凭据(`apiUrl` / `apiKey` / `primaryModel`)有两种给法:
|
|
169
|
+
|
|
170
|
+
- **内联**:随排程请求带三件套,冻结进任务行(一直以来的方式,继续支持)。
|
|
171
|
+
- **引用**:凭据先用 `PUT /llm-credentials` 集中登记,排程 payload 里只带
|
|
172
|
+
`credRefs: { chat: '<credId>' }`。到点投递时按 credId 现读——换 Key 覆盖对应
|
|
173
|
+
行就够了,所有引用它的任务(包括角色在 fire 里给自己排的、客户端根本不知道
|
|
174
|
+
存在的那些)下次触发用的都是新凭据。
|
|
175
|
+
|
|
176
|
+
| 端点 | 语义 |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `PUT /llm-credentials` | 批量登记 / 覆盖。body =(加密后的)`{ credentials: [{ credId, value: { apiUrl, apiKey, primaryModel } }] }`,一批 ≤100 条,单用户 ≤500 行 |
|
|
179
|
+
| `GET /llm-credentials` | 对账清单 `{ credentials: [{ credId, updatedAt }] }`。**凭据本体永远不回传** |
|
|
180
|
+
| `DELETE /llm-credentials` | 删除。body =(加密后的)`{ credIds: [...] }` 或 `{ all: true }` |
|
|
181
|
+
|
|
182
|
+
客户端侧对应 `client.putLlmCredentials(credentials)` / `listLlmCredentials()` / `deleteLlmCredentials(opts)`。
|
|
183
|
+
|
|
184
|
+
`credId` 是客户端起名的**不透明字符串**(1–128 字符、不含控制字符),服务端不
|
|
185
|
+
解释语义。约定:`char:<charId>/<purpose>`(角色级)、`global/<purpose>`(全
|
|
186
|
+
局)。`credRefs` 的 purpose 键里只有 `chat` 由服务端消费(fire 时的主 LLM 调
|
|
187
|
+
用);其余 purpose(如情绪评估的副 API)归宿主 hook 侧,fire hook 的 ctx 上有
|
|
188
|
+
`resolveLlmCredential(credId)` 可按需取用(每次返回新对象;拿到就用,别挂到
|
|
189
|
+
ctx / metadata / push 上)。
|
|
190
|
+
|
|
191
|
+
配套的规则:
|
|
192
|
+
|
|
193
|
+
- 排程 / 更新时对 `credRefs` 里的**全部** credId 做存在性检查,缺的返回
|
|
194
|
+
`409 CREDENTIAL_NOT_FOUND` 并点名(先登记再排程)。
|
|
195
|
+
- `credRefs.chat` 与内联三件套在同一个请求里不能都传(`400`):chat 凭据只能
|
|
196
|
+
有一个来源。
|
|
197
|
+
- fire 时的解析顺序:`credRefs.chat` → 查表;行没了 → 退回任务里的内联三件套
|
|
198
|
+
(如有);都没有 → 本轮失败,`lastError` 记 `CREDENTIAL_MISSING`,走常规重
|
|
199
|
+
试——补传凭据后下一轮自愈。
|
|
200
|
+
- hook 的 `ctx.scheduleTask()` 自排任务时:父任务带 `credRefs` 就复制**引用**
|
|
201
|
+
(不复制凭据本体),换 Key 自动作用于整条自排链;存量内联任务照旧复制三件套。
|
|
202
|
+
- 任务投影(`GET /messages` / hook 的 `ctx.task`)带 `credRefs`(只是名字,不
|
|
203
|
+
是机密),客户端对账用;凭据本体照旧被白名单挡在外面。
|
|
204
|
+
- 数据库侧是 `llm_credentials` 表(`(user_id, cred_id)` 主键,`encrypted_value`
|
|
205
|
+
密文,时间戳 ISO8601 文本)。内置的 D1 / pg / neon 适配器都实现了,
|
|
206
|
+
`initSchema()` 会建表。自定义适配器要补 `upsertLlmCredentials` /
|
|
207
|
+
`getLlmCredentials` / `listLlmCredentials` / `deleteLlmCredentials` 四个方法,
|
|
208
|
+
缺任何一个这几个端点返回 501、带 `credRefs` 的排程也会被拒。
|
|
209
|
+
- 特性探测:`GET /capabilities` 的 `features` 含 `'llm-credentials'`。
|
|
210
|
+
|
|
163
211
|
## 读一条任务:列表 vs 单条
|
|
164
212
|
|
|
165
213
|
| 端点 | 给什么 |
|
|
@@ -173,12 +221,13 @@ const message = body.length <= remainingBytes ? body : body.slice(0, remainingBy
|
|
|
173
221
|
|
|
174
222
|
## 更新任务时能改哪些字段
|
|
175
223
|
|
|
176
|
-
`PUT /update-message` 的可写字段:`contactName` / `avatarUrl` / `userMessage` / `completePrompt` / `messages` / `nextSendAt` / `recurrenceType` / `tzId` / `metadata` / `maxTokens` / `temperature` / `splitPattern
|
|
224
|
+
`PUT /update-message` 的可写字段:`contactName` / `avatarUrl` / `userMessage` / `completePrompt` / `messages` / `nextSendAt` / `recurrenceType` / `tzId` / `metadata` / `maxTokens` / `temperature` / `splitPattern`、凭据三件套 `apiUrl` / `apiKey` / `primaryModel`,以及凭据引用 `credRefs`。
|
|
177
225
|
|
|
178
226
|
- `contactName` 必须是非空字符串(口径与排程时一致),空串 / `null` / 非字符串一律 `400`。用户给角色改了名之后,之前排的任务推送出来的通知标题(「来自 <contactName>」)靠它跟着改。
|
|
179
227
|
- `metadata` 是整体替换,不深合并——只改一个子字段的读-改-写流程见上一节。
|
|
180
228
|
- `avatarUrl` 显式传 `null` 是「不改」而不是「清空」(§6.2 的软清空策略要求非法头像被摘掉时保留旧头像,「摘掉」和「传了个 null」在这一层是同一件事)。
|
|
181
229
|
- 凭据三件套传 `null` 同样只是忽略:清掉任何一个,任务到点就发不出去。
|
|
230
|
+
- `credRefs` 是整体替换(语义同 `metadata`),同样做存在性检查;与内联三件套在同一个请求里混着传返回 `400`。给存量内联任务补 `credRefs` 时不动已存的三件套——那份留作 fire 时表行缺失的兜底。
|
|
182
231
|
- `pushSubscription` 不收(`400 PUSH_SUBSCRIPTION_NOT_ACCEPTED`),它是用户级的一份,走 `PUT /push-subscription`。
|
|
183
232
|
|
|
184
233
|
## 推送自带任务身份
|
|
@@ -382,6 +431,108 @@ await runScheduledTick({
|
|
|
382
431
|
|
|
383
432
|
`lease_until` / `retry_after` / `serialize_group`(都可空)。走 `POST /init-tenant`(或任何一次 `initSchema`)会自动给已有的表补上,跑几次都没事;手工维护表结构的看 `examples/cloudflare-single-user/schema.sql`,Postgres 侧对应三句 `ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS …`。分组串行还多一个索引 `idx_serialize_group_lease`,`initSchema` 一并建。
|
|
384
433
|
|
|
434
|
+
## 表结构自查(`getSchemaVersion` / `ensureSchema`)
|
|
435
|
+
|
|
436
|
+
建表语句是 `CREATE TABLE IF NOT EXISTS`,已经存在的表不会被改动,后加的列靠 `initSchema()` 里的 ALTER 补。升级到新版之后没人再跑一次建表的话,这个部署的表就停在旧形状上——cron 每分钟挂在缺的那一列上,任务一条都不发,而前端界面一切正常。
|
|
437
|
+
|
|
438
|
+
这两个函数把「我需要什么 / 现在是什么 / 帮我补齐」露出来:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
import { getSchemaVersion, ensureSchema } from '@rei-standard/amsg-server/cloudflare';
|
|
442
|
+
|
|
443
|
+
const state = await getSchemaVersion(db);
|
|
444
|
+
// → { current: '2.6.0' | null, required: '2.6.0', ok: true | false, missing: [] }
|
|
445
|
+
|
|
446
|
+
if (!state.ok) {
|
|
447
|
+
const fixed = await ensureSchema(db); // 建表 + 补列 + 建索引,重复调没事
|
|
448
|
+
// → { current, required, ok, missing, migrated: true, schema }
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
| 字段 | 是什么 |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `required` | 这一版代码需要的表结构版本(导出常量 `SCHEMA_VERSION`)。表结构自己的版本号,只在表 / 列 / 关键索引变化时抬,与包版本各走各的 |
|
|
455
|
+
| `current` | 活库当前满足的版本:够用就是 `required` 那个值,缺东西就是 `null`(只知道不够用,不知道它停在哪一版) |
|
|
456
|
+
| `ok` | 需要的表 / 列 / 关键索引是不是都在 |
|
|
457
|
+
| `missing` | 缺什么,形如 `table:message_outbox` / `column:scheduled_messages.last_error` / `index:uidx_uuid`。整张表缺席时只报这一张表,不逐列展开 |
|
|
458
|
+
| `migrated`(只有 `ensureSchema` 有) | 这次有没有真的跑 `initSchema()`。本来就够用 → `false`,`schema` 也是 `null` |
|
|
459
|
+
|
|
460
|
+
「需要什么」是从建表语句里解析出来的,不是另抄一份清单:抄的那份漏掉新列的话,自查会对着一个缺列的库回「一切正常」。
|
|
461
|
+
|
|
462
|
+
什么时候调、缺了怎么提示用户,由宿主决定——库不会在每次请求里偷偷迁移。`POST /init-tenant` 的行为一点没变(它内部做的就是 `initSchema()`)。
|
|
463
|
+
|
|
464
|
+
单用户 Worker 上有走 `env` 的同名方法:`worker.getSchemaVersion(env)` / `worker.ensureSchema(env)`,省得自己再造一次适配器。
|
|
465
|
+
|
|
466
|
+
自查要求适配器实现 `describeSchema()`(活库里现在有哪些表 / 列 / 索引,只读)。内置适配器里目前只有 D1 实现了;别的适配器调这两个函数会抛错,而不是假装一切正常。
|
|
467
|
+
|
|
468
|
+
## 只跑指定那一条任务(`runTask`)
|
|
469
|
+
|
|
470
|
+
`scheduled()` 的语义是「扫一遍所有到期任务」。刚落库的任务想立刻跑起来时,触发一次全量扫描是能达到目的,但那样多个执行者会去扫同一批任务,宿主只能退回单实例串行才不重复发。
|
|
471
|
+
|
|
472
|
+
`runTask` 只跑指定那一条,走的是 cron 完全同一条投递链(占位、租约心跳、过期守卫、失败重试 / 终态、hook 全套):
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
// 单用户 Worker:从 env 拿库和配置
|
|
476
|
+
const result = await worker.runTask(uuid, env);
|
|
477
|
+
|
|
478
|
+
// 自己攒 ctx 的宿主(和 runScheduledTick 同一份 ctx)
|
|
479
|
+
import { runTask } from '@rei-standard/amsg-server/cloudflare';
|
|
480
|
+
const result = await runTask(ctx, uuid);
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
跑起来了是 `{ ran: true, summary }`(`summary` 与 `runScheduledTick` 的返回同构,`totalTasks` 恒为 1)。不跑的几种情形分开回报,宿主不用猜:
|
|
484
|
+
|
|
485
|
+
| `reason` | 什么意思 | 附带 |
|
|
486
|
+
|---|---|---|
|
|
487
|
+
| `not_found` | 没有这条 uuid。一次性任务发完即删,所以「已经发完了」的那条也落在这里 | — |
|
|
488
|
+
| `already_settled` | 行还在,但已经是终态 | `status`(`sent` / `failed`) |
|
|
489
|
+
| `not_due` | 还没到触发时刻 | `nextSendAt` |
|
|
490
|
+
| `retry_pending` | 上次投递失败,还在退避窗口里 | `retryAfter` |
|
|
491
|
+
| `not_configured` | VAPID / webpush 没配齐,跑了也只是白扣这条任务一次重试(只有 `worker.runTask` 有这一种) | — |
|
|
492
|
+
|
|
493
|
+
这个入口只是换了个触发器,不是绕过排期的后门:没到点、在退避窗口里的都不跑。`already_settled` 要求适配器实现 `getTaskStatusByUuidOnly(uuid)`(D1 / pg / neon 都实现了);不实现的自定义适配器把这种情况并进 `not_found`。
|
|
494
|
+
|
|
495
|
+
## 出错时的真实原因
|
|
496
|
+
|
|
497
|
+
`fetch()` 兜底的 500 一直是 `{ success: false, error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' } }`。这两个字段一个没动,真因加在 `error.cause` 上:
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"success": false,
|
|
502
|
+
"error": {
|
|
503
|
+
"code": "INTERNAL_ERROR",
|
|
504
|
+
"message": "服务器内部错误",
|
|
505
|
+
"cause": {
|
|
506
|
+
"stage": "request",
|
|
507
|
+
"name": "Error",
|
|
508
|
+
"message": "D1_ERROR: no such table: message_outbox"
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
| 字段 | 是什么 |
|
|
515
|
+
|---|---|
|
|
516
|
+
| `stage` | 在哪一段炸的:`config` = 构建配置时(少了 binding、环境变量丢了),`request` = 路由或处理器抛错 |
|
|
517
|
+
| `name` | 错误类型(`error.name`,认不出来时是 `Error`) |
|
|
518
|
+
| `message` | 错误消息,长得像凭据的串已遮掉、超长截断到 500 字符 |
|
|
519
|
+
| `code` | 错误自带的 `code` 字符串,有才带 |
|
|
520
|
+
|
|
521
|
+
只带错误类型和消息文本:密钥、用户数据、任务正文都不在 `error.message` 上,也不往这里放。
|
|
522
|
+
|
|
523
|
+
cron 那条路上没有调用方能读到响应,所以另开两个出口:
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
export default createSingleUserCloudflareWorker(buildConfig, {
|
|
527
|
+
onError({ stage, error, cause, path }) {
|
|
528
|
+
// fetch / cron 任何一段出错都会调一次(best-effort,自身抛错只记日志)
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
- `onError` 放在工厂的第二个参数上,而不是 `buildConfig` 的返回值里:`buildConfig` 自己抛错时配置里的东西一个都读不到,而那恰恰是最需要被看见的一种故障。`stage` 在 cron 路径上是 `config`(配置构建失败 / VAPID 没配齐)或 `tick`(那一跳抛错)。VAPID 没配齐这一支没有异常对象,`error` 为 `null`、`cause.name` 是 `VapidNotConfigured`。
|
|
534
|
+
- `scheduled()` 现在有返回值:`{ ok: true, summary }` 或 `{ ok: false, cause }`。Cloudflare 不看它,是给「自己包一层再转调 `scheduled`」的宿主和测试用的。
|
|
535
|
+
|
|
385
536
|
## 导出 API(Exports)
|
|
386
537
|
|
|
387
538
|
包根(`@rei-standard/amsg-server`):
|
|
@@ -391,6 +542,10 @@ await runScheduledTick({
|
|
|
391
542
|
- `createSingleUserCloudflareWorker` — 单用户 Cloudflare Worker 一键装配(`fetch` + `scheduled` 两个入口)
|
|
392
543
|
- `createAdapter` / `createD1Adapter` — pg·neon / Cloudflare D1 数据库适配器
|
|
393
544
|
- `runScheduledTick` — 手动触发一轮到期任务投递(自定义 cron 宿主、要调 `claimLeaseMs` 时用)
|
|
545
|
+
- `runTask` — 只跑指定那一条任务(与 cron 同一条投递链)
|
|
546
|
+
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION` — 表结构自查与补齐
|
|
547
|
+
- `summarizeErrorCause` — 把异常压成响应体里 `error.cause` 那个形状(自己包一层路由、想回同样形状时用同一份)
|
|
548
|
+
- `NonRetryableError` / `isNonRetryableError` — hook 侧标注「重试也好不了」的失败
|
|
394
549
|
- `createWebCryptoWebPush` — 纯 Web Crypto 的 Web Push 发送器(不依赖 `web-push` 包)
|
|
395
550
|
- `createTenantToken` / `verifyTenantToken`
|
|
396
551
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
@@ -401,7 +556,9 @@ await runScheduledTick({
|
|
|
401
556
|
|
|
402
557
|
`@rei-standard/amsg-server/cloudflare` 子路径 —— 只含「单用户 + D1 + Web Crypto 推送」这条子图,不引用多租户装配线和 pg / neon / `web-push`,所以 D1-only 安装(不装可选数据库 peer)也能干净打包,Worker 不需要 `nodejs_compat` 兼容 flag:
|
|
403
558
|
|
|
404
|
-
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick`
|
|
559
|
+
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick` / `runTask`
|
|
560
|
+
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION`
|
|
561
|
+
- `summarizeErrorCause` / `NonRetryableError` / `isNonRetryableError`
|
|
405
562
|
- `createWebCryptoWebPush` / `measurePushPayload` / `MAX_PUSH_PAYLOAD_BYTES` / `WEB_PUSH_MAX_BODY_BYTES` / `WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES`
|
|
406
563
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
407
564
|
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ export class D1Adapter {
|
|
|
36
36
|
error: any;
|
|
37
37
|
})[];
|
|
38
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* 活库里现在实际有哪些表 / 列 / 索引。
|
|
41
|
+
*
|
|
42
|
+
* 只读不写,纯粹如实回报:拿它跟这一版需要的清单对照的活儿在
|
|
43
|
+
* lib/schema-version.js(`getSchemaVersion` / `ensureSchema`)。库升级后表结
|
|
44
|
+
* 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
|
|
45
|
+
* 界面上一切正常——这个方法就是让宿主查得出来。
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
|
|
48
|
+
*/
|
|
49
|
+
describeSchema(): Promise<{
|
|
50
|
+
tables: Record<string, string[]>;
|
|
51
|
+
indexes: string[];
|
|
52
|
+
}>;
|
|
39
53
|
dropSchema(): Promise<void>;
|
|
40
54
|
createTask(params: any): Promise<any>;
|
|
41
55
|
/**
|
|
@@ -53,6 +67,16 @@ export class D1Adapter {
|
|
|
53
67
|
createTaskSuperseding(params: import("./interface.js").InsertTaskParams, supersedesUuid: string): Promise<any>;
|
|
54
68
|
getTaskByUuid(uuid: any, userId: any): Promise<any>;
|
|
55
69
|
getTaskByUuidOnly(uuid: any): Promise<any>;
|
|
70
|
+
/**
|
|
71
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(getTaskByUuidOnly 只看
|
|
72
|
+
* pending 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
73
|
+
*
|
|
74
|
+
* @param {string} uuid
|
|
75
|
+
* @returns {Promise<{ status: string }|null>}
|
|
76
|
+
*/
|
|
77
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
78
|
+
status: string;
|
|
79
|
+
} | null>;
|
|
56
80
|
updateTaskById(taskId: any, updates: any): Promise<any>;
|
|
57
81
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<{
|
|
58
82
|
uuid: any;
|
|
@@ -213,6 +237,51 @@ export class D1Adapter {
|
|
|
213
237
|
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
214
238
|
*/
|
|
215
239
|
deletePushSubscription(userId: string): Promise<boolean>;
|
|
240
|
+
/**
|
|
241
|
+
* 批量 upsert 这个用户的凭据(value 已是密文,加密在上层)。已存在的行
|
|
242
|
+
* 覆盖 encrypted_value 并刷 updated_at,created_at 保留首次写入的时刻。
|
|
243
|
+
*
|
|
244
|
+
* @param {string} userId
|
|
245
|
+
* @param {Array<{ credId: string, encryptedValue: string }>} entries
|
|
246
|
+
* @returns {Promise<number>} 实际写入/覆盖的行数
|
|
247
|
+
*/
|
|
248
|
+
upsertLlmCredentials(userId: string, entries: Array<{
|
|
249
|
+
credId: string;
|
|
250
|
+
encryptedValue: string;
|
|
251
|
+
}>): Promise<number>;
|
|
252
|
+
/**
|
|
253
|
+
* 按 cred_id 批量读这个用户的凭据行(`encrypted_value` 是密文,解密在上
|
|
254
|
+
* 层)。排程时的存在性检查和 fire 时的解析共用这一个口。
|
|
255
|
+
*
|
|
256
|
+
* @param {string} userId
|
|
257
|
+
* @param {string[]} credIds
|
|
258
|
+
* @returns {Promise<Array<{ cred_id: string, encrypted_value: string, updated_at: string }>>}
|
|
259
|
+
*/
|
|
260
|
+
getLlmCredentials(userId: string, credIds: string[]): Promise<Array<{
|
|
261
|
+
cred_id: string;
|
|
262
|
+
encrypted_value: string;
|
|
263
|
+
updated_at: string;
|
|
264
|
+
}>>;
|
|
265
|
+
/**
|
|
266
|
+
* 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出
|
|
267
|
+
* 这个方法)。按 cred_id 排序,输出稳定。
|
|
268
|
+
*
|
|
269
|
+
* @param {string} userId
|
|
270
|
+
* @returns {Promise<Array<{ cred_id: string, updated_at: string }>>}
|
|
271
|
+
*/
|
|
272
|
+
listLlmCredentials(userId: string): Promise<Array<{
|
|
273
|
+
cred_id: string;
|
|
274
|
+
updated_at: string;
|
|
275
|
+
}>>;
|
|
276
|
+
/**
|
|
277
|
+
* 删凭据。`credIds` 传数组删指定那几行;传 null 删这个用户的全部
|
|
278
|
+
* (「清空云端数据」用)。
|
|
279
|
+
*
|
|
280
|
+
* @param {string} userId
|
|
281
|
+
* @param {string[]|null} credIds
|
|
282
|
+
* @returns {Promise<number>} 删掉的行数
|
|
283
|
+
*/
|
|
284
|
+
deleteLlmCredentials(userId: string, credIds?: string[] | null): Promise<number>;
|
|
216
285
|
/**
|
|
217
286
|
* 发送前把这一批 push 落进 outbox(一次 batch)。(user_id, message_id)
|
|
218
287
|
* 唯一:重试同一 occurrence 带着同一批 messageId 再来时更新 payload、不加
|
|
@@ -29,6 +29,15 @@ export type DbAdapter = {
|
|
|
29
29
|
* Create the scheduled_messages table and all indexes.
|
|
30
30
|
*/
|
|
31
31
|
initSchema: () => Promise<InitSchemaResult>;
|
|
32
|
+
/**
|
|
33
|
+
* (可选)活库里现在实际有哪些表 / 列 / 索引,只读不写。
|
|
34
|
+
* `getSchemaVersion` / `ensureSchema`(lib/schema-version.js)拿它跟这一版
|
|
35
|
+
* 需要的清单对照;不实现的适配器调那两个函数会抛错。内置只有 D1 实现。
|
|
36
|
+
*/
|
|
37
|
+
describeSchema?: () => Promise<{
|
|
38
|
+
tables: Record<string, string[]>;
|
|
39
|
+
indexes: string[];
|
|
40
|
+
}>;
|
|
32
41
|
/**
|
|
33
42
|
* Drop the scheduled_messages table (CASCADE).
|
|
34
43
|
*/
|
|
@@ -45,6 +54,14 @@ export type DbAdapter = {
|
|
|
45
54
|
* Fetch a single pending task by uuid only (used by instant processing).
|
|
46
55
|
*/
|
|
47
56
|
getTaskByUuidOnly: (uuid: string) => Promise<TaskRow | null>;
|
|
57
|
+
/**
|
|
58
|
+
* (可选)这条 uuid 现在是什么状态——不限用户,也不限状态。`runTask` 用它
|
|
59
|
+
* 把「这条已经跑完进终态了」和「压根没这条」分开回报;不实现时两种都归
|
|
60
|
+
* `not_found`。
|
|
61
|
+
*/
|
|
62
|
+
getTaskStatusByUuidOnly?: (uuid: string) => Promise<{
|
|
63
|
+
status: string;
|
|
64
|
+
} | null>;
|
|
48
65
|
/**
|
|
49
66
|
* Partially update a task row by its numeric id.
|
|
50
67
|
* 实现了 `claimTask` 的适配器还要认 `lease_until` 和 `retry_after`(含写
|
|
@@ -162,6 +179,39 @@ export type DbAdapter = {
|
|
|
162
179
|
* (建了也永远发不出去)。内置的 D1 / pg / neon 适配器都实现了。
|
|
163
180
|
*/
|
|
164
181
|
deletePushSubscription?: (userId: string) => Promise<boolean>;
|
|
182
|
+
/**
|
|
183
|
+
* 批量覆盖写这个用户的 LLM 凭据(`encryptedValue` 是密文,加密在上层)。
|
|
184
|
+
* 已存在的行覆盖 encrypted_value 并刷 updated_at。返回实际写入/覆盖的行数。
|
|
185
|
+
*/
|
|
186
|
+
upsertLlmCredentials?: (userId: string, entries: Array<{
|
|
187
|
+
credId: string;
|
|
188
|
+
encryptedValue: string;
|
|
189
|
+
}>) => Promise<number>;
|
|
190
|
+
/**
|
|
191
|
+
* 按 cred_id 批量读凭据行(密文原样返回)。排程时的存在性检查和 fire 时的
|
|
192
|
+
* 解析共用这一个口。
|
|
193
|
+
*/
|
|
194
|
+
getLlmCredentials?: (userId: string, credIds: string[]) => Promise<Array<{
|
|
195
|
+
cred_id: string;
|
|
196
|
+
encrypted_value: string;
|
|
197
|
+
updated_at: string;
|
|
198
|
+
}>>;
|
|
199
|
+
/**
|
|
200
|
+
* 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出这个
|
|
201
|
+
* 方法)。按 cred_id 排序。
|
|
202
|
+
*/
|
|
203
|
+
listLlmCredentials?: (userId: string) => Promise<Array<{
|
|
204
|
+
cred_id: string;
|
|
205
|
+
updated_at: string;
|
|
206
|
+
}>>;
|
|
207
|
+
/**
|
|
208
|
+
* 删凭据:数组删指定那几行,null 删全部。返回删掉的行数。
|
|
209
|
+
*
|
|
210
|
+
* llm_credentials 四个方法要么都实现、要么都不实现:缺任何一个,
|
|
211
|
+
* `PUT/GET/DELETE /llm-credentials` 返回 501,带 `credRefs` 的
|
|
212
|
+
* `POST /schedule-message` 也会被拒。内置的 D1 / pg / neon 适配器都实现了。
|
|
213
|
+
*/
|
|
214
|
+
deleteLlmCredentials?: (userId: string, credIds: string[] | null) => Promise<number>;
|
|
165
215
|
/**
|
|
166
216
|
* (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
|
|
167
217
|
* pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
|
package/dist/adapters/neon.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ export class NeonAdapter {
|
|
|
35
35
|
createTask(params: any): Promise<Record<string, any>>;
|
|
36
36
|
getTaskByUuid(uuid: any, userId: any): Promise<Record<string, any>>;
|
|
37
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>;
|
|
38
48
|
updateTaskById(taskId: any, updates: any): Promise<Record<string, any>>;
|
|
39
49
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<Record<string, any>>;
|
|
40
50
|
deleteTaskById(taskId: any): Promise<boolean>;
|
|
@@ -58,4 +68,15 @@ export class NeonAdapter {
|
|
|
58
68
|
}>;
|
|
59
69
|
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
60
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>;
|
|
61
82
|
}
|
|
@@ -101,6 +101,53 @@ export function upsertPushSubscription(query: PgQuery, userId: string, encrypted
|
|
|
101
101
|
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
102
102
|
*/
|
|
103
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>;
|
|
104
151
|
/**
|
|
105
152
|
* pg / neon 共用的 Postgres 查询实现。
|
|
106
153
|
*
|
package/dist/adapters/pg.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export class PgAdapter {
|
|
|
37
37
|
createTask(params: any): Promise<any[]>;
|
|
38
38
|
getTaskByUuid(uuid: any, userId: any): Promise<any[]>;
|
|
39
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>;
|
|
40
50
|
updateTaskById(taskId: any, updates: any): Promise<any[]>;
|
|
41
51
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<any[]>;
|
|
42
52
|
deleteTaskById(taskId: any): Promise<boolean>;
|
|
@@ -60,4 +70,15 @@ export class PgAdapter {
|
|
|
60
70
|
}>;
|
|
61
71
|
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
62
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>;
|
|
63
84
|
}
|
|
@@ -35,6 +35,7 @@ export const INDEXES: ({
|
|
|
35
35
|
critical: boolean;
|
|
36
36
|
})[];
|
|
37
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";
|
|
38
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";
|
|
39
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";
|
|
40
41
|
export const UPDATABLE_COLUMNS: Set<string>;
|
|
@@ -32,5 +32,18 @@ export const SQLITE_INDEXES: {
|
|
|
32
32
|
}[];
|
|
33
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
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";
|
|
35
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";
|
|
36
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
|
+
};
|
|
@@ -93,6 +93,16 @@ var PUSH_SUBSCRIPTION_TABLE_SQL = `
|
|
|
93
93
|
updated_at BIGINT NOT NULL
|
|
94
94
|
)
|
|
95
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
|
+
`;
|
|
96
106
|
var VERIFY_TABLE_SQL = `
|
|
97
107
|
SELECT table_name
|
|
98
108
|
FROM information_schema.tables
|
|
@@ -127,6 +137,7 @@ export {
|
|
|
127
137
|
MIGRATIONS,
|
|
128
138
|
INDEXES,
|
|
129
139
|
PUSH_SUBSCRIPTION_TABLE_SQL,
|
|
140
|
+
LLM_CREDENTIALS_TABLE_SQL,
|
|
130
141
|
VERIFY_TABLE_SQL,
|
|
131
142
|
COLUMNS_SQL,
|
|
132
143
|
UPDATABLE_COLUMNS
|
|
@@ -84,6 +84,46 @@ async function deletePushSubscription(query, userId) {
|
|
|
84
84
|
);
|
|
85
85
|
return rows.length > 0;
|
|
86
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
|
+
|
|
87
127
|
|
|
88
128
|
|
|
89
129
|
|
|
@@ -92,4 +132,4 @@ async function deletePushSubscription(query, userId) {
|
|
|
92
132
|
|
|
93
133
|
|
|
94
134
|
|
|
95
|
-
exports.claimTask = claimTask; exports.renewTaskLease = renewTaskLease; exports.getTaskStatusInfo = getTaskStatusInfo; exports.getPushSubscription = getPushSubscription; exports.upsertPushSubscription = upsertPushSubscription; exports.deletePushSubscription = deletePushSubscription;
|
|
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;
|