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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { modelsForSelection, UV_VERSION } from './manifest.js'
|
|
8
|
+
import { DEFAULT_MODEL_SELECTION, type ModelSelection } from './model-catalog.js'
|
|
9
|
+
import { writeModelSelection } from './selection.js'
|
|
10
|
+
import { runProcess, type ProcessOptions } from './process.js'
|
|
11
|
+
|
|
12
|
+
export const requirementsFile = fileURLToPath(new URL('../../../python/requirements.txt', import.meta.url))
|
|
13
|
+
export const setupScript = fileURLToPath(new URL('../../../python/prepare-models.py', import.meta.url))
|
|
14
|
+
export function managedPython(root: string): string { return process.platform === 'win32' ? join(root, 'python', 'Scripts', 'python.exe') : join(root, 'python', 'bin', 'python') }
|
|
15
|
+
|
|
16
|
+
async function download(url: string, signal: AbortSignal): Promise<Buffer> {
|
|
17
|
+
const response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]) })
|
|
18
|
+
if (!response.ok) throw new Error(`下载失败(HTTP ${response.status}):${url}`)
|
|
19
|
+
return Buffer.from(await response.arrayBuffer())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function findExecutable(root: string, name: string): Promise<string | undefined> {
|
|
23
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
24
|
+
const path = join(root, entry.name)
|
|
25
|
+
if (entry.isFile() && entry.name === name) return path
|
|
26
|
+
if (entry.isDirectory()) { const found = await findExecutable(path, name); if (found) return found }
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Prefer existing uv; otherwise install a checksummed release in this resource root. */
|
|
32
|
+
async function ensureUv(root: string, signal: AbortSignal, log: (line: string) => void, run: typeof runProcess): Promise<string> {
|
|
33
|
+
const name = process.platform === 'win32' ? 'uv.exe' : 'uv'
|
|
34
|
+
for (const candidate of [join(root, 'tools', name), 'uv', join(homedir(), '.local', 'bin', name), join(homedir(), '.cargo', 'bin', name)]) {
|
|
35
|
+
try { await run(candidate, ['--version'], { signal }); return candidate } catch { if (signal.aborted) throw new Error('ENVIRONMENT_CANCELLED') }
|
|
36
|
+
}
|
|
37
|
+
const arch = process.arch === 'arm64' ? 'aarch64' : process.arch === 'x64' ? 'x86_64' : undefined
|
|
38
|
+
const target = process.platform === 'darwin' ? 'apple-darwin' : process.platform === 'linux' ? 'unknown-linux-gnu' : process.platform === 'win32' ? 'pc-windows-msvc' : undefined
|
|
39
|
+
if (!arch || !target) throw new Error(`暂不支持自动安装:${process.platform}/${process.arch},请手动安装 uv 后重试`)
|
|
40
|
+
const extension = process.platform === 'win32' ? 'zip' : 'tar.gz'
|
|
41
|
+
const asset = `uv-${arch}-${target}.${extension}`
|
|
42
|
+
const url = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${asset}`
|
|
43
|
+
log('正在下载 Python 安装工具 uv')
|
|
44
|
+
const checksum = (await download(`${url}.sha256`, signal)).toString().match(/[a-f0-9]{64}/i)?.[0]
|
|
45
|
+
const archive = await download(url, signal)
|
|
46
|
+
if (!checksum || createHash('sha256').update(archive).digest('hex') !== checksum.toLowerCase()) throw new Error('uv 下载文件校验失败,请重试')
|
|
47
|
+
const scratch = join(root, `.setup-uv-${randomUUID()}`)
|
|
48
|
+
await mkdir(scratch, { recursive: true })
|
|
49
|
+
try {
|
|
50
|
+
const path = join(scratch, asset)
|
|
51
|
+
await writeFile(path, archive)
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const script = join(scratch, 'expand.ps1')
|
|
54
|
+
await writeFile(script, 'param([string]$Archive,[string]$Destination)\nExpand-Archive -LiteralPath $Archive -DestinationPath $Destination\n')
|
|
55
|
+
await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script, path, join(scratch, 'expanded')], { signal })
|
|
56
|
+
} else await run('tar', ['-xzf', path, '-C', scratch], { signal })
|
|
57
|
+
const binary = await findExecutable(scratch, name)
|
|
58
|
+
if (!binary) throw new Error('uv 安装包缺少可执行文件')
|
|
59
|
+
const destination = join(root, 'tools', name)
|
|
60
|
+
await mkdir(dirname(destination), { recursive: true })
|
|
61
|
+
await copyFile(binary, destination)
|
|
62
|
+
if (process.platform !== 'win32') await chmod(destination, 0o755)
|
|
63
|
+
return destination
|
|
64
|
+
} finally { await rm(scratch, { recursive: true, force: true }) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function installLock(root: string): Promise<() => Promise<void>> {
|
|
68
|
+
const path = join(root, 'environment-install.lock')
|
|
69
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
const file = await open(path, 'wx')
|
|
72
|
+
try { await file.writeFile(JSON.stringify({ pid: process.pid, at: new Date().toISOString() })) }
|
|
73
|
+
catch (cause) { await rm(path, { force: true }); throw cause }
|
|
74
|
+
finally { await file.close() }
|
|
75
|
+
return () => rm(path, { force: true })
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
78
|
+
let pid = 0
|
|
79
|
+
try { pid = Number(JSON.parse(await readFile(path, 'utf8')).pid) } catch { throw new Error('安装锁异常,请检查 resource/environment-install.lock') }
|
|
80
|
+
if (!Number.isInteger(pid) || pid <= 0) throw new Error('安装锁异常,请检查 resource/environment-install.lock')
|
|
81
|
+
try { process.kill(pid, 0); throw new Error('另一个进程正在准备环境,请等待完成后重试') }
|
|
82
|
+
catch (cause) {
|
|
83
|
+
if ((cause as NodeJS.ErrnoException).code !== 'ESRCH') throw cause
|
|
84
|
+
await rm(path, { force: true })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error('无法获取环境安装锁')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function installRequiredEnvironment(root: string, signal: AbortSignal, stage: (label: string) => void, log: (line: string) => void, runner: typeof runProcess = runProcess, selection: ModelSelection = DEFAULT_MODEL_SELECTION, basePython = process.env.DSH_PYTHON || 'python3'): Promise<void> {
|
|
92
|
+
await mkdir(root, { recursive: true })
|
|
93
|
+
const unlock = await installLock(root)
|
|
94
|
+
const env = { ...process.env, UV_CACHE_DIR: join(root, 'cache', 'uv'), HF_HOME: join(root, 'cache', 'huggingface'), PIP_DISABLE_PIP_VERSION_CHECK: '1' }
|
|
95
|
+
const run = (command: string, args: string[], extra: ProcessOptions = {}): Promise<string> => runner(command, args, { env, signal, timeoutMs: 30 * 60_000, onLine: log, ...extra })
|
|
96
|
+
try {
|
|
97
|
+
stage('准备模型运行时')
|
|
98
|
+
const uv = await ensureUv(root, signal, log, runner)
|
|
99
|
+
const versionOutput = await run(basePython, ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], { timeoutMs: 15_000 })
|
|
100
|
+
const baseVersion = versionOutput.trim().split(/\r?\n/).at(-1)
|
|
101
|
+
if (!baseVersion || !/^3\.(10|11|12|13)$/.test(baseVersion)) {
|
|
102
|
+
throw new Error(`DSH Python 版本不受支持:${baseVersion || '无法读取'};需要 Python 3.10–3.13`)
|
|
103
|
+
}
|
|
104
|
+
const python = managedPython(root)
|
|
105
|
+
let compatible = false
|
|
106
|
+
if (existsSync(python)) {
|
|
107
|
+
try {
|
|
108
|
+
const output = await run(python, ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], { timeoutMs: 15_000 })
|
|
109
|
+
compatible = output.trim().split(/\r?\n/).at(-1) === baseVersion
|
|
110
|
+
} catch { if (signal.aborted) throw new Error('ENVIRONMENT_CANCELLED') }
|
|
111
|
+
}
|
|
112
|
+
if (!compatible) {
|
|
113
|
+
if (existsSync(join(root, 'python'))) {
|
|
114
|
+
const backup = join(root, `python-backup-${randomUUID()}`)
|
|
115
|
+
await rename(join(root, 'python'), backup)
|
|
116
|
+
log(`原模型运行环境不可用,已保留备份:${backup}`)
|
|
117
|
+
}
|
|
118
|
+
await run(uv, ['venv', '--python', basePython, '--python-preference', 'only-system', join(root, 'python')])
|
|
119
|
+
}
|
|
120
|
+
stage('安装推理依赖')
|
|
121
|
+
await run(uv, ['pip', 'install', '--reinstall', '--python', python, '-r', requirementsFile])
|
|
122
|
+
const selectedModels = modelsForSelection(selection)
|
|
123
|
+
for (const model of selectedModels) {
|
|
124
|
+
stage(model.kind === 'embedding' ? '下载向量模型' : '下载重排模型')
|
|
125
|
+
await run(python, [setupScript, '--repo', model.repo, '--revision', model.revision, '--destination', join(root, 'models', model.directory)])
|
|
126
|
+
}
|
|
127
|
+
await writeModelSelection(root, selection)
|
|
128
|
+
} finally { await unlock() }
|
|
129
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DEFAULT_MODEL_SELECTION, embeddingModel, rerankModel } from './model-catalog.js'
|
|
2
|
+
|
|
3
|
+
/** Revisions are immutable Hub commit IDs, not a moving main branch. */
|
|
4
|
+
export const MODEL_REVISIONS = {
|
|
5
|
+
'bge-small-zh-v1.5': '7999e1d3359715c523056ef9478215996d62a620',
|
|
6
|
+
'bge-base-zh-v1.5': 'f03589ceff5aac7111bd60cfc7d497ca17ecac65',
|
|
7
|
+
'bge-large-zh-v1.5': '79e7739b6ab944e86d6171e44d24c997fc1e0116',
|
|
8
|
+
'wemm-embedding-2b-quantized': '524bbe61e1b8ce4673e4e17dcf6a8694a3c45993',
|
|
9
|
+
'wemm-embedding-2b': 'bbd6cd4bf52cfc6716f752a2df80b2706720bd95',
|
|
10
|
+
'bge-reranker-base': '2cfc18c9415c912f9d8155881c133215df768a70',
|
|
11
|
+
'bge-reranker-large': '55611d7bca2a7133960a6d3b71e083071bbfc312',
|
|
12
|
+
'qwen3-reranker-0.6b': 'e61197ed45024b0ed8a2d74b80b4d909f1255473',
|
|
13
|
+
'qwen3-reranker-4b': '22e683669bc0f0bd69640a1354a6d0aebcfeede5',
|
|
14
|
+
'qwen3-reranker-8b': '77d193c791ed757ca307ee72715aa132723da912',
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export function modelsForSelection(selection = DEFAULT_MODEL_SELECTION) {
|
|
18
|
+
const embedding = embeddingModel(selection.embedding)
|
|
19
|
+
const rerank = rerankModel(selection.rerank)
|
|
20
|
+
return [
|
|
21
|
+
{ ...embedding, kind: 'embedding' as const, revision: MODEL_REVISIONS[embedding.id] },
|
|
22
|
+
{ ...rerank, kind: 'rerank' as const, revision: MODEL_REVISIONS[rerank.id] },
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const REQUIRED_MODELS = modelsForSelection()
|
|
27
|
+
export const UV_VERSION = '0.13.1'
|
|
28
|
+
export const ENVIRONMENT_PROBE_VERSION = 2
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Models supported by the local embedding and reranking workers. Revisions are pinned in manifest.ts. */
|
|
2
|
+
export const EMBEDDING_MODELS = [
|
|
3
|
+
{
|
|
4
|
+
id: 'bge-small-zh-v1.5', repo: 'BAAI/bge-small-zh-v1.5', directory: 'bge-small-zh-v1.5',
|
|
5
|
+
label: 'BGE Small 中文', dimension: 512, download: '约 96 MB',
|
|
6
|
+
description: '资源占用最低,适合先跑通本地知识检索。',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'bge-base-zh-v1.5', repo: 'BAAI/bge-base-zh-v1.5', directory: 'bge-base-zh-v1.5',
|
|
10
|
+
label: 'BGE Base 中文', dimension: 768, download: '约 409 MB',
|
|
11
|
+
description: '效果与资源占用较均衡。',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'bge-large-zh-v1.5', repo: 'BAAI/bge-large-zh-v1.5', directory: 'bge-large-zh-v1.5',
|
|
15
|
+
label: 'BGE Large 中文', dimension: 1024, download: '约 1.3 GB',
|
|
16
|
+
description: '中文向量效果优先,需要更多内存与磁盘。',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'wemm-embedding-2b-quantized', repo: 'ewin-reg/WeMM-Embedding-2B-Quantized', directory: 'WeMM-Embedding-2B-Quantized',
|
|
20
|
+
label: 'WeMM Embedding 2B 社区量化版', dimension: 2048, download: '约 1.8 GB',
|
|
21
|
+
description: '社区维护的 INT4 / FP8 量化模型,非腾讯官方发布;模型卡标注 license: other,请查看协议。',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'wemm-embedding-2b', repo: 'tencent/WeMM-Embedding-2B', directory: 'WeMM-Embedding-2B',
|
|
25
|
+
label: '腾讯 WeMM Embedding 2B(BF16)', dimension: 2048, download: '约 5.5 GB',
|
|
26
|
+
description: '多模态向量模型,支持文本和图片;下载与推理资源占用较高。',
|
|
27
|
+
},
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
export const RERANK_MODELS = [
|
|
31
|
+
{
|
|
32
|
+
id: 'bge-reranker-base', repo: 'BAAI/bge-reranker-base', directory: 'bge-reranker-base',
|
|
33
|
+
label: 'BGE Reranker Base', download: '约 1.1 GB',
|
|
34
|
+
description: '中文重排的轻量选项。',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'qwen3-reranker-0.6b', repo: 'Qwen/Qwen3-Reranker-0.6B', directory: 'Qwen3-Reranker-0.6B',
|
|
38
|
+
label: 'Qwen3 Reranker 0.6B', download: '约 1.2 GB',
|
|
39
|
+
description: '支持 100 多种语言,适合先试用 Qwen 重排。',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'bge-reranker-large', repo: 'BAAI/bge-reranker-large', directory: 'bge-reranker-large',
|
|
43
|
+
label: 'BGE Reranker Large', download: '约 2.2 GB',
|
|
44
|
+
description: '中文和英文重排效果优先,推理速度较慢。',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'qwen3-reranker-4b', repo: 'Qwen/Qwen3-Reranker-4B', directory: 'Qwen3-Reranker-4B',
|
|
48
|
+
label: 'Qwen3 Reranker 4B', download: '约 8.1 GB',
|
|
49
|
+
description: 'Qwen 中档重排模型;请先确认磁盘与内存充足。',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'qwen3-reranker-8b', repo: 'Qwen/Qwen3-Reranker-8B', directory: 'Qwen3-Reranker-8B',
|
|
53
|
+
label: 'Qwen3 Reranker 8B', download: '约 16.4 GB',
|
|
54
|
+
description: '资源占用最高;下载前请确认有足够磁盘与内存。',
|
|
55
|
+
},
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
export type EmbeddingModelId = (typeof EMBEDDING_MODELS)[number]['id']
|
|
59
|
+
export type RerankModelId = (typeof RERANK_MODELS)[number]['id']
|
|
60
|
+
export interface ModelSelection { embedding: EmbeddingModelId; rerank: RerankModelId }
|
|
61
|
+
export interface ModelOptionView {
|
|
62
|
+
id: string
|
|
63
|
+
label: string
|
|
64
|
+
dimension?: number
|
|
65
|
+
download: string
|
|
66
|
+
description: string
|
|
67
|
+
modelCardUrl?: string
|
|
68
|
+
}
|
|
69
|
+
export const DEFAULT_MODEL_SELECTION: ModelSelection = {
|
|
70
|
+
embedding: 'bge-small-zh-v1.5',
|
|
71
|
+
rerank: 'bge-reranker-base',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isModelSelection(value: unknown): value is ModelSelection {
|
|
75
|
+
if (!value || typeof value !== 'object') return false
|
|
76
|
+
const selection = value as Record<string, unknown>
|
|
77
|
+
return EMBEDDING_MODELS.some(model => model.id === selection.embedding)
|
|
78
|
+
&& RERANK_MODELS.some(model => model.id === selection.rerank)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseModelSelection(value: unknown, fallback = DEFAULT_MODEL_SELECTION): ModelSelection {
|
|
82
|
+
if (value === undefined || value === null) return { ...fallback }
|
|
83
|
+
if (!isModelSelection(value)) throw new Error('模型选择无效,请重新选择向量模型和重排模型')
|
|
84
|
+
return { embedding: value.embedding, rerank: value.rerank }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function embeddingModel(id: EmbeddingModelId) {
|
|
88
|
+
return EMBEDDING_MODELS.find(model => model.id === id)!
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function rerankModel(id: RerankModelId) {
|
|
92
|
+
return RERANK_MODELS.find(model => model.id === id)!
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function modelOptionsView() {
|
|
96
|
+
const present = <T extends { id: string; label: string; download: string; description: string; repo: string }>(model: T) => ({
|
|
97
|
+
id: model.id, label: model.label, download: model.download, description: model.description,
|
|
98
|
+
modelCardUrl: `https://huggingface.co/${model.repo}`,
|
|
99
|
+
...('dimension' in model ? { dimension: model.dimension as number } : {}),
|
|
100
|
+
})
|
|
101
|
+
return {
|
|
102
|
+
embedding: EMBEDDING_MODELS.map(present),
|
|
103
|
+
rerank: RERANK_MODELS.map(present),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export interface ProcessOptions {
|
|
4
|
+
cwd?: string
|
|
5
|
+
env?: NodeJS.ProcessEnv
|
|
6
|
+
signal?: AbortSignal
|
|
7
|
+
timeoutMs?: number
|
|
8
|
+
onLine?: (line: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** No shell, bounded output, and only this child PID is stopped. */
|
|
12
|
+
export function runProcess(command: string, args: string[], options: ProcessOptions = {}): Promise<string> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
if (options.signal?.aborted) return reject(new Error('ENVIRONMENT_CANCELLED'))
|
|
15
|
+
const child = spawn(command, args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
16
|
+
let output = '', tail = '', settled = false, stopError: Error | undefined
|
|
17
|
+
let killTimer: ReturnType<typeof setTimeout> | undefined
|
|
18
|
+
const end = (error?: Error): void => {
|
|
19
|
+
if (settled) return
|
|
20
|
+
settled = true
|
|
21
|
+
clearTimeout(timer)
|
|
22
|
+
clearTimeout(killTimer)
|
|
23
|
+
options.signal?.removeEventListener('abort', cancel)
|
|
24
|
+
if (error) reject(error)
|
|
25
|
+
else resolve(output)
|
|
26
|
+
}
|
|
27
|
+
// Keep the install lock until this owned child has actually exited.
|
|
28
|
+
const stop = (error: Error): void => {
|
|
29
|
+
if (settled || stopError) return
|
|
30
|
+
stopError = error
|
|
31
|
+
clearTimeout(timer)
|
|
32
|
+
child.kill('SIGTERM')
|
|
33
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 1500)
|
|
34
|
+
}
|
|
35
|
+
const cancel = (): void => stop(new Error('ENVIRONMENT_CANCELLED'))
|
|
36
|
+
const timer = setTimeout(() => stop(new Error(`环境步骤超时:${command}`)), options.timeoutMs ?? 30_000)
|
|
37
|
+
options.signal?.addEventListener('abort', cancel, { once: true })
|
|
38
|
+
const receive = (data: Buffer): void => {
|
|
39
|
+
const text = data.toString()
|
|
40
|
+
output = (output + text).slice(-128_000)
|
|
41
|
+
tail = (tail + text).slice(-16_000)
|
|
42
|
+
const lines = tail.split(/[\r\n]+/)
|
|
43
|
+
tail = lines.pop() ?? ''
|
|
44
|
+
for (const line of lines) if (line.trim()) options.onLine?.(line.trim())
|
|
45
|
+
}
|
|
46
|
+
child.stdout.on('data', receive)
|
|
47
|
+
child.stderr.on('data', receive)
|
|
48
|
+
child.once('error', error => end(error))
|
|
49
|
+
child.once('close', code => {
|
|
50
|
+
if (tail.trim()) options.onLine?.(tail.trim())
|
|
51
|
+
end(stopError ?? (code === 0 ? undefined : new Error(`环境步骤失败(${code ?? '已停止'}):${output.slice(-3000).trim() || command}`)))
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { DEFAULT_MODEL_SELECTION, embeddingModel, isModelSelection, type ModelSelection } from './model-catalog.js'
|
|
5
|
+
|
|
6
|
+
export const MODEL_SELECTION_FILE = 'environment-model-selection.json'
|
|
7
|
+
|
|
8
|
+
export function readModelSelection(resourceRoot: string): ModelSelection {
|
|
9
|
+
try {
|
|
10
|
+
const value: unknown = JSON.parse(readFileSync(join(resourceRoot, MODEL_SELECTION_FILE), 'utf8'))
|
|
11
|
+
if (isModelSelection(value)) return { embedding: value.embedding, rerank: value.rerank }
|
|
12
|
+
} catch { /* first install or damaged selection: use the shipped defaults */ }
|
|
13
|
+
return { ...DEFAULT_MODEL_SELECTION }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function selectedEmbedding(resourceRoot: string | undefined): { path?: string; dimension?: number } {
|
|
17
|
+
if (!resourceRoot) return {}
|
|
18
|
+
const model = embeddingModel(readModelSelection(resourceRoot).embedding)
|
|
19
|
+
const path = join(resourceRoot, 'models', model.directory)
|
|
20
|
+
return existsSync(path) ? { path, dimension: model.dimension } : {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function writeModelSelection(resourceRoot: string, selection: ModelSelection): Promise<void> {
|
|
24
|
+
await mkdir(resourceRoot, { recursive: true })
|
|
25
|
+
const destination = join(resourceRoot, MODEL_SELECTION_FILE)
|
|
26
|
+
const temporary = `${destination}.${process.pid}.tmp`
|
|
27
|
+
await writeFile(temporary, `${JSON.stringify(selection, null, 2)}\n`, 'utf8')
|
|
28
|
+
await rename(temporary, destination)
|
|
29
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
5
|
+
import { embeddingFingerprint } from '../compiler/embedding-identity.js'
|
|
6
|
+
import type { RuntimeKnowledgeConfig } from '../types.js'
|
|
7
|
+
import { installRequiredEnvironment, requirementsFile } from './install.js'
|
|
8
|
+
import { modelOptionsView, parseModelSelection, type ModelSelection } from './model-catalog.js'
|
|
9
|
+
import { ENVIRONMENT_PROBE_VERSION } from './manifest.js'
|
|
10
|
+
import { runProcess } from './process.js'
|
|
11
|
+
import { readModelSelection, writeModelSelection } from './selection.js'
|
|
12
|
+
import type { EnvironmentItem, EnvironmentItemId, EnvironmentReport, EnvironmentView } from './types.js'
|
|
13
|
+
|
|
14
|
+
export interface EnvironmentOptions {
|
|
15
|
+
resourceRoot: string
|
|
16
|
+
snapshot: () => { config: RuntimeKnowledgeConfig; paths: { python: string; embeddingModel: string; rerankModel: string } }
|
|
17
|
+
probe: (kind: 'embedding' | 'rerank') => Promise<{ ok: boolean; message: string }>
|
|
18
|
+
reload: () => void
|
|
19
|
+
run?: typeof runProcess
|
|
20
|
+
install?: typeof installRequiredEnvironment
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const labels = { embedding: '向量模型', rerank: '重排模型' }
|
|
24
|
+
function blankReport(): EnvironmentReport {
|
|
25
|
+
return { state: 'unchecked', required: true, items: (['embedding', 'rerank'] as const).map(id => ({ id, label: labels[id], state: 'unchecked', message: '首次使用前必须完成模型推理自检' })) }
|
|
26
|
+
}
|
|
27
|
+
function stamp(path: string): unknown { try { const value = statSync(path); return [value.size, value.mtimeMs] } catch { return null } }
|
|
28
|
+
function sitePackagesPath(pythonPath: string): string {
|
|
29
|
+
const environmentRoot = dirname(dirname(pythonPath))
|
|
30
|
+
const windows = join(environmentRoot, 'Lib', 'site-packages')
|
|
31
|
+
try { if (statSync(windows).isDirectory()) return windows } catch { /* use the platform-neutral lib layout */ }
|
|
32
|
+
const libraryRoot = join(environmentRoot, 'lib')
|
|
33
|
+
try {
|
|
34
|
+
const versionDirectory = readdirSync(libraryRoot).find(name => /^python\d+\.\d+$/.test(name))
|
|
35
|
+
if (versionDirectory) return join(libraryRoot, versionDirectory, 'site-packages')
|
|
36
|
+
} catch { /* no venv/site-packages directory */ }
|
|
37
|
+
return join(libraryRoot, 'site-packages')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function modelFilesProblem(path: string): string | undefined {
|
|
41
|
+
if (!isAbsolute(path)) return '尚未准备本地模型,请安装默认资源或填写已下载模型的完整路径'
|
|
42
|
+
try {
|
|
43
|
+
const files = readdirSync(path)
|
|
44
|
+
if (!files.includes('config.json')) return '模型缺少 config.json'
|
|
45
|
+
const weights = files.filter(name => /\.(safetensors|bin)$/.test(name))
|
|
46
|
+
if (!weights.length || weights.some(name => statSync(join(path, name)).size < 1024)) return '模型权重缺失或下载不完整'
|
|
47
|
+
const tokenizer = ['tokenizer.json', 'sentencepiece.bpe.model', 'vocab.txt'].some(name => files.includes(name) && statSync(join(path, name)).size >= 1024)
|
|
48
|
+
if (!tokenizer) return '模型缺少完整分词器词表'
|
|
49
|
+
} catch { return '模型目录不存在或无法读取' }
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Preparation and readiness are shared by all bases in one deployment. */
|
|
54
|
+
export class KnowledgeEnvironment {
|
|
55
|
+
private report = blankReport()
|
|
56
|
+
private job?: EnvironmentView['job']
|
|
57
|
+
private checkPending?: Promise<EnvironmentReport>
|
|
58
|
+
private work?: Promise<void>
|
|
59
|
+
private controller?: AbortController
|
|
60
|
+
private checkController?: AbortController
|
|
61
|
+
private disposed = false
|
|
62
|
+
constructor(private readonly options: EnvironmentOptions) {}
|
|
63
|
+
|
|
64
|
+
private fingerprint(): string {
|
|
65
|
+
const { config, paths } = this.options.snapshot()
|
|
66
|
+
const env = dirname(dirname(paths.python))
|
|
67
|
+
return createHash('sha256').update(JSON.stringify({
|
|
68
|
+
probe: ENVIRONMENT_PROBE_VERSION, paths,
|
|
69
|
+
provider: config.embeddingProvider, rerank: config.rerankProvider, device: config.embeddingDevice, dimension: config.embeddingDimension, endpoint: config.embeddingBaseUrl,
|
|
70
|
+
embedding: embeddingFingerprint(paths.embeddingModel, this.options.resourceRoot), rerankModel: embeddingFingerprint(paths.rerankModel, this.options.resourceRoot),
|
|
71
|
+
python: stamp(paths.python), venv: stamp(join(env, 'pyvenv.cfg')), dependencies: stamp(sitePackagesPath(paths.python)), requirements: stamp(requirementsFile),
|
|
72
|
+
})).digest('hex')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
view(): EnvironmentView {
|
|
76
|
+
if (!this.checkPending && this.report.state === 'ready' && this.report.fingerprint !== this.fingerprint()) this.report = blankReport()
|
|
77
|
+
const selection = readModelSelection(this.options.resourceRoot)
|
|
78
|
+
return structuredClone({ report: this.report, selection, models: modelOptionsView(), ...(this.job ? { job: this.job } : {}) })
|
|
79
|
+
}
|
|
80
|
+
invalidate(): void { if (!this.checkPending) this.report = blankReport() }
|
|
81
|
+
|
|
82
|
+
async requireReady(): Promise<void> {
|
|
83
|
+
if (this.disposed) throw new Error('环境服务已关闭')
|
|
84
|
+
if (this.job?.state === 'running') throw new Error('环境正在准备或自检,请等待完成后再编译和召回')
|
|
85
|
+
const view = this.view()
|
|
86
|
+
const recent = view.report.checkedAt && Date.now() - Date.parse(view.report.checkedAt) < 5 * 60_000
|
|
87
|
+
const report = view.report.state === 'ready' && recent ? view.report : await this.check(true)
|
|
88
|
+
if (report.state !== 'ready') throw new Error(`环境未就绪:${report.items.filter(item => item.state !== 'ready').map(item => `${item.label}:${item.message}`).join(';')}。请在 Tita 设置中完成环境准备`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
check(deep = true): Promise<EnvironmentReport> {
|
|
92
|
+
if (this.checkPending) return this.checkPending
|
|
93
|
+
const pending = this.performCheck(deep)
|
|
94
|
+
this.checkPending = pending
|
|
95
|
+
void pending.finally(() => { if (this.checkPending === pending) this.checkPending = undefined }).catch(() => undefined)
|
|
96
|
+
return pending
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private item(id: EnvironmentItemId, patch: Partial<EnvironmentItem>): void {
|
|
100
|
+
this.report.items = this.report.items.map(item => item.id === id ? { ...item, ...patch } : item)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async performCheck(deep: boolean): Promise<EnvironmentReport> {
|
|
104
|
+
const checkController = new AbortController()
|
|
105
|
+
this.checkController = checkController
|
|
106
|
+
const signal = this.controller ? AbortSignal.any([this.controller.signal, checkController.signal]) : checkController.signal
|
|
107
|
+
const fingerprint = this.fingerprint()
|
|
108
|
+
const { config, paths } = this.options.snapshot()
|
|
109
|
+
this.report = blankReport()
|
|
110
|
+
this.report.state = 'checking'
|
|
111
|
+
for (const id of ['embedding', 'rerank'] as const) {
|
|
112
|
+
if (signal.aborted || this.disposed) break
|
|
113
|
+
const path = id === 'embedding' ? paths.embeddingModel : paths.rerankModel
|
|
114
|
+
this.item(id, { path })
|
|
115
|
+
const disabled = id === 'embedding' ? config.embeddingProvider === 'none' : config.rerankProvider === 'none'
|
|
116
|
+
const problem = disabled ? '此能力是必需项,请启用模型'
|
|
117
|
+
: id === 'embedding' && config.embeddingProvider === 'ollama' ? undefined : modelFilesProblem(path)
|
|
118
|
+
if (problem) { this.item(id, { state: 'missing', message: problem }); continue }
|
|
119
|
+
if (!deep) { this.item(id, { state: 'unchecked', message: '资源已找到,尚未完成实际推理自检' }); continue }
|
|
120
|
+
this.item(id, { state: 'checking', message: '加载模型并运行推理自检,首次加载可能较慢' })
|
|
121
|
+
try {
|
|
122
|
+
const result = await this.options.probe(id)
|
|
123
|
+
this.item(id, { state: result.ok ? 'ready' : 'failed', message: result.message })
|
|
124
|
+
} catch (cause) { this.item(id, { state: 'failed', message: cause instanceof Error ? cause.message : String(cause) }) }
|
|
125
|
+
}
|
|
126
|
+
const changed = fingerprint !== this.fingerprint()
|
|
127
|
+
if (changed) this.report.items = this.report.items.map(item => item.state === 'ready'
|
|
128
|
+
? { ...item, state: 'unchecked', message: '自检期间配置或资源发生变化,请重新自检' }
|
|
129
|
+
: item)
|
|
130
|
+
if (signal.aborted || this.disposed) this.report.items = this.report.items.map(item => item.state === 'checking'
|
|
131
|
+
? { ...item, state: 'unchecked', message: '自检已停止,请重新运行' }
|
|
132
|
+
: item)
|
|
133
|
+
this.report.state = this.report.items.every(item => item.state === 'ready') ? 'ready' : 'blocked'
|
|
134
|
+
this.report.checkedAt = new Date().toISOString()
|
|
135
|
+
this.report.fingerprint = fingerprint
|
|
136
|
+
if (this.checkController === checkController) this.checkController = undefined
|
|
137
|
+
return structuredClone(this.report)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
start(action: 'check' | 'install', requestedSelection?: unknown): EnvironmentView {
|
|
141
|
+
if (this.disposed) throw new Error('环境服务已关闭')
|
|
142
|
+
if (this.job?.state === 'running') return this.view()
|
|
143
|
+
const previousSelection = readModelSelection(this.options.resourceRoot)
|
|
144
|
+
const selection: ModelSelection = action === 'install'
|
|
145
|
+
? parseModelSelection(requestedSelection, previousSelection)
|
|
146
|
+
: previousSelection
|
|
147
|
+
this.checkController?.abort()
|
|
148
|
+
this.controller = new AbortController()
|
|
149
|
+
const controller = this.controller
|
|
150
|
+
this.job = { id: randomUUID(), state: 'running', stage: action === 'install' ? '准备安装' : '环境自检', startedAt: new Date().toISOString(), logs: [] }
|
|
151
|
+
const job = this.job
|
|
152
|
+
const log = (line: string): void => { job.logs = [...job.logs, line.slice(-1500)].slice(-12) }
|
|
153
|
+
const work = async (): Promise<void> => {
|
|
154
|
+
try {
|
|
155
|
+
// A first-use guard can already be checking. Do not install into that probe.
|
|
156
|
+
if (this.checkPending) await this.checkPending
|
|
157
|
+
if (controller.signal.aborted) throw new Error('ENVIRONMENT_CANCELLED')
|
|
158
|
+
if (action === 'install') {
|
|
159
|
+
this.report = blankReport()
|
|
160
|
+
this.report.state = 'checking'
|
|
161
|
+
await (this.options.install ?? installRequiredEnvironment)(this.options.resourceRoot, controller.signal, label => {
|
|
162
|
+
job.stage = label
|
|
163
|
+
for (const item of this.report.items) if (item.state === 'checking') this.item(item.id, { state: 'unchecked', message: '准备步骤已完成,等待实际自检' })
|
|
164
|
+
const id = label.includes('向量') ? 'embedding' : label.includes('重排') ? 'rerank' : undefined
|
|
165
|
+
if (id) this.item(id, { state: 'checking', message: label })
|
|
166
|
+
}, log, this.options.run ?? runProcess, selection)
|
|
167
|
+
this.options.reload()
|
|
168
|
+
} else this.options.reload()
|
|
169
|
+
job.stage = '运行实际推理自检'
|
|
170
|
+
const report = await this.check(true)
|
|
171
|
+
if (controller.signal.aborted) throw new Error('ENVIRONMENT_CANCELLED')
|
|
172
|
+
if (report.state !== 'ready') throw new Error(report.items.filter(item => item.state !== 'ready').map(item => `${item.label}:${item.message}`).join(';'))
|
|
173
|
+
await mkdir(this.options.resourceRoot, { recursive: true })
|
|
174
|
+
await writeFile(join(this.options.resourceRoot, 'environment-last-check.json'), JSON.stringify(report, null, 2))
|
|
175
|
+
job.state = 'succeeded'; job.stage = '环境已就绪'
|
|
176
|
+
} catch (cause) {
|
|
177
|
+
if (action === 'install') {
|
|
178
|
+
const activeSelection = readModelSelection(this.options.resourceRoot)
|
|
179
|
+
if (activeSelection.embedding !== previousSelection.embedding || activeSelection.rerank !== previousSelection.rerank) {
|
|
180
|
+
try {
|
|
181
|
+
await writeModelSelection(this.options.resourceRoot, previousSelection)
|
|
182
|
+
this.options.reload()
|
|
183
|
+
this.report = blankReport()
|
|
184
|
+
log('所选模型未通过自检,已恢复此前启用的模型')
|
|
185
|
+
} catch (rollbackCause) {
|
|
186
|
+
log(`恢复此前模型选择失败:${rollbackCause instanceof Error ? rollbackCause.message : String(rollbackCause)}`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
job.state = controller.signal.aborted ? 'cancelled' : 'failed'
|
|
191
|
+
job.error = controller.signal.aborted ? '已停止,已下载的资源可在下次重试时复用' : cause instanceof Error ? cause.message : String(cause)
|
|
192
|
+
job.stage = controller.signal.aborted ? '已停止' : '准备未完成'
|
|
193
|
+
if (this.report.state !== 'ready') {
|
|
194
|
+
this.report.state = 'blocked'
|
|
195
|
+
for (const item of this.report.items) if (item.state === 'checking') this.item(item.id, { state: controller.signal.aborted ? 'unchecked' : 'failed', message: job.error })
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
job.finishedAt = new Date().toISOString()
|
|
199
|
+
if (this.controller === controller) this.controller = undefined
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
this.work = Promise.resolve().then(work)
|
|
203
|
+
return this.view()
|
|
204
|
+
}
|
|
205
|
+
async wait(): Promise<void> { await this.work }
|
|
206
|
+
cancel(): void {
|
|
207
|
+
this.controller?.abort(); this.checkController?.abort()
|
|
208
|
+
if (this.job?.state === 'running') this.job.stage = '正在停止,请等待当前步骤结束'
|
|
209
|
+
}
|
|
210
|
+
close(): void { this.disposed = true; this.cancel() }
|
|
211
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Installation state is deployment-wide; it is not knowledge or a build artifact. */
|
|
2
|
+
import type { ModelOptionView, ModelSelection } from './model-catalog.js'
|
|
3
|
+
|
|
4
|
+
export type EnvironmentItemId = 'embedding' | 'rerank'
|
|
5
|
+
export type EnvironmentItemState = 'unchecked' | 'checking' | 'ready' | 'missing' | 'failed'
|
|
6
|
+
export interface EnvironmentItem {
|
|
7
|
+
id: EnvironmentItemId
|
|
8
|
+
label: string
|
|
9
|
+
state: EnvironmentItemState
|
|
10
|
+
message: string
|
|
11
|
+
path?: string
|
|
12
|
+
}
|
|
13
|
+
export interface EnvironmentReport {
|
|
14
|
+
state: 'unchecked' | 'checking' | 'blocked' | 'ready'
|
|
15
|
+
required: true
|
|
16
|
+
checkedAt?: string
|
|
17
|
+
fingerprint?: string
|
|
18
|
+
items: EnvironmentItem[]
|
|
19
|
+
}
|
|
20
|
+
export interface EnvironmentJob {
|
|
21
|
+
id: string
|
|
22
|
+
state: 'running' | 'succeeded' | 'failed' | 'cancelled'
|
|
23
|
+
stage: string
|
|
24
|
+
startedAt: string
|
|
25
|
+
finishedAt?: string
|
|
26
|
+
error?: string
|
|
27
|
+
logs: string[]
|
|
28
|
+
}
|
|
29
|
+
export interface EnvironmentView {
|
|
30
|
+
report: EnvironmentReport
|
|
31
|
+
job?: EnvironmentJob
|
|
32
|
+
selection: ModelSelection
|
|
33
|
+
models: { embedding: ModelOptionView[]; rerank: ModelOptionView[] }
|
|
34
|
+
}
|
package/src/events.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { normalizeKnowledge } from './knowledge.js'
|
|
1
3
|
import type { CardCoordinate } from './types.js'
|
|
2
4
|
import type { TrialEvent, TrialSource } from './wiki-trial-types.js'
|
|
3
5
|
|
|
@@ -43,3 +45,47 @@ export interface EventEntity {
|
|
|
43
45
|
createdAt: string
|
|
44
46
|
updatedAt: string
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
export function compositeEventIdentity(occurredAt: string | null, coordinate: CardCoordinate, symptom: string | null): string {
|
|
50
|
+
const payload = [occurredAt, coordinate.objectId, coordinate.actionId, coordinate.sceneId, normalizeKnowledge(symptom ?? '')]
|
|
51
|
+
return 'composite:' + createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 24)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Recompute the aggregate after records are appended, pruned or redirected. */
|
|
55
|
+
export function eventEntityFromRecords(input: {
|
|
56
|
+
id: string
|
|
57
|
+
identityKey: string
|
|
58
|
+
identityType: EventEntity['identityType']
|
|
59
|
+
records: EventSourceRecord[]
|
|
60
|
+
createdAt: string
|
|
61
|
+
updatedAt?: string
|
|
62
|
+
}): EventEntity {
|
|
63
|
+
const { id, identityKey, identityType, records } = input
|
|
64
|
+
const first = records[0]
|
|
65
|
+
const firstCoordinate = records.find(record => record.coordinateIds.objectId || record.coordinateIds.actionId || record.coordinateIds.sceneId)
|
|
66
|
+
const sources = new Map<string, TrialSource>()
|
|
67
|
+
for (const record of records) {
|
|
68
|
+
for (const source of record.sources) sources.set(`${source.documentId}\u0001${source.quote}`, source)
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id, identityKey, identityType,
|
|
72
|
+
externalId: records.find(record => record.externalId)?.externalId ?? null,
|
|
73
|
+
title: first?.title ?? id,
|
|
74
|
+
summary: records.find(record => record.summary.trim())?.summary ?? first?.title ?? id,
|
|
75
|
+
coordinate: {
|
|
76
|
+
object: records.find(record => record.coordinate.object)?.coordinate.object ?? null,
|
|
77
|
+
action: records.find(record => record.coordinate.action)?.coordinate.action ?? null,
|
|
78
|
+
scene: records.find(record => record.coordinate.scene)?.coordinate.scene ?? null,
|
|
79
|
+
},
|
|
80
|
+
coordinateIds: firstCoordinate?.coordinateIds ?? { objectId: '', actionId: '', sceneId: '' },
|
|
81
|
+
occurredAt: records.find(record => record.occurredAt)?.occurredAt ?? null,
|
|
82
|
+
symptom: records.find(record => record.symptom)?.symptom ?? null,
|
|
83
|
+
timeline: records.flatMap(record => record.timeline.map(step => ({ ...step, sourceId: record.sourceId }))),
|
|
84
|
+
rootCauses: records.flatMap(record => record.rootCause ? [{ text: record.rootCause, status: record.rootCauseStatus, sourceId: record.sourceId }] : []),
|
|
85
|
+
outcomes: records.flatMap(record => record.outcome ? [{ text: record.outcome, sourceId: record.sourceId }] : []),
|
|
86
|
+
missing: [...new Set(records.flatMap(record => record.missing))],
|
|
87
|
+
sources: [...sources.values()], records,
|
|
88
|
+
createdAt: input.createdAt,
|
|
89
|
+
updatedAt: input.updatedAt ?? new Date().toISOString(),
|
|
90
|
+
}
|
|
91
|
+
}
|