@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +759 -2
- package/dist/app.d.ts +384 -0
- package/dist/app.js +1148 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +353 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.js +171 -0
- package/dist/handlers/agentHandlers.d.ts +18 -0
- package/dist/handlers/agentHandlers.js +20 -0
- package/dist/handlers/assetHandlers.d.ts +29 -0
- package/dist/handlers/assetHandlers.js +35 -0
- package/dist/handlers/capabilities.d.ts +26 -0
- package/dist/handlers/capabilities.js +138 -0
- package/dist/handlers/completionHandlers.d.ts +18 -0
- package/dist/handlers/completionHandlers.js +13 -0
- package/dist/handlers/configHandlers.d.ts +31 -0
- package/dist/handlers/configHandlers.js +59 -0
- package/dist/handlers/conversationHandlers.d.ts +118 -0
- package/dist/handlers/conversationHandlers.js +314 -0
- package/dist/handlers/handoffHandlers.d.ts +17 -0
- package/dist/handlers/handoffHandlers.js +14 -0
- package/dist/handlers/hostHandlers.d.ts +39 -0
- package/dist/handlers/hostHandlers.js +56 -0
- package/dist/handlers/index.d.ts +128 -0
- package/dist/handlers/index.js +92 -0
- package/dist/handlers/messageHandlers.d.ts +11 -0
- package/dist/handlers/messageHandlers.js +10 -0
- package/dist/handlers/modelHandlers.d.ts +17 -0
- package/dist/handlers/modelHandlers.js +19 -0
- package/dist/handlers/noticeHandlers.d.ts +24 -0
- package/dist/handlers/noticeHandlers.js +24 -0
- package/dist/handlers/questionHandlers.d.ts +16 -0
- package/dist/handlers/questionHandlers.js +16 -0
- package/dist/handlers/searchHandlers.d.ts +12 -0
- package/dist/handlers/searchHandlers.js +12 -0
- package/dist/handlers/workspaceHandlers.d.ts +29 -0
- package/dist/handlers/workspaceHandlers.js +47 -0
- package/dist/i18n/en.d.ts +243 -0
- package/dist/i18n/en.js +270 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +72 -0
- package/dist/i18n/zh.d.ts +240 -0
- package/dist/i18n/zh.js +257 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/net/UploadReceiver.d.ts +186 -0
- package/dist/net/UploadReceiver.js +494 -0
- package/dist/net/WsHub.d.ts +231 -0
- package/dist/net/WsHub.js +689 -0
- package/dist/net/auth.d.ts +73 -0
- package/dist/net/auth.js +74 -0
- package/dist/net/discovery.d.ts +72 -0
- package/dist/net/discovery.js +171 -0
- package/dist/net/index.d.ts +14 -0
- package/dist/net/index.js +10 -0
- package/dist/runtime/ChatRunner.d.ts +374 -0
- package/dist/runtime/ChatRunner.js +1204 -0
- package/dist/runtime/ConversationDrafts.d.ts +43 -0
- package/dist/runtime/ConversationDrafts.js +47 -0
- package/dist/runtime/RunRegistry.d.ts +79 -0
- package/dist/runtime/RunRegistry.js +108 -0
- package/dist/runtime/ScopeRegistry.d.ts +153 -0
- package/dist/runtime/ScopeRegistry.js +259 -0
- package/dist/runtime/TransientRunner.d.ts +90 -0
- package/dist/runtime/TransientRunner.js +179 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.js +10 -0
- package/dist/services/AgentService.d.ts +150 -0
- package/dist/services/AgentService.js +718 -0
- package/dist/services/AssetService.d.ts +338 -0
- package/dist/services/AssetService.js +739 -0
- package/dist/services/AutoNaming.d.ts +92 -0
- package/dist/services/AutoNaming.js +235 -0
- package/dist/services/CompletionService.d.ts +122 -0
- package/dist/services/CompletionService.js +442 -0
- package/dist/services/ConfigService.d.ts +171 -0
- package/dist/services/ConfigService.js +695 -0
- package/dist/services/ConversationService.d.ts +274 -0
- package/dist/services/ConversationService.js +902 -0
- package/dist/services/CredentialService.d.ts +33 -0
- package/dist/services/CredentialService.js +86 -0
- package/dist/services/DiscoveryService.d.ts +182 -0
- package/dist/services/DiscoveryService.js +998 -0
- package/dist/services/HandoffService.d.ts +189 -0
- package/dist/services/HandoffService.js +414 -0
- package/dist/services/Logger.d.ts +130 -0
- package/dist/services/Logger.js +256 -0
- package/dist/services/MessageService.d.ts +88 -0
- package/dist/services/MessageService.js +280 -0
- package/dist/services/ModelService.d.ts +89 -0
- package/dist/services/ModelService.js +489 -0
- package/dist/services/NoticeService.d.ts +93 -0
- package/dist/services/NoticeService.js +217 -0
- package/dist/services/QuestionService.d.ts +184 -0
- package/dist/services/QuestionService.js +457 -0
- package/dist/services/SearchService.d.ts +54 -0
- package/dist/services/SearchService.js +117 -0
- package/dist/services/WorkspaceService.d.ts +96 -0
- package/dist/services/WorkspaceService.js +213 -0
- package/dist/services/imageSize.d.ts +41 -0
- package/dist/services/imageSize.js +146 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +15 -0
- package/dist/services/secretMask.d.ts +24 -0
- package/dist/services/secretMask.js +34 -0
- package/dist/storage/AssetRepository.d.ts +87 -0
- package/dist/storage/AssetRepository.js +180 -0
- package/dist/storage/ConversationRepository.d.ts +55 -0
- package/dist/storage/ConversationRepository.js +128 -0
- package/dist/storage/CredentialRepository.d.ts +28 -0
- package/dist/storage/CredentialRepository.js +47 -0
- package/dist/storage/Database.d.ts +95 -0
- package/dist/storage/Database.js +180 -0
- package/dist/storage/MessageRepository.d.ts +104 -0
- package/dist/storage/MessageRepository.js +323 -0
- package/dist/storage/MetaRepository.d.ts +21 -0
- package/dist/storage/MetaRepository.js +33 -0
- package/dist/storage/NoticeRepository.d.ts +31 -0
- package/dist/storage/NoticeRepository.js +89 -0
- package/dist/storage/QuestionRepository.d.ts +34 -0
- package/dist/storage/QuestionRepository.js +96 -0
- package/dist/storage/WorkspaceRepository.d.ts +25 -0
- package/dist/storage/WorkspaceRepository.js +44 -0
- package/dist/storage/dbWorker.d.ts +10 -0
- package/dist/storage/dbWorker.js +132 -0
- package/dist/storage/index.d.ts +20 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/migrations.d.ts +26 -0
- package/dist/storage/migrations.js +207 -0
- package/dist/storage/types.d.ts +298 -0
- package/dist/storage/types.js +14 -0
- package/dist/wire/errors.d.ts +105 -0
- package/dist/wire/errors.js +143 -0
- package/dist/wire/events.d.ts +102 -0
- package/dist/wire/events.js +13 -0
- package/dist/wire/frames.d.ts +106 -0
- package/dist/wire/frames.js +156 -0
- package/dist/wire/index.d.ts +31 -0
- package/dist/wire/index.js +31 -0
- package/dist/wire/methods.d.ts +1896 -0
- package/dist/wire/methods.js +99 -0
- package/dist/wire/states.d.ts +487 -0
- package/dist/wire/states.js +61 -0
- package/package.json +63 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 素材的三张表:`assets`(资源的唯一真相)、`conversation_assets`(谁引用了它)与
|
|
3
|
+
* `asset_vendor_files`(它的一部分可失效缓存)。
|
|
4
|
+
*
|
|
5
|
+
* 三张表的 SQL 放在同一个文件里,是因为它们的生命周期绑在一起,而另外两张都没有独立含义:
|
|
6
|
+
*
|
|
7
|
+
* - `assets` 一行 = 数据根下一份文件 + 它的类型、大小、摘要、像素尺寸。`sha256` 全库唯一:
|
|
8
|
+
* 同一份字节在整台机器上只有一行、一份磁盘,所以这一行是「这张图是什么」的唯一答案。
|
|
9
|
+
* 它不记「属于哪段对话」——一张图可以同时被好几段对话引用。
|
|
10
|
+
* - `conversation_assets` 一行 = 「某条消息引用了某份素材」。归属与「几张会话引用了它」都从
|
|
11
|
+
* 这张表算;消息被删(撤回 / 删对话)时关系行跟着级联消失,不需要谁去减计数。
|
|
12
|
+
* `assetId` 用 `ON DELETE RESTRICT`:还有引用时素材行在库里删不掉(护栏)。
|
|
13
|
+
* - `asset_vendor_files` 一行 = 「这个素材在这个端点上对应厂商的哪个 file_id」。厂商的
|
|
14
|
+
* `file_id` 是端点维度资源,所以它只当缓存:删光也能从 `assets` 重建(下次发送时
|
|
15
|
+
* 重新上传一遍),`ON DELETE CASCADE` 让它跟着素材走。
|
|
16
|
+
*
|
|
17
|
+
* 存储层只做 SQL:不判断文件在不在、不上传、不决定「缓存能不能用」,也不判「该不该删字节」
|
|
18
|
+
* (那是素材服务的事:它按 {@link AssetRepository.countReferences} 的结果决定)。
|
|
19
|
+
*/
|
|
20
|
+
import type { Database } from "./Database.js";
|
|
21
|
+
import type { AssetCreateInput, AssetRow, AssetVendorFileRow, AssetVendorFileUpsertInput, ConversationAssetRow } from "./types.js";
|
|
22
|
+
/** 插入一个素材行的结果:`created` 为 false 说明库里已经有同摘要的那一行(全局去重命中) */
|
|
23
|
+
export interface AssetUpsertOutcome {
|
|
24
|
+
row: AssetRow;
|
|
25
|
+
created: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** 一个素材的缓存行分布(`describeVendorFiles` 的出参) */
|
|
28
|
+
export interface AssetVendorFilePresence {
|
|
29
|
+
/** 这个端点上已经有缓存行 */
|
|
30
|
+
onEndpoint: boolean;
|
|
31
|
+
/** 别的端点上有 */
|
|
32
|
+
elsewhere: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class AssetRepository {
|
|
35
|
+
private readonly db;
|
|
36
|
+
constructor(db: Database);
|
|
37
|
+
create(input: AssetCreateInput): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* 插入,但「库里已经有同摘要的那一行」时不报错、把既有行返回。
|
|
40
|
+
*
|
|
41
|
+
* 两个并发上传撞在一起(同一张图连传两次)时,唯一约束会挡住第二次插入;这时返回先到的那一行,
|
|
42
|
+
* 让上层把刚写出去的文件删掉即可 —— 而不是抛一个「库里已经有这条」的错误给用户。
|
|
43
|
+
*
|
|
44
|
+
* 两条语句走同一个 `tx`(声明式批次):插入真正生效与否由第一条的结果决定,第二条只负责把
|
|
45
|
+
* 「现在库里是哪一行」读回来。
|
|
46
|
+
*/
|
|
47
|
+
upsert(input: AssetCreateInput): Promise<AssetUpsertOutcome>;
|
|
48
|
+
get(id: string): Promise<AssetRow | undefined>;
|
|
49
|
+
/** 按内容摘要找:全局去重(`asset.begin` 带 sha256 时走这一条) */
|
|
50
|
+
findByDigest(sha256: string): Promise<AssetRow | undefined>;
|
|
51
|
+
/** 一段对话引用的全部素材(同一份素材被这段对话里两条消息引用时只出现一次) */
|
|
52
|
+
listByConversation(conversationId: string): Promise<AssetRow[]>;
|
|
53
|
+
/** 一条消息引用的全部素材 */
|
|
54
|
+
listByMessage(messageId: string): Promise<AssetRow[]>;
|
|
55
|
+
/**
|
|
56
|
+
* 记一行「这条消息引用了这份素材」。已经记过(同一 message + asset)时返回 0,
|
|
57
|
+
* 否则返回 1:消息落库那一刻扫 `content` 里的 `__asset_id` 写行,重复写是常态。
|
|
58
|
+
*/
|
|
59
|
+
linkAsset(input: ConversationAssetRow): Promise<number>;
|
|
60
|
+
/** 这份素材被几行引用(含同一段对话里的多条消息) */
|
|
61
|
+
countReferences(assetId: string): Promise<number>;
|
|
62
|
+
/** 「几张会话引用了它」:`COUNT(DISTINCT conversationId)` */
|
|
63
|
+
countConversations(assetId: string): Promise<number>;
|
|
64
|
+
/** 一份素材被哪些对话引用(「贴到过哪几个项目」) */
|
|
65
|
+
listConversationIds(assetId: string): Promise<string[]>;
|
|
66
|
+
/** 一条消息引用了哪些素材 id(撤回要回填输入框时用;关系行随消息级联删除,先读一次) */
|
|
67
|
+
listAssetIdsByMessage(messageId: string): Promise<string[]>;
|
|
68
|
+
/** 一段对话引用过哪些素材 id(删对话前先读一次:删完之后关系行就没了) */
|
|
69
|
+
listAssetIdsByConversation(conversationId: string): Promise<string[]>;
|
|
70
|
+
delete(id: string): Promise<number>;
|
|
71
|
+
/**
|
|
72
|
+
* 归零才删:关系行还在时这条语句删不掉(`NOT EXISTS` 与 `ON DELETE RESTRICT` 是同一件事的
|
|
73
|
+
* 两道写法,前者让调用方拿到 0 行、后者保证真要删也不会带走别人的引用)。
|
|
74
|
+
*/
|
|
75
|
+
deleteIfUnreferenced(id: string): Promise<number>;
|
|
76
|
+
/** 缓存命中:这个素材在这个端点上已经有厂商的 file_id */
|
|
77
|
+
getVendorFile(assetId: string, endpointId: string): Promise<AssetVendorFileRow | undefined>;
|
|
78
|
+
/** 写入(或覆盖)一条缓存行:同一对 `(assetId, endpointId)` 只有一行 */
|
|
79
|
+
upsertVendorFile(input: AssetVendorFileUpsertInput): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* 这个素材的缓存行分布:这个端点上有没有、别的端点上有没有。
|
|
82
|
+
*
|
|
83
|
+
* 两个事实一次查完,是因为它们要一起看才说得清该走哪条路:这个端点上有就是命中复用,
|
|
84
|
+
* 只有别的端点上有就是换过端点(不重传、走内联),都没有才上传。
|
|
85
|
+
*/
|
|
86
|
+
describeVendorFiles(assetId: string, endpointId: string): Promise<AssetVendorFilePresence>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 素材的三张表:`assets`(资源的唯一真相)、`conversation_assets`(谁引用了它)与
|
|
3
|
+
* `asset_vendor_files`(它的一部分可失效缓存)。
|
|
4
|
+
*
|
|
5
|
+
* 三张表的 SQL 放在同一个文件里,是因为它们的生命周期绑在一起,而另外两张都没有独立含义:
|
|
6
|
+
*
|
|
7
|
+
* - `assets` 一行 = 数据根下一份文件 + 它的类型、大小、摘要、像素尺寸。`sha256` 全库唯一:
|
|
8
|
+
* 同一份字节在整台机器上只有一行、一份磁盘,所以这一行是「这张图是什么」的唯一答案。
|
|
9
|
+
* 它不记「属于哪段对话」——一张图可以同时被好几段对话引用。
|
|
10
|
+
* - `conversation_assets` 一行 = 「某条消息引用了某份素材」。归属与「几张会话引用了它」都从
|
|
11
|
+
* 这张表算;消息被删(撤回 / 删对话)时关系行跟着级联消失,不需要谁去减计数。
|
|
12
|
+
* `assetId` 用 `ON DELETE RESTRICT`:还有引用时素材行在库里删不掉(护栏)。
|
|
13
|
+
* - `asset_vendor_files` 一行 = 「这个素材在这个端点上对应厂商的哪个 file_id」。厂商的
|
|
14
|
+
* `file_id` 是端点维度资源,所以它只当缓存:删光也能从 `assets` 重建(下次发送时
|
|
15
|
+
* 重新上传一遍),`ON DELETE CASCADE` 让它跟着素材走。
|
|
16
|
+
*
|
|
17
|
+
* 存储层只做 SQL:不判断文件在不在、不上传、不决定「缓存能不能用」,也不判「该不该删字节」
|
|
18
|
+
* (那是素材服务的事:它按 {@link AssetRepository.countReferences} 的结果决定)。
|
|
19
|
+
*/
|
|
20
|
+
const COLUMNS = "id, name, path, mime, bytes, sha256, width, height, createdAt";
|
|
21
|
+
const VENDOR_COLUMNS = "assetId, endpointId, vendorFileId, createdAt";
|
|
22
|
+
export class AssetRepository {
|
|
23
|
+
db;
|
|
24
|
+
constructor(db) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
}
|
|
27
|
+
async create(input) {
|
|
28
|
+
await this.db.run(`INSERT INTO assets (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
29
|
+
input.id,
|
|
30
|
+
input.name ?? null,
|
|
31
|
+
input.path,
|
|
32
|
+
input.mime,
|
|
33
|
+
input.bytes,
|
|
34
|
+
input.sha256,
|
|
35
|
+
input.width ?? null,
|
|
36
|
+
input.height ?? null,
|
|
37
|
+
input.createdAt,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 插入,但「库里已经有同摘要的那一行」时不报错、把既有行返回。
|
|
42
|
+
*
|
|
43
|
+
* 两个并发上传撞在一起(同一张图连传两次)时,唯一约束会挡住第二次插入;这时返回先到的那一行,
|
|
44
|
+
* 让上层把刚写出去的文件删掉即可 —— 而不是抛一个「库里已经有这条」的错误给用户。
|
|
45
|
+
*
|
|
46
|
+
* 两条语句走同一个 `tx`(声明式批次):插入真正生效与否由第一条的结果决定,第二条只负责把
|
|
47
|
+
* 「现在库里是哪一行」读回来。
|
|
48
|
+
*/
|
|
49
|
+
async upsert(input) {
|
|
50
|
+
const [inserted, found] = await this.db.tx((tx) => [
|
|
51
|
+
tx.run(`INSERT INTO assets (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
|
+
ON CONFLICT (sha256) DO NOTHING`, [
|
|
53
|
+
input.id,
|
|
54
|
+
input.name ?? null,
|
|
55
|
+
input.path,
|
|
56
|
+
input.mime,
|
|
57
|
+
input.bytes,
|
|
58
|
+
input.sha256,
|
|
59
|
+
input.width ?? null,
|
|
60
|
+
input.height ?? null,
|
|
61
|
+
input.createdAt,
|
|
62
|
+
]),
|
|
63
|
+
tx.get(`SELECT ${COLUMNS} FROM assets WHERE sha256 = ?`, [input.sha256]),
|
|
64
|
+
]);
|
|
65
|
+
if (!found) {
|
|
66
|
+
// 插入没成功又没有既有行:只可能是唯一约束之外的原因被 DO NOTHING 吞掉了,如实报错
|
|
67
|
+
throw new Error("asset upsert produced no row");
|
|
68
|
+
}
|
|
69
|
+
return { row: found, created: inserted.changes > 0 };
|
|
70
|
+
}
|
|
71
|
+
async get(id) {
|
|
72
|
+
return this.db.get(`SELECT ${COLUMNS} FROM assets WHERE id = ?`, [id]);
|
|
73
|
+
}
|
|
74
|
+
/** 按内容摘要找:全局去重(`asset.begin` 带 sha256 时走这一条) */
|
|
75
|
+
async findByDigest(sha256) {
|
|
76
|
+
return this.db.get(`SELECT ${COLUMNS} FROM assets WHERE sha256 = ?`, [sha256]);
|
|
77
|
+
}
|
|
78
|
+
/** 一段对话引用的全部素材(同一份素材被这段对话里两条消息引用时只出现一次) */
|
|
79
|
+
async listByConversation(conversationId) {
|
|
80
|
+
return this.db.all(`SELECT DISTINCT ${columnList("a")}
|
|
81
|
+
FROM assets a
|
|
82
|
+
JOIN conversation_assets ca ON ca.assetId = a.id
|
|
83
|
+
WHERE ca.conversationId = ?
|
|
84
|
+
ORDER BY a.createdAt ASC, a.id ASC`, [conversationId]);
|
|
85
|
+
}
|
|
86
|
+
/** 一条消息引用的全部素材 */
|
|
87
|
+
async listByMessage(messageId) {
|
|
88
|
+
return this.db.all(`SELECT DISTINCT ${columnList("a")}
|
|
89
|
+
FROM assets a
|
|
90
|
+
JOIN conversation_assets ca ON ca.assetId = a.id
|
|
91
|
+
WHERE ca.messageId = ?
|
|
92
|
+
ORDER BY a.createdAt ASC, a.id ASC`, [messageId]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 记一行「这条消息引用了这份素材」。已经记过(同一 message + asset)时返回 0,
|
|
96
|
+
* 否则返回 1:消息落库那一刻扫 `content` 里的 `__asset_id` 写行,重复写是常态。
|
|
97
|
+
*/
|
|
98
|
+
async linkAsset(input) {
|
|
99
|
+
const result = await this.db.run(`INSERT INTO conversation_assets (conversationId, messageId, assetId, createdAt)
|
|
100
|
+
VALUES (?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT (messageId, assetId) DO NOTHING`, [input.conversationId, input.messageId, input.assetId, input.createdAt]);
|
|
102
|
+
return result.changes;
|
|
103
|
+
}
|
|
104
|
+
/** 这份素材被几行引用(含同一段对话里的多条消息) */
|
|
105
|
+
async countReferences(assetId) {
|
|
106
|
+
const row = await this.db.get("SELECT COUNT(*) AS n FROM conversation_assets WHERE assetId = ?", [assetId]);
|
|
107
|
+
return row?.n ?? 0;
|
|
108
|
+
}
|
|
109
|
+
/** 「几张会话引用了它」:`COUNT(DISTINCT conversationId)` */
|
|
110
|
+
async countConversations(assetId) {
|
|
111
|
+
const row = await this.db.get("SELECT COUNT(DISTINCT conversationId) AS n FROM conversation_assets WHERE assetId = ?", [assetId]);
|
|
112
|
+
return row?.n ?? 0;
|
|
113
|
+
}
|
|
114
|
+
/** 一份素材被哪些对话引用(「贴到过哪几个项目」) */
|
|
115
|
+
async listConversationIds(assetId) {
|
|
116
|
+
const rows = await this.db.all(`SELECT DISTINCT conversationId FROM conversation_assets
|
|
117
|
+
WHERE assetId = ?
|
|
118
|
+
ORDER BY conversationId ASC`, [assetId]);
|
|
119
|
+
return rows.map((row) => row.conversationId);
|
|
120
|
+
}
|
|
121
|
+
/** 一条消息引用了哪些素材 id(撤回要回填输入框时用;关系行随消息级联删除,先读一次) */
|
|
122
|
+
async listAssetIdsByMessage(messageId) {
|
|
123
|
+
const rows = await this.db.all(`SELECT DISTINCT assetId FROM conversation_assets
|
|
124
|
+
WHERE messageId = ?
|
|
125
|
+
ORDER BY assetId ASC`, [messageId]);
|
|
126
|
+
return rows.map((row) => row.assetId);
|
|
127
|
+
}
|
|
128
|
+
/** 一段对话引用过哪些素材 id(删对话前先读一次:删完之后关系行就没了) */
|
|
129
|
+
async listAssetIdsByConversation(conversationId) {
|
|
130
|
+
const rows = await this.db.all(`SELECT DISTINCT assetId FROM conversation_assets
|
|
131
|
+
WHERE conversationId = ?
|
|
132
|
+
ORDER BY assetId ASC`, [conversationId]);
|
|
133
|
+
return rows.map((row) => row.assetId);
|
|
134
|
+
}
|
|
135
|
+
async delete(id) {
|
|
136
|
+
const result = await this.db.run("DELETE FROM assets WHERE id = ?", [id]);
|
|
137
|
+
return result.changes;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 归零才删:关系行还在时这条语句删不掉(`NOT EXISTS` 与 `ON DELETE RESTRICT` 是同一件事的
|
|
141
|
+
* 两道写法,前者让调用方拿到 0 行、后者保证真要删也不会带走别人的引用)。
|
|
142
|
+
*/
|
|
143
|
+
async deleteIfUnreferenced(id) {
|
|
144
|
+
const result = await this.db.run(`DELETE FROM assets
|
|
145
|
+
WHERE id = ?
|
|
146
|
+
AND NOT EXISTS (SELECT 1 FROM conversation_assets WHERE assetId = ?)`, [id, id]);
|
|
147
|
+
return result.changes;
|
|
148
|
+
}
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
// 厂商文件 id 的缓存(asset_vendor_files)
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
/** 缓存命中:这个素材在这个端点上已经有厂商的 file_id */
|
|
153
|
+
async getVendorFile(assetId, endpointId) {
|
|
154
|
+
return this.db.get(`SELECT ${VENDOR_COLUMNS} FROM asset_vendor_files WHERE assetId = ? AND endpointId = ?`, [assetId, endpointId]);
|
|
155
|
+
}
|
|
156
|
+
/** 写入(或覆盖)一条缓存行:同一对 `(assetId, endpointId)` 只有一行 */
|
|
157
|
+
async upsertVendorFile(input) {
|
|
158
|
+
await this.db.run(`INSERT INTO asset_vendor_files (${VENDOR_COLUMNS}) VALUES (?, ?, ?, ?)
|
|
159
|
+
ON CONFLICT (assetId, endpointId) DO UPDATE SET
|
|
160
|
+
vendorFileId = excluded.vendorFileId`, [input.assetId, input.endpointId, input.vendorFileId, input.createdAt]);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 这个素材的缓存行分布:这个端点上有没有、别的端点上有没有。
|
|
164
|
+
*
|
|
165
|
+
* 两个事实一次查完,是因为它们要一起看才说得清该走哪条路:这个端点上有就是命中复用,
|
|
166
|
+
* 只有别的端点上有就是换过端点(不重传、走内联),都没有才上传。
|
|
167
|
+
*/
|
|
168
|
+
async describeVendorFiles(assetId, endpointId) {
|
|
169
|
+
const row = await this.db.get(`SELECT
|
|
170
|
+
EXISTS (SELECT 1 FROM asset_vendor_files WHERE assetId = ? AND endpointId = ?) AS onEndpoint,
|
|
171
|
+
EXISTS (SELECT 1 FROM asset_vendor_files WHERE assetId = ? AND endpointId <> ?) AS elsewhere`, [assetId, endpointId, assetId, endpointId]);
|
|
172
|
+
return { onEndpoint: row?.onEndpoint === 1, elsewhere: row?.elsewhere === 1 };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** 给某个表别名拼一份带前缀的列名(join 时不能直接复用裸列名) */
|
|
176
|
+
function columnList(alias) {
|
|
177
|
+
return COLUMNS.split(",")
|
|
178
|
+
.map((column) => `${alias}.${column.trim()}`)
|
|
179
|
+
.join(", ");
|
|
180
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `conversations` 表仓储:只做 SQL,不含业务判断。
|
|
3
|
+
*
|
|
4
|
+
* 排序口径照 PRD D13:置顶优先,其次创建时间倒序。改名、活动、打开都**不**改变顺序,
|
|
5
|
+
* 所以 `orderBy` 里没有 `updatedAt`。
|
|
6
|
+
*/
|
|
7
|
+
import type { Database } from "./Database.js";
|
|
8
|
+
import type { Conversation, ConversationCreateInput } from "./types.js";
|
|
9
|
+
export declare class ConversationRepository {
|
|
10
|
+
private readonly db;
|
|
11
|
+
constructor(db: Database);
|
|
12
|
+
/**
|
|
13
|
+
* 新建对话:名字是创建时按时间起的原名,所以 `nameIsAuto` 落 1(R35)。
|
|
14
|
+
* 用户改名与自动命名各有自己的方法,不从这条路径走。
|
|
15
|
+
*/
|
|
16
|
+
create(input: ConversationCreateInput): Promise<void>;
|
|
17
|
+
get(id: string): Promise<Conversation | undefined>;
|
|
18
|
+
listByWorkspace(workspaceId: string): Promise<Conversation[]>;
|
|
19
|
+
/** 全部对话(跨 Workspace),排序口径同上 */
|
|
20
|
+
list(): Promise<Conversation[]>;
|
|
21
|
+
/**
|
|
22
|
+
* 用户改名(PRD F-C3):名字归用户,`nameIsAuto` 置 0(R35)。
|
|
23
|
+
* 身份与顺序都不受影响(PRD D1 / D13),只记一次修改时间。
|
|
24
|
+
*/
|
|
25
|
+
renameByUser(id: string, name: string, updatedAt: number): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* 自动命名(PRD D2):写入起好的名字,并把它标成自动起的那一个(`nameIsAuto` = 1)。
|
|
28
|
+
*
|
|
29
|
+
* 与用户改名分开成两个方法:两者的差别只在 `nameIsAuto` 这一列,靠调用方选方法来表达
|
|
30
|
+
* 语义,读侧不必再猜名字是谁改的。
|
|
31
|
+
*/
|
|
32
|
+
applyAutoName(id: string, name: string, updatedAt: number): Promise<number>;
|
|
33
|
+
setPinned(id: string, pinned: boolean, updatedAt: number): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* 换这段会话的模型,并把思考档位清空。
|
|
36
|
+
*
|
|
37
|
+
* 两句合成一条语句是故意的:档位属于模型(同一个档位名在不同模型的策略表里对应不同
|
|
38
|
+
* 参数),只改 `modelId` 不改档位会留下一个在新模型上说得通、参数却属于旧模型的档位。
|
|
39
|
+
*/
|
|
40
|
+
setModel(id: string, modelId: string, updatedAt: number): Promise<number>;
|
|
41
|
+
/** 写这段会话的思考档位;`null` 表示回到「跟随助手定义」 */
|
|
42
|
+
setThinkingLevel(id: string, thinkingLevel: string | null, updatedAt: number): Promise<number>;
|
|
43
|
+
/** 只推进修改时间(新增消息等结构变化后调用) */
|
|
44
|
+
touch(id: string, updatedAt: number): Promise<number>;
|
|
45
|
+
/** 硬删对话(PRD D17),消息与素材行随之级联删除 */
|
|
46
|
+
delete(id: string): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* 按名字搜索(PRD D16 的「按名称」)。
|
|
49
|
+
*
|
|
50
|
+
* 名字搜索不走 FTS:名字短、还要支持只有一两个字的查询,`LIKE` 的子串语义正好对得上,
|
|
51
|
+
* 也省掉维护第二张 FTS 表。
|
|
52
|
+
*/
|
|
53
|
+
searchByName(keyword: string, workspaceId: string | undefined, limit: number): Promise<Conversation[]>;
|
|
54
|
+
countByWorkspace(workspaceId: string): Promise<number>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `conversations` 表仓储:只做 SQL,不含业务判断。
|
|
3
|
+
*
|
|
4
|
+
* 排序口径照 PRD D13:置顶优先,其次创建时间倒序。改名、活动、打开都**不**改变顺序,
|
|
5
|
+
* 所以 `orderBy` 里没有 `updatedAt`。
|
|
6
|
+
*/
|
|
7
|
+
/** 读写路径共用的列清单(顺序与 `toConversation` 的形状一致) */
|
|
8
|
+
const COLUMNS = "id, workspaceId, name, nameIsAuto, pinned, agentId, modelId, thinkingLevel, createdAt, updatedAt";
|
|
9
|
+
/** 列表/搜索统一用这个顺序:置顶在前,再按创建时间倒序(PRD D13) */
|
|
10
|
+
const ORDER_BY = "ORDER BY pinned DESC, createdAt DESC, id DESC";
|
|
11
|
+
function toConversation(row) {
|
|
12
|
+
return { ...row, pinned: row.pinned !== 0, nameIsAuto: row.nameIsAuto !== 0 };
|
|
13
|
+
}
|
|
14
|
+
/** LIKE 的通配符要转义,否则用户搜 "100%" 会变成任意匹配 */
|
|
15
|
+
function escapeLike(keyword) {
|
|
16
|
+
return keyword.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
17
|
+
}
|
|
18
|
+
export class ConversationRepository {
|
|
19
|
+
db;
|
|
20
|
+
constructor(db) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 新建对话:名字是创建时按时间起的原名,所以 `nameIsAuto` 落 1(R35)。
|
|
25
|
+
* 用户改名与自动命名各有自己的方法,不从这条路径走。
|
|
26
|
+
*/
|
|
27
|
+
async create(input) {
|
|
28
|
+
const updatedAt = input.updatedAt ?? input.createdAt;
|
|
29
|
+
await this.db.run(`INSERT INTO conversations (${COLUMNS}) VALUES (?, ?, ?, 1, 0, ?, ?, ?, ?, ?)`, [
|
|
30
|
+
input.id,
|
|
31
|
+
input.workspaceId,
|
|
32
|
+
input.name,
|
|
33
|
+
input.agentId,
|
|
34
|
+
input.modelId,
|
|
35
|
+
input.thinkingLevel ?? null,
|
|
36
|
+
input.createdAt,
|
|
37
|
+
updatedAt,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
async get(id) {
|
|
41
|
+
const row = await this.db.get(`SELECT ${COLUMNS} FROM conversations WHERE id = ?`, [id]);
|
|
42
|
+
return row ? toConversation(row) : undefined;
|
|
43
|
+
}
|
|
44
|
+
async listByWorkspace(workspaceId) {
|
|
45
|
+
const rows = await this.db.all(`SELECT ${COLUMNS} FROM conversations WHERE workspaceId = ? ${ORDER_BY}`, [workspaceId]);
|
|
46
|
+
return rows.map(toConversation);
|
|
47
|
+
}
|
|
48
|
+
/** 全部对话(跨 Workspace),排序口径同上 */
|
|
49
|
+
async list() {
|
|
50
|
+
const rows = await this.db.all(`SELECT ${COLUMNS} FROM conversations ${ORDER_BY}`);
|
|
51
|
+
return rows.map(toConversation);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 用户改名(PRD F-C3):名字归用户,`nameIsAuto` 置 0(R35)。
|
|
55
|
+
* 身份与顺序都不受影响(PRD D1 / D13),只记一次修改时间。
|
|
56
|
+
*/
|
|
57
|
+
async renameByUser(id, name, updatedAt) {
|
|
58
|
+
const result = await this.db.run("UPDATE conversations SET name = ?, nameIsAuto = 0, updatedAt = ? WHERE id = ?", [name, updatedAt, id]);
|
|
59
|
+
return result.changes;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 自动命名(PRD D2):写入起好的名字,并把它标成自动起的那一个(`nameIsAuto` = 1)。
|
|
63
|
+
*
|
|
64
|
+
* 与用户改名分开成两个方法:两者的差别只在 `nameIsAuto` 这一列,靠调用方选方法来表达
|
|
65
|
+
* 语义,读侧不必再猜名字是谁改的。
|
|
66
|
+
*/
|
|
67
|
+
async applyAutoName(id, name, updatedAt) {
|
|
68
|
+
const result = await this.db.run("UPDATE conversations SET name = ?, nameIsAuto = 1, updatedAt = ? WHERE id = ?", [name, updatedAt, id]);
|
|
69
|
+
return result.changes;
|
|
70
|
+
}
|
|
71
|
+
async setPinned(id, pinned, updatedAt) {
|
|
72
|
+
const result = await this.db.run("UPDATE conversations SET pinned = ?, updatedAt = ? WHERE id = ?", [pinned ? 1 : 0, updatedAt, id]);
|
|
73
|
+
return result.changes;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 换这段会话的模型,并把思考档位清空。
|
|
77
|
+
*
|
|
78
|
+
* 两句合成一条语句是故意的:档位属于模型(同一个档位名在不同模型的策略表里对应不同
|
|
79
|
+
* 参数),只改 `modelId` 不改档位会留下一个在新模型上说得通、参数却属于旧模型的档位。
|
|
80
|
+
*/
|
|
81
|
+
async setModel(id, modelId, updatedAt) {
|
|
82
|
+
const result = await this.db.run("UPDATE conversations SET modelId = ?, thinkingLevel = NULL, updatedAt = ? WHERE id = ?", [modelId, updatedAt, id]);
|
|
83
|
+
return result.changes;
|
|
84
|
+
}
|
|
85
|
+
/** 写这段会话的思考档位;`null` 表示回到「跟随助手定义」 */
|
|
86
|
+
async setThinkingLevel(id, thinkingLevel, updatedAt) {
|
|
87
|
+
const result = await this.db.run("UPDATE conversations SET thinkingLevel = ?, updatedAt = ? WHERE id = ?", [thinkingLevel, updatedAt, id]);
|
|
88
|
+
return result.changes;
|
|
89
|
+
}
|
|
90
|
+
/** 只推进修改时间(新增消息等结构变化后调用) */
|
|
91
|
+
async touch(id, updatedAt) {
|
|
92
|
+
const result = await this.db.run("UPDATE conversations SET updatedAt = ? WHERE id = ?", [
|
|
93
|
+
updatedAt,
|
|
94
|
+
id,
|
|
95
|
+
]);
|
|
96
|
+
return result.changes;
|
|
97
|
+
}
|
|
98
|
+
/** 硬删对话(PRD D17),消息与素材行随之级联删除 */
|
|
99
|
+
async delete(id) {
|
|
100
|
+
const result = await this.db.run("DELETE FROM conversations WHERE id = ?", [id]);
|
|
101
|
+
return result.changes;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 按名字搜索(PRD D16 的「按名称」)。
|
|
105
|
+
*
|
|
106
|
+
* 名字搜索不走 FTS:名字短、还要支持只有一两个字的查询,`LIKE` 的子串语义正好对得上,
|
|
107
|
+
* 也省掉维护第二张 FTS 表。
|
|
108
|
+
*/
|
|
109
|
+
async searchByName(keyword, workspaceId, limit) {
|
|
110
|
+
const pattern = `%${escapeLike(keyword)}%`;
|
|
111
|
+
if (workspaceId === undefined) {
|
|
112
|
+
const rows = await this.db.all(`SELECT ${COLUMNS} FROM conversations
|
|
113
|
+
WHERE name LIKE ? ESCAPE '\\'
|
|
114
|
+
${ORDER_BY}
|
|
115
|
+
LIMIT ?`, [pattern, limit]);
|
|
116
|
+
return rows.map(toConversation);
|
|
117
|
+
}
|
|
118
|
+
const rows = await this.db.all(`SELECT ${COLUMNS} FROM conversations
|
|
119
|
+
WHERE workspaceId = ? AND name LIKE ? ESCAPE '\\'
|
|
120
|
+
${ORDER_BY}
|
|
121
|
+
LIMIT ?`, [workspaceId, pattern, limit]);
|
|
122
|
+
return rows.map(toConversation);
|
|
123
|
+
}
|
|
124
|
+
async countByWorkspace(workspaceId) {
|
|
125
|
+
const row = await this.db.get("SELECT count(*) AS n FROM conversations WHERE workspaceId = ?", [workspaceId]);
|
|
126
|
+
return row?.n ?? 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `credentials` 表仓储:凭证的多串管理(PRD L11:可多串、带备注、可单独吊销)。
|
|
3
|
+
*
|
|
4
|
+
* 只存 sha256 摘要:明文只存在于生成它的那一层(创建时返回一次给用户),
|
|
5
|
+
* 存储层既不生成明文、也不还原明文。
|
|
6
|
+
*/
|
|
7
|
+
import type { Database } from "./Database.js";
|
|
8
|
+
import type { CredentialCreateInput, CredentialInfo, CredentialRow } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* 凭证摘要:sha256 十六进制。
|
|
11
|
+
*
|
|
12
|
+
* 上下游都按同一个口径算,所以这函数放在存储层旁边 —— 校验方(接线的鉴权)与写入方
|
|
13
|
+
* 用同一份实现,不会出现一边加盐一边不加这种对不上的情况。
|
|
14
|
+
*/
|
|
15
|
+
export declare function hashCredential(token: string): string;
|
|
16
|
+
export declare class CredentialRepository {
|
|
17
|
+
private readonly db;
|
|
18
|
+
constructor(db: Database);
|
|
19
|
+
create(input: CredentialCreateInput): Promise<void>;
|
|
20
|
+
get(id: string): Promise<CredentialRow | undefined>;
|
|
21
|
+
/** 列出全部凭证(含已吊销的),不含 hash —— 展示层不需要它 */
|
|
22
|
+
list(): Promise<CredentialInfo[]>;
|
|
23
|
+
/** 按摘要找一串仍然有效的凭证(鉴权用) */
|
|
24
|
+
findActiveByHash(hash: string): Promise<CredentialRow | undefined>;
|
|
25
|
+
/** 吊销(不删行:留着能看出「这串曾经存在过」) */
|
|
26
|
+
revoke(id: string, revokedAt: number): Promise<number>;
|
|
27
|
+
countActive(): Promise<number>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `credentials` 表仓储:凭证的多串管理(PRD L11:可多串、带备注、可单独吊销)。
|
|
3
|
+
*
|
|
4
|
+
* 只存 sha256 摘要:明文只存在于生成它的那一层(创建时返回一次给用户),
|
|
5
|
+
* 存储层既不生成明文、也不还原明文。
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
const COLUMNS = "id, note, hash, createdAt, revokedAt";
|
|
9
|
+
const INFO_COLUMNS = "id, note, createdAt, revokedAt";
|
|
10
|
+
/**
|
|
11
|
+
* 凭证摘要:sha256 十六进制。
|
|
12
|
+
*
|
|
13
|
+
* 上下游都按同一个口径算,所以这函数放在存储层旁边 —— 校验方(接线的鉴权)与写入方
|
|
14
|
+
* 用同一份实现,不会出现一边加盐一边不加这种对不上的情况。
|
|
15
|
+
*/
|
|
16
|
+
export function hashCredential(token) {
|
|
17
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
18
|
+
}
|
|
19
|
+
export class CredentialRepository {
|
|
20
|
+
db;
|
|
21
|
+
constructor(db) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
}
|
|
24
|
+
async create(input) {
|
|
25
|
+
await this.db.run(`INSERT INTO credentials (${COLUMNS}) VALUES (?, ?, ?, ?, NULL)`, [input.id, input.note, input.hash, input.createdAt]);
|
|
26
|
+
}
|
|
27
|
+
async get(id) {
|
|
28
|
+
return this.db.get(`SELECT ${COLUMNS} FROM credentials WHERE id = ?`, [id]);
|
|
29
|
+
}
|
|
30
|
+
/** 列出全部凭证(含已吊销的),不含 hash —— 展示层不需要它 */
|
|
31
|
+
async list() {
|
|
32
|
+
return this.db.all(`SELECT ${INFO_COLUMNS} FROM credentials ORDER BY createdAt ASC, id ASC`);
|
|
33
|
+
}
|
|
34
|
+
/** 按摘要找一串仍然有效的凭证(鉴权用) */
|
|
35
|
+
async findActiveByHash(hash) {
|
|
36
|
+
return this.db.get(`SELECT ${COLUMNS} FROM credentials WHERE hash = ? AND revokedAt IS NULL`, [hash]);
|
|
37
|
+
}
|
|
38
|
+
/** 吊销(不删行:留着能看出「这串曾经存在过」) */
|
|
39
|
+
async revoke(id, revokedAt) {
|
|
40
|
+
const result = await this.db.run("UPDATE credentials SET revokedAt = ? WHERE id = ? AND revokedAt IS NULL", [revokedAt, id]);
|
|
41
|
+
return result.changes;
|
|
42
|
+
}
|
|
43
|
+
async countActive() {
|
|
44
|
+
const row = await this.db.get("SELECT count(*) AS n FROM credentials WHERE revokedAt IS NULL");
|
|
45
|
+
return row?.n ?? 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库门面:对外只有 `all` / `get` / `run` / `tx` / `close` 五个异步方法,内部把每条
|
|
3
|
+
* 请求变成一条 worker 消息,交给 `dbWorker.ts` 在独立线程里用同步的 `DatabaseSync` 执行。
|
|
4
|
+
*
|
|
5
|
+
* 主线程因此完全不碰 SQL 执行:不会阻塞事件循环,也不会有「一个慢查询卡住所有 WebSocket
|
|
6
|
+
* 连接」这种事。
|
|
7
|
+
*
|
|
8
|
+
* 实例由创建者持有并注入给各 Repository(依赖从单根出发),本模块不导出任何模块级单例。
|
|
9
|
+
*/
|
|
10
|
+
import type { SQLInputValue } from "node:sqlite";
|
|
11
|
+
/** 写语句的返回(worker 侧已把 bigint 转成 number,可安全跨线程传输) */
|
|
12
|
+
export interface RunResult {
|
|
13
|
+
changes: number;
|
|
14
|
+
lastInsertRowid: number;
|
|
15
|
+
}
|
|
16
|
+
/** 事务里一条语句的执行方式:整表读、单行读、写 */
|
|
17
|
+
export type TxKind = "all" | "get" | "run";
|
|
18
|
+
/**
|
|
19
|
+
* 在 `tx` 回调里登记一条语句后拿到的凭据:批次跑完后靠它把结果从 `tx` 的返回值里取出来。
|
|
20
|
+
*
|
|
21
|
+
* `index` 是这条语句在批次里的位置。`output` 只在类型上标记「这条语句会产出什么形状」,
|
|
22
|
+
* 运行时是 `undefined`,不参与跨线程传输。
|
|
23
|
+
*/
|
|
24
|
+
export interface TxStatement<T> {
|
|
25
|
+
readonly index: number;
|
|
26
|
+
readonly output?: T;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `tx` 回调拿到的客户端:`all` / `get` / `run` 都只是**登记**语句,按登记顺序排进同一批。
|
|
30
|
+
*
|
|
31
|
+
* 批次在回调返回后一次性送进 worker,在同一个事务里串行执行;读到的值由回调返回哪几条
|
|
32
|
+
* 凭据决定(返回凭据数组 → 拿到等长的结果,返回单条凭据 → 拿到单条结果,什么都不返回 →
|
|
33
|
+
* 只是一批写)。回调自己抛错时批次还没发出去,库里不会有任何改动。
|
|
34
|
+
*/
|
|
35
|
+
export interface TxClient {
|
|
36
|
+
all<T>(sql: string, params?: SQLInputValue[]): TxStatement<T[]>;
|
|
37
|
+
get<T>(sql: string, params?: SQLInputValue[]): TxStatement<T | undefined>;
|
|
38
|
+
run(sql: string, params?: SQLInputValue[]): TxStatement<RunResult>;
|
|
39
|
+
}
|
|
40
|
+
/** 回调能返回的东西:不关心结果、一条语句的凭据、或一组凭据 */
|
|
41
|
+
export type TxPlan = TxStatement<unknown> | readonly TxStatement<unknown>[] | void;
|
|
42
|
+
/** 单条凭据对应结果的类型 */
|
|
43
|
+
export type TxValue<S> = S extends TxStatement<infer R> ? R : never;
|
|
44
|
+
/**
|
|
45
|
+
* `tx` 的返回值:回调返回一组凭据时是等长的结果元组,返回单条凭据时是那条的结果,
|
|
46
|
+
* 什么都不返回时是 `void`。
|
|
47
|
+
*/
|
|
48
|
+
export type TxOutcome<S extends TxPlan> = S extends readonly TxStatement<unknown>[] ? {
|
|
49
|
+
[K in keyof S]: TxValue<S[K]>;
|
|
50
|
+
} : S extends TxStatement<unknown> ? TxValue<S> : void;
|
|
51
|
+
export declare class Database {
|
|
52
|
+
private readonly dbPath;
|
|
53
|
+
private worker;
|
|
54
|
+
private nextId;
|
|
55
|
+
private readonly pending;
|
|
56
|
+
private closed;
|
|
57
|
+
private closing;
|
|
58
|
+
/** @param dbPath SQLite 数据库文件路径 */
|
|
59
|
+
constructor(dbPath: string);
|
|
60
|
+
/** 多行查询 */
|
|
61
|
+
all<T>(sql: string, params?: SQLInputValue[]): Promise<T[]>;
|
|
62
|
+
/** 单行查询;没有命中的行时返回 `undefined` */
|
|
63
|
+
get<T>(sql: string, params?: SQLInputValue[]): Promise<T | undefined>;
|
|
64
|
+
/** 写语句(INSERT / UPDATE / DELETE) */
|
|
65
|
+
run(sql: string, params?: SQLInputValue[]): Promise<RunResult>;
|
|
66
|
+
/**
|
|
67
|
+
* 事务:回调里用 `client.all` / `get` / `run` 登记语句,回调返回后整批在 worker 里
|
|
68
|
+
* `BEGIN IMMEDIATE` → 按登记顺序逐条执行 → `COMMIT`,任一条失败就整体 ROLLBACK。
|
|
69
|
+
*
|
|
70
|
+
* 回调返回的凭据决定拿回哪些结果,所以「先读后写」是一次往返、一个事务:
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* const [found, written] = await db.tx((q) => [
|
|
74
|
+
* q.get<{ n: number }>("SELECT n FROM t WHERE id = ?", [id]),
|
|
75
|
+
* q.run("UPDATE t SET n = n + 1 WHERE id = ?", [id]),
|
|
76
|
+
* ]);
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* 回调抛错时还什么都没发出去,等于什么都没写,直接向上抛。
|
|
80
|
+
*/
|
|
81
|
+
tx<const S extends TxPlan>(fn: (client: TxClient) => S | Promise<S>): Promise<TxOutcome<S>>;
|
|
82
|
+
/**
|
|
83
|
+
* 关闭:先拒绝所有挂起的请求、把实例标记为已关闭(之后任何调用都直接 reject),
|
|
84
|
+
* 再请 worker 关库退出;worker 没在超时内退出就强制终止,避免 `close()` 自己悬挂。
|
|
85
|
+
*/
|
|
86
|
+
close(): Promise<void>;
|
|
87
|
+
private doClose;
|
|
88
|
+
private assertOpen;
|
|
89
|
+
/** 统一执行通道:分配请求 id、挂起 Promise、把消息交给 worker */
|
|
90
|
+
private dispatch;
|
|
91
|
+
/** 懒启动:第一次真正要碰数据库时才起 worker */
|
|
92
|
+
private workerHandle;
|
|
93
|
+
private settle;
|
|
94
|
+
private failAll;
|
|
95
|
+
}
|