@dsh-libing/knowledge-base 0.1.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 +16 -0
- package/README.md +80 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/lib/chunks.d.ts +8 -0
- package/lib/chunks.js +197 -0
- package/lib/client-types/client/BrowsePanel.d.ts +13 -0
- package/lib/client-types/client/Icon.d.ts +4 -0
- package/lib/client-types/client/ImageReview.d.ts +20 -0
- package/lib/client-types/client/ImportPanel.d.ts +19 -0
- package/lib/client-types/client/JobsPanel.d.ts +6 -0
- package/lib/client-types/client/KnowledgePage.d.ts +7 -0
- package/lib/client-types/client/Metadata.d.ts +10 -0
- package/lib/client-types/client/QualityReport.d.ts +6 -0
- package/lib/client-types/client/SettingsPanel.d.ts +6 -0
- package/lib/client-types/client/SourceText.d.ts +3 -0
- package/lib/client-types/client/UsageGuide.d.ts +8 -0
- package/lib/client-types/client/api.d.ts +953 -0
- package/lib/client-types/client/common.d.ts +11 -0
- package/lib/client-types/client/index.d.ts +14 -0
- package/lib/client-types/client/locales.d.ts +284 -0
- package/lib/client-types/client/searchCommand.d.ts +2 -0
- package/lib/client-types/client/useKnowledge.d.ts +251 -0
- package/lib/client-types/contract.d.ts +1197 -0
- package/lib/client-types/data.d.ts +1972 -0
- package/lib/client-types/settings.d.ts +33 -0
- package/lib/client-types/shared.d.ts +95 -0
- package/lib/client.js +305 -0
- package/lib/config.d.ts +13 -0
- package/lib/config.js +13 -0
- package/lib/contract.d.ts +1197 -0
- package/lib/contract.js +18 -0
- package/lib/data.d.ts +1972 -0
- package/lib/data.js +80 -0
- package/lib/document/convert.d.ts +5 -0
- package/lib/document/convert.js +325 -0
- package/lib/document/pdf-data.d.ts +99 -0
- package/lib/document/pdf-data.js +9 -0
- package/lib/document/pdf-layout.d.ts +24 -0
- package/lib/document/pdf-layout.js +234 -0
- package/lib/document/pdf-session.d.ts +39 -0
- package/lib/document/pdf-session.js +127 -0
- package/lib/document/pdf-vision.d.ts +26 -0
- package/lib/document/pdf-vision.js +115 -0
- package/lib/document/pdf-worker.d.ts +1 -0
- package/lib/document/pdf-worker.js +210 -0
- package/lib/document/pdf.d.ts +3 -0
- package/lib/document/pdf.js +218 -0
- package/lib/document/zip.d.ts +3 -0
- package/lib/document/zip.js +59 -0
- package/lib/figures/process.d.ts +187 -0
- package/lib/figures/process.js +159 -0
- package/lib/figures/prompts.d.ts +7 -0
- package/lib/figures/prompts.js +27 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +42 -0
- package/lib/indexes.d.ts +38 -0
- package/lib/indexes.js +101 -0
- package/lib/jobs.d.ts +2806 -0
- package/lib/jobs.js +435 -0
- package/lib/models.d.ts +24 -0
- package/lib/models.js +120 -0
- package/lib/retrieval.d.ts +209 -0
- package/lib/retrieval.js +173 -0
- package/lib/routes.d.ts +3 -0
- package/lib/routes.js +152 -0
- package/lib/runtime.d.ts +48 -0
- package/lib/runtime.js +41 -0
- package/lib/settings.d.ts +33 -0
- package/lib/settings.js +19 -0
- package/lib/shared.d.ts +95 -0
- package/lib/shared.js +17 -0
- package/lib/skills.d.ts +3 -0
- package/lib/skills.js +21 -0
- package/lib/store.d.ts +409 -0
- package/lib/store.js +328 -0
- package/lib/tools.d.ts +3 -0
- package/lib/tools.js +72 -0
- package/lib/util.d.ts +25 -0
- package/lib/util.js +89 -0
- package/package.json +125 -0
- package/skills/libing-kb-build/SKILL.md +10 -0
- package/skills/libing-kb-search/SKILL.md +10 -0
- package/skills/libing-knowledge-base/SKILL.md +15 -0
- package/skills/libing-knowledge-base/references/build.md +23 -0
- package/skills/libing-knowledge-base/references/search.md +21 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Settings } from './config.ts';
|
|
2
|
+
import type { Store } from './store.ts';
|
|
3
|
+
import type { PublishedDocument, DocInfo, Chunk, Snapshot } from './data.ts';
|
|
4
|
+
import type { CatalogArgs, SearchArgs, ReadArgs } from './shared.ts';
|
|
5
|
+
export type WindowOptions = {
|
|
6
|
+
segmentIndex?: number | undefined;
|
|
7
|
+
offset?: number | undefined;
|
|
8
|
+
windowBudget?: number | undefined;
|
|
9
|
+
};
|
|
10
|
+
export interface ReadWindow {
|
|
11
|
+
segmentIndex: number;
|
|
12
|
+
startLine: number | undefined;
|
|
13
|
+
endLine: number | undefined;
|
|
14
|
+
charOffset?: number;
|
|
15
|
+
partial?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface WindowRead {
|
|
18
|
+
text: string;
|
|
19
|
+
startLine?: number;
|
|
20
|
+
endLine?: number;
|
|
21
|
+
complete: boolean;
|
|
22
|
+
continuation: {
|
|
23
|
+
segmentIndex: number;
|
|
24
|
+
offset: number;
|
|
25
|
+
} | null;
|
|
26
|
+
windows?: ReadWindow[];
|
|
27
|
+
truncated?: boolean;
|
|
28
|
+
totalSegments?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function queryTerms(query: string, keywords?: string[]): string[];
|
|
31
|
+
export declare function pdfScope(doc: Pick<DocInfo, 'sourceFormat' | 'selectedPages' | 'pageCount'>): string;
|
|
32
|
+
declare function source(doc: PublishedDocument, chunk: Chunk, revision: string, read?: WindowRead): {
|
|
33
|
+
docId: string;
|
|
34
|
+
title: string;
|
|
35
|
+
sourceVersion: string;
|
|
36
|
+
sourceHash: string;
|
|
37
|
+
revision: string;
|
|
38
|
+
headingPath: string[];
|
|
39
|
+
path: string;
|
|
40
|
+
contentHash: string;
|
|
41
|
+
locations?: import("./data.ts").Source[];
|
|
42
|
+
locationsTruncated?: boolean;
|
|
43
|
+
sourceFormat?: string;
|
|
44
|
+
pageCount?: number | undefined;
|
|
45
|
+
selectedPages?: {
|
|
46
|
+
start: number;
|
|
47
|
+
end: number;
|
|
48
|
+
count: number;
|
|
49
|
+
total: number;
|
|
50
|
+
partial: boolean;
|
|
51
|
+
} | undefined;
|
|
52
|
+
scope?: string;
|
|
53
|
+
pageRange?: number[] | undefined;
|
|
54
|
+
};
|
|
55
|
+
export declare class Retrieval {
|
|
56
|
+
readonly store: Store;
|
|
57
|
+
readonly config: Settings;
|
|
58
|
+
constructor(store: Store, config: Settings);
|
|
59
|
+
catalog(args?: CatalogArgs): Promise<{
|
|
60
|
+
knowledgeBases: ({
|
|
61
|
+
kbId: string;
|
|
62
|
+
name: string;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
deletedAt?: string | undefined;
|
|
65
|
+
purge?: {
|
|
66
|
+
startedAt: string;
|
|
67
|
+
jobIds: string[];
|
|
68
|
+
error?: string | undefined;
|
|
69
|
+
} | undefined;
|
|
70
|
+
} & {
|
|
71
|
+
revision: string | null;
|
|
72
|
+
coverageIncomplete: boolean;
|
|
73
|
+
})[];
|
|
74
|
+
truncated: boolean;
|
|
75
|
+
} | {
|
|
76
|
+
kbId: string;
|
|
77
|
+
revision: string;
|
|
78
|
+
catalogId: string;
|
|
79
|
+
page: number;
|
|
80
|
+
path: string;
|
|
81
|
+
content: string;
|
|
82
|
+
nextPage: number | null;
|
|
83
|
+
}>;
|
|
84
|
+
search(args: SearchArgs): Promise<{
|
|
85
|
+
kbId: string;
|
|
86
|
+
revision: string;
|
|
87
|
+
query: string;
|
|
88
|
+
terms: string[];
|
|
89
|
+
candidates: Candidate[];
|
|
90
|
+
evidence: Evidence[];
|
|
91
|
+
warnings: SearchWarning[];
|
|
92
|
+
truncated: boolean;
|
|
93
|
+
totalCandidates: number;
|
|
94
|
+
continuation: {
|
|
95
|
+
chunkId: string;
|
|
96
|
+
}[];
|
|
97
|
+
}>;
|
|
98
|
+
read(args: ReadArgs): Promise<{
|
|
99
|
+
assetRefs: string[];
|
|
100
|
+
relatedChunks: string[];
|
|
101
|
+
warnings: string[];
|
|
102
|
+
text: string;
|
|
103
|
+
startLine?: number;
|
|
104
|
+
endLine?: number;
|
|
105
|
+
complete: boolean;
|
|
106
|
+
continuation: {
|
|
107
|
+
segmentIndex: number;
|
|
108
|
+
offset: number;
|
|
109
|
+
} | null;
|
|
110
|
+
windows?: ReadWindow[];
|
|
111
|
+
truncated?: boolean;
|
|
112
|
+
totalSegments?: number;
|
|
113
|
+
kbId: string;
|
|
114
|
+
chunkId: string;
|
|
115
|
+
revision: string;
|
|
116
|
+
source: {
|
|
117
|
+
docId: string;
|
|
118
|
+
title: string;
|
|
119
|
+
sourceVersion: string;
|
|
120
|
+
sourceHash: string;
|
|
121
|
+
revision: string;
|
|
122
|
+
headingPath: string[];
|
|
123
|
+
path: string;
|
|
124
|
+
contentHash: string;
|
|
125
|
+
locations?: import("./data.ts").Source[];
|
|
126
|
+
locationsTruncated?: boolean;
|
|
127
|
+
sourceFormat?: string;
|
|
128
|
+
pageCount?: number | undefined;
|
|
129
|
+
selectedPages?: {
|
|
130
|
+
start: number;
|
|
131
|
+
end: number;
|
|
132
|
+
count: number;
|
|
133
|
+
total: number;
|
|
134
|
+
partial: boolean;
|
|
135
|
+
} | undefined;
|
|
136
|
+
scope?: string;
|
|
137
|
+
pageRange?: number[] | undefined;
|
|
138
|
+
};
|
|
139
|
+
}>;
|
|
140
|
+
readSnapshot(kbId: string, snapshot: Snapshot & {
|
|
141
|
+
chunk: Chunk;
|
|
142
|
+
content: string;
|
|
143
|
+
}, args: ReadArgs): {
|
|
144
|
+
assetRefs: string[];
|
|
145
|
+
relatedChunks: string[];
|
|
146
|
+
warnings: string[];
|
|
147
|
+
text: string;
|
|
148
|
+
startLine?: number;
|
|
149
|
+
endLine?: number;
|
|
150
|
+
complete: boolean;
|
|
151
|
+
continuation: {
|
|
152
|
+
segmentIndex: number;
|
|
153
|
+
offset: number;
|
|
154
|
+
} | null;
|
|
155
|
+
windows?: ReadWindow[];
|
|
156
|
+
truncated?: boolean;
|
|
157
|
+
totalSegments?: number;
|
|
158
|
+
kbId: string;
|
|
159
|
+
chunkId: string;
|
|
160
|
+
revision: string;
|
|
161
|
+
source: {
|
|
162
|
+
docId: string;
|
|
163
|
+
title: string;
|
|
164
|
+
sourceVersion: string;
|
|
165
|
+
sourceHash: string;
|
|
166
|
+
revision: string;
|
|
167
|
+
headingPath: string[];
|
|
168
|
+
path: string;
|
|
169
|
+
contentHash: string;
|
|
170
|
+
locations?: import("./data.ts").Source[];
|
|
171
|
+
locationsTruncated?: boolean;
|
|
172
|
+
sourceFormat?: string;
|
|
173
|
+
pageCount?: number | undefined;
|
|
174
|
+
selectedPages?: {
|
|
175
|
+
start: number;
|
|
176
|
+
end: number;
|
|
177
|
+
count: number;
|
|
178
|
+
total: number;
|
|
179
|
+
partial: boolean;
|
|
180
|
+
} | undefined;
|
|
181
|
+
scope?: string;
|
|
182
|
+
pageRange?: number[] | undefined;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export type EvidenceSource = ReturnType<typeof source>;
|
|
187
|
+
export interface Candidate {
|
|
188
|
+
chunkId: string;
|
|
189
|
+
score: number;
|
|
190
|
+
matchedTerms: string[];
|
|
191
|
+
title: string;
|
|
192
|
+
summary: string;
|
|
193
|
+
fullChars: number;
|
|
194
|
+
reviewStatus: Chunk['reviewStatus'];
|
|
195
|
+
source: EvidenceSource;
|
|
196
|
+
}
|
|
197
|
+
export interface Evidence extends WindowRead {
|
|
198
|
+
chunkId: string;
|
|
199
|
+
source: EvidenceSource;
|
|
200
|
+
assetRefs: string[];
|
|
201
|
+
relatedChunks: string[];
|
|
202
|
+
}
|
|
203
|
+
export interface SearchWarning {
|
|
204
|
+
docId?: string;
|
|
205
|
+
chunkId?: string;
|
|
206
|
+
kind?: string;
|
|
207
|
+
message: string;
|
|
208
|
+
}
|
|
209
|
+
export {};
|
package/lib/retrieval.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { required } from "./util.js";
|
|
2
|
+
import { chars, clip, id, text } from "./util.js";
|
|
3
|
+
export function queryTerms(query, keywords = []) {
|
|
4
|
+
query = text(query, '检索问题', 2000);
|
|
5
|
+
if (!Array.isArray(keywords) || keywords.length > 32 || keywords.some(x => typeof x !== 'string' || chars(x) > 100))
|
|
6
|
+
throw new Error('keywords 必须是最多 32 个短文本');
|
|
7
|
+
const tokens = query.match(/[A-Za-z][A-Za-z0-9_.+#/-]+|[\p{Script=Han}]+/gu) || [];
|
|
8
|
+
return [...new Set([...keywords, ...tokens, ...tokens.filter(t => /\p{Script=Han}/u.test(t) && t.length > 4).flatMap(t => Array.from({ length: t.length - 1 }, (_, i) => t.slice(i, i + 2)))]
|
|
9
|
+
.map(t => t.trim().toLowerCase()).filter(t => t.length >= 2))].slice(0, 64);
|
|
10
|
+
}
|
|
11
|
+
function bounded(value, max) {
|
|
12
|
+
if (chars(JSON.stringify(value)) > max)
|
|
13
|
+
throw new Error('输出预算不足,请缩小检索范围或提高 evidenceMaxChars');
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function pdfScope(doc) { return doc.sourceFormat === 'pdf' ? `PDF 实际页序 ${doc.selectedPages?.start || 1}–${doc.selectedPages?.end || doc.pageCount} / ${doc.pageCount}${required(doc).selectedPages?.partial ? ';仅覆盖所选页范围' : ''}` : ''; }
|
|
17
|
+
function source(doc, chunk, revision, read) {
|
|
18
|
+
const locations = read?.windows ? read.windows.flatMap(w => chunk.segments[w.segmentIndex]?.sourceLocations || []) : chunk.sourceLocations || [];
|
|
19
|
+
const unique = [...new Map(locations.map(l => [JSON.stringify(l), l])).values()];
|
|
20
|
+
const pages = unique.map(l => l.page);
|
|
21
|
+
return { ...doc.sourceFormat === 'pdf' ? { sourceFormat: 'pdf', pageCount: doc.pageCount, selectedPages: doc.selectedPages, scope: pdfScope(doc), pageRange: pages.length ? [Math.min(...pages), Math.max(...pages)] : chunk.pageRange, ...read ? { locations: unique.slice(0, 32), locationsTruncated: unique.length > 32 } : {} } : {}, docId: doc.docId, title: doc.title, sourceVersion: doc.sourceVersion, sourceHash: doc.sourceHash, revision, headingPath: chunk.headingPath, path: chunk.path, contentHash: chunk.contentHash };
|
|
22
|
+
}
|
|
23
|
+
function windows(chunk, content, terms, config, options = {}) {
|
|
24
|
+
if (chars(content) <= Math.min(config.chunkTargetChars, config.evidenceMaxChars - 3000, options.windowBudget ?? Infinity) && options.segmentIndex === undefined)
|
|
25
|
+
return { text: content, startLine: 1, endLine: content.split('\n').length, complete: true, continuation: null };
|
|
26
|
+
let selected = chunk.segments.map((s, i) => ({ ...s, index: i })).filter(s => terms.some(t => s.text.toLowerCase().includes(t)));
|
|
27
|
+
if (options.segmentIndex !== undefined)
|
|
28
|
+
selected = chunk.segments[options.segmentIndex] ? [{ ...required(chunk.segments[options.segmentIndex]), index: options.segmentIndex }] : [];
|
|
29
|
+
if (!selected.length)
|
|
30
|
+
selected = chunk.segments.length ? [{ ...required(chunk.segments[0]), index: 0 }] : [];
|
|
31
|
+
if (!selected.length)
|
|
32
|
+
return { text: '', complete: true, continuation: null };
|
|
33
|
+
const indices = new Set();
|
|
34
|
+
for (const s of selected) {
|
|
35
|
+
for (let i = options.segmentIndex === undefined ? Math.max(0, s.index - 1) : s.index; i <= Math.min(chunk.segments.length - 1, s.index + 1); i++)
|
|
36
|
+
indices.add(i);
|
|
37
|
+
}
|
|
38
|
+
const ordered = [...indices].sort((a, b) => a - b);
|
|
39
|
+
const read = [];
|
|
40
|
+
let body = '', continuation = null;
|
|
41
|
+
const max = Math.max(128, Math.min(config.evidenceMaxChars - 2000, options.windowBudget || config.evidenceMaxChars - 2000));
|
|
42
|
+
for (const i of ordered) {
|
|
43
|
+
const s = required(chunk.segments[i], '语义块'), heading = s.startLine === undefined || s.endLine === undefined ? `[${s.type} · segment ${i}]\n` : `[L${s.startLine}–L${s.endLine} · ${s.type}]\n`;
|
|
44
|
+
let value = s.text;
|
|
45
|
+
const start = options.segmentIndex === i ? options.offset || 0 : 0;
|
|
46
|
+
if (start > chars(value))
|
|
47
|
+
throw new Error('续读字符偏移超过语义块');
|
|
48
|
+
value = Array.from(value).slice(start).join('');
|
|
49
|
+
if (chars(body + heading + value) > max) {
|
|
50
|
+
if (read.length) {
|
|
51
|
+
continuation = { segmentIndex: i, offset: 0 };
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const characters = Array.from(s.text), hit = terms.map(t => value.toLowerCase().indexOf(t)).filter(x => x >= 0).sort((a, b) => a - b)[0];
|
|
55
|
+
const offset = options.segmentIndex !== undefined ? start : (hit === undefined ? 0 : Math.max(0, chars(value.slice(0, hit)) - 250));
|
|
56
|
+
const available = max - chars(heading) - 50;
|
|
57
|
+
value = clip(characters.slice(offset).join(''), available);
|
|
58
|
+
continuation = offset + chars(value) < characters.length ? { segmentIndex: i, offset: offset + chars(value) } : i + 1 < chunk.segments.length ? { segmentIndex: i + 1, offset: 0 } : null;
|
|
59
|
+
read.push({ segmentIndex: i, startLine: s.startLine, endLine: s.endLine, charOffset: offset, partial: true });
|
|
60
|
+
body += heading + value + '\n[此语义块仅返回部分,请按 continuation 续读]\n';
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
body += heading + value + '\n\n';
|
|
64
|
+
read.push({ segmentIndex: i, startLine: s.startLine, endLine: s.endLine, ...start ? { charOffset: start, partial: true } : {} });
|
|
65
|
+
}
|
|
66
|
+
const unread = chunk.segments.length - read.length;
|
|
67
|
+
const next = (read.at(-1)?.segmentIndex ?? -1) + 1;
|
|
68
|
+
return { text: body, windows: read, complete: unread === 0 && !read.some(r => r.partial), truncated: unread > 0 || read.some(r => r.partial), continuation: continuation || (next < chunk.segments.length ? { segmentIndex: next, offset: 0 } : null), totalSegments: chunk.segments.length };
|
|
69
|
+
}
|
|
70
|
+
export class Retrieval {
|
|
71
|
+
store;
|
|
72
|
+
config;
|
|
73
|
+
constructor(store, config) {
|
|
74
|
+
this.store = store;
|
|
75
|
+
this.config = config;
|
|
76
|
+
}
|
|
77
|
+
async catalog(args = {}) {
|
|
78
|
+
if (!args.kbId) {
|
|
79
|
+
const kbs = await this.store.list(), entries = [];
|
|
80
|
+
for (const kb of kbs) {
|
|
81
|
+
const current = await this.store.current(kb.kbId);
|
|
82
|
+
entries.push({ ...kb, revision: current?.revision || null, coverageIncomplete: current?.coverageIncomplete || false });
|
|
83
|
+
}
|
|
84
|
+
const value = { knowledgeBases: entries, truncated: false };
|
|
85
|
+
while (chars(JSON.stringify(value)) > this.config.catalogMaxChars && value.knowledgeBases.length) {
|
|
86
|
+
value.knowledgeBases.pop();
|
|
87
|
+
value.truncated = true;
|
|
88
|
+
}
|
|
89
|
+
return bounded(value, this.config.catalogMaxChars);
|
|
90
|
+
}
|
|
91
|
+
const kb = await this.store.kb(args.kbId), snapshot = await this.store.revision(kb.kbId, args.revision);
|
|
92
|
+
const catalogId = args.docId ? `${id(args.docId)}-${args.nodeId ? id(args.nodeId) : 'root'}` : 'root';
|
|
93
|
+
const catalog = snapshot.manifest.catalogs[catalogId];
|
|
94
|
+
if (!catalog)
|
|
95
|
+
throw new Error('目录节点不存在');
|
|
96
|
+
const page = args.page === undefined ? 0 : args.page;
|
|
97
|
+
if (!Number.isSafeInteger(page) || page < 0 || !catalog.pages[page])
|
|
98
|
+
throw new Error('索引页不存在');
|
|
99
|
+
const actual = required(catalog.pages[page]);
|
|
100
|
+
// Markdown is the model-facing index; do not duplicate its entries in the same result.
|
|
101
|
+
const value = { kbId: kb.kbId, revision: snapshot.revision, catalogId, page, path: actual.path, content: actual.content, nextPage: page + 1 < catalog.pages.length ? page + 1 : null };
|
|
102
|
+
return bounded(value, this.config.catalogMaxChars + 1000);
|
|
103
|
+
}
|
|
104
|
+
async search(args) {
|
|
105
|
+
const kb = await this.store.kb(args.kbId), snapshot = await this.store.revision(kb.kbId, args.revision);
|
|
106
|
+
const terms = queryTerms(args.query, args.keywords), scored = [];
|
|
107
|
+
for (const doc of snapshot.manifest.documents) {
|
|
108
|
+
if (args.docId && doc.docId !== args.docId)
|
|
109
|
+
continue;
|
|
110
|
+
for (const chunk of doc.chunks) {
|
|
111
|
+
const content = await this.store.chunkContent(snapshot, chunk), body = content.toLowerCase();
|
|
112
|
+
const hits = terms.filter(t => body.includes(t)), title = chunk.headingPath.join('/').toLowerCase(), keys = (chunk.keywords || []).map(k => k.toLowerCase());
|
|
113
|
+
if (!hits.length)
|
|
114
|
+
continue;
|
|
115
|
+
const score = hits.reduce((n, t) => n + (keys.includes(t) ? 12 : 0) + (title.includes(t) ? 6 : 0) + Math.min(5, body.split(t).length - 1), 0);
|
|
116
|
+
scored.push({ doc, chunk, content, score, hits });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
scored.sort((a, b) => b.score - a.score || a.chunk.path.localeCompare(b.chunk.path));
|
|
120
|
+
const limit = args.limit === undefined ? this.config.candidateLimit : args.limit;
|
|
121
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 30)
|
|
122
|
+
throw new Error('limit 必须为 1–30');
|
|
123
|
+
const value = { kbId: kb.kbId, revision: snapshot.revision, query: args.query, terms, candidates: [], evidence: [], warnings: [], truncated: scored.length > limit, totalCandidates: scored.length, continuation: [] };
|
|
124
|
+
const top = scored.slice(0, limit);
|
|
125
|
+
for (const { doc, chunk, score, hits, content } of top) {
|
|
126
|
+
const candidate = { chunkId: chunk.chunkId, score, matchedTerms: hits, title: chunk.title, summary: chunk.summary, fullChars: chunk.fullChars, reviewStatus: chunk.reviewStatus, source: source(required(doc), chunk, snapshot.revision) };
|
|
127
|
+
value.candidates.push(candidate);
|
|
128
|
+
const remaining = this.config.evidenceMaxChars - chars(JSON.stringify(value)) - 800;
|
|
129
|
+
const read = windows(chunk, content, terms, this.config, { windowBudget: Math.max(128, Math.min(remaining, 6000)) });
|
|
130
|
+
const evidence = { chunkId: chunk.chunkId, ...read, source: source(required(doc), chunk, snapshot.revision, read), assetRefs: chunk.assetRefs, relatedChunks: chunk.relatedChunks };
|
|
131
|
+
if (required(doc).selectedPages?.partial && !value.warnings.some(w => w.docId === doc.docId && w.kind === 'scope'))
|
|
132
|
+
value.warnings.push({ docId: doc.docId, kind: 'scope', message: pdfScope(doc) });
|
|
133
|
+
if (required(doc).coverageIncomplete && !value.warnings.some(w => w.docId === doc.docId))
|
|
134
|
+
value.warnings.push({ docId: doc.docId, message: '此文档存在已接受的覆盖缺口,请结合质量报告核对' });
|
|
135
|
+
if (chunk.reviewStatus === 'needs-review')
|
|
136
|
+
value.warnings.push({ chunkId: chunk.chunkId, message: '涉及未解决的图片描述或待复核信息' });
|
|
137
|
+
value.evidence.push(evidence);
|
|
138
|
+
if (chars(JSON.stringify(value)) > this.config.evidenceMaxChars) {
|
|
139
|
+
value.evidence.pop();
|
|
140
|
+
value.continuation.push({ chunkId: chunk.chunkId });
|
|
141
|
+
value.truncated = true;
|
|
142
|
+
}
|
|
143
|
+
if (chars(JSON.stringify(value)) > this.config.evidenceMaxChars) {
|
|
144
|
+
value.candidates.pop();
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return bounded(value, this.config.evidenceMaxChars);
|
|
149
|
+
}
|
|
150
|
+
async read(args) {
|
|
151
|
+
const kb = await this.store.kb(args.kbId), snapshot = await this.store.readChunk(kb.kbId, args.revision, args.chunkId);
|
|
152
|
+
return this.readSnapshot(kb.kbId, snapshot, args);
|
|
153
|
+
}
|
|
154
|
+
readSnapshot(kbId, snapshot, args) {
|
|
155
|
+
const doc = snapshot.manifest.documents.find(d => d.docId === snapshot.chunk.docId);
|
|
156
|
+
const terms = args.query ? queryTerms(args.query, args.keywords) : [];
|
|
157
|
+
for (const key of ['segmentIndex', 'offset'])
|
|
158
|
+
if (args[key] !== undefined && (!Number.isSafeInteger(args[key]) || args[key] < 0))
|
|
159
|
+
throw new Error(`${key} 必须为非负整数`);
|
|
160
|
+
if (args.offset !== undefined && args.segmentIndex === undefined)
|
|
161
|
+
throw new Error('offset 必须与 segmentIndex 一起提供');
|
|
162
|
+
if (args.segmentIndex !== undefined && args.segmentIndex >= snapshot.chunk.segments.length)
|
|
163
|
+
throw new Error('续读语义块不存在');
|
|
164
|
+
let budget = this.config.evidenceMaxChars - 2000;
|
|
165
|
+
for (;;) {
|
|
166
|
+
const read = windows(snapshot.chunk, snapshot.content, terms, this.config, { ...args, windowBudget: budget });
|
|
167
|
+
const result = { kbId, chunkId: snapshot.chunk.chunkId, revision: snapshot.revision, source: source(required(doc), snapshot.chunk, snapshot.revision, read), ...read, assetRefs: snapshot.chunk.assetRefs, relatedChunks: snapshot.chunk.relatedChunks, warnings: [...required(doc).coverageIncomplete ? ['当前文档存在覆盖缺口'] : [], ...required(doc).selectedPages?.partial ? [pdfScope(required(doc))] : []] };
|
|
168
|
+
if (chars(JSON.stringify(result)) <= this.config.evidenceMaxChars || budget <= 128)
|
|
169
|
+
return bounded(result, this.config.evidenceMaxChars);
|
|
170
|
+
budget = Math.max(128, Math.floor(budget * .6));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
package/lib/routes.d.ts
ADDED
package/lib/routes.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { API, browserBuildSchema, jobActionSchema, catalogQuerySchema, readQuerySchema, searchSchema, assetQuerySchema, revisionQuerySchema } from "./shared.js";
|
|
5
|
+
import { figureDataSchema, overlaySchema } from "./data.js";
|
|
6
|
+
import { inspectPdf } from "./document/pdf-session.js";
|
|
7
|
+
import { hash, safePath, message, required, fileBytes, checkAbort } from "./util.js";
|
|
8
|
+
const HOST_VERSION = z.object({ version: z.string() }).parse(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))).version;
|
|
9
|
+
const id = z.string().regex(/^[a-z0-9][a-z0-9-]{0,95}$/u), name = z.string().trim().min(1).max(120), digest = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
10
|
+
const libraryArgs = z.strictObject({ kbId: id }), managementArgs = libraryArgs.extend({ confirmName: name });
|
|
11
|
+
const reviewSchema = z.strictObject({ kbId: id, revision: id, docId: id, occurrenceId: id, expectedHash: digest, action: z.enum(['reidentify', 'restore']).optional(), figure: z.union([figureDataSchema.strict(), z.strictObject({ ignore: z.literal(true) }), z.strictObject({ skip: z.literal(true) })]).optional(), visionProvider: z.string().optional(), visionModel: z.string().optional() }).refine(value => Boolean(value.action) !== Boolean(value.figure), '指定一个复核操作或内容');
|
|
12
|
+
const draftQuery = z.strictObject({ jobId: id }), sourceQuery = revisionQuerySchema.extend({ docId: id }), draftAssetQuery = draftQuery.extend({ docId: id, assetId: id });
|
|
13
|
+
const draftReadQuery = draftQuery.extend(readQuerySchema.omit({ kbId: true, revision: true }).shape);
|
|
14
|
+
const headers = { 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer' };
|
|
15
|
+
function query(request) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, value] of new URL(request.url).searchParams) {
|
|
18
|
+
if (Object.hasOwn(result, key))
|
|
19
|
+
throw new Error('重复查询参数');
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
async function bytes(request, max) {
|
|
25
|
+
const reader = request.body?.getReader();
|
|
26
|
+
if (!reader)
|
|
27
|
+
return Buffer.alloc(0);
|
|
28
|
+
const parts = [];
|
|
29
|
+
let size = 0;
|
|
30
|
+
try {
|
|
31
|
+
for (;;) {
|
|
32
|
+
checkAbort(request.signal);
|
|
33
|
+
const { value, done } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
size += value.byteLength;
|
|
37
|
+
if (size > max)
|
|
38
|
+
throw new Error('请求内容超过大小上限');
|
|
39
|
+
parts.push(Buffer.from(value));
|
|
40
|
+
}
|
|
41
|
+
return Buffer.concat(parts, size);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await reader.cancel().catch(() => { });
|
|
45
|
+
reader.releaseLock();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function body(request, schema) {
|
|
49
|
+
if (request.headers.get('content-type')?.split(';')[0]?.trim() !== 'application/json')
|
|
50
|
+
throw new Error('请求必须使用 application/json');
|
|
51
|
+
return schema.parse(JSON.parse((await bytes(request, 128 * 1024)).toString('utf8')));
|
|
52
|
+
}
|
|
53
|
+
export function routes(runtime, ctx) {
|
|
54
|
+
const { store, jobs, retrieval, config } = runtime;
|
|
55
|
+
const table = new Map();
|
|
56
|
+
const add = (path, method, handler) => {
|
|
57
|
+
let methods = table.get(path);
|
|
58
|
+
if (!methods) {
|
|
59
|
+
methods = new Map();
|
|
60
|
+
table.set(path, methods);
|
|
61
|
+
}
|
|
62
|
+
methods.set(method, handler);
|
|
63
|
+
};
|
|
64
|
+
const json = (value, status = 200) => Response.json(value, { status, headers });
|
|
65
|
+
const binary = (data, mime) => new Response(new Uint8Array(data), { headers: { ...headers, 'content-type': mime, 'content-security-policy': "default-src 'none'; sandbox" } });
|
|
66
|
+
const original = async (snapshot, docId) => {
|
|
67
|
+
const doc = snapshot.manifest.documents.find(d => d.docId === docId);
|
|
68
|
+
if (!doc || doc.sourceFormat !== 'pdf')
|
|
69
|
+
throw new Error('PDF 原件不存在于所选版本中');
|
|
70
|
+
return binary((await store.rawSource(snapshot.kbId, doc)).data, 'application/pdf');
|
|
71
|
+
};
|
|
72
|
+
const image = async (asset, file) => {
|
|
73
|
+
if (!['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'].includes(asset.mime))
|
|
74
|
+
throw new Error('不支持浏览此图片格式,请提供 PNG/JPEG 预览');
|
|
75
|
+
const data = await fileBytes(file, config.maxUncompressedBytes);
|
|
76
|
+
if (`a-${hash(data).slice(0, 20)}` !== asset.assetId)
|
|
77
|
+
throw new Error('图片内容与清单不一致');
|
|
78
|
+
return binary(data, asset.mime);
|
|
79
|
+
};
|
|
80
|
+
const tree = (snapshot, kbId) => ({ kbId, revision: snapshot.revision, coverageIncomplete: snapshot.manifest.coverageIncomplete, report: snapshot.manifest.report, documents: snapshot.manifest.documents.map(doc => ({ ...doc, chunks: doc.chunks.map(({ segments: _segments, anchors: _anchors, ...chunk }) => chunk) })) });
|
|
81
|
+
add('list', 'GET', async (request) => { z.strictObject({}).parse(query(request)); return ({ hostVersion: HOST_VERSION, kbs: await Promise.all((await store.list()).map(async (kb) => ({ ...kb, ...await store.inspect(kb.kbId) }))), deletedKbs: await store.deleted(), activities: await store.activities(), jobs: jobs.list(), dataRoot: store.root, config: { chunkTargetChars: config.chunkTargetChars, visionProvider: config.visionProvider, visionModel: config.visionModel, maxInputBytes: config.maxInputBytes } }); });
|
|
82
|
+
add('create', 'POST', async (request) => store.create((await body(request, z.strictObject({ name }))).name));
|
|
83
|
+
add('delete', 'POST', async (request) => { const args = await body(request, managementArgs); return jobs.trash(args.kbId, args.confirmName); });
|
|
84
|
+
add('restore', 'POST', async (request) => jobs.restore((await body(request, libraryArgs)).kbId));
|
|
85
|
+
add('purge', 'POST', async (request) => { const args = await body(request, managementArgs.extend({ confirmIrreversible: z.literal(true) })); return jobs.purge(args.kbId, args.confirmName, args.confirmIrreversible); });
|
|
86
|
+
add('upload', 'POST', async (request) => {
|
|
87
|
+
if (request.headers.get('content-type')?.split(';')[0]?.trim() !== 'application/octet-stream')
|
|
88
|
+
throw new Error('上传必须使用 application/octet-stream');
|
|
89
|
+
const args = z.strictObject({ filename: z.string().min(1).max(200) }).parse(query(request));
|
|
90
|
+
return store.upload(await bytes(request, config.maxInputBytes), args.filename);
|
|
91
|
+
});
|
|
92
|
+
add('inspect-document', 'POST', async (request) => {
|
|
93
|
+
const { receiptId } = await body(request, z.strictObject({ receiptId: id })), uploaded = await store.uploaded(receiptId);
|
|
94
|
+
if (!/\.pdf$/iu.test(uploaded.filename))
|
|
95
|
+
throw new Error('文档预检只支持 PDF');
|
|
96
|
+
return inspectPdf(await safePath(store.root, join('uploads', receiptId + '.bin')), config, request.signal);
|
|
97
|
+
});
|
|
98
|
+
add('source', 'GET', async (request) => { const args = sourceQuery.parse(query(request)), kb = await store.kb(args.kbId); return original({ ...await store.revision(kb.kbId, args.revision), kbId: kb.kbId }, args.docId); });
|
|
99
|
+
add('draft-source', 'GET', async (request) => { const args = draftQuery.extend({ docId: id }).parse(query(request)); return original(await jobs.preview(args.jobId), args.docId); });
|
|
100
|
+
add('build', 'POST', async (request) => jobs.submit(await body(request, browserBuildSchema)));
|
|
101
|
+
add('job', 'POST', async (request) => { const args = await body(request, jobActionSchema); return jobs.action(args.jobId, args.action, args); });
|
|
102
|
+
add('draft', 'GET', async (request) => { const snapshot = await jobs.preview(draftQuery.parse(query(request)).jobId); return { ...tree(snapshot, snapshot.kbId), previewJobId: snapshot.jobId, coverageIncomplete: true }; });
|
|
103
|
+
add('draft-read', 'GET', async (request) => {
|
|
104
|
+
const { jobId, ...args } = draftReadQuery.parse(query(request)), snapshot = await jobs.preview(jobId), chunk = required(snapshot.manifest.documents.flatMap(d => d.chunks).find(c => c.chunkId === args.chunkId), '草稿片段');
|
|
105
|
+
const value = retrieval.readSnapshot(snapshot.kbId, { ...snapshot, chunk, content: await store.chunkContent(snapshot, chunk) }, { ...args, kbId: snapshot.kbId });
|
|
106
|
+
return { ...value, previewJobId: jobId, warnings: ['待发布预览:未向 Agent 检索开放', ...value.warnings] };
|
|
107
|
+
});
|
|
108
|
+
add('draft-asset', 'GET', async (request) => { const args = draftAssetQuery.parse(query(request)), snapshot = await jobs.preview(args.jobId), doc = required(snapshot.manifest.documents.find(d => d.docId === args.docId), '草稿文档'), asset = required(doc.assets.find(a => a.assetId === args.assetId), '草稿图片'); return image(asset, await safePath(snapshot.root, join('sources', doc.docId, asset.path))); });
|
|
109
|
+
add('catalog', 'GET', async (request) => retrieval.catalog(catalogQuerySchema.parse(query(request))));
|
|
110
|
+
add('tree', 'GET', async (request) => { const args = revisionQuerySchema.parse(query(request)), kb = await store.kb(args.kbId), snapshot = await store.revision(kb.kbId, args.revision), overlays = await store.overlays(kb.kbId), value = tree(snapshot, kb.kbId); return { ...value, location: await store.location(kb.kbId, snapshot.revision), documents: value.documents.map(doc => ({ ...doc, figures: doc.figures.map(figure => ({ ...figure, expectedHash: hash(overlays[figure.overlayKey] || null) })) })) }; });
|
|
111
|
+
add('read', 'GET', async (request) => retrieval.read(readQuerySchema.parse(query(request))));
|
|
112
|
+
add('search', 'POST', async (request) => retrieval.search(await body(request, searchSchema)));
|
|
113
|
+
add('asset', 'GET', async (request) => { const args = assetQuerySchema.parse(query(request)), kb = await store.kb(args.kbId), asset = await store.asset(kb.kbId, args.revision, args.docId, args.assetId); return image(asset.asset, asset.file); });
|
|
114
|
+
add('schema', 'GET', async (request) => { const kb = await store.kb(libraryArgs.parse(query(request)).kbId), content = await store.schema(kb.kbId); return { content, hash: hash(content) }; });
|
|
115
|
+
add('schema', 'POST', async (request) => { const args = await body(request, libraryArgs.extend({ content: z.string().max(20000), expectedHash: digest })), kb = await store.kb(args.kbId); return store.setSchema(kb.kbId, args.content, args.expectedHash); });
|
|
116
|
+
add('review', 'POST', async (request) => {
|
|
117
|
+
const args = await body(request, reviewSchema), kb = await store.kb(args.kbId);
|
|
118
|
+
const figure = args.action ? null : args.figure && 'ignore' in args.figure ? { ignore: true } : args.figure && 'skip' in args.figure ? { skip: true, types: ['decoration'], description: '人工确认装饰图', visibleLabels: [], relations: [], uncertainties: [], needsReview: false } : args.figure;
|
|
119
|
+
if (figure && JSON.stringify(figure).length > config.figureMaxChars * 3)
|
|
120
|
+
throw new Error('图片复核内容超过预算');
|
|
121
|
+
return jobs.review(kb.kbId, args.revision, args.docId, args.occurrenceId, figure === null ? null : overlaySchema.parse(figure), args.expectedHash, { restore: args.action === 'restore', visionProvider: args.visionProvider, visionModel: args.visionModel });
|
|
122
|
+
});
|
|
123
|
+
add('models', 'GET', async (request) => {
|
|
124
|
+
z.strictObject({}).parse(query(request));
|
|
125
|
+
checkAbort(request.signal);
|
|
126
|
+
const providers = [];
|
|
127
|
+
for (const provider of ctx.llm.listProviders()) {
|
|
128
|
+
try {
|
|
129
|
+
providers.push({ id: provider.id, name: provider.name || provider.id, models: (await ctx.llm.listModels(provider.id)).map(model => ({ id: model.id, name: model.name || model.id, inputModalities: model.inputModalities })) });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
providers.push({ id: provider.id, name: provider.name || provider.id, models: [], error: message(error) });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { providers };
|
|
136
|
+
});
|
|
137
|
+
for (const [path, methods] of table)
|
|
138
|
+
ctx.effect(() => ctx.connection.fetch.register({ path: `${API}/${path}`, methods: [...methods.keys()], requestBody: path === 'upload' ? 'streaming' : 'buffered', fetch: async (request) => {
|
|
139
|
+
try {
|
|
140
|
+
await runtime.ready;
|
|
141
|
+
checkAbort(request.signal);
|
|
142
|
+
const handler = required(methods.get(z.enum(['GET', 'POST', 'HEAD']).parse(request.method)), '请求方法'), value = await runtime.run(signal => handler(new Request(request, { signal })), request.signal);
|
|
143
|
+
return value instanceof Response ? value : json(value);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const detail = message(error), status = error instanceof z.ZodError || error instanceof SyntaxError ? 400 : /变化|冲突|已存在|未退出|正在关闭/u.test(detail) ? 409 : /无效|必须|不支持|不能|只能|不在回收站|超过|过大|缺少|缺失|尚无|不存在|提供|请选择|待复核|重复/u.test(detail) ? 400 : 500;
|
|
147
|
+
if (status === 500)
|
|
148
|
+
ctx.logger.warn(`knowledge-base: ${detail}`);
|
|
149
|
+
return json({ error: detail }, status);
|
|
150
|
+
}
|
|
151
|
+
} }), `libing-knowledge-base: ${path}`);
|
|
152
|
+
}
|
package/lib/runtime.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Config } from './config.ts';
|
|
3
|
+
import { Store } from './store.ts';
|
|
4
|
+
import { Models } from './models.ts';
|
|
5
|
+
import { Jobs } from './jobs.ts';
|
|
6
|
+
import { Retrieval } from './retrieval.ts';
|
|
7
|
+
/** One root owner; admitted tasks are durable and the close promise drains them once. */
|
|
8
|
+
export declare function createRuntime(ctx: Context, raw?: Config): {
|
|
9
|
+
config: {
|
|
10
|
+
dataRoot: string;
|
|
11
|
+
chunkTargetChars: number;
|
|
12
|
+
evidenceMaxChars: number;
|
|
13
|
+
catalogMaxChars: number;
|
|
14
|
+
candidateLimit: number;
|
|
15
|
+
maxInputBytes: number;
|
|
16
|
+
maxUncompressedBytes: number;
|
|
17
|
+
maxArchiveEntries: number;
|
|
18
|
+
modelTimeoutMs: number;
|
|
19
|
+
modelAttempts: number;
|
|
20
|
+
visionConcurrency: number;
|
|
21
|
+
maxModelCalls: number;
|
|
22
|
+
maxModelTokens: number;
|
|
23
|
+
visionMaxEdge: number;
|
|
24
|
+
visionTileSize: number;
|
|
25
|
+
visionMaxTiles: number;
|
|
26
|
+
modelMaxOutputTokens: number;
|
|
27
|
+
summaryMaxChars: number;
|
|
28
|
+
figureMaxChars: number;
|
|
29
|
+
pdfMaxPages: number;
|
|
30
|
+
pdfMaxPagePixels: number;
|
|
31
|
+
pdfPageTimeoutMs: number;
|
|
32
|
+
libreOfficePath: string;
|
|
33
|
+
visionProvider: string;
|
|
34
|
+
visionModel: string;
|
|
35
|
+
summaryProvider: string;
|
|
36
|
+
summaryModel: string;
|
|
37
|
+
skipImages: boolean;
|
|
38
|
+
acceptPartial: boolean;
|
|
39
|
+
};
|
|
40
|
+
store: Store;
|
|
41
|
+
models: Models;
|
|
42
|
+
jobs: Jobs;
|
|
43
|
+
retrieval: Retrieval;
|
|
44
|
+
ready: Promise<void>;
|
|
45
|
+
run: <T>(operation: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal) => Promise<T>;
|
|
46
|
+
close: () => Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
export type Runtime = ReturnType<typeof createRuntime>;
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { checkAbort } from "./util.js";
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { resolveConfig } from "./config.js";
|
|
4
|
+
import { Store } from "./store.js";
|
|
5
|
+
import { Models } from "./models.js";
|
|
6
|
+
import { Jobs } from "./jobs.js";
|
|
7
|
+
import { Retrieval } from "./retrieval.js";
|
|
8
|
+
/** One root owner; admitted tasks are durable and the close promise drains them once. */
|
|
9
|
+
export function createRuntime(ctx, raw = {}) {
|
|
10
|
+
const config = resolveConfig(raw), store = new Store(config), models = new Models(ctx, config), jobs = new Jobs(store, models, config), retrieval = new Retrieval(store, config);
|
|
11
|
+
const ready = mkdir(store.root, { recursive: true }).then(() => jobs.init());
|
|
12
|
+
let closing, stopped = false;
|
|
13
|
+
const controller = new AbortController(), pending = new Set();
|
|
14
|
+
const run = (operation, signal) => {
|
|
15
|
+
if (stopped)
|
|
16
|
+
return Promise.reject(new Error('任务服务正在关闭'));
|
|
17
|
+
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
18
|
+
const value = Promise.resolve().then(() => { checkAbort(combined); return operation(combined); });
|
|
19
|
+
pending.add(value);
|
|
20
|
+
void value.finally(() => pending.delete(value)).catch(() => { });
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
const close = () => {
|
|
24
|
+
if (closing)
|
|
25
|
+
return closing;
|
|
26
|
+
stopped = true;
|
|
27
|
+
controller.abort(new Error('任务服务正在关闭'));
|
|
28
|
+
jobs.stop();
|
|
29
|
+
closing = (async () => {
|
|
30
|
+
try {
|
|
31
|
+
await ready;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await Promise.allSettled(pending);
|
|
35
|
+
await jobs.close();
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return closing;
|
|
39
|
+
};
|
|
40
|
+
return { config, store, models, jobs, retrieval, ready, run, close };
|
|
41
|
+
}
|