dsh-data-domain-knowledge 0.1.0-alpha.11 → 0.1.0-alpha.13
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 +64 -13
- package/eval/README.md +19 -8
- package/eval/evaluate-retrieval.mjs +17 -8
- package/eval/queries.example.jsonl +1 -0
- package/lib/client.js +2294 -2096
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +2 -2
- package/lib/types/axis-registry.d.ts +9 -4
- package/lib/types/axis-registry.d.ts.map +1 -1
- package/lib/types/axis-registry.js +62 -23
- package/lib/types/axis-registry.js.map +1 -1
- package/lib/types/card-organize.d.ts +5 -0
- package/lib/types/card-organize.d.ts.map +1 -1
- package/lib/types/card-organize.js +15 -0
- package/lib/types/card-organize.js.map +1 -1
- package/lib/types/compiler/embedding-identity.d.ts +3 -0
- package/lib/types/compiler/embedding-identity.d.ts.map +1 -0
- package/lib/types/compiler/embedding-identity.js +24 -0
- package/lib/types/compiler/embedding-identity.js.map +1 -0
- package/lib/types/compiler/snapshot.d.ts +16 -0
- package/lib/types/compiler/snapshot.d.ts.map +1 -0
- package/lib/types/compiler/snapshot.js +2 -0
- package/lib/types/compiler/snapshot.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +2 -0
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/environment/install.d.ts +7 -0
- package/lib/types/environment/install.d.ts.map +1 -0
- package/lib/types/environment/install.js +172 -0
- package/lib/types/environment/install.js.map +1 -0
- package/lib/types/environment/manifest.d.ts +208 -0
- package/lib/types/environment/manifest.d.ts.map +1 -0
- package/lib/types/environment/manifest.js +26 -0
- package/lib/types/environment/manifest.js.map +1 -0
- package/lib/types/environment/model-catalog.d.ts +191 -0
- package/lib/types/environment/model-catalog.d.ts.map +1 -0
- package/lib/types/environment/model-catalog.js +91 -0
- package/lib/types/environment/model-catalog.js.map +1 -0
- package/lib/types/environment/process.d.ts +10 -0
- package/lib/types/environment/process.d.ts.map +1 -0
- package/lib/types/environment/process.js +54 -0
- package/lib/types/environment/process.js.map +1 -0
- package/lib/types/environment/selection.d.ts +9 -0
- package/lib/types/environment/selection.d.ts.map +1 -0
- package/lib/types/environment/selection.js +29 -0
- package/lib/types/environment/selection.js.map +1 -0
- package/lib/types/environment/service.d.ts +47 -0
- package/lib/types/environment/service.d.ts.map +1 -0
- package/lib/types/environment/service.js +253 -0
- package/lib/types/environment/service.js.map +1 -0
- package/lib/types/environment/types.d.ts +37 -0
- package/lib/types/environment/types.d.ts.map +1 -0
- package/lib/types/environment/types.js +2 -0
- package/lib/types/environment/types.js.map +1 -0
- package/lib/types/events.d.ts +10 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +38 -1
- package/lib/types/events.js.map +1 -1
- package/lib/types/images.d.ts +26 -0
- package/lib/types/images.d.ts.map +1 -0
- package/lib/types/images.js +196 -0
- package/lib/types/images.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +59 -3
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +3 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +9 -0
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge.d.ts +14 -6
- package/lib/types/knowledge.d.ts.map +1 -1
- package/lib/types/knowledge.js +56 -39
- package/lib/types/knowledge.js.map +1 -1
- package/lib/types/layout.d.ts +2 -0
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +2 -0
- package/lib/types/layout.js.map +1 -1
- package/lib/types/retrieval/vector-index.d.ts +31 -0
- package/lib/types/retrieval/vector-index.d.ts.map +1 -0
- package/lib/types/retrieval/vector-index.js +15 -0
- package/lib/types/retrieval/vector-index.js.map +1 -0
- package/lib/types/retrieval/visibility.d.ts +6 -0
- package/lib/types/retrieval/visibility.d.ts.map +1 -0
- package/lib/types/retrieval/visibility.js +11 -0
- package/lib/types/retrieval/visibility.js.map +1 -0
- package/lib/types/runtime.d.ts +36 -6
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +520 -190
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/source-evidence.d.ts +6 -0
- package/lib/types/source-evidence.d.ts.map +1 -0
- package/lib/types/source-evidence.js +28 -0
- package/lib/types/source-evidence.js.map +1 -0
- package/lib/types/store/axis-references.d.ts +5 -0
- package/lib/types/store/axis-references.d.ts.map +1 -0
- package/lib/types/store/axis-references.js +106 -0
- package/lib/types/store/axis-references.js.map +1 -0
- package/lib/types/store/derived.d.ts +11 -0
- package/lib/types/store/derived.d.ts.map +1 -1
- package/lib/types/store/derived.js +31 -6
- package/lib/types/store/derived.js.map +1 -1
- package/lib/types/store/knowledge-db.d.ts +4 -1
- package/lib/types/store/knowledge-db.d.ts.map +1 -1
- package/lib/types/store/knowledge-db.js +15 -9
- package/lib/types/store/knowledge-db.js.map +1 -1
- package/lib/types/store/migrate.d.ts.map +1 -1
- package/lib/types/store/migrate.js +16 -5
- package/lib/types/store/migrate.js.map +1 -1
- package/lib/types/types.d.ts +7 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +3 -4
- package/lib/types/types.js.map +1 -1
- package/package.json +3 -1
- package/python/prepare-models.py +19 -0
- package/python/requirements.txt +8 -6
- package/scripts/doctor.mjs +14 -2
- package/scripts/environment.mjs +29 -0
- package/scripts/setup-embedding.sh +11 -2
- package/src/axis-registry.ts +59 -23
- package/src/card-organize.ts +15 -0
- package/src/client/EnvironmentSetup.tsx +145 -0
- package/src/client/KnowledgePanel.tsx +9 -4
- package/src/compiler/embedding-identity.ts +21 -0
- package/src/compiler/snapshot.ts +17 -0
- package/src/diagnosis/store.ts +1 -0
- package/src/environment/install.ts +129 -0
- package/src/environment/manifest.ts +28 -0
- package/src/environment/model-catalog.ts +105 -0
- package/src/environment/process.ts +54 -0
- package/src/environment/selection.ts +29 -0
- package/src/environment/service.ts +211 -0
- package/src/environment/types.ts +34 -0
- package/src/events.ts +46 -0
- package/src/images.ts +203 -0
- package/src/index.ts +54 -3
- package/src/knowledge-bases.ts +9 -0
- package/src/knowledge.ts +56 -40
- package/src/layout.ts +2 -0
- package/src/retrieval/vector-index.ts +44 -0
- package/src/retrieval/visibility.ts +14 -0
- package/src/runtime.ts +473 -233
- package/src/source-evidence.ts +30 -0
- package/src/store/axis-references.ts +96 -0
- package/src/store/derived.ts +31 -6
- package/src/store/knowledge-db.ts +16 -9
- package/src/store/migrate.ts +15 -5
- package/src/types.ts +10 -5
package/src/images.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
3
|
+
import { basename, extname, join } from 'node:path'
|
|
4
|
+
import { ASSETS_DIR, PUBLISHED_IMAGE_MARKER } from './layout.js'
|
|
5
|
+
import { writeBufferAtomic, writeTextAtomic } from './fs-atomic.js'
|
|
6
|
+
|
|
7
|
+
/** Maximum decoded image size accepted by `images.publish`. */
|
|
8
|
+
export const MAX_PUBLISHED_IMAGE_BYTES = 20 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
const IMAGE_FORMATS = [
|
|
11
|
+
{ mimeType: 'image/png', extension: '.png' },
|
|
12
|
+
{ mimeType: 'image/jpeg', extension: '.jpg' },
|
|
13
|
+
{ mimeType: 'image/gif', extension: '.gif' },
|
|
14
|
+
{ mimeType: 'image/webp', extension: '.webp' },
|
|
15
|
+
{ mimeType: 'image/avif', extension: '.avif' },
|
|
16
|
+
{ mimeType: 'image/bmp', extension: '.bmp' },
|
|
17
|
+
{ mimeType: 'image/svg+xml', extension: '.svg' },
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
export class ImagePublishError extends Error {
|
|
21
|
+
constructor(readonly code: string, message: string, readonly suggestion?: string) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.name = 'ImagePublishError'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PublishKnowledgeImageInput {
|
|
28
|
+
filePath?: unknown
|
|
29
|
+
contentBase64?: unknown
|
|
30
|
+
filename?: unknown
|
|
31
|
+
mimeType?: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PublishedKnowledgeImage {
|
|
35
|
+
kb: string
|
|
36
|
+
filename: string
|
|
37
|
+
path: string
|
|
38
|
+
url: string
|
|
39
|
+
markdown: string
|
|
40
|
+
mimeType: string
|
|
41
|
+
bytes: number
|
|
42
|
+
sha256: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function invalid(message: string, suggestion?: string): never {
|
|
46
|
+
throw new ImagePublishError('IMAGE_INVALID', message, suggestion)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeMimeType(value: string): string {
|
|
50
|
+
const mimeType = value.split(';', 1)[0]!.trim().toLowerCase()
|
|
51
|
+
return mimeType === 'image/jpg' ? 'image/jpeg' : mimeType
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectImageMimeType(bytes: Buffer): string | undefined {
|
|
55
|
+
if (bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'image/png'
|
|
56
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg'
|
|
57
|
+
if (bytes.length >= 6 && (bytes.toString('ascii', 0, 6) === 'GIF87a' || bytes.toString('ascii', 0, 6) === 'GIF89a')) return 'image/gif'
|
|
58
|
+
if (bytes.length >= 12 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP') return 'image/webp'
|
|
59
|
+
if (bytes.length >= 12 && bytes.toString('ascii', 0, 2) === 'BM') return 'image/bmp'
|
|
60
|
+
if (bytes.length >= 12 && bytes.toString('ascii', 4, 8) === 'ftyp') {
|
|
61
|
+
const brands = bytes.toString('ascii', 8, Math.min(bytes.length, 32))
|
|
62
|
+
if (/(?:avif|avis)/.test(brands)) return 'image/avif'
|
|
63
|
+
}
|
|
64
|
+
// SVG is text, so recognize its root element after an optional XML declaration.
|
|
65
|
+
const text = bytes.toString('utf8', 0, Math.min(bytes.length, 4096)).replace(/^\uFEFF/, '').trimStart()
|
|
66
|
+
if (/^(?:<\?xml\b[^>]*>\s*)?<svg(?:\s|>)/i.test(text)) return 'image/svg+xml'
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function decodeBase64(value: string): { bytes: Buffer; declaredMimeType?: string } {
|
|
71
|
+
let encoded = value.trim()
|
|
72
|
+
let declaredMimeType: string | undefined
|
|
73
|
+
if (encoded.startsWith('data:')) {
|
|
74
|
+
const match = /^data:([^;,]+);base64,([\s\S]*)$/i.exec(encoded)
|
|
75
|
+
if (!match) invalid('content_base64 的 Data URI 格式无效。', '传标准 Base64,或传 data:image/png;base64,... 形式。')
|
|
76
|
+
declaredMimeType = normalizeMimeType(match[1]!)
|
|
77
|
+
encoded = match[2]!
|
|
78
|
+
}
|
|
79
|
+
encoded = encoded.replace(/\s+/g, '')
|
|
80
|
+
if (encoded.length > Math.ceil(MAX_PUBLISHED_IMAGE_BYTES * 4 / 3) + 8) {
|
|
81
|
+
throw new ImagePublishError('IMAGE_TOO_LARGE', `图片超过 ${MAX_PUBLISHED_IMAGE_BYTES / 1024 / 1024} MiB 限制。`)
|
|
82
|
+
}
|
|
83
|
+
if (encoded.length % 4 === 1 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}={0,2}|[A-Za-z0-9+/]{3}=?)?$/.test(encoded)) {
|
|
84
|
+
invalid('content_base64 不是有效的 Base64。')
|
|
85
|
+
}
|
|
86
|
+
const bytes = Buffer.from(encoded, 'base64')
|
|
87
|
+
if (bytes.length === 0) invalid('图片内容不能为空。')
|
|
88
|
+
if (bytes.length > MAX_PUBLISHED_IMAGE_BYTES) {
|
|
89
|
+
throw new ImagePublishError('IMAGE_TOO_LARGE', `图片超过 ${MAX_PUBLISHED_IMAGE_BYTES / 1024 / 1024} MiB 限制。`)
|
|
90
|
+
}
|
|
91
|
+
if (bytes.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) invalid('content_base64 不是有效的 Base64。')
|
|
92
|
+
return { bytes, ...(declaredMimeType ? { declaredMimeType } : {}) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function safeFilename(value: unknown, fallback: string, extension: string): string {
|
|
96
|
+
const raw = typeof value === 'string' ? value.trim() : ''
|
|
97
|
+
const leaf = (raw.replace(/\\/g, '/').split('/').pop() ?? '').replace(/[\u0000-\u001f\u007f]/g, '')
|
|
98
|
+
const suppliedExtension = extname(leaf)
|
|
99
|
+
const stem = (suppliedExtension ? leaf.slice(0, -suppliedExtension.length) : leaf)
|
|
100
|
+
.replace(/[^\p{L}\p{N}._ -]/gu, '_')
|
|
101
|
+
.replace(/\s+/g, '-')
|
|
102
|
+
.replace(/^\.+$/, '')
|
|
103
|
+
.slice(0, 120)
|
|
104
|
+
return `${stem || fallback}${extension}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function readExisting(path: string): Promise<Buffer | undefined> {
|
|
108
|
+
try { return await readFile(path) } catch (cause) {
|
|
109
|
+
if ((cause as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined
|
|
110
|
+
throw cause
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Store one validated image using the knowledge assets content-hash bucket layout. */
|
|
115
|
+
export async function publishKnowledgeImage(
|
|
116
|
+
rootDir: string,
|
|
117
|
+
kb: string,
|
|
118
|
+
input: PublishKnowledgeImageInput,
|
|
119
|
+
): Promise<PublishedKnowledgeImage> {
|
|
120
|
+
const filePath = typeof input.filePath === 'string' ? input.filePath.trim() : ''
|
|
121
|
+
const base64 = typeof input.contentBase64 === 'string' ? input.contentBase64 : ''
|
|
122
|
+
if (Boolean(filePath) === Boolean(base64)) {
|
|
123
|
+
invalid('file_path 和 content_base64 必须且只能提供一个。')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let bytes: Buffer
|
|
127
|
+
let sourceName = ''
|
|
128
|
+
let dataUriMimeType: string | undefined
|
|
129
|
+
if (filePath) {
|
|
130
|
+
let details
|
|
131
|
+
try { details = await stat(filePath) } catch (cause) {
|
|
132
|
+
throw new ImagePublishError('IMAGE_SOURCE_NOT_FOUND', `图片文件无法读取:${cause instanceof Error ? cause.message : String(cause)}`)
|
|
133
|
+
}
|
|
134
|
+
if (!details.isFile()) throw new ImagePublishError('IMAGE_SOURCE_NOT_FILE', 'file_path 必须指向一个文件。')
|
|
135
|
+
if (details.size > MAX_PUBLISHED_IMAGE_BYTES) {
|
|
136
|
+
throw new ImagePublishError('IMAGE_TOO_LARGE', `图片超过 ${MAX_PUBLISHED_IMAGE_BYTES / 1024 / 1024} MiB 限制。`)
|
|
137
|
+
}
|
|
138
|
+
try { bytes = await readFile(filePath) } catch (cause) {
|
|
139
|
+
throw new ImagePublishError('IMAGE_SOURCE_NOT_FOUND', `图片文件无法读取:${cause instanceof Error ? cause.message : String(cause)}`)
|
|
140
|
+
}
|
|
141
|
+
sourceName = basename(filePath)
|
|
142
|
+
} else {
|
|
143
|
+
const decoded = decodeBase64(base64)
|
|
144
|
+
bytes = decoded.bytes
|
|
145
|
+
dataUriMimeType = decoded.declaredMimeType
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (bytes.length === 0) invalid('图片内容不能为空。')
|
|
149
|
+
if (bytes.length > MAX_PUBLISHED_IMAGE_BYTES) {
|
|
150
|
+
throw new ImagePublishError('IMAGE_TOO_LARGE', `图片超过 ${MAX_PUBLISHED_IMAGE_BYTES / 1024 / 1024} MiB 限制。`)
|
|
151
|
+
}
|
|
152
|
+
const mimeType = detectImageMimeType(bytes)
|
|
153
|
+
if (!mimeType) invalid('文件内容不是支持的图片格式(PNG、JPEG、GIF、WebP、AVIF、BMP、SVG)。')
|
|
154
|
+
const declaredMimeType = typeof input.mimeType === 'string' && input.mimeType.trim()
|
|
155
|
+
? normalizeMimeType(input.mimeType)
|
|
156
|
+
: dataUriMimeType
|
|
157
|
+
if (declaredMimeType && declaredMimeType !== mimeType) {
|
|
158
|
+
invalid(`声明的 MIME 类型 ${declaredMimeType} 与图片内容 ${mimeType} 不一致。`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const format = IMAGE_FORMATS.find(item => item.mimeType === mimeType)!
|
|
162
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
163
|
+
const bucket = sha256.slice(0, 12)
|
|
164
|
+
const filename = safeFilename(input.filename ?? sourceName, `image-${bucket}`, format.extension)
|
|
165
|
+
const assetDir = join(rootDir, ASSETS_DIR, bucket)
|
|
166
|
+
const assetPath = join(assetDir, filename)
|
|
167
|
+
let existing = await readExisting(assetPath)
|
|
168
|
+
let storedFilename = filename
|
|
169
|
+
|
|
170
|
+
// The established bucket uses only the first 12 hash characters. If that prefix and the
|
|
171
|
+
// supplied filename both collide with different bytes, add more hash characters instead of
|
|
172
|
+
// replacing the existing asset.
|
|
173
|
+
if (existing && !existing.equals(bytes)) {
|
|
174
|
+
const extension = format.extension
|
|
175
|
+
const stem = filename.slice(0, -extension.length)
|
|
176
|
+
storedFilename = `${stem}-${sha256.slice(12, 20)}${extension}`
|
|
177
|
+
existing = await readExisting(join(assetDir, storedFilename))
|
|
178
|
+
if (existing && !existing.equals(bytes)) {
|
|
179
|
+
storedFilename = `image-${sha256}${extension}`
|
|
180
|
+
existing = await readExisting(join(assetDir, storedFilename))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (existing && !existing.equals(bytes)) {
|
|
185
|
+
throw new ImagePublishError('ASSET_NAME_COLLISION', '目标文件名已被不同内容占用,未覆盖现有图片。')
|
|
186
|
+
}
|
|
187
|
+
if (!existing?.equals(bytes)) await writeBufferAtomic(join(assetDir, storedFilename), bytes)
|
|
188
|
+
// Standalone hosted images have no SourceRaw or card by design. Keep that intent beside the
|
|
189
|
+
// asset so the orphan sweep does not mistake it for a disposable extraction artifact.
|
|
190
|
+
await writeTextAtomic(join(assetDir, PUBLISHED_IMAGE_MARKER), 'images.publish\n')
|
|
191
|
+
const relativePath = `${ASSETS_DIR}/${bucket}/${storedFilename}`
|
|
192
|
+
const url = `/knowledge/assets/${encodeURIComponent(kb)}/${relativePath.split('/').map(encodeURIComponent).join('/')}`
|
|
193
|
+
return {
|
|
194
|
+
kb,
|
|
195
|
+
filename: storedFilename,
|
|
196
|
+
path: relativePath,
|
|
197
|
+
url,
|
|
198
|
+
markdown: ``,
|
|
199
|
+
mimeType,
|
|
200
|
+
bytes: bytes.length,
|
|
201
|
+
sha256,
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type ModelSeam,
|
|
28
28
|
} from './runtime.js'
|
|
29
29
|
import { unwrapGroup } from './settings-volatile.js'
|
|
30
|
+
import { KnowledgeEnvironment } from './environment/service.js'
|
|
30
31
|
import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js'
|
|
31
32
|
import type { ModelCall } from './extract-model.js'
|
|
32
33
|
import { runWikiTrial, trialSummary } from './wiki-trial.js'
|
|
@@ -43,6 +44,7 @@ import type { DiagnosisRunRecord } from './diagnosis/types.js'
|
|
|
43
44
|
import { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE } from './settings-schema.js'
|
|
44
45
|
import { DEFAULT_KB_ID, KnowledgeBases } from './knowledge-bases.js'
|
|
45
46
|
import type { KnowledgeQuery } from './knowledge.js'
|
|
47
|
+
import { ImagePublishError, publishKnowledgeImage } from './images.js'
|
|
46
48
|
import {
|
|
47
49
|
applyConvert, CONVERT_SETTINGS_NAMESPACE, CONVERT_SETTINGS_SCHEMA, MOUNT_PATH as CONVERT_MOUNT_PATH,
|
|
48
50
|
type ConvertHost,
|
|
@@ -515,7 +517,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
515
517
|
},
|
|
516
518
|
}
|
|
517
519
|
}
|
|
520
|
+
let environment: KnowledgeEnvironment
|
|
518
521
|
const runtimeOptions = (): KnowledgeRuntimeOptions => ({
|
|
522
|
+
environmentGuard: () => environment.requireReady(),
|
|
519
523
|
// 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
|
|
520
524
|
resourceRoot: resourceRoot(config.agentRoot),
|
|
521
525
|
// 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
|
|
@@ -552,11 +556,19 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
552
556
|
// 0044:非默认库的 runtime 也要拿到模型接缝。只传 `config` 不够 —— 默认库走上面
|
|
553
557
|
// 那个 `new KnowledgeRuntime(... runtimeOptions())`,别的库走注册表这条支路,
|
|
554
558
|
// 两边必须都带上,否则「新建一个库 → 导入文档」静默变成规则抽取。
|
|
555
|
-
|
|
559
|
+
runtimeOptions: () => ({ resourceRoot: resourceRoot(config.agentRoot), modelSeam: modelSeamFor, environmentGuard: () => environment.requireReady() }),
|
|
556
560
|
})
|
|
557
561
|
compileModelForBase = id => bases.compileModelOf(id)
|
|
558
562
|
runtime.baseId = bases.defaultId
|
|
559
563
|
activeKnowledgeBases = bases
|
|
564
|
+
environment = new KnowledgeEnvironment({
|
|
565
|
+
resourceRoot: resourceRoot(config.agentRoot),
|
|
566
|
+
snapshot: () => ({ config: runtime.config, paths: runtime.resourcePaths() }),
|
|
567
|
+
probe: kind => runtime.environmentProbe(kind),
|
|
568
|
+
reload: () => { runtime.reloadResources(); bases.reloadResources() },
|
|
569
|
+
})
|
|
570
|
+
// Startup checks imports and resource files; model inference runs on demand.
|
|
571
|
+
void environment.check(false).catch(() => undefined)
|
|
560
572
|
|
|
561
573
|
type CompileModelCatalogItem = { provider: string; id: string; name: string; description?: string; available: boolean; current?: boolean }
|
|
562
574
|
const compileModelCatalog = async (kbId: string): Promise<{
|
|
@@ -652,6 +664,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
652
664
|
const previousAgentRoot = config.agentRoot
|
|
653
665
|
config = normalizeKnowledge(pluginConfig.knowledge)
|
|
654
666
|
runtime.updateConfig(config)
|
|
667
|
+
bases.updateRuntimeConfig(config)
|
|
655
668
|
/**
|
|
656
669
|
* 转换设置挂在同一个 entry 上,volatile 事件需要让转换宿主重新读取。
|
|
657
670
|
*/
|
|
@@ -764,7 +777,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
764
777
|
applyIntent(ctx, { intentRoot: () => intentRoot(config.agentRoot) })
|
|
765
778
|
|
|
766
779
|
/**
|
|
767
|
-
*
|
|
780
|
+
* 需要本轮用户审批的知识写工具。`images.publish` 只写入本地素材目录,按用户要求免审批。
|
|
768
781
|
*
|
|
769
782
|
* 0044 换过一批名字:`write_knowledge_card`(只接受已确认提议)退役了,因为正文现在
|
|
770
783
|
* 只读、卡片由编译直接写入;`list_card_proposals` 也退役了,因为待办队列不存在了。
|
|
@@ -811,6 +824,31 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
811
824
|
})),
|
|
812
825
|
})))
|
|
813
826
|
|
|
827
|
+
ctx.tools.register(tool(
|
|
828
|
+
'images.publish',
|
|
829
|
+
'Publish one local image into a knowledge base assets bucket without user approval. Pass exactly one of `file_path` or `content_base64` (standard Base64 or a data URI); optional `filename` overrides the stored name, and optional `mime_type` must match the file bytes. Supports PNG, JPEG, GIF, WebP, AVIF, BMP and SVG up to 20 MiB. Returns the local asset URL and Markdown image reference.',
|
|
830
|
+
{
|
|
831
|
+
file_path: { type: 'string', description: '宿主可读取的本地图片文件路径;与 content_base64 二选一' },
|
|
832
|
+
content_base64: { type: 'string', description: '图片的标准 Base64,或 data:image/...;base64,...;与 file_path 二选一' },
|
|
833
|
+
filename: { type: 'string', description: '可选文件名;默认使用 file_path 的文件名,Base64 输入默认 image-<hash>.<ext>' },
|
|
834
|
+
mime_type: { type: 'string', description: '可选 MIME 类型;必须与图片二进制内容一致' },
|
|
835
|
+
kb: { type: 'string', description: '知识库 id;省略时写入默认库' },
|
|
836
|
+
},
|
|
837
|
+
async args => withRuntime(args.kb, async target => {
|
|
838
|
+
try {
|
|
839
|
+
return success(await publishKnowledgeImage(target.rootDir, target.baseId, {
|
|
840
|
+
filePath: args.file_path,
|
|
841
|
+
contentBase64: args.content_base64,
|
|
842
|
+
filename: args.filename,
|
|
843
|
+
mimeType: args.mime_type,
|
|
844
|
+
}))
|
|
845
|
+
} catch (cause) {
|
|
846
|
+
if (cause instanceof ImagePublishError) return failure({}, cause.code, cause.message, false, cause.suggestion)
|
|
847
|
+
return failure({}, 'IMAGE_PUBLISH_FAILED', `图片写入失败:${cause instanceof Error ? cause.message : String(cause)}`, true)
|
|
848
|
+
}
|
|
849
|
+
}),
|
|
850
|
+
))
|
|
851
|
+
|
|
814
852
|
ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). DSH requests one user approval before execution.', { title: { type: 'string' }, content: { type: 'string', required: true }, external_id: { type: 'string' }, metadata: { type: 'json' }, source_id: { type: 'string' }, allow_duplicate_title: { type: 'boolean', description: '默认 false:标题与已有来源重复时拒收(SOURCE_TITLE_DUPLICATED),要求改标题。传 true 才允许并存。' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.addSource({ title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, ...(args.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}), lineage: lineageOf(args, exec) })), {
|
|
815
853
|
call: addSourceCallView,
|
|
816
854
|
meta: (args, value) => addSourceMetaOf(args, dataOf(value)),
|
|
@@ -1003,6 +1041,19 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
1003
1041
|
},
|
|
1004
1042
|
))
|
|
1005
1043
|
} },
|
|
1044
|
+
{ kind: 'exact' as const, path: '/knowledge/environment', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
1045
|
+
if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '只支持 GET'))
|
|
1046
|
+
return send(res, 200, success(environment.view()))
|
|
1047
|
+
} },
|
|
1048
|
+
{ kind: 'exact' as const, path: '/knowledge/environment/prepare', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
1049
|
+
if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '只支持 POST'))
|
|
1050
|
+
try {
|
|
1051
|
+
const body = await readBody(req, 4000) as { action?: string; selection?: unknown } | null
|
|
1052
|
+
if (body?.action === 'cancel') { environment.cancel(); return send(res, 200, success(environment.view())) }
|
|
1053
|
+
if (body?.action !== 'check' && body?.action !== 'install') return send(res, 400, failure({}, 'ACTION_INVALID', 'action 必须是 check、install 或 cancel', false))
|
|
1054
|
+
return send(res, 200, success(environment.start(body.action, body.selection)))
|
|
1055
|
+
} catch (cause) { return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause))) }
|
|
1056
|
+
} },
|
|
1006
1057
|
{ kind: 'exact' as const, path: '/knowledge/embedding/health', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
1007
1058
|
const resolved = resolveBase(query(req).searchParams.get('kb'))
|
|
1008
1059
|
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
@@ -2160,6 +2211,6 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
2160
2211
|
} },
|
|
2161
2212
|
]
|
|
2162
2213
|
const disposers = routes.map(register)
|
|
2163
|
-
return () => { runtime.close(); for (const dispose of disposers) void dispose() }
|
|
2214
|
+
return () => { environment.close(); runtime.close(); bases.closeAll(); for (const dispose of disposers) void dispose() }
|
|
2164
2215
|
})
|
|
2165
2216
|
}
|
package/src/knowledge-bases.ts
CHANGED
|
@@ -280,6 +280,15 @@ export class KnowledgeBases {
|
|
|
280
280
|
this.runtimes.clear()
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/** Volatile settings apply to every cached base, including non-default bases. */
|
|
284
|
+
updateRuntimeConfig(config: Partial<import('./types.js').RuntimeKnowledgeConfig>): void {
|
|
285
|
+
for (const runtime of this.runtimes.values()) runtime.updateConfig(config)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
reloadResources(): void {
|
|
289
|
+
for (const runtime of this.runtimes.values()) runtime.reloadResources()
|
|
290
|
+
}
|
|
291
|
+
|
|
283
292
|
// ------------------------------------------------------------- 统计刷新
|
|
284
293
|
|
|
285
294
|
/**
|
package/src/knowledge.ts
CHANGED
|
@@ -21,22 +21,21 @@
|
|
|
21
21
|
* 还把两个来源都抹了。**也不许删掉任何一条** —— 删了就没有「冲突」这件事了,
|
|
22
22
|
* 而冲突本身是要给人看的信号。
|
|
23
23
|
*
|
|
24
|
-
* ##
|
|
24
|
+
* ## 稳定身份与判重键
|
|
25
25
|
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* 新行使用 `k_<归一化文本的 SHA-256>`,存量 ID 保留。判重先比较归一化文本,
|
|
27
|
+
* 不把哈希相同当作事实相同。「多来源支持」往同一行追加版本证据。
|
|
28
|
+
* 人工编辑正文保留 ID 并增加内容版本;重新抽出不同文本则保留为另一条知识。
|
|
29
29
|
*
|
|
30
30
|
* ## 为什么存在记忆段
|
|
31
31
|
*
|
|
32
32
|
* Knowledge 不可重建:它是抽取产物 + 人的裁决。重跑一次抽取不会得到同样的结果,
|
|
33
|
-
*
|
|
33
|
+
* 而人的裁决根本没有第二次。所以它与轴注册表共享经过属主、版本校验的 `knowledge.db`。
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { mkdir } from 'node:fs/promises'
|
|
37
|
-
import { dirname } from 'node:path'
|
|
38
36
|
import { createHash } from 'node:crypto'
|
|
39
37
|
import { loadSqlite, type SqliteDatabase } from './fts.js'
|
|
38
|
+
import { KnowledgeDb } from './store/knowledge-db.js'
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
41
|
* 一条 Knowledge 的**真值状态**。
|
|
@@ -93,6 +92,10 @@ export interface KnowledgeSource {
|
|
|
93
92
|
* 「这条知识依据的是第 3 版,而那版已经被人改过了」。
|
|
94
93
|
*/
|
|
95
94
|
toVersion?: number
|
|
95
|
+
/** Evidence changes do not change the truth state of the Knowledge. */
|
|
96
|
+
currentVersion?: number
|
|
97
|
+
evidenceStatus?: 'current' | 'historical' | 'unversioned' | 'missing' | 'human'
|
|
98
|
+
quoteInCurrentVersion?: boolean
|
|
96
99
|
createdAt: string
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -147,9 +150,12 @@ export interface KnowledgeCardRef {
|
|
|
147
150
|
*/
|
|
148
151
|
export function normalizeKnowledge(text: string): string {
|
|
149
152
|
return text
|
|
153
|
+
.normalize('NFKC')
|
|
150
154
|
.toLowerCase()
|
|
151
155
|
.replace(/\s+/g, '')
|
|
152
|
-
|
|
156
|
+
// Operators, decimal points, signs and identifier separators carry meaning.
|
|
157
|
+
.replace(/[,。、;:?!,;:?!“”‘’]/g, '')
|
|
158
|
+
.replace(/\.$/g, '')
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
/** FNV-1a(32 位):只用来生成稳定短 id,不用于任何安全目的。 */
|
|
@@ -172,7 +178,7 @@ function fnv1a(value: string): number {
|
|
|
172
178
|
* 如果未来一个库混合多个 Scope 或有效期,必须先把 Scope/有效性纳入判重策略,再改变 id。
|
|
173
179
|
*/
|
|
174
180
|
export function knowledgeId(text: string): string {
|
|
175
|
-
return `k_${
|
|
181
|
+
return `k_${createHash('sha256').update(normalizeKnowledge(text), 'utf8').digest('hex')}`
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
export interface KnowledgeIngest {
|
|
@@ -458,8 +464,9 @@ CREATE INDEX IF NOT EXISTS revision_blob_idx ON revision(blob_hash);
|
|
|
458
464
|
*/
|
|
459
465
|
export class KnowledgeStore {
|
|
460
466
|
private db: SqliteDatabase | null = null
|
|
467
|
+
private owner: KnowledgeDb | null = null
|
|
461
468
|
|
|
462
|
-
constructor(private readonly file: string) {}
|
|
469
|
+
constructor(private readonly file: string, private readonly shared?: SqliteDatabase) {}
|
|
463
470
|
|
|
464
471
|
/**
|
|
465
472
|
* 这个文件里有没有某张表。
|
|
@@ -481,14 +488,23 @@ export class KnowledgeStore {
|
|
|
481
488
|
const sqlite = await loadSqlite()
|
|
482
489
|
if (sqlite === null) return false
|
|
483
490
|
try {
|
|
484
|
-
|
|
485
|
-
|
|
491
|
+
if (this.shared) this.db = this.shared
|
|
492
|
+
else {
|
|
493
|
+
const opened = await KnowledgeDb.open(this.file, sqlite.DatabaseSync)
|
|
494
|
+
if (!opened.ok) return false
|
|
495
|
+
this.owner = opened.db
|
|
496
|
+
this.db = opened.db.connection()
|
|
497
|
+
}
|
|
486
498
|
this.db.exec(SCHEMA)
|
|
499
|
+
// The retired source_type table can predate revision-bound evidence too.
|
|
500
|
+
if (!hasColumn(this.db, 'knowledge_sources', 'to_version')) {
|
|
501
|
+
this.db.exec('ALTER TABLE knowledge_sources ADD COLUMN to_version INTEGER')
|
|
502
|
+
}
|
|
487
503
|
/*
|
|
488
504
|
* 旧库的 `knowledge_sources` 带有已经撤掉的 `source_type NOT NULL` 列。
|
|
489
505
|
* `CREATE TABLE IF NOT EXISTS` 不会改变旧表;如果直接继续,新的出处写入会因
|
|
490
506
|
* 没有给这个列赋值而全部失败,表现成「抽取报告有知识,但 Knowledge 为 0」。
|
|
491
|
-
*
|
|
507
|
+
* 只重建这一张出处表,保留原始证据数据,并让迁移具备事务性。
|
|
492
508
|
*/
|
|
493
509
|
if (hasColumn(this.db, 'knowledge_sources', 'source_type')) {
|
|
494
510
|
this.db.exec('BEGIN IMMEDIATE')
|
|
@@ -521,26 +537,26 @@ export class KnowledgeStore {
|
|
|
521
537
|
throw cause
|
|
522
538
|
}
|
|
523
539
|
}
|
|
524
|
-
/*
|
|
525
|
-
* 0062 v2 补的列。老库(`CREATE TABLE IF NOT EXISTS` 对已存在的表是空操作)不会
|
|
526
|
-
* 自己长出它来,所以要显式 ALTER —— 与知识库文件「版本不符走迁移、不重建」同一政策。
|
|
527
|
-
*/
|
|
528
|
-
if (!hasColumn(this.db, 'knowledge_sources', 'to_version')) {
|
|
529
|
-
this.db.exec('ALTER TABLE knowledge_sources ADD COLUMN to_version INTEGER')
|
|
530
|
-
}
|
|
531
540
|
if (!hasColumn(this.db, 'knowledge', 'head_version')) {
|
|
532
541
|
this.db.exec("ALTER TABLE knowledge ADD COLUMN head_version INTEGER NOT NULL DEFAULT 0")
|
|
533
542
|
}
|
|
543
|
+
// Keep legacy IDs and references; replace only the lossy comparison key.
|
|
544
|
+
const reindex = this.db.prepare('UPDATE knowledge SET normalized = ? WHERE id = ?')
|
|
545
|
+
for (const row of this.db.prepare('SELECT id, text, normalized FROM knowledge').all() as Array<{ id: string; text: string; normalized: string }>) {
|
|
546
|
+
const normalized = normalizeKnowledge(row.text)
|
|
547
|
+
if (normalized !== row.normalized) reindex.run(normalized, row.id)
|
|
548
|
+
}
|
|
534
549
|
this.backfillContentVersions()
|
|
535
550
|
return true
|
|
536
551
|
} catch {
|
|
552
|
+
this.owner?.close()
|
|
553
|
+
this.owner = null
|
|
537
554
|
this.db = null
|
|
538
555
|
return false
|
|
539
556
|
}
|
|
540
557
|
}
|
|
541
558
|
|
|
542
|
-
available(): boolean {
|
|
543
|
-
}
|
|
559
|
+
available(): boolean { return this.db !== null }
|
|
544
560
|
|
|
545
561
|
/**
|
|
546
562
|
* 为旧 Knowledge 行补首版。
|
|
@@ -613,9 +629,12 @@ export class KnowledgeStore {
|
|
|
613
629
|
const text = input.text.trim()
|
|
614
630
|
if (!text) throw new Error('KNOWLEDGE_TEXT_REQUIRED')
|
|
615
631
|
const normalized = normalizeKnowledge(text)
|
|
616
|
-
|
|
632
|
+
// Edited and migrated facts keep their identity. A hash is not proof of equality.
|
|
633
|
+
const match = this.db.prepare("SELECT id FROM knowledge WHERE normalized = ? AND merged_into = '' ORDER BY created_at, id LIMIT 1").get(normalized) as { id: string } | undefined
|
|
634
|
+
const id = match?.id ?? knowledgeId(text)
|
|
617
635
|
const now = new Date().toISOString()
|
|
618
|
-
const existing =
|
|
636
|
+
const existing = this.read(id)
|
|
637
|
+
if (existing !== null && normalizeKnowledge(existing.text) !== normalized) throw new Error('KNOWLEDGE_ID_COLLISION')
|
|
619
638
|
|
|
620
639
|
if (existing === null) {
|
|
621
640
|
this.db.exec('BEGIN')
|
|
@@ -627,7 +646,7 @@ export class KnowledgeStore {
|
|
|
627
646
|
(input.objectId ?? '').trim(), (input.actionId ?? '').trim(), (input.sceneId ?? '').trim(),
|
|
628
647
|
input.status ?? 'candidate', (input.runId ?? '').trim(), '', 0, now, now,
|
|
629
648
|
)
|
|
630
|
-
const added =
|
|
649
|
+
const added = this.appendSources(id, input.sources ?? [])
|
|
631
650
|
this.appendContentVersion(id, text, {
|
|
632
651
|
actor: input.actor ?? 'compiler',
|
|
633
652
|
kind: input.revisionKind ?? 'auto',
|
|
@@ -636,7 +655,7 @@ export class KnowledgeStore {
|
|
|
636
655
|
})
|
|
637
656
|
this.db.prepare('UPDATE knowledge SET head_version = 1 WHERE id = ?').run(id)
|
|
638
657
|
this.db.exec('COMMIT')
|
|
639
|
-
return { knowledge:
|
|
658
|
+
return { knowledge: this.read(id)!, merged: false, addedSources: added }
|
|
640
659
|
} catch (cause) {
|
|
641
660
|
this.db.exec('ROLLBACK')
|
|
642
661
|
throw cause
|
|
@@ -664,7 +683,7 @@ export class KnowledgeStore {
|
|
|
664
683
|
incomingCoordinate.objectId, incomingCoordinate.actionId, incomingCoordinate.sceneId, now, id,
|
|
665
684
|
)
|
|
666
685
|
}
|
|
667
|
-
const added =
|
|
686
|
+
const added = this.appendSources(id, input.sources ?? [])
|
|
668
687
|
if (existingHasCoordinate || !incomingHasCoordinate) {
|
|
669
688
|
this.db.prepare('UPDATE knowledge SET updated_at = ? WHERE id = ?').run(now, id)
|
|
670
689
|
}
|
|
@@ -713,25 +732,18 @@ export class KnowledgeStore {
|
|
|
713
732
|
}
|
|
714
733
|
|
|
715
734
|
/** 追加来源。返回这次真正加进去几条(去重之后)。 */
|
|
716
|
-
private
|
|
735
|
+
private appendSources(id: string, sources: Array<Partial<KnowledgeSource>>): number {
|
|
717
736
|
if (sources.length === 0) return 0
|
|
718
|
-
const current =
|
|
737
|
+
const current = this.read(id)
|
|
719
738
|
const existingByKey = new Map((current?.sources ?? []).map(source => [sourceKey(source), source]))
|
|
720
739
|
const seen = new Set(existingByKey.keys())
|
|
721
|
-
let ordinal =
|
|
740
|
+
let ordinal = Number((this.db!.prepare('SELECT COALESCE(MAX(ordinal), -1) + 1 AS n FROM knowledge_sources WHERE knowledge_id = ?').get(id) as { n: number }).n)
|
|
722
741
|
let added = 0
|
|
723
742
|
const now = new Date().toISOString()
|
|
724
743
|
for (const source of sources) {
|
|
725
744
|
const row = normalizeSource(source, now)
|
|
726
745
|
const key = sourceKey(row)
|
|
727
746
|
if (seen.has(key)) {
|
|
728
|
-
// 同一出处的来源版本是血缘元数据,不是新的 Knowledge 内容版本。
|
|
729
|
-
const previous = existingByKey.get(key)
|
|
730
|
-
if (row.toVersion !== undefined && previous?.toVersion !== row.toVersion) {
|
|
731
|
-
this.db!.prepare(
|
|
732
|
-
'UPDATE knowledge_sources SET to_version = ? WHERE knowledge_id = ? AND document_id = ? AND quote = ? AND author = ? AND statement = ?',
|
|
733
|
-
).run(row.toVersion, id, row.documentId, row.quote, row.author, row.statement)
|
|
734
|
-
}
|
|
735
747
|
continue
|
|
736
748
|
}
|
|
737
749
|
seen.add(key)
|
|
@@ -748,7 +760,10 @@ export class KnowledgeStore {
|
|
|
748
760
|
return added
|
|
749
761
|
}
|
|
750
762
|
|
|
751
|
-
async get(id: string): Promise<Knowledge | null> {
|
|
763
|
+
async get(id: string): Promise<Knowledge | null> { return this.read(id) }
|
|
764
|
+
|
|
765
|
+
private read(id: string): Knowledge | null {
|
|
766
|
+
if (this.db === null) throw new Error('KNOWLEDGE_STORE_UNAVAILABLE')
|
|
752
767
|
const row = this.db.prepare('SELECT * FROM knowledge WHERE id = ?').get(id) as Record<string, unknown> | undefined
|
|
753
768
|
if (row === undefined) return null
|
|
754
769
|
const sources = this.db.prepare(
|
|
@@ -1341,7 +1356,8 @@ export class KnowledgeStore {
|
|
|
1341
1356
|
}
|
|
1342
1357
|
|
|
1343
1358
|
close(): void {
|
|
1344
|
-
this.
|
|
1359
|
+
this.owner?.close()
|
|
1360
|
+
this.owner = null
|
|
1345
1361
|
this.db = null
|
|
1346
1362
|
}
|
|
1347
1363
|
}
|
|
@@ -1447,7 +1463,7 @@ function hasColumn(db: { prepare: (sql: string) => { all: (...params: unknown[])
|
|
|
1447
1463
|
|
|
1448
1464
|
function sourceKey(source: KnowledgeSource): string {
|
|
1449
1465
|
return source.documentId
|
|
1450
|
-
? `d\u0001${source.documentId}\u0001${source.quote}`
|
|
1466
|
+
? `d\u0001${source.documentId}\u0001${source.toVersion ?? ''}\u0001${source.quote}`
|
|
1451
1467
|
: `h\u0001${source.author}\u0001${source.statement}`
|
|
1452
1468
|
}
|
|
1453
1469
|
|
package/src/layout.ts
CHANGED
|
@@ -381,6 +381,8 @@ export function intentRoot(agentRoot: string): string {
|
|
|
381
381
|
export const KNOWLEDGE_DB_FILE = 'knowledge.db'
|
|
382
382
|
export const DERIVED_DB_FILE = 'derived.db'
|
|
383
383
|
export const ASSETS_DIR = 'assets'
|
|
384
|
+
/** Hidden marker: standalone published images must survive source/card orphan sweeps. */
|
|
385
|
+
export const PUBLISHED_IMAGE_MARKER = '.dsh-published-image'
|
|
384
386
|
/**
|
|
385
387
|
* 原始输入的落点(0067)。与 {@link ASSETS_DIR} **并列且同构**:一个装内容里提取出来的
|
|
386
388
|
* 图,一个装用户送进来的原件 —— 两者都是「从这个知识库里能直接拿到手的东西」,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { KnowledgeConfig } from '../types.js'
|
|
2
|
+
import type { DerivedDb } from '../store/derived.js'
|
|
3
|
+
|
|
4
|
+
export interface VectorItem {
|
|
5
|
+
unitId: string
|
|
6
|
+
contentHash: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface VectorManifest {
|
|
10
|
+
version: number
|
|
11
|
+
createdAt: string
|
|
12
|
+
provider: KnowledgeConfig['embeddingProvider']
|
|
13
|
+
model: string
|
|
14
|
+
modelFingerprint?: string
|
|
15
|
+
/**
|
|
16
|
+
* 编码实现的指纹(0031)。旧清单没有这个字段 → 视为不兼容,于是修复编码器之后
|
|
17
|
+
* 一定触发一次真正的重编码,而不是把旧向量按 contentHash 复用回来。
|
|
18
|
+
*/
|
|
19
|
+
encoder?: string
|
|
20
|
+
device: string
|
|
21
|
+
dimension: number
|
|
22
|
+
quantization: 'binary+hamming+int8'
|
|
23
|
+
itemCount: number
|
|
24
|
+
items: VectorItem[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LoadedVectorIndex {
|
|
28
|
+
manifest: VectorManifest
|
|
29
|
+
binary: Buffer
|
|
30
|
+
int8: Buffer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Validate a generation's BLOBs before either reuse or query. */
|
|
34
|
+
export function decodeVectorIndex(loaded: ReturnType<DerivedDb['vectorIndex']>, encoder: string): LoadedVectorIndex | null {
|
|
35
|
+
if (!loaded) return null
|
|
36
|
+
const manifest = loaded.manifest as VectorManifest
|
|
37
|
+
if (!manifest || manifest.quantization !== 'binary+hamming+int8' || manifest.encoder !== encoder
|
|
38
|
+
|| !Number.isInteger(manifest.dimension) || manifest.dimension < 8 || manifest.dimension % 8 !== 0
|
|
39
|
+
|| !Number.isInteger(manifest.itemCount) || !Array.isArray(manifest.items)
|
|
40
|
+
|| manifest.itemCount !== manifest.items.length
|
|
41
|
+
|| loaded.binary.length !== manifest.itemCount * Math.ceil(manifest.dimension / 8)
|
|
42
|
+
|| loaded.int8.length !== manifest.itemCount * manifest.dimension) return null
|
|
43
|
+
return { ...loaded, manifest }
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContextGraph } from '../types.js'
|
|
2
|
+
import type { KnowledgeDb } from '../store/knowledge-db.js'
|
|
3
|
+
|
|
4
|
+
export function liveGraphKeys(db: KnowledgeDb): Set<string> {
|
|
5
|
+
const rows = db.raw("SELECT entity_type AS type, entity_id AS id FROM card UNION SELECT 'source' AS type, entity_id AS id FROM source").all() as Array<{ type: string; id: string }>
|
|
6
|
+
return new Set(rows.map(row => `${row.type}:${row.id}`))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A retained generation must not restore entities that the user deleted. */
|
|
10
|
+
export function visibleGraph(graph: ContextGraph, live: Set<string>): ContextGraph {
|
|
11
|
+
const nodes = graph.nodes.filter(node => live.has(`${node.type}:${node.id}`))
|
|
12
|
+
const keys = new Set(nodes.map(node => `${node.type}:${node.id}`))
|
|
13
|
+
return { nodes, edges: graph.edges.filter(edge => keys.has(String(edge.from)) && keys.has(String(edge.to))) }
|
|
14
|
+
}
|