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/lib/types/runtime.js
CHANGED
|
@@ -13,6 +13,7 @@ import { MAX_LOG_ITEMS, OperationLog, operationId } from './oplog.js';
|
|
|
13
13
|
import { ExtractionRunStore, extractionRunId } from './extraction-runs.js';
|
|
14
14
|
import { AxisRegistry, normalizeAxisName } from './axis-registry.js';
|
|
15
15
|
import { KNOWLEDGE_STATE_LABEL, KnowledgeStore } from './knowledge.js';
|
|
16
|
+
import { compositeEventIdentity, eventEntityFromRecords } from './events.js';
|
|
16
17
|
import { buildAliasIndex, candidateEntityId, extractCandidates, normalizeCandidates, RELATES_TO } from './extract.js';
|
|
17
18
|
import { skeletonBody } from './extract-model.js';
|
|
18
19
|
import { applyTrialAxisResolution, normalizeKnowledge, runWikiTrial, trialSummary } from './wiki-trial.js';
|
|
@@ -22,14 +23,22 @@ import { FTS_FILE, loadSqlite, segment } from './fts.js';
|
|
|
22
23
|
import { attachImagesToKnowledge, insertImage, planSourceImages } from './source-images.js';
|
|
23
24
|
import { buildWikiIndex, cardKey, deadLinksOf, planRelated, relatedEntriesOf, stripRelatedSection, RELATED_METADATA_KEY, } from './wiki-finalize.js';
|
|
24
25
|
import { applyRerank, applyThreshold, evidenceExcerpts, graphBoost, matchOf, noteIndexText, reviewWeight, rrfFuse, tokenSet } from './recall-rank.js';
|
|
25
|
-
import { ASSETS_DIR, KNOWLEDGE_DB_FILE, SOURCE_RAW_DIR, defaultAgentRoot, knowledgeBaseRoot } from './layout.js';
|
|
26
|
+
import { ASSETS_DIR, KNOWLEDGE_DB_FILE, PUBLISHED_IMAGE_MARKER, SOURCE_RAW_DIR, defaultAgentRoot, knowledgeBaseRoot } from './layout.js';
|
|
26
27
|
import { KnowledgeDb } from './store/knowledge-db.js';
|
|
27
28
|
import { exportKnowledgeTree as exportTree, migrateKnowledgeTree } from './store/migrate.js';
|
|
29
|
+
import { COMPILE_SNAPSHOT } from './compiler/snapshot.js';
|
|
30
|
+
import { decodeVectorIndex } from './retrieval/vector-index.js';
|
|
31
|
+
import { liveGraphKeys, visibleGraph } from './retrieval/visibility.js';
|
|
32
|
+
import { embeddingFingerprint } from './compiler/embedding-identity.js';
|
|
28
33
|
import { DerivedDb, DERIVED_DB_FILE } from './store/derived.js';
|
|
29
34
|
import { cardVersions as viewCardVersions, documentDetail as viewDocumentDetail, documentList as viewDocumentList, documentVersions as viewDocumentVersions } from './store/views.js';
|
|
35
|
+
import { assessKnowledgeEvidence, hasCurrentSupport } from './source-evidence.js';
|
|
36
|
+
import { redirectAxisReferences } from './store/axis-references.js';
|
|
30
37
|
import { unlinkIfExists, writeTextAtomic } from './fs-atomic.js';
|
|
31
38
|
import { envelopeFailure, envelopeSuccess } from 'dsh-data-contracts/envelope';
|
|
32
39
|
import { analyzeQuery, relationCandidates } from './query-analysis.js';
|
|
40
|
+
import { readModelSelection, selectedEmbedding } from './environment/selection.js';
|
|
41
|
+
import { rerankModel as rerankModelOption } from './environment/model-catalog.js';
|
|
33
42
|
// **知识树只有三段**(v1.1 起):`01_source_raw` / `02_wiki` / `03_compiled`。
|
|
34
43
|
//
|
|
35
44
|
// 撤掉的两段,理由都写在这里,避免下一个人重新加回来:
|
|
@@ -405,12 +414,18 @@ function withTitleAlias(metadata, previousTitle, nextTitle) {
|
|
|
405
414
|
* 同一句话在上下文里出现很多遍。完整出处在知识接口里。
|
|
406
415
|
*/
|
|
407
416
|
function recallKnowledgeOf(row, section) {
|
|
408
|
-
const first = row.sources.find(source => source.
|
|
417
|
+
const first = row.sources.find(source => source.evidenceStatus === 'current' && source.quoteInCurrentVersion)
|
|
418
|
+
?? row.sources.find(source => source.quote.trim() !== '') ?? row.sources[0];
|
|
409
419
|
return {
|
|
410
420
|
id: row.id,
|
|
411
421
|
text: row.text,
|
|
412
422
|
state: row.status,
|
|
413
423
|
section,
|
|
424
|
+
claimVerification: 'unverified',
|
|
425
|
+
requiresReview: !hasCurrentSupport(row),
|
|
426
|
+
...(first?.evidenceStatus ? { evidenceStatus: first.evidenceStatus } : {}),
|
|
427
|
+
...(first?.toVersion === undefined ? {} : { sourceVersion: first.toVersion }),
|
|
428
|
+
...(first?.currentVersion === undefined ? {} : { currentSourceVersion: first.currentVersion }),
|
|
414
429
|
...(first?.quote ? { quote: first.quote } : {}),
|
|
415
430
|
...(first?.documentId ? { sourceId: first.documentId } : {}),
|
|
416
431
|
...(first && first.headingPath.length > 0 ? { headingPath: first.headingPath } : {}),
|
|
@@ -701,42 +716,6 @@ function withEventReferences(content, rawRefs, eventsById) {
|
|
|
701
716
|
});
|
|
702
717
|
return `${base ? `${base}\n\n` : ''}## 关联 Event\n\n${bullets.join('\n')}`;
|
|
703
718
|
}
|
|
704
|
-
function eventEntityFromRecords(input) {
|
|
705
|
-
const { id, identityKey, identityType, records } = input;
|
|
706
|
-
const first = records[0];
|
|
707
|
-
const firstCoordinate = records.find(record => record.coordinateIds.objectId || record.coordinateIds.actionId || record.coordinateIds.sceneId);
|
|
708
|
-
const sources = new Map();
|
|
709
|
-
for (const record of records) {
|
|
710
|
-
for (const source of record.sources)
|
|
711
|
-
sources.set(`${source.documentId}\u0001${source.quote}`, source);
|
|
712
|
-
}
|
|
713
|
-
return {
|
|
714
|
-
id,
|
|
715
|
-
identityKey,
|
|
716
|
-
identityType,
|
|
717
|
-
externalId: records.find(record => record.externalId)?.externalId ?? null,
|
|
718
|
-
title: first?.title ?? id,
|
|
719
|
-
summary: records.find(record => record.summary.trim())?.summary ?? first?.title ?? id,
|
|
720
|
-
coordinate: {
|
|
721
|
-
object: records.find(record => record.coordinate.object)?.coordinate.object ?? null,
|
|
722
|
-
action: records.find(record => record.coordinate.action)?.coordinate.action ?? null,
|
|
723
|
-
scene: records.find(record => record.coordinate.scene)?.coordinate.scene ?? null,
|
|
724
|
-
},
|
|
725
|
-
coordinateIds: firstCoordinate?.coordinateIds ?? { objectId: '', actionId: '', sceneId: '' },
|
|
726
|
-
occurredAt: records.find(record => record.occurredAt)?.occurredAt ?? null,
|
|
727
|
-
symptom: records.find(record => record.symptom)?.symptom ?? null,
|
|
728
|
-
timeline: records.flatMap(record => record.timeline.map(step => ({ ...step, sourceId: record.sourceId }))),
|
|
729
|
-
rootCauses: records.flatMap(record => record.rootCause
|
|
730
|
-
? [{ text: record.rootCause, status: record.rootCauseStatus, sourceId: record.sourceId }]
|
|
731
|
-
: []),
|
|
732
|
-
outcomes: records.flatMap(record => record.outcome ? [{ text: record.outcome, sourceId: record.sourceId }] : []),
|
|
733
|
-
missing: [...new Set(records.flatMap(record => record.missing))],
|
|
734
|
-
sources: [...sources.values()],
|
|
735
|
-
records,
|
|
736
|
-
createdAt: input.createdAt,
|
|
737
|
-
updatedAt: input.updatedAt ?? new Date().toISOString(),
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
719
|
/** 把 `incoming` 按小节折进 `existing`;结构对不上时整段追加。 */
|
|
741
720
|
function foldBody(existing, incoming) {
|
|
742
721
|
if (!existing)
|
|
@@ -1410,8 +1389,8 @@ class JsonLineWorker {
|
|
|
1410
1389
|
/**
|
|
1411
1390
|
* 解析三个「家当」路径(0029)。
|
|
1412
1391
|
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1392
|
+
* 资源目录内的隔离解释器优先于 DSH_PYTHON;自定义配置路径仍由构造函数处理。
|
|
1393
|
+
* 未准备资源时回退到 DSH_PYTHON、`python3` 或 HF 模型名。
|
|
1415
1394
|
*
|
|
1416
1395
|
* `modelId` 把本地模型的**身份**与它的位置解耦,避免同一 Agent 根目录下的
|
|
1417
1396
|
* resource 目录变化导致向量清单误判为配置不匹配。
|
|
@@ -1425,15 +1404,22 @@ function resolveResources(resourceRoot) {
|
|
|
1425
1404
|
return fallback;
|
|
1426
1405
|
};
|
|
1427
1406
|
const python = firstExisting([
|
|
1407
|
+
root && process.platform === 'win32' ? join(root, 'python', 'Scripts', 'python.exe') : undefined,
|
|
1428
1408
|
root ? join(root, 'python', 'bin', 'python') : undefined,
|
|
1429
|
-
], 'python3');
|
|
1409
|
+
], process.env.DSH_PYTHON || 'python3');
|
|
1410
|
+
const selected = selectedEmbedding(root);
|
|
1411
|
+
const selection = root ? readModelSelection(root) : undefined;
|
|
1412
|
+
const selectedRerank = root && selection ? join(root, 'models', rerankModelOption(selection.rerank).directory) : undefined;
|
|
1430
1413
|
const embeddingModel = firstExisting([
|
|
1414
|
+
selected?.path,
|
|
1431
1415
|
root ? join(root, 'models', PREFERRED_LOCAL_EMBEDDING_MODEL_DIR) : undefined,
|
|
1432
1416
|
root ? join(root, 'models', LOCAL_EMBEDDING_MODEL_DIR) : undefined,
|
|
1433
1417
|
], FALLBACK_EMBEDDING_MODEL);
|
|
1434
1418
|
const rerankModel = firstExisting([
|
|
1419
|
+
selectedRerank,
|
|
1435
1420
|
root ? join(root, 'models', LOCAL_RERANK_MODEL_DIR) : undefined,
|
|
1436
1421
|
], FALLBACK_RERANK_MODEL);
|
|
1422
|
+
const embeddingDimension = selected?.path && embeddingModel === selected.path ? selected.dimension : undefined;
|
|
1437
1423
|
const modelId = (path) => {
|
|
1438
1424
|
for (const base of [root]) {
|
|
1439
1425
|
if (!base || !path.startsWith(`${base}${sep}`))
|
|
@@ -1443,7 +1429,13 @@ function resolveResources(resourceRoot) {
|
|
|
1443
1429
|
}
|
|
1444
1430
|
return path;
|
|
1445
1431
|
};
|
|
1446
|
-
return { python, embeddingModel, rerankModel, modelId };
|
|
1432
|
+
return { python, embeddingModel, rerankModel, embeddingDimension, modelId };
|
|
1433
|
+
}
|
|
1434
|
+
/** An inherited DSH_PYTHON value selects the base interpreter, not the dependency environment. */
|
|
1435
|
+
function resolveConfiguredPython(configured, resourcePython) {
|
|
1436
|
+
const selected = configured?.trim();
|
|
1437
|
+
const hostPython = process.env.DSH_PYTHON?.trim();
|
|
1438
|
+
return selected && selected !== hostPython ? selected : resourcePython;
|
|
1447
1439
|
}
|
|
1448
1440
|
class CompileTaskCancelledError extends Error {
|
|
1449
1441
|
constructor() {
|
|
@@ -1504,6 +1496,7 @@ function blankProgress(runId, only, startedAt, state = 'running', queuedAt = sta
|
|
|
1504
1496
|
};
|
|
1505
1497
|
}
|
|
1506
1498
|
export class KnowledgeRuntime {
|
|
1499
|
+
environmentGuard;
|
|
1507
1500
|
/** 当前生效的配置。volatile 改动会就地合并进来(见 `updateConfig`),所以它不是只读的。 */
|
|
1508
1501
|
config;
|
|
1509
1502
|
rootDir;
|
|
@@ -1599,6 +1592,8 @@ export class KnowledgeRuntime {
|
|
|
1599
1592
|
*/
|
|
1600
1593
|
derived;
|
|
1601
1594
|
derivedProblem;
|
|
1595
|
+
publication;
|
|
1596
|
+
recallSnapshotCache;
|
|
1602
1597
|
/** 段内布局整理是否已经跑过(每个 runtime 一次)。 */
|
|
1603
1598
|
segmentsOrganized = false;
|
|
1604
1599
|
/**
|
|
@@ -1632,6 +1627,7 @@ export class KnowledgeRuntime {
|
|
|
1632
1627
|
vectorIndex = null;
|
|
1633
1628
|
vectorError;
|
|
1634
1629
|
embeddingWorker = null;
|
|
1630
|
+
embeddingWorkerFingerprint;
|
|
1635
1631
|
/** Separate process so a rerank model failure cannot take embeddings down. */
|
|
1636
1632
|
rerankWorker = null;
|
|
1637
1633
|
/** Cache the first rerank failure; do not respawn a doomed model per recall. */
|
|
@@ -1650,6 +1646,7 @@ export class KnowledgeRuntime {
|
|
|
1650
1646
|
* 收到一次全量重编码。清单该记「是哪一个模型」,不是「它在哪」。
|
|
1651
1647
|
*/
|
|
1652
1648
|
embeddingModelId;
|
|
1649
|
+
resourceRoot;
|
|
1653
1650
|
/**
|
|
1654
1651
|
* 这个知识库的记忆段(0032)。
|
|
1655
1652
|
*
|
|
@@ -1671,9 +1668,13 @@ export class KnowledgeRuntime {
|
|
|
1671
1668
|
this.extractionNoteProvider = options.extractionNote;
|
|
1672
1669
|
this.memoryDir = options.memoryRoot ? resolve(options.memoryRoot) : join(this.rootDir, '04_memory');
|
|
1673
1670
|
this.staticModelCall = options.modelCall;
|
|
1671
|
+
this.environmentGuard = options.environmentGuard;
|
|
1674
1672
|
this.modelSeamProvider = options.modelSeam;
|
|
1673
|
+
this.resourceRoot = options.resourceRoot;
|
|
1675
1674
|
const resources = resolveResources(options.resourceRoot);
|
|
1676
|
-
this.
|
|
1675
|
+
if (!this.config.embeddingModel?.trim() && resources.embeddingDimension)
|
|
1676
|
+
this.config.embeddingDimension = resources.embeddingDimension;
|
|
1677
|
+
this.pythonPath = resolveConfiguredPython(config.python, resources.python);
|
|
1677
1678
|
this.embeddingModelPath = config.embeddingModel?.trim() || resources.embeddingModel;
|
|
1678
1679
|
this.rerankModelPath = config.rerankModel?.trim() || resources.rerankModel;
|
|
1679
1680
|
this.embeddingModelId = resources.modelId(this.embeddingModelPath);
|
|
@@ -1684,6 +1685,9 @@ export class KnowledgeRuntime {
|
|
|
1684
1685
|
* **每次现问**,不是在构造时定死:插件 apply 的时刻早于 `agent-default-model` 挂载,
|
|
1685
1686
|
* 那时问一次只会得到 undefined,而定死之后就再也不会变(见 `modelCallProvider`)。
|
|
1686
1687
|
*/
|
|
1688
|
+
embeddingFingerprint() {
|
|
1689
|
+
return embeddingFingerprint(this.embeddingModelPath, this.resourceRoot, this.config.embeddingProvider === 'ollama' ? this.config.embeddingBaseUrl : undefined);
|
|
1690
|
+
}
|
|
1687
1691
|
modelSeamNow() {
|
|
1688
1692
|
if (this.modelSeamProvider)
|
|
1689
1693
|
return this.modelSeamProvider(this.baseId);
|
|
@@ -1708,10 +1712,16 @@ export class KnowledgeRuntime {
|
|
|
1708
1712
|
updateConfig(patch) {
|
|
1709
1713
|
const previous = this.config;
|
|
1710
1714
|
this.config = { ...previous, ...patch };
|
|
1715
|
+
const resources = resolveResources(this.resourceRoot);
|
|
1716
|
+
// A managed model preset owns its output dimension. Custom model paths keep
|
|
1717
|
+
// the advanced dimension setting from the plugin config.
|
|
1718
|
+
if (!this.config.embeddingModel?.trim() && resources.embeddingDimension)
|
|
1719
|
+
this.config.embeddingDimension = resources.embeddingDimension;
|
|
1711
1720
|
const modelChanged = previous.embeddingProvider !== this.config.embeddingProvider ||
|
|
1712
1721
|
previous.embeddingModel !== this.config.embeddingModel ||
|
|
1713
1722
|
previous.embeddingDimension !== this.config.embeddingDimension ||
|
|
1714
1723
|
previous.embeddingDevice !== this.config.embeddingDevice ||
|
|
1724
|
+
previous.embeddingBaseUrl !== this.config.embeddingBaseUrl ||
|
|
1715
1725
|
previous.python !== this.config.python;
|
|
1716
1726
|
const rerankChanged = previous.rerankProvider !== this.config.rerankProvider ||
|
|
1717
1727
|
previous.rerankModel !== this.config.rerankModel ||
|
|
@@ -1722,18 +1732,19 @@ export class KnowledgeRuntime {
|
|
|
1722
1732
|
// 旧索引建在旧模型/旧维度下,留着它只会给出没有意义的相似度。
|
|
1723
1733
|
this.vectorIndex = null;
|
|
1724
1734
|
this.vectorError = undefined;
|
|
1735
|
+
this.encoderHealth = undefined;
|
|
1725
1736
|
}
|
|
1726
1737
|
if (rerankChanged) {
|
|
1727
1738
|
this.rerankWorker?.close();
|
|
1728
1739
|
this.rerankWorker = null;
|
|
1729
1740
|
this.rerankUnavailable = undefined;
|
|
1730
1741
|
}
|
|
1731
|
-
|
|
1732
|
-
this.pythonPath = patch.python.trim() || this.pythonPath;
|
|
1742
|
+
this.pythonPath = resolveConfiguredPython(this.config.python, resources.python);
|
|
1733
1743
|
if (patch.embeddingModel !== undefined)
|
|
1734
|
-
this.embeddingModelPath = patch.embeddingModel.trim() ||
|
|
1744
|
+
this.embeddingModelPath = patch.embeddingModel.trim() || resources.embeddingModel;
|
|
1735
1745
|
if (patch.rerankModel !== undefined)
|
|
1736
|
-
this.rerankModelPath = patch.rerankModel.trim() ||
|
|
1746
|
+
this.rerankModelPath = patch.rerankModel.trim() || resources.rerankModel;
|
|
1747
|
+
this.embeddingModelId = resources.modelId(this.embeddingModelPath);
|
|
1737
1748
|
}
|
|
1738
1749
|
// ------------------------------------------------- 固定卡片模板
|
|
1739
1750
|
/** 当前系统固定的卡片模板;不再按知识库维护卡片类型注册表。 */
|
|
@@ -2200,22 +2211,7 @@ export class KnowledgeRuntime {
|
|
|
2200
2211
|
if (this.vectorIndex)
|
|
2201
2212
|
return this.vectorIndex;
|
|
2202
2213
|
try {
|
|
2203
|
-
|
|
2204
|
-
if (loaded === undefined)
|
|
2205
|
-
return null;
|
|
2206
|
-
const manifest = loaded.manifest;
|
|
2207
|
-
if (manifest.quantization !== VECTOR_QUANTIZATION || !Number.isInteger(manifest.dimension) || !Array.isArray(manifest.items))
|
|
2208
|
-
return null;
|
|
2209
|
-
// 编码实现换了 → 库里这份索引不是「同一个东西」,当作不存在(于是 recall 会如实降级,
|
|
2210
|
-
// 而不是拿一份用别的编码器算出来的向量去排序)。
|
|
2211
|
-
if (manifest.encoder !== ENCODER_FINGERPRINT)
|
|
2212
|
-
return null;
|
|
2213
|
-
const binary = loaded.binary;
|
|
2214
|
-
const int8 = loaded.int8;
|
|
2215
|
-
const binaryWidth = Math.ceil(manifest.dimension / 8);
|
|
2216
|
-
if (manifest.itemCount !== manifest.items.length || binary.length !== manifest.itemCount * binaryWidth || int8.length !== manifest.itemCount * manifest.dimension)
|
|
2217
|
-
return null;
|
|
2218
|
-
this.vectorIndex = { manifest, binary, int8 };
|
|
2214
|
+
this.vectorIndex = decodeVectorIndex((await this.derivedHandle())?.vectorIndex(), ENCODER_FINGERPRINT);
|
|
2219
2215
|
return this.vectorIndex;
|
|
2220
2216
|
}
|
|
2221
2217
|
catch {
|
|
@@ -2261,6 +2257,13 @@ export class KnowledgeRuntime {
|
|
|
2261
2257
|
async encodeEmbeddings(inputs) {
|
|
2262
2258
|
if (this.config.embeddingProvider === 'none')
|
|
2263
2259
|
throw new Error('EMBEDDING_DISABLED');
|
|
2260
|
+
const fingerprint = this.embeddingFingerprint();
|
|
2261
|
+
if (fingerprint !== this.embeddingWorkerFingerprint) {
|
|
2262
|
+
this.embeddingWorker?.close();
|
|
2263
|
+
this.embeddingWorker = null;
|
|
2264
|
+
this.embeddingWorkerFingerprint = fingerprint;
|
|
2265
|
+
this.encoderHealth = undefined;
|
|
2266
|
+
}
|
|
2264
2267
|
// Keep each JSON response bounded for large stores. The WeMM process is
|
|
2265
2268
|
// persistent, so chunking does not reload the 1.8GB model between chunks.
|
|
2266
2269
|
const chunkSize = Math.max(1, this.config.embeddingBatchSize * 64);
|
|
@@ -2289,10 +2292,23 @@ export class KnowledgeRuntime {
|
|
|
2289
2292
|
return { vectors, device };
|
|
2290
2293
|
}
|
|
2291
2294
|
close() {
|
|
2295
|
+
for (const task of this.compileTasks.values()) {
|
|
2296
|
+
if (['succeeded', 'failed', 'cancelled'].includes(task.progress.state))
|
|
2297
|
+
continue;
|
|
2298
|
+
task.cancelRequested = true;
|
|
2299
|
+
task.pauseRequested = false;
|
|
2300
|
+
task.resume?.();
|
|
2301
|
+
}
|
|
2292
2302
|
this.embeddingWorker?.close();
|
|
2293
2303
|
this.embeddingWorker = null;
|
|
2294
2304
|
this.rerankWorker?.close();
|
|
2295
2305
|
this.rerankWorker = null;
|
|
2306
|
+
this.oplog?.close();
|
|
2307
|
+
this.extractionRunsStore?.close();
|
|
2308
|
+
this.axesStore?.close();
|
|
2309
|
+
this.knowledgeDb?.close();
|
|
2310
|
+
this.derived?.close();
|
|
2311
|
+
this.store?.close();
|
|
2296
2312
|
}
|
|
2297
2313
|
/** Both the encoder and the reranker speak the same JSONL worker protocol. */
|
|
2298
2314
|
spawnWorker(label = 'EMBEDDING') {
|
|
@@ -2333,11 +2349,18 @@ export class KnowledgeRuntime {
|
|
|
2333
2349
|
// 「这份台账派生出了哪些结论」就回到关键词路回答 —— 那本来就是词面问题,不是图问题。
|
|
2334
2350
|
const titleOf = (sourceId) => byId.get(sourceId);
|
|
2335
2351
|
try {
|
|
2336
|
-
|
|
2352
|
+
const db = await this.derivedHandle();
|
|
2353
|
+
if (!db)
|
|
2354
|
+
throw new Error('DERIVED_STORE_UNAVAILABLE');
|
|
2355
|
+
const documents = units.map(unit => ({
|
|
2337
2356
|
unitId: unit.unitId,
|
|
2338
2357
|
title: unit.title,
|
|
2339
2358
|
content: noteIndexText({ title: unit.title, content: unit.content, sourceRefs: unit.sourceRefs }, titleOf),
|
|
2340
|
-
}))
|
|
2359
|
+
}));
|
|
2360
|
+
if (this.publication)
|
|
2361
|
+
this.publication.fts = documents;
|
|
2362
|
+
else
|
|
2363
|
+
db.rebuildFts(documents);
|
|
2341
2364
|
this.ftsError = undefined;
|
|
2342
2365
|
return 'ready';
|
|
2343
2366
|
}
|
|
@@ -2347,6 +2370,8 @@ export class KnowledgeRuntime {
|
|
|
2347
2370
|
}
|
|
2348
2371
|
}
|
|
2349
2372
|
async buildVectorIndex(units, version) {
|
|
2373
|
+
if (this.publication)
|
|
2374
|
+
this.publication.vectors = null;
|
|
2350
2375
|
if (this.config.embeddingProvider === 'none')
|
|
2351
2376
|
return { status: 'skipped', count: 0, reused: 0 };
|
|
2352
2377
|
if (this.config.embeddingDimension % 8 !== 0)
|
|
@@ -2355,7 +2380,7 @@ export class KnowledgeRuntime {
|
|
|
2355
2380
|
if (inputs.length === 0)
|
|
2356
2381
|
return { status: 'ready', count: 0, reused: 0, dimension: this.config.embeddingDimension, device: 'none' };
|
|
2357
2382
|
const previous = await this.loadVectorIndex();
|
|
2358
|
-
const compatiblePrevious = previous && previous.manifest.dimension === this.config.embeddingDimension && previous.manifest.provider === this.config.embeddingProvider && previous.manifest.model === this.embeddingModelId && previous.manifest.encoder === ENCODER_FINGERPRINT ? previous : null;
|
|
2383
|
+
const compatiblePrevious = previous && previous.manifest.dimension === this.config.embeddingDimension && previous.manifest.provider === this.config.embeddingProvider && previous.manifest.model === this.embeddingModelId && previous.manifest.modelFingerprint === this.embeddingFingerprint() && previous.manifest.encoder === ENCODER_FINGERPRINT ? previous : null;
|
|
2359
2384
|
const previousById = new Map((compatiblePrevious?.manifest.items ?? []).map((item, index) => [item.unitId, { item, index }]));
|
|
2360
2385
|
const binaryWidth = Math.ceil(this.config.embeddingDimension / 8);
|
|
2361
2386
|
const binary = Buffer.alloc(inputs.length * binaryWidth);
|
|
@@ -2408,9 +2433,20 @@ export class KnowledgeRuntime {
|
|
|
2408
2433
|
this.encoderHealth = { ok: false, reason: `编码器退化:${inputs.length} 条 note 只产出 ${distinct.size} 个不同向量` };
|
|
2409
2434
|
throw new Error(`EMBEDDING_INDEX_DEGENERATE: ${inputs.length} 条只产出 ${distinct.size} 个不同向量`);
|
|
2410
2435
|
}
|
|
2411
|
-
const manifest = { version, createdAt: new Date().toISOString(), provider: this.config.embeddingProvider, model: this.embeddingModelId, encoder: ENCODER_FINGERPRINT, device, dimension: this.config.embeddingDimension, quantization: VECTOR_QUANTIZATION, itemCount: inputs.length, items: inputs.map(item => ({ unitId: item.unit.unitId, contentHash: item.contentHash })) };
|
|
2412
|
-
|
|
2413
|
-
|
|
2436
|
+
const manifest = { version, createdAt: new Date().toISOString(), provider: this.config.embeddingProvider, model: this.embeddingModelId, modelFingerprint: this.embeddingFingerprint(), encoder: ENCODER_FINGERPRINT, device, dimension: this.config.embeddingDimension, quantization: VECTOR_QUANTIZATION, itemCount: inputs.length, items: inputs.map(item => ({ unitId: item.unit.unitId, contentHash: item.contentHash })) };
|
|
2437
|
+
/*
|
|
2438
|
+
* 向量、它的量化缓冲与清单**一起**写进派生库:三个东西必须同一次落地,
|
|
2439
|
+
* 分三次写文件时崩在中间会留下「清单说有 40 条、缓冲只有 30 条」的半成品。
|
|
2440
|
+
*/
|
|
2441
|
+
const db = await this.derivedHandle();
|
|
2442
|
+
if (!db)
|
|
2443
|
+
throw new Error('DERIVED_STORE_UNAVAILABLE');
|
|
2444
|
+
if (this.publication)
|
|
2445
|
+
this.publication.vectors = { manifest, binary, int8 };
|
|
2446
|
+
else {
|
|
2447
|
+
db.putVectorIndex(manifest, binary, int8);
|
|
2448
|
+
this.vectorIndex = { manifest, binary, int8 };
|
|
2449
|
+
}
|
|
2414
2450
|
this.vectorError = undefined;
|
|
2415
2451
|
return { status: 'ready', count: inputs.length, reused, dimension: this.config.embeddingDimension, device };
|
|
2416
2452
|
}
|
|
@@ -2426,7 +2462,7 @@ export class KnowledgeRuntime {
|
|
|
2426
2462
|
for (const artifact of [WIKI_NOTES, LEGACY_UNITS]) {
|
|
2427
2463
|
try {
|
|
2428
2464
|
const notes = JSON.parse(await this.readArtifact(artifact));
|
|
2429
|
-
if (Array.isArray(notes)
|
|
2465
|
+
if (Array.isArray(notes)) {
|
|
2430
2466
|
this.units = notes;
|
|
2431
2467
|
break;
|
|
2432
2468
|
}
|
|
@@ -2650,7 +2686,15 @@ export class KnowledgeRuntime {
|
|
|
2650
2686
|
*/
|
|
2651
2687
|
let compile = null;
|
|
2652
2688
|
if (input.compile !== false) {
|
|
2653
|
-
|
|
2689
|
+
let rebuilt;
|
|
2690
|
+
try {
|
|
2691
|
+
rebuilt = await this.rebuild('wiki', input.lineage);
|
|
2692
|
+
}
|
|
2693
|
+
catch (cause) {
|
|
2694
|
+
warnings.push(`来源已写入,编译未完成:${cause instanceof Error ? cause.message : String(cause)}`);
|
|
2695
|
+
}
|
|
2696
|
+
if (rebuilt === undefined)
|
|
2697
|
+
return success({ written, failed, attempted: items.length, compile }, warnings);
|
|
2654
2698
|
if (rebuilt.ok === false) {
|
|
2655
2699
|
warnings.push(`来源已写入,但编译失败:${rebuilt.error?.message ?? '未知原因'}`);
|
|
2656
2700
|
}
|
|
@@ -2939,7 +2983,14 @@ export class KnowledgeRuntime {
|
|
|
2939
2983
|
*
|
|
2940
2984
|
* 失败一律返回 `null`,**不抛** —— 「知识库不可用」是一条要显示的结论,不是一个异常。
|
|
2941
2985
|
*/
|
|
2986
|
+
storeHandleOpening;
|
|
2942
2987
|
async storeHandle() {
|
|
2988
|
+
if (this.store !== undefined)
|
|
2989
|
+
return this.store;
|
|
2990
|
+
this.storeHandleOpening ??= this.openStoreHandle();
|
|
2991
|
+
return this.storeHandleOpening;
|
|
2992
|
+
}
|
|
2993
|
+
async openStoreHandle() {
|
|
2943
2994
|
if (this.store !== undefined)
|
|
2944
2995
|
return this.store;
|
|
2945
2996
|
const sqlite = await loadSqlite();
|
|
@@ -3124,7 +3175,14 @@ export class KnowledgeRuntime {
|
|
|
3124
3175
|
this.migrationNotes.push(`已把 03_compiled 的 ${imported} 份产物搬进 derived.db`);
|
|
3125
3176
|
}
|
|
3126
3177
|
/** 派生库(`<库根>/derived.db`):编译产物,**可整体删掉重算**。 */
|
|
3178
|
+
derivedHandleOpening;
|
|
3127
3179
|
async derivedHandle() {
|
|
3180
|
+
if (this.derived !== undefined)
|
|
3181
|
+
return this.derived;
|
|
3182
|
+
this.derivedHandleOpening ??= this.openDerivedHandle();
|
|
3183
|
+
return this.derivedHandleOpening;
|
|
3184
|
+
}
|
|
3185
|
+
async openDerivedHandle() {
|
|
3128
3186
|
if (this.derived !== undefined)
|
|
3129
3187
|
return this.derived;
|
|
3130
3188
|
const sqlite = await loadSqlite();
|
|
@@ -3157,7 +3215,10 @@ export class KnowledgeRuntime {
|
|
|
3157
3215
|
const db = await this.derivedHandle();
|
|
3158
3216
|
if (db === null)
|
|
3159
3217
|
throw new Error('派生库不可用');
|
|
3160
|
-
|
|
3218
|
+
if (this.publication)
|
|
3219
|
+
this.publication.artifacts.set(name, body);
|
|
3220
|
+
else
|
|
3221
|
+
db.putArtifact(name, body);
|
|
3161
3222
|
}
|
|
3162
3223
|
async hasArtifact(name) {
|
|
3163
3224
|
return (await this.derivedHandle())?.artifact(name) !== undefined;
|
|
@@ -3586,7 +3647,31 @@ export class KnowledgeRuntime {
|
|
|
3586
3647
|
}
|
|
3587
3648
|
for (const card of (await this.loadCards()).filter(item => item.sourceRefs.includes(docId))) {
|
|
3588
3649
|
try {
|
|
3589
|
-
|
|
3650
|
+
const placements = db.placementsOf([card.entityId]).get(card.entityId) ?? [];
|
|
3651
|
+
const remaining = [];
|
|
3652
|
+
for (const row of placements) {
|
|
3653
|
+
const item = await store?.get(row.knowledgeId);
|
|
3654
|
+
if (item)
|
|
3655
|
+
remaining.push({ row, knowledge: item });
|
|
3656
|
+
}
|
|
3657
|
+
const sections = new Map();
|
|
3658
|
+
for (const { row, knowledge: item } of remaining.sort((a, b) => a.row.ordinal - b.row.ordinal)) {
|
|
3659
|
+
const heading = row.section || '知识';
|
|
3660
|
+
const lines = sections.get(heading) ?? [];
|
|
3661
|
+
lines.push(`${item.text}[^${item.id}]`);
|
|
3662
|
+
sections.set(heading, lines);
|
|
3663
|
+
}
|
|
3664
|
+
const content = sections.size > 0
|
|
3665
|
+
? [...sections].map(([heading, lines]) => `## ${heading}\n\n${lines.join('\n\n')}`).join('\n\n')
|
|
3666
|
+
: card.content;
|
|
3667
|
+
const updated = await this.writeCard({
|
|
3668
|
+
...card, content,
|
|
3669
|
+
...(sections.size > 0 ? { summary: remaining[0].knowledge.text, placements: remaining.map(item => item.row) } : {}),
|
|
3670
|
+
metadata: { ...card.metadata, layout: undefined, citations: undefined, evidence_needs_review: sections.size === 0 },
|
|
3671
|
+
sourceRefs: card.sourceRefs.filter(ref => ref !== docId), origin: 'internal',
|
|
3672
|
+
});
|
|
3673
|
+
if (!updated.ok)
|
|
3674
|
+
throw new Error(updated.error.message);
|
|
3590
3675
|
cardsDetached += 1;
|
|
3591
3676
|
}
|
|
3592
3677
|
catch (cause) {
|
|
@@ -3839,7 +3924,7 @@ export class KnowledgeRuntime {
|
|
|
3839
3924
|
}));
|
|
3840
3925
|
const hash = layoutHash({
|
|
3841
3926
|
knowledgeIds: knowledge.map(item => item.id),
|
|
3842
|
-
knowledgeContext: knowledge.map(item =>
|
|
3927
|
+
knowledgeContext: listed.knowledge.map(item => JSON.stringify({ id: item.id, text: item.text, version: item.version, headings: item.sources.map(source => source.headingPath), coordinate: input.coordinate })),
|
|
3843
3928
|
profileId: input.profileId,
|
|
3844
3929
|
});
|
|
3845
3930
|
const layout = { profile: input.profileId, hash, origin: 'model' };
|
|
@@ -3938,7 +4023,7 @@ export class KnowledgeRuntime {
|
|
|
3938
4023
|
raw: raw.trim().replace(/\s+/g, ' ').slice(0, 400),
|
|
3939
4024
|
});
|
|
3940
4025
|
}
|
|
3941
|
-
const checked = checkOrganize(plan, context.knowledge.map(item => item.id), { ...(await this.relatedForCard(card)) });
|
|
4026
|
+
const checked = checkOrganize(plan, context.knowledge.map(item => item.id), { knowledge: context.knowledge, ...(await this.relatedForCard(card)) });
|
|
3942
4027
|
const problems = [...parsed.problems.map(problem => problem.message), ...checked.problems.map(problem => problem.message)];
|
|
3943
4028
|
const placements = problems.length === 0 ? placementsOf(plan, checked.cited) : [];
|
|
3944
4029
|
const images = await this.imagesForCard(card);
|
|
@@ -4127,6 +4212,7 @@ export class KnowledgeRuntime {
|
|
|
4127
4212
|
const problems = [...parsed.problems];
|
|
4128
4213
|
if (plan !== undefined) {
|
|
4129
4214
|
const checked = checkOrganize(plan, knowledge.map(item => item.id), {
|
|
4215
|
+
knowledge,
|
|
4130
4216
|
...(input.related === undefined ? {} : { related: input.related }),
|
|
4131
4217
|
});
|
|
4132
4218
|
problems.push(...checked.problems);
|
|
@@ -4474,7 +4560,7 @@ export class KnowledgeRuntime {
|
|
|
4474
4560
|
return notes;
|
|
4475
4561
|
}
|
|
4476
4562
|
/** 1-hop neighbourhood of a set of wiki cards inside the compiled graph. */
|
|
4477
|
-
oneHopGraph(keys, includeProblem) {
|
|
4563
|
+
oneHopGraph(keys, includeProblem, graph = this.graph) {
|
|
4478
4564
|
const visible = (node) => includeProblem || node.status !== 'problem';
|
|
4479
4565
|
const edges = this.graph.edges.filter(edge => keys.has(String(edge.from)) || keys.has(String(edge.to))).slice(0, 50);
|
|
4480
4566
|
const ids = new Set(keys);
|
|
@@ -4579,7 +4665,7 @@ export class KnowledgeRuntime {
|
|
|
4579
4665
|
for (const artifact of [WIKI_NOTES, LEGACY_UNITS]) {
|
|
4580
4666
|
try {
|
|
4581
4667
|
const notes = JSON.parse(await this.readArtifact(artifact));
|
|
4582
|
-
if (Array.isArray(notes)
|
|
4668
|
+
if (Array.isArray(notes))
|
|
4583
4669
|
return notes;
|
|
4584
4670
|
}
|
|
4585
4671
|
catch { /* the first build has not happened yet */ }
|
|
@@ -4603,7 +4689,11 @@ export class KnowledgeRuntime {
|
|
|
4603
4689
|
return contentHashOf(JSON.stringify({
|
|
4604
4690
|
sourceId: source.sourceId,
|
|
4605
4691
|
content: contentHashOf(source.content),
|
|
4606
|
-
|
|
4692
|
+
title: source.title,
|
|
4693
|
+
compiler: 'knowledge-extract-v2',
|
|
4694
|
+
extractWithModel: this.config.extractWithModel,
|
|
4695
|
+
extractionNote: this.extractionNote(),
|
|
4696
|
+
layoutProfile: this.config.layoutProfileId,
|
|
4607
4697
|
declared: contentHashOf(JSON.stringify(declared)),
|
|
4608
4698
|
registry: registryHash,
|
|
4609
4699
|
}));
|
|
@@ -4639,7 +4729,7 @@ export class KnowledgeRuntime {
|
|
|
4639
4729
|
onOrganize, runId) {
|
|
4640
4730
|
const schema = this.wikiSchema();
|
|
4641
4731
|
const enabledTypes = schema.types.filter(def => def.enabled);
|
|
4642
|
-
const
|
|
4732
|
+
const registry = await this.axisRegistry();
|
|
4643
4733
|
const outcome = this.emptyExtractionOutcome({});
|
|
4644
4734
|
/**
|
|
4645
4735
|
* 模型调用**每轮解析一次**,而且解析不到时**如实报出来**(0044)。
|
|
@@ -4650,6 +4740,7 @@ export class KnowledgeRuntime {
|
|
|
4650
4740
|
*/
|
|
4651
4741
|
const seam = this.config.extractWithModel ? this.modelSeamNow() : {};
|
|
4652
4742
|
const call = seam.call;
|
|
4743
|
+
const registryHash = contentHashOf(JSON.stringify({ types: enabledTypes, axesRevision: registry?.inputRevision() ?? 0, model: { available: Boolean(call), provider: seam.provider, model: seam.model, reasoningEffort: seam.reasoningEffort } }));
|
|
4653
4744
|
if (this.config.extractWithModel && !call) {
|
|
4654
4745
|
outcome.warnings.push(`模型抽取已开启,但取不到可用的模型调用:${seam.why ?? '原因未知'} —— 本次退回规则抽取:`
|
|
4655
4746
|
+ '卡片正文是按 `##` 小节切下来的原文,判重也只能比字面,同一实体换个标题写法会落成两张卡。');
|
|
@@ -4676,9 +4767,9 @@ export class KnowledgeRuntime {
|
|
|
4676
4767
|
if (runId)
|
|
4677
4768
|
await this.checkpointCompileTask(runId);
|
|
4678
4769
|
const fingerprint = this.extractionFingerprint(source, registryHash);
|
|
4679
|
-
outcome.extractionHashes[source.sourceId] = fingerprint;
|
|
4680
4770
|
if (previousHashes[source.sourceId] === fingerprint) {
|
|
4681
4771
|
outcome.sourcesSkipped += 1;
|
|
4772
|
+
outcome.extractionHashes[source.sourceId] = fingerprint;
|
|
4682
4773
|
const kept = persisted[source.sourceId] ?? [];
|
|
4683
4774
|
if (kept.length > 0) {
|
|
4684
4775
|
outcome.relationsBySource[source.sourceId] = kept;
|
|
@@ -4692,7 +4783,9 @@ export class KnowledgeRuntime {
|
|
|
4692
4783
|
try {
|
|
4693
4784
|
// 同一轮里后一份来源应当能看到前一份来源刚抽出来的实体,所以判重索引
|
|
4694
4785
|
// 每份来源都用「磁盘上的卡片 + 这一轮 pending 的卡片」重建。
|
|
4695
|
-
await this.compileSource(source, byKey, pending, enabledTypes, outcome, call, knowledgeIds, runId, seam);
|
|
4786
|
+
const complete = await this.compileSource(source, byKey, pending, enabledTypes, outcome, call, knowledgeIds, runId, seam);
|
|
4787
|
+
if (complete !== false)
|
|
4788
|
+
outcome.extractionHashes[source.sourceId] = fingerprint;
|
|
4696
4789
|
}
|
|
4697
4790
|
catch (cause) {
|
|
4698
4791
|
if (cause instanceof CompileTaskCancelledError)
|
|
@@ -4724,7 +4817,7 @@ export class KnowledgeRuntime {
|
|
|
4724
4817
|
*/
|
|
4725
4818
|
const folded = new Map();
|
|
4726
4819
|
for (const [key, item] of pending)
|
|
4727
|
-
folded.set(key,
|
|
4820
|
+
folded.set(key, item.content);
|
|
4728
4821
|
const view = [];
|
|
4729
4822
|
for (const [key, card] of byKey)
|
|
4730
4823
|
view.push({ key, content: card.content, sourceRefs: card.sourceRefs });
|
|
@@ -4801,9 +4894,14 @@ export class KnowledgeRuntime {
|
|
|
4801
4894
|
const contentBeforeEvents = preserveModelLayout ? existing.content : content;
|
|
4802
4895
|
const finalContent = withEventReferences(contentBeforeEvents, item.metadata.event_refs, eventsById);
|
|
4803
4896
|
const finalSummary = preserveModelLayout ? existing.summary : item.summary;
|
|
4804
|
-
|
|
4897
|
+
let finalPlacements = preserveModelLayout || item.placements === undefined
|
|
4805
4898
|
? undefined
|
|
4806
4899
|
: translatePlacements(item.placements, knowledgeIds);
|
|
4900
|
+
if (finalPlacements !== undefined && existing) {
|
|
4901
|
+
const db = await this.storeHandle();
|
|
4902
|
+
const previous = db?.placementsOf([existing.entityId]).get(existing.entityId) ?? [];
|
|
4903
|
+
finalPlacements = [...new Map([...previous, ...finalPlacements].map(row => [row.knowledgeId, row])).values()];
|
|
4904
|
+
}
|
|
4807
4905
|
const titledMetadata = withTitleAlias(metadata, existing?.title, finalTitle);
|
|
4808
4906
|
const finalMetadata = titledMetadata;
|
|
4809
4907
|
const unchanged = existing !== undefined
|
|
@@ -4831,6 +4929,8 @@ export class KnowledgeRuntime {
|
|
|
4831
4929
|
origin: 'compile',
|
|
4832
4930
|
});
|
|
4833
4931
|
if (written.ok === false) {
|
|
4932
|
+
for (const ref of item.sourceRefs)
|
|
4933
|
+
delete outcome.extractionHashes[ref];
|
|
4834
4934
|
outcome.warnings.push(`卡片 ${item.entityId} 写入失败(${written.error.code}):${written.error.message}`);
|
|
4835
4935
|
continue;
|
|
4836
4936
|
}
|
|
@@ -5096,6 +5196,7 @@ export class KnowledgeRuntime {
|
|
|
5096
5196
|
* 后一份文档可能把前一份的知识并进去,落位要指活着的那一行。
|
|
5097
5197
|
*/
|
|
5098
5198
|
knowledgeIds, compileRunId, modelInfo) {
|
|
5199
|
+
let extractionComplete = call === undefined;
|
|
5099
5200
|
// 判重索引要看到「这一轮前面几份来源刚抽出来的卡片」,否则同一实体跨两份文档
|
|
5100
5201
|
// 会各建一张 —— 而那正是这次改造要修掉的那个 bug。
|
|
5101
5202
|
const cards = [...byKey.values()];
|
|
@@ -5128,6 +5229,9 @@ export class KnowledgeRuntime {
|
|
|
5128
5229
|
}
|
|
5129
5230
|
/** 撞名报了没有 —— 同一个名字被三个坐标撞到时报三次是噪声。 */
|
|
5130
5231
|
const collisionReported = new Set();
|
|
5232
|
+
const sourceDb = await this.storeHandle();
|
|
5233
|
+
const sourceHash = createHash('sha256').update(source.content, 'utf8').digest('hex');
|
|
5234
|
+
const sourceVersion = sourceDb?.versions('source', source.sourceId).find(row => row.blobHash === sourceHash)?.version;
|
|
5131
5235
|
const ruleCandidates = extractCandidates(source, enabledTypes);
|
|
5132
5236
|
outcome.candidatesExtracted += ruleCandidates.length;
|
|
5133
5237
|
const ruleNormalized = normalizeCandidates(ruleCandidates, buildAliasIndex(cards));
|
|
@@ -5229,9 +5333,10 @@ export class KnowledgeRuntime {
|
|
|
5229
5333
|
}),
|
|
5230
5334
|
});
|
|
5231
5335
|
/**
|
|
5232
|
-
*
|
|
5233
|
-
*
|
|
5336
|
+
* 记录抽取轨迹,再写入事实真源。轨迹是审计;事实写入不完整时不能保存成功指纹,
|
|
5337
|
+
* 下一次增量编译必须能够重试。
|
|
5234
5338
|
*/
|
|
5339
|
+
extractionComplete = report.model.ok;
|
|
5235
5340
|
const summary = trialSummary(report);
|
|
5236
5341
|
await this.recordExtractionRun({
|
|
5237
5342
|
id: runId, docId: source.sourceId, title: source.title, summary,
|
|
@@ -5241,6 +5346,7 @@ export class KnowledgeRuntime {
|
|
|
5241
5346
|
await this.checkpointCompileTask(compileRunId);
|
|
5242
5347
|
const ingested = await this.ingestTrialKnowledge({
|
|
5243
5348
|
runId, knowledge: report.knowledge,
|
|
5349
|
+
sourceVersions: { [source.sourceId]: sourceVersion },
|
|
5244
5350
|
objects: report.objects, actions: report.actions, scenes: report.scenes,
|
|
5245
5351
|
});
|
|
5246
5352
|
let eventIngested = {
|
|
@@ -5260,6 +5366,8 @@ export class KnowledgeRuntime {
|
|
|
5260
5366
|
outcome.warnings.push(`Event 没有写入知识库:${cause instanceof Error ? cause.message : String(cause)}`);
|
|
5261
5367
|
}
|
|
5262
5368
|
outcome.warnings.push(...eventIngested.warnings);
|
|
5369
|
+
if (ingested.skipped > 0 || eventIngested.skipped > 0)
|
|
5370
|
+
extractionComplete = false;
|
|
5263
5371
|
// 落位要用库内的 id。这一份报告里每一条知识的映射都并进整轮那一份 ——
|
|
5264
5372
|
// 后面的来源可能命中同一张卡,那时落位指的行也得是活的。
|
|
5265
5373
|
Object.assign(knowledgeIds, ingested.ids);
|
|
@@ -5276,7 +5384,7 @@ export class KnowledgeRuntime {
|
|
|
5276
5384
|
outcome.axesAmbiguous += axes.filter(axis => axis.outcome === 'ambiguous').length;
|
|
5277
5385
|
outcome.knowledgeMerged += ingested.merged;
|
|
5278
5386
|
if (ingested.skipped > 0) {
|
|
5279
|
-
outcome.warnings.push(`${ingested.skipped}
|
|
5387
|
+
outcome.warnings.push(`${ingested.skipped} 条知识未进入真源(出处核验或写入失败),已保留抽取轨迹,下一次编译会重试`);
|
|
5280
5388
|
}
|
|
5281
5389
|
if (eventIngested.skipped > 0) {
|
|
5282
5390
|
outcome.warnings.push(`${eventIngested.skipped} 个 Event 没有可核验出处,未进入 Event 真源`);
|
|
@@ -5515,6 +5623,7 @@ export class KnowledgeRuntime {
|
|
|
5515
5623
|
...(outcome.relationsBySource[source.sourceId] ?? []),
|
|
5516
5624
|
...ruleNormalized.relations,
|
|
5517
5625
|
];
|
|
5626
|
+
return extractionComplete;
|
|
5518
5627
|
}
|
|
5519
5628
|
/**
|
|
5520
5629
|
* 把关系候选装配成 wiki↔wiki 的边。
|
|
@@ -5645,7 +5754,7 @@ export class KnowledgeRuntime {
|
|
|
5645
5754
|
* 调用点必须在**按卡片去重之后**:先扩展再去重会把邻居与命中混在一起,
|
|
5646
5755
|
* 而上下文里必须分得清「这是召回来的」还是「这是延伸出来的」。
|
|
5647
5756
|
*/
|
|
5648
|
-
expandWiki(seedKeys, cardByKey, includeProblems) {
|
|
5757
|
+
expandWiki(seedKeys, cardByKey, includeProblems, graph = this.graph) {
|
|
5649
5758
|
const seeds = new Set(seedKeys);
|
|
5650
5759
|
const visible = (card) => includeProblems || card.status !== 'problem';
|
|
5651
5760
|
const via = new Map();
|
|
@@ -5656,7 +5765,7 @@ export class KnowledgeRuntime {
|
|
|
5656
5765
|
via.set(key, bucket);
|
|
5657
5766
|
};
|
|
5658
5767
|
const edges = [];
|
|
5659
|
-
for (const edge of
|
|
5768
|
+
for (const edge of graph.edges) {
|
|
5660
5769
|
const type = String(edge.type);
|
|
5661
5770
|
if (type === 'supported_by')
|
|
5662
5771
|
continue;
|
|
@@ -6207,7 +6316,12 @@ export class KnowledgeRuntime {
|
|
|
6207
6316
|
* 与抽取轨迹**同一个数据库文件**(`<memoryDir>/knowledge.sqlite`):它们是同一层
|
|
6208
6317
|
* (领域实体与它的审计),而解析一条知识要同时读两边 —— 分两个文件只是多一次打开。
|
|
6209
6318
|
*/
|
|
6319
|
+
axisRegistryOpening;
|
|
6210
6320
|
async axisRegistry() {
|
|
6321
|
+
this.axisRegistryOpening ??= this.openAxisRegistry();
|
|
6322
|
+
return this.axisRegistryOpening;
|
|
6323
|
+
}
|
|
6324
|
+
async openAxisRegistry() {
|
|
6211
6325
|
if (this.axesReady !== null)
|
|
6212
6326
|
return this.axesReady ? this.axesStore : null;
|
|
6213
6327
|
/*
|
|
@@ -6215,7 +6329,10 @@ export class KnowledgeRuntime {
|
|
|
6215
6329
|
* 记忆段里那个 `knowledge.sqlite`。理由是同一条:它们是**知识**(不可重建的命名决定
|
|
6216
6330
|
* 与人的裁决),必须跟着知识库一起备份、迁移、删除;而记忆段只装交互记录。
|
|
6217
6331
|
*/
|
|
6218
|
-
const
|
|
6332
|
+
const db = await this.storeHandle();
|
|
6333
|
+
if (db === null)
|
|
6334
|
+
return null;
|
|
6335
|
+
const registry = new AxisRegistry(this.safePath(KNOWLEDGE_DB_FILE), db.connection());
|
|
6219
6336
|
const ok = await registry.open();
|
|
6220
6337
|
this.axesStore = ok ? registry : null;
|
|
6221
6338
|
this.axesReady = ok;
|
|
@@ -6728,7 +6845,13 @@ export class KnowledgeRuntime {
|
|
|
6728
6845
|
/** 并两个实体。被并的那个不删,转成 `merged` 并指向目标。 */
|
|
6729
6846
|
async mergeAxes(fromId, intoId) {
|
|
6730
6847
|
const registry = await this.axisRegistry();
|
|
6731
|
-
|
|
6848
|
+
const db = await this.storeHandle();
|
|
6849
|
+
if (!registry || !db)
|
|
6850
|
+
return false;
|
|
6851
|
+
const merged = await registry.merge(fromId, intoId, kind => redirectAxisReferences(db, kind, fromId, intoId));
|
|
6852
|
+
if (merged)
|
|
6853
|
+
this.cards = await this.loadCards();
|
|
6854
|
+
return merged;
|
|
6732
6855
|
}
|
|
6733
6856
|
/** 加一个别名。`scopeId` 空串 = 全局。 */
|
|
6734
6857
|
async addAxisAlias(id, alias, scopeId = '') {
|
|
@@ -6743,15 +6866,23 @@ export class KnowledgeRuntime {
|
|
|
6743
6866
|
/**
|
|
6744
6867
|
* Knowledge 库(0063 §2):事实真源。
|
|
6745
6868
|
*
|
|
6746
|
-
*
|
|
6869
|
+
* 与轴注册表共享经过校验的 `knowledge.db` 连接,使用各自的表。
|
|
6747
6870
|
* 两个模块互不认识,靠 id 字符串耦合 —— 代价是没有数据库级引用完整性,
|
|
6748
6871
|
* 收益是「一条知识挂了一个不存在的坐标」会被 §5.7 的坐标核验**报出来**,
|
|
6749
6872
|
* 而不是被数据库静默拒绝。
|
|
6750
6873
|
*/
|
|
6874
|
+
knowledgeStoreOpening;
|
|
6751
6875
|
async knowledgeStore() {
|
|
6876
|
+
this.knowledgeStoreOpening ??= this.openKnowledgeStore();
|
|
6877
|
+
return this.knowledgeStoreOpening;
|
|
6878
|
+
}
|
|
6879
|
+
async openKnowledgeStore() {
|
|
6752
6880
|
if (this.knowledgeReady !== null)
|
|
6753
6881
|
return this.knowledgeReady ? this.knowledgeDb : null;
|
|
6754
|
-
const
|
|
6882
|
+
const db = await this.storeHandle();
|
|
6883
|
+
if (db === null)
|
|
6884
|
+
return null;
|
|
6885
|
+
const store = new KnowledgeStore(this.safePath(KNOWLEDGE_DB_FILE), db.connection());
|
|
6755
6886
|
const ok = await store.open();
|
|
6756
6887
|
this.knowledgeDb = ok ? store : null;
|
|
6757
6888
|
this.knowledgeReady = ok;
|
|
@@ -6774,13 +6905,12 @@ export class KnowledgeRuntime {
|
|
|
6774
6905
|
/*
|
|
6775
6906
|
* 给每一处出处补上**它依据的是哪一版**(0062 v2)。
|
|
6776
6907
|
*
|
|
6777
|
-
*
|
|
6778
|
-
*
|
|
6779
|
-
* 来源被人改过之后,没有任何东西能回答那条知识还算不算数。
|
|
6908
|
+
* 抽取链路带实际读取版本;直接写入没有指定版本时,补当前来源版本。
|
|
6909
|
+
* 版本用于追踪证据变化,不能据此裁定 Knowledge 是否失效。
|
|
6780
6910
|
*/
|
|
6781
6911
|
const db = await this.storeHandle();
|
|
6782
6912
|
const sources = (input.sources ?? []).map(source => {
|
|
6783
|
-
if (!source.documentId)
|
|
6913
|
+
if (!source.documentId || source.toVersion !== undefined)
|
|
6784
6914
|
return source;
|
|
6785
6915
|
const version = db?.source(source.documentId)?.version;
|
|
6786
6916
|
return version === undefined ? source : { ...source, toVersion: version };
|
|
@@ -6806,7 +6936,9 @@ export class KnowledgeRuntime {
|
|
|
6806
6936
|
* 调用方可以显式传 `false` 关掉它(试跑那条链路不需要这个字段)。
|
|
6807
6937
|
*/
|
|
6808
6938
|
try {
|
|
6809
|
-
|
|
6939
|
+
const page = await store.list({ withCards: true, ...query });
|
|
6940
|
+
const db = await this.storeHandle();
|
|
6941
|
+
return db ? { ...page, knowledge: page.knowledge.map(row => assessKnowledgeEvidence(row, db)) } : page;
|
|
6810
6942
|
}
|
|
6811
6943
|
catch {
|
|
6812
6944
|
return null;
|
|
@@ -6821,7 +6953,8 @@ export class KnowledgeRuntime {
|
|
|
6821
6953
|
const knowledge = await store.get(id);
|
|
6822
6954
|
if (knowledge === null)
|
|
6823
6955
|
return null;
|
|
6824
|
-
|
|
6956
|
+
const db = await this.storeHandle();
|
|
6957
|
+
return { knowledge: db ? assessKnowledgeEvidence(knowledge, db) : knowledge, versions: await store.versions(id) };
|
|
6825
6958
|
}
|
|
6826
6959
|
catch {
|
|
6827
6960
|
return null;
|
|
@@ -6889,7 +7022,7 @@ export class KnowledgeRuntime {
|
|
|
6889
7022
|
const cardOrphans = cards.filter(card => card.sourceRefs.every(ref => !live.has(ref))).length;
|
|
6890
7023
|
const events = (await this.loadEvents()).filter(event => event.records.some(record => !live.has(record.sourceId))).length;
|
|
6891
7024
|
/**
|
|
6892
|
-
*
|
|
7025
|
+
* 未被引用的来源图片也进这张账(2026-10-05)。`images.publish` 写入的独立图片会被识别并保留。
|
|
6893
7026
|
*
|
|
6894
7027
|
* 少了它,「来源与卡片都清完了、只剩一堆没人引用的图」这种状态在界面上**一个字都没有**
|
|
6895
7028
|
* —— 清理孤儿的入口因此不出现,而用户唯一能看到的读数(「收录 N 个文件」)又把它算成
|
|
@@ -6897,7 +7030,7 @@ export class KnowledgeRuntime {
|
|
|
6897
7030
|
* 「按一下能清掉几个」是同几个。
|
|
6898
7031
|
*/
|
|
6899
7032
|
const names = await this.assetBucketNames();
|
|
6900
|
-
const assets = names.length === 0 ? 0 : this.unreferencedBuckets(names, sources, cards).length;
|
|
7033
|
+
const assets = names.length === 0 ? 0 : (await this.unreferencedBuckets(names, sources, cards)).length;
|
|
6901
7034
|
return { knowledge, cards: cardOrphans, events, assets };
|
|
6902
7035
|
}
|
|
6903
7036
|
/** 图片池里的桶名(`01_source_raw/_assets/<bucket>/`)。没有池子就是空的。 */
|
|
@@ -6925,8 +7058,9 @@ export class KnowledgeRuntime {
|
|
|
6925
7058
|
return [...names];
|
|
6926
7059
|
}
|
|
6927
7060
|
/**
|
|
6928
|
-
* 还有没有人在引用这个桶 ——
|
|
6929
|
-
*
|
|
7061
|
+
* 还有没有人在引用这个桶 —— 判据是来源正文、来源 front matter 的 `metadata.assets`、
|
|
7062
|
+
* 卡片正文出现过 `_assets/<bucket>/`,或者它是 `images.publish` 写入的独立图床图片。
|
|
7063
|
+
* 独立图片没有来源/卡片引用是正常状态,必须保留。
|
|
6930
7064
|
*
|
|
6931
7065
|
* 为什么三处都要看:桶名是**内容哈希的前 12 位**(`convert/ingest.ts`),不是来源 id。
|
|
6932
7066
|
* 同一份内容以两个分类导入会共用同一个桶;而卡片正文会把图片引用**复制进去**
|
|
@@ -6934,7 +7068,7 @@ export class KnowledgeRuntime {
|
|
|
6934
7068
|
*
|
|
6935
7069
|
* 不看 `03_compiled/`:那是派生物,下一次 `rebuild` 会把指向已删图片的引用一起扔掉。
|
|
6936
7070
|
*/
|
|
6937
|
-
unreferencedBuckets(names, sources, cards) {
|
|
7071
|
+
async unreferencedBuckets(names, sources, cards) {
|
|
6938
7072
|
const referenced = new Set();
|
|
6939
7073
|
/**
|
|
6940
7074
|
* **两种引用形式都算**:`assets/<桶>/`(新写的)与 `_assets/<桶>/`(存量)。
|
|
@@ -6958,7 +7092,44 @@ export class KnowledgeRuntime {
|
|
|
6958
7092
|
}
|
|
6959
7093
|
for (const card of cards)
|
|
6960
7094
|
collect(card.content);
|
|
6961
|
-
|
|
7095
|
+
const unreferenced = [];
|
|
7096
|
+
for (const name of names) {
|
|
7097
|
+
if (referenced.has(name))
|
|
7098
|
+
continue;
|
|
7099
|
+
if (await this.isPublishedImageBucket(name))
|
|
7100
|
+
continue;
|
|
7101
|
+
unreferenced.push(name);
|
|
7102
|
+
}
|
|
7103
|
+
return unreferenced;
|
|
7104
|
+
}
|
|
7105
|
+
/**
|
|
7106
|
+
* `images.publish` 的图片不需要被来源或卡片引用。新写入的桶有明确标记;同时兼容
|
|
7107
|
+
* 标记加入前已发布的图片:它们以图片自身的 SHA-256 前 12 位作为桶名。
|
|
7108
|
+
*/
|
|
7109
|
+
async isPublishedImageBucket(name) {
|
|
7110
|
+
for (const root of this.assetRoots()) {
|
|
7111
|
+
if (existsSync(join(root, name, PUBLISHED_IMAGE_MARKER)))
|
|
7112
|
+
return true;
|
|
7113
|
+
}
|
|
7114
|
+
// 旧版本 images.publish 只会写入知识库根目录的 assets/,因此只在这里做哈希识别。
|
|
7115
|
+
if (!/^[a-f0-9]{12}$/i.test(name))
|
|
7116
|
+
return false;
|
|
7117
|
+
const dir = join(this.rootDir, ASSETS_DIR, name);
|
|
7118
|
+
try {
|
|
7119
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
7120
|
+
if (!entry.isFile() || !/\.(?:png|jpe?g|gif|webp|avif|bmp|svg)$/i.test(entry.name))
|
|
7121
|
+
continue;
|
|
7122
|
+
const path = join(dir, entry.name);
|
|
7123
|
+
const details = await stat(path);
|
|
7124
|
+
if (details.size > 20 * 1024 * 1024)
|
|
7125
|
+
continue;
|
|
7126
|
+
const bytes = await readFile(path);
|
|
7127
|
+
if (createHash('sha256').update(bytes).digest('hex').slice(0, 12) === name)
|
|
7128
|
+
return true;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
catch { /* 桶目录可能在扫描期间被删除 */ }
|
|
7132
|
+
return false;
|
|
6962
7133
|
}
|
|
6963
7134
|
/**
|
|
6964
7135
|
* 回收 `source_raw/` 里**没有人再指向的底稿桶**(0067)。
|
|
@@ -7036,7 +7207,7 @@ export class KnowledgeRuntime {
|
|
|
7036
7207
|
return { buckets: 0, files: 0, problems: [] };
|
|
7037
7208
|
const problems = [];
|
|
7038
7209
|
const cards = this.cards.length > 0 ? this.cards : await this.loadCards();
|
|
7039
|
-
const doomed = this.unreferencedBuckets(names, await this.loadSources(), cards);
|
|
7210
|
+
const doomed = await this.unreferencedBuckets(names, await this.loadSources(), cards);
|
|
7040
7211
|
let buckets = 0;
|
|
7041
7212
|
let files = 0;
|
|
7042
7213
|
for (const name of doomed) {
|
|
@@ -7191,7 +7362,7 @@ export class KnowledgeRuntime {
|
|
|
7191
7362
|
async ingestTrialKnowledge(input) {
|
|
7192
7363
|
const store = await this.knowledgeStore();
|
|
7193
7364
|
if (store === null)
|
|
7194
|
-
return { saved: 0, merged: 0, skipped:
|
|
7365
|
+
return { saved: 0, merged: 0, skipped: input.knowledge.length, ids: {} };
|
|
7195
7366
|
const axes = { objects: input.objects, actions: input.actions, scenes: input.scenes };
|
|
7196
7367
|
let saved = 0;
|
|
7197
7368
|
let merged = 0;
|
|
@@ -7205,7 +7376,7 @@ export class KnowledgeRuntime {
|
|
|
7205
7376
|
}
|
|
7206
7377
|
try {
|
|
7207
7378
|
const coordinate = coordinateIdsOf(axes, item.object, item.action, item.scene);
|
|
7208
|
-
const result = await
|
|
7379
|
+
const result = await this.ingestKnowledge({
|
|
7209
7380
|
text: item.text,
|
|
7210
7381
|
objectId: coordinate.objectId,
|
|
7211
7382
|
actionId: coordinate.actionId,
|
|
@@ -7213,11 +7384,16 @@ export class KnowledgeRuntime {
|
|
|
7213
7384
|
runId: input.runId,
|
|
7214
7385
|
sources: usable.map(source => ({
|
|
7215
7386
|
documentId: source.documentId,
|
|
7387
|
+
...(input.sourceVersions?.[source.documentId] === undefined ? {} : { toVersion: input.sourceVersions[source.documentId] }),
|
|
7216
7388
|
quote: source.quote,
|
|
7217
7389
|
headingPath: source.headingPath,
|
|
7218
7390
|
...(source.locator ? { locator: source.locator } : {}),
|
|
7219
7391
|
})),
|
|
7220
7392
|
});
|
|
7393
|
+
if (result === null) {
|
|
7394
|
+
skipped += 1;
|
|
7395
|
+
continue;
|
|
7396
|
+
}
|
|
7221
7397
|
if (result.merged)
|
|
7222
7398
|
merged += 1;
|
|
7223
7399
|
else
|
|
@@ -7402,13 +7578,7 @@ export class KnowledgeRuntime {
|
|
|
7402
7578
|
}
|
|
7403
7579
|
}
|
|
7404
7580
|
else if (preciseTime(event.occurredAt) && hasCompleteCoordinate(coordinateIds) && event.symptom?.trim()) {
|
|
7405
|
-
identityKey =
|
|
7406
|
-
event.occurredAt,
|
|
7407
|
-
coordinateIds.objectId,
|
|
7408
|
-
coordinateIds.actionId,
|
|
7409
|
-
coordinateIds.sceneId,
|
|
7410
|
-
normalizeKnowledge(event.symptom),
|
|
7411
|
-
]));
|
|
7581
|
+
identityKey = compositeEventIdentity(event.occurredAt, coordinateIds, event.symptom);
|
|
7412
7582
|
identityType = 'composite';
|
|
7413
7583
|
}
|
|
7414
7584
|
else {
|
|
@@ -7526,7 +7696,14 @@ export class KnowledgeRuntime {
|
|
|
7526
7696
|
*/
|
|
7527
7697
|
async rebuild(only = 'all', lineage, options = {}) {
|
|
7528
7698
|
const runId = newCompileRunId();
|
|
7529
|
-
|
|
7699
|
+
let result;
|
|
7700
|
+
try {
|
|
7701
|
+
result = await this.enqueueCompile(runId, only, lineage, options);
|
|
7702
|
+
}
|
|
7703
|
+
catch (cause) {
|
|
7704
|
+
await this.settleProgressFailure(runId, cause);
|
|
7705
|
+
throw cause;
|
|
7706
|
+
}
|
|
7530
7707
|
/*
|
|
7531
7708
|
* 同步调用方拿的是返回值,但**别人可能在轮询同一条进度记录**(面板正开着编译对话框,
|
|
7532
7709
|
* 而 Agent 起了这一次)。结论两个出口都要有。
|
|
@@ -7620,7 +7797,7 @@ export class KnowledgeRuntime {
|
|
|
7620
7797
|
queued: false,
|
|
7621
7798
|
finishedAt: current.finishedAt ?? new Date().toISOString(),
|
|
7622
7799
|
failed: true,
|
|
7623
|
-
outcome:
|
|
7800
|
+
outcome: `编译失败:${message}`,
|
|
7624
7801
|
});
|
|
7625
7802
|
await this.patchCompileOperation(runId, 'failed', `编译失败:${message}`);
|
|
7626
7803
|
}
|
|
@@ -7676,6 +7853,52 @@ export class KnowledgeRuntime {
|
|
|
7676
7853
|
* 它」—— 那两件事分别在 `enqueueCompile()` 与 `rebuild()` / `startCompile()` 里。
|
|
7677
7854
|
*/
|
|
7678
7855
|
async compileOnce(runId, only = 'all', lineage, options = {}) {
|
|
7856
|
+
await this.environmentGuard?.();
|
|
7857
|
+
this.publication = { artifacts: new Map() };
|
|
7858
|
+
const configAtStart = JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() });
|
|
7859
|
+
try {
|
|
7860
|
+
await this.loadCompiledState();
|
|
7861
|
+
const result = await this.prepareCompile(runId, only, lineage, options);
|
|
7862
|
+
await this.checkpointCompileTask(runId);
|
|
7863
|
+
if (result.ok) {
|
|
7864
|
+
const db = await this.derivedHandle();
|
|
7865
|
+
if (!db)
|
|
7866
|
+
throw new Error('DERIVED_STORE_UNAVAILABLE');
|
|
7867
|
+
const truth = await this.storeHandle();
|
|
7868
|
+
const knowledge = await this.knowledgeStore();
|
|
7869
|
+
const rows = [];
|
|
7870
|
+
if (knowledge) {
|
|
7871
|
+
for (let offset = 0;; offset += 500) {
|
|
7872
|
+
const page = await knowledge.list({ limit: 500, offset });
|
|
7873
|
+
rows.push(...page.knowledge);
|
|
7874
|
+
if (offset + page.knowledge.length >= page.total)
|
|
7875
|
+
break;
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
// Embeddings-only rebuild keeps the published card and knowledge generation.
|
|
7879
|
+
const previous = db.artifact(COMPILE_SNAPSHOT);
|
|
7880
|
+
const snapshot = only === 'embeddings' && previous
|
|
7881
|
+
? { ...JSON.parse(previous), version: result.data.version, compileRunId: runId }
|
|
7882
|
+
: {
|
|
7883
|
+
version: result.data.version, compileRunId: runId,
|
|
7884
|
+
cards: this.cards, notes: this.units, graph: this.graph,
|
|
7885
|
+
diagnosisGraph: this.diagnosisGraph,
|
|
7886
|
+
placements: [...(truth?.placements() ?? new Map())], knowledge: rows,
|
|
7887
|
+
};
|
|
7888
|
+
this.publication.artifacts.set(COMPILE_SNAPSHOT, JSON.stringify(snapshot));
|
|
7889
|
+
await this.checkpointCompileTask(runId);
|
|
7890
|
+
if (JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() }) !== configAtStart)
|
|
7891
|
+
throw new Error('COMPILE_CONFIG_CHANGED: 编译期间设置已变化,请重新编译');
|
|
7892
|
+
db.publish(this.publication);
|
|
7893
|
+
this.vectorIndex = null;
|
|
7894
|
+
}
|
|
7895
|
+
return result;
|
|
7896
|
+
}
|
|
7897
|
+
finally {
|
|
7898
|
+
this.publication = undefined;
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
async prepareCompile(runId, only = 'all', lineage, options = {}) {
|
|
7679
7902
|
/**
|
|
7680
7903
|
* 进度记录**先占位**:`runId` 一落下去,这一趟就有了名字 —— 之后无论从哪个出口结束
|
|
7681
7904
|
* (三种 return,或者抛异常),收口都能认出这条记录属于谁。
|
|
@@ -7744,24 +7967,17 @@ export class KnowledgeRuntime {
|
|
|
7744
7967
|
this.writeCompileProgress({ ...(this.compileProgress ?? blankProgress(runId, only, startedAt, 'running', queuedAt)), ...patch });
|
|
7745
7968
|
};
|
|
7746
7969
|
/**
|
|
7747
|
-
*
|
|
7748
|
-
*
|
|
7749
|
-
* 它在 `rebuild()` 的**每一个** return 之前调用(一共三处出口)。少了任何一处,
|
|
7750
|
-
* 那次编译的进度就会永远停在「进行中」—— 而界面拿它当「正在跑」,于是按钮一直灰着。
|
|
7970
|
+
* 四段准备完成,保存结论。事务发布之前仍是 running;发布成功后由
|
|
7971
|
+
* settleProgressReport 收口,发布失败则由 settleProgressFailure 收口。
|
|
7751
7972
|
*/
|
|
7752
7973
|
const finishProgress = (outcome) => {
|
|
7753
7974
|
const current = this.compileProgress;
|
|
7754
7975
|
if (current === null)
|
|
7755
7976
|
return;
|
|
7756
|
-
|
|
7757
|
-
* 收口时**立刻存档**,不只写槽位:槽位会被下一趟(排在后面的那一趟)顶掉,
|
|
7758
|
-
* 而存档按编号留着 —— 发起编译的那一方无论何时来读,都读得到自己那一份。
|
|
7759
|
-
*/
|
|
7760
|
-
this.rememberCompile({
|
|
7977
|
+
this.writeCompileProgress({
|
|
7761
7978
|
...current,
|
|
7762
|
-
state: '
|
|
7979
|
+
state: 'running',
|
|
7763
7980
|
queued: false,
|
|
7764
|
-
finishedAt: new Date().toISOString(),
|
|
7765
7981
|
sourcesDone: Math.max(current.sourcesDone, current.sourcesTotal),
|
|
7766
7982
|
steps: current.steps.map(step => ({ ...step, state: 'done' })),
|
|
7767
7983
|
outcome,
|
|
@@ -8023,8 +8239,8 @@ export class KnowledgeRuntime {
|
|
|
8023
8239
|
*/
|
|
8024
8240
|
const incremental = only === 'wiki';
|
|
8025
8241
|
if (only === 'embeddings') {
|
|
8026
|
-
|
|
8027
|
-
|
|
8242
|
+
await this.loadCompiledState();
|
|
8243
|
+
Object.assign(cardHashes, previousHashes);
|
|
8028
8244
|
}
|
|
8029
8245
|
else {
|
|
8030
8246
|
const previousNotes = incremental ? await this.readNotesArtifact() : [];
|
|
@@ -8106,7 +8322,9 @@ export class KnowledgeRuntime {
|
|
|
8106
8322
|
const diagnosis = await this.diagnosisStore().readGraph();
|
|
8107
8323
|
const diagnosisHash = contentHashOf(JSON.stringify({ nodes: diagnosis.nodes, edges: diagnosis.edges }));
|
|
8108
8324
|
const diagnosisChanged = diagnosisHash !== previousManifest.diagnosisHash;
|
|
8109
|
-
const
|
|
8325
|
+
const embeddingIdentity = JSON.stringify({ provider: this.config.embeddingProvider, model: this.embeddingModelId, fingerprint: this.embeddingFingerprint(), dimension: this.config.embeddingDimension, encoder: ENCODER_FINGERPRINT, baseUrl: this.config.embeddingBaseUrl });
|
|
8326
|
+
const embeddingChanged = previousManifest.embeddingIdentity !== embeddingIdentity;
|
|
8327
|
+
const corpusChanged = embeddingChanged || !incremental || previousVersion === 0 || cardsRecompiled > 0 || cardsRemoved > 0 || diagnosisChanged || derivedRefreshed || indexChanged;
|
|
8110
8328
|
if (!corpusChanged) {
|
|
8111
8329
|
await this.checkpointCompileTask(runId);
|
|
8112
8330
|
// 这一趟的编号是调用方给的(0066):进度记录、报告、manifest 用同一个编号。
|
|
@@ -8129,7 +8347,7 @@ export class KnowledgeRuntime {
|
|
|
8129
8347
|
// 用户刚点了编译却看到上次的统计,会以为按钮没生效。version 不变,所以
|
|
8130
8348
|
// 这个写入既不改索引也不让向量缓存失效。extractionHashes 必须跟着更新:
|
|
8131
8349
|
// 来源变了但没抽出卡片时,不落新指纹会让下一次又把它重抽一遍。
|
|
8132
|
-
await this.writeArtifact('build-manifest.json', JSON.stringify({ version: previousVersion, builtAt: previousManifest.builtAt, sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embedding: previousManifest.embedding, diagnosisHash, indexHash, compileRunId, lastBuild: { only, incremental: true, skipped: true, cardsReused, cardsRecompiled: 0, cardsAdded: 0, cardsRemoved: 0, notesReused, notesRecompiled: 0, ...extractionCounts, ...finalizeCounts, relationsExtracted: 0, relationsDropped: 0 }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8350
|
+
await this.writeArtifact('build-manifest.json', JSON.stringify({ version: previousVersion, builtAt: previousManifest.builtAt, sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embeddingIdentity: JSON.stringify({ provider: this.config.embeddingProvider, model: this.embeddingModelId, fingerprint: this.embeddingFingerprint(), dimension: this.config.embeddingDimension, encoder: ENCODER_FINGERPRINT, baseUrl: this.config.embeddingBaseUrl }), embedding: previousManifest.embedding, diagnosisHash, indexHash, compileRunId, lastBuild: { only, incremental: true, skipped: true, cardsReused, cardsRecompiled: 0, cardsAdded: 0, cardsRemoved: 0, notesReused, notesRecompiled: 0, ...extractionCounts, ...finalizeCounts, relationsExtracted: 0, relationsDropped: 0 }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8133
8351
|
const detail = detailOf({ skipped: true, version: previousVersion, cards: { created: [], updated: [], reused: reusedTitles.slice(0, MAX_LOG_ITEMS), removed: [] } });
|
|
8134
8352
|
report.detail = detail;
|
|
8135
8353
|
await settleOp(detail, `没有变化:复用 ${notesReused} 条召回片段,索引仍为 v${previousVersion}。`, 'done');
|
|
@@ -8198,6 +8416,8 @@ export class KnowledgeRuntime {
|
|
|
8198
8416
|
await this.writeArtifact(DIAGNOSIS_GRAPH, JSON.stringify({ version, builtAt, nodes: diagnosis.nodes, edges: diagnosis.edges }, null, 2));
|
|
8199
8417
|
await this.checkpointCompileTask(runId);
|
|
8200
8418
|
const embeddingResult = await this.buildVectorIndex(this.units, version);
|
|
8419
|
+
if (this.environmentGuard && embeddingResult.status === 'unavailable')
|
|
8420
|
+
throw new Error(`向量索引构建失败:${embeddingResult.error ?? '未知原因'}。请重新自检环境后重试编译`);
|
|
8201
8421
|
await this.checkpointCompileTask(runId);
|
|
8202
8422
|
setStep('build', 'done', [
|
|
8203
8423
|
`知识图谱 ${this.graph.nodes.length} 节点 / ${this.graph.edges.length} 边`
|
|
@@ -8210,7 +8430,7 @@ export class KnowledgeRuntime {
|
|
|
8210
8430
|
]);
|
|
8211
8431
|
// 这一趟的编号是调用方给的(0066):进度记录、报告、manifest 用同一个编号。
|
|
8212
8432
|
const compileRunId = runId;
|
|
8213
|
-
await this.writeArtifact('build-manifest.json', JSON.stringify({ version, builtAt, sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embedding: embeddingResult, fts: ftsResult, compileRunId, diagnosisHash, indexHash, diagnosisGraph: { nodes: diagnosis.nodes.length, edges: diagnosis.edges.length }, lastBuild: { only, incremental: cardsReused > 0, skipped: false, cardsReused, cardsRecompiled, cardsAdded, cardsRemoved: removedCount, notesReused, notesRecompiled, ...extractionCounts, ...finalizeCounts, relationsExtracted, relationsDropped }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8433
|
+
await this.writeArtifact('build-manifest.json', JSON.stringify({ version, builtAt, sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embeddingIdentity: JSON.stringify({ provider: this.config.embeddingProvider, model: this.embeddingModelId, fingerprint: this.embeddingFingerprint(), dimension: this.config.embeddingDimension, encoder: ENCODER_FINGERPRINT, baseUrl: this.config.embeddingBaseUrl }), embedding: embeddingResult, fts: ftsResult, compileRunId, diagnosisHash, indexHash, diagnosisGraph: { nodes: diagnosis.nodes.length, edges: diagnosis.edges.length }, lastBuild: { only, incremental: cardsReused > 0, skipped: false, cardsReused, cardsRecompiled, cardsAdded, cardsRemoved: removedCount, notesReused, notesRecompiled, ...extractionCounts, ...finalizeCounts, relationsExtracted, relationsDropped }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8214
8434
|
const report = {
|
|
8215
8435
|
runId: compileRunId, compileRunId, version, only,
|
|
8216
8436
|
cards: this.cards.length, units: this.units.length, notes: this.units.length,
|
|
@@ -8258,6 +8478,8 @@ export class KnowledgeRuntime {
|
|
|
8258
8478
|
const version = previousVersion + 1;
|
|
8259
8479
|
await this.checkpointCompileTask(runId);
|
|
8260
8480
|
const embeddingResult = await this.buildVectorIndex(this.units, version);
|
|
8481
|
+
if (this.environmentGuard && embeddingResult.status === 'unavailable')
|
|
8482
|
+
throw new Error(`向量索引构建失败:${embeddingResult.error ?? '未知原因'}。请重新自检环境后重试编译`);
|
|
8261
8483
|
await this.checkpointCompileTask(runId);
|
|
8262
8484
|
// 只重建向量这一趟只跑第五段(其余四段在开跑时就标成「本次不涉及」,见 `blankSteps`)。
|
|
8263
8485
|
setStep('build', 'done', [
|
|
@@ -8268,7 +8490,7 @@ export class KnowledgeRuntime {
|
|
|
8268
8490
|
]);
|
|
8269
8491
|
// 这一趟的编号是调用方给的(0066):进度记录、报告、manifest 用同一个编号。
|
|
8270
8492
|
const compileRunId = runId;
|
|
8271
|
-
await this.writeArtifact('build-manifest.json', JSON.stringify({ version, builtAt: new Date().toISOString(), sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embedding: embeddingResult, compileRunId, lastBuild: { only, incremental: embeddingResult.reused > 0, skipped: false, notesReused: embeddingResult.reused, notesRecompiled: Math.max(0, embeddingResult.count - embeddingResult.reused), ...extractionCounts, relationsExtracted: 0, relationsDropped: 0 }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8493
|
+
await this.writeArtifact('build-manifest.json', JSON.stringify({ ...previousManifest, version, builtAt: new Date().toISOString(), sourceCount: sources.length, cardCount: this.cards.length, noteCount: this.units.length, cardHashes, extractionHashes: extraction.extractionHashes, embeddingIdentity: JSON.stringify({ provider: this.config.embeddingProvider, model: this.embeddingModelId, fingerprint: this.embeddingFingerprint(), dimension: this.config.embeddingDimension, encoder: ENCODER_FINGERPRINT, baseUrl: this.config.embeddingBaseUrl }), embedding: embeddingResult, compileRunId, lastBuild: { only, incremental: embeddingResult.reused > 0, skipped: false, notesReused: embeddingResult.reused, notesRecompiled: Math.max(0, embeddingResult.count - embeddingResult.reused), ...extractionCounts, relationsExtracted: 0, relationsDropped: 0 }, ...(lineage ? { lineage } : {}) }, null, 2));
|
|
8272
8494
|
const report = {
|
|
8273
8495
|
runId: compileRunId, compileRunId, version, only,
|
|
8274
8496
|
cards: this.cards.length, units: this.units.length, notes: this.units.length,
|
|
@@ -8347,6 +8569,8 @@ export class KnowledgeRuntime {
|
|
|
8347
8569
|
rightNorm += right[index] * right[index];
|
|
8348
8570
|
}
|
|
8349
8571
|
const cosine = dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm) || 1);
|
|
8572
|
+
if (leftNorm < 1e-12 || rightNorm < 1e-12)
|
|
8573
|
+
throw new Error('ENCODER_PROBE_ZERO_VECTOR: 编码器返回零向量');
|
|
8350
8574
|
if (!Number.isFinite(cosine) || cosine > ENCODER_SELF_SIMILARITY_MAX) {
|
|
8351
8575
|
this.encoderHealth = { ok: false, reason: `编码器对两段无关文本给出同一个向量(cos = ${cosine.toFixed(4)}),向量路已停用` };
|
|
8352
8576
|
return this.encoderHealth;
|
|
@@ -8485,15 +8709,15 @@ export class KnowledgeRuntime {
|
|
|
8485
8709
|
}
|
|
8486
8710
|
}
|
|
8487
8711
|
/** 关键词路:FTS5 + BM25(0031)。索引不在时如实降级,不退回旧的计数打分。 */
|
|
8488
|
-
async ftsRoute(query, eligible, pool, threshold, titleOf) {
|
|
8712
|
+
async ftsRoute(query, eligible, pool, threshold, titleOf, captured) {
|
|
8489
8713
|
const eligibleIds = new Set(eligible.map(note => note.unitId));
|
|
8490
8714
|
const derived = await this.derivedHandle();
|
|
8491
8715
|
if (derived === null)
|
|
8492
8716
|
return { ranking: [], rows: [], diagnostic: { route: 'fts', status: 'degraded', reason: '当前 Node 没有 node:sqlite(需要 22.5+),关键词路已降级' } };
|
|
8493
|
-
if (!derived.hasFts())
|
|
8717
|
+
if (!(captured?.ready ?? derived.hasFts()))
|
|
8494
8718
|
return { ranking: [], rows: [], diagnostic: { route: 'fts', status: 'degraded', reason: '关键词索引还没建(在 derived.db 里),先编译一次' } };
|
|
8495
8719
|
try {
|
|
8496
|
-
const found = derived.searchFts(query, pool).filter(hit => eligibleIds.has(hit.unitId));
|
|
8720
|
+
const found = (captured?.hits ?? derived.searchFts(query, pool)).filter(hit => eligibleIds.has(hit.unitId));
|
|
8497
8721
|
/**
|
|
8498
8722
|
* 覆盖率门槛(见 `MIN_QUERY_COVERAGE`):只蹭到一两个高频单字的命中在这里淘汰,
|
|
8499
8723
|
* 于是「整批都弱」也能弃答 —— 这是相对阈值做不到的那一半。
|
|
@@ -8563,8 +8787,8 @@ export class KnowledgeRuntime {
|
|
|
8563
8787
|
* 阈值就完全失去意义(余弦 0 会被映射成 0.5,轻松过线)。原始余弦是绝对量,可以直接
|
|
8564
8788
|
* 用来判「这段内容和问题到底像不像」—— 弃答主要靠它和重排阈值。
|
|
8565
8789
|
*/
|
|
8566
|
-
async vectorRoute(query, eligibleIds, pool, threshold, expectedVersion) {
|
|
8567
|
-
const index = await this.loadVectorIndex();
|
|
8790
|
+
async vectorRoute(query, eligibleIds, pool, threshold, expectedVersion, captured) {
|
|
8791
|
+
const index = captured === undefined ? await this.loadVectorIndex() : captured;
|
|
8568
8792
|
if (!index) {
|
|
8569
8793
|
const reason = '向量索引不存在或损坏,请执行 embeddings 编译重建';
|
|
8570
8794
|
return { ranking: [], rows: [], diagnostic: { route: 'vector', status: 'degraded', reason }, health: { ok: false, reason } };
|
|
@@ -8577,7 +8801,7 @@ export class KnowledgeRuntime {
|
|
|
8577
8801
|
if (!health.ok)
|
|
8578
8802
|
return { ranking: [], rows: [], diagnostic: { route: 'vector', status: 'degraded', reason: health.reason }, health };
|
|
8579
8803
|
try {
|
|
8580
|
-
const { scores, embedMs, scanMs } = await this.vectorRecall(query, eligibleIds, pool);
|
|
8804
|
+
const { scores, embedMs, scanMs } = await this.vectorRecall(query, eligibleIds, pool, index);
|
|
8581
8805
|
const ranking = [...scores.entries()].map(([unitId, score]) => ({ unitId, score })).sort((left, right) => right.score - left.score || left.unitId.localeCompare(right.unitId));
|
|
8582
8806
|
const { kept, dropped, topScore, rows } = applyThreshold(ranking, threshold);
|
|
8583
8807
|
return {
|
|
@@ -8618,11 +8842,11 @@ export class KnowledgeRuntime {
|
|
|
8618
8842
|
* 是因为两张图的节点含义不同(一个是卡片,一个是字段级节点),合成一张会让
|
|
8619
8843
|
* 图谱 Tab 看到一张卡片散成七个点。
|
|
8620
8844
|
*/
|
|
8621
|
-
diagnosisHits(queryTokens) {
|
|
8845
|
+
diagnosisHits(queryTokens, graph = this.diagnosisGraph) {
|
|
8622
8846
|
const seed = new Map();
|
|
8623
|
-
if (
|
|
8847
|
+
if (graph.nodes.length === 0)
|
|
8624
8848
|
return seed;
|
|
8625
|
-
for (const node of
|
|
8849
|
+
for (const node of graph.nodes) {
|
|
8626
8850
|
const title = String(node.title ?? '').toLocaleLowerCase();
|
|
8627
8851
|
const id = String(node.id ?? '').toLocaleLowerCase();
|
|
8628
8852
|
let score = 0;
|
|
@@ -8636,7 +8860,7 @@ export class KnowledgeRuntime {
|
|
|
8636
8860
|
seed.set(String(node.id), score);
|
|
8637
8861
|
}
|
|
8638
8862
|
// 关系词命中:两端各得 1 分。中文提问不会写 `refutes`,所以走 RELATION_QUERY_WORDS。
|
|
8639
|
-
for (const edge of
|
|
8863
|
+
for (const edge of graph.edges) {
|
|
8640
8864
|
const type = String(edge.kind ?? '');
|
|
8641
8865
|
const relation = (RELATION_QUERY_WORDS[type] ?? type.toLocaleLowerCase().replace(/_/g, ' ')).toLocaleLowerCase();
|
|
8642
8866
|
let score = 0;
|
|
@@ -8652,7 +8876,7 @@ export class KnowledgeRuntime {
|
|
|
8652
8876
|
return seed;
|
|
8653
8877
|
const expanded = new Map(seed);
|
|
8654
8878
|
for (const [id, score] of seed) {
|
|
8655
|
-
for (const edge of
|
|
8879
|
+
for (const edge of graph.edges) {
|
|
8656
8880
|
const from = String(edge.source);
|
|
8657
8881
|
const to = String(edge.target);
|
|
8658
8882
|
if (from === id)
|
|
@@ -8663,8 +8887,8 @@ export class KnowledgeRuntime {
|
|
|
8663
8887
|
}
|
|
8664
8888
|
return expanded;
|
|
8665
8889
|
}
|
|
8666
|
-
async graphBoostRanking(query, eligible, includeProblems) {
|
|
8667
|
-
if (
|
|
8890
|
+
async graphBoostRanking(query, eligible, includeProblems, graph = this.graph, diagnosisGraph = this.diagnosisGraph) {
|
|
8891
|
+
if (graph.nodes.length === 0)
|
|
8668
8892
|
return [];
|
|
8669
8893
|
const queryTokens = segment(query);
|
|
8670
8894
|
if (queryTokens.length === 0)
|
|
@@ -8675,9 +8899,9 @@ export class KnowledgeRuntime {
|
|
|
8675
8899
|
const nodeKey = (node) => `${String(node.type)}:${String(node.id)}`;
|
|
8676
8900
|
/** 卡片级节点 id → 它的 `entity_type`。归因图的节点 id 与卡片 id 相等(§14.1)。 */
|
|
8677
8901
|
const typeByEntityId = new Map();
|
|
8678
|
-
for (const node of
|
|
8902
|
+
for (const node of graph.nodes)
|
|
8679
8903
|
typeByEntityId.set(String(node.id), String(node.type));
|
|
8680
|
-
for (const node of
|
|
8904
|
+
for (const node of graph.nodes) {
|
|
8681
8905
|
if (!visible(node))
|
|
8682
8906
|
continue;
|
|
8683
8907
|
const title = String(node.title ?? '').toLocaleLowerCase();
|
|
@@ -8692,7 +8916,7 @@ export class KnowledgeRuntime {
|
|
|
8692
8916
|
if (score > 0)
|
|
8693
8917
|
boost(nodeKey(node), score);
|
|
8694
8918
|
}
|
|
8695
|
-
for (const edge of
|
|
8919
|
+
for (const edge of graph.edges) {
|
|
8696
8920
|
const type = String(edge.type ?? '');
|
|
8697
8921
|
const relation = (RELATION_QUERY_WORDS[type] ?? type.toLocaleLowerCase().replace(/_/g, ' ')).toLocaleLowerCase();
|
|
8698
8922
|
let score = 0;
|
|
@@ -8712,7 +8936,7 @@ export class KnowledgeRuntime {
|
|
|
8712
8936
|
* 归因图上命中的节点,映射回同 id 的卡片级节点加权 —— **不需要合图**,
|
|
8713
8937
|
* 因为两张图的节点 id 本来就相等(§14.1)。
|
|
8714
8938
|
*/
|
|
8715
|
-
for (const [id, score] of this.diagnosisHits(queryTokens)) {
|
|
8939
|
+
for (const [id, score] of this.diagnosisHits(queryTokens, diagnosisGraph)) {
|
|
8716
8940
|
const type = typeByEntityId.get(id);
|
|
8717
8941
|
if (type)
|
|
8718
8942
|
boost(`${type}:${id}`, score);
|
|
@@ -8721,7 +8945,7 @@ export class KnowledgeRuntime {
|
|
|
8721
8945
|
return [];
|
|
8722
8946
|
const expanded = new Map(byNodeKey);
|
|
8723
8947
|
for (const [key, score] of byNodeKey) {
|
|
8724
|
-
for (const edge of
|
|
8948
|
+
for (const edge of graph.edges) {
|
|
8725
8949
|
const from = String(edge.from);
|
|
8726
8950
|
const to = String(edge.to);
|
|
8727
8951
|
if (from === key)
|
|
@@ -8804,7 +9028,7 @@ export class KnowledgeRuntime {
|
|
|
8804
9028
|
* 这次召回少一条证据,而正确的答案是换成它被并进的那一条(`mergedInto` 只跳一跳:
|
|
8805
9029
|
* 并进的目标自己不会再被并 —— 那正是「并」这个动作的定义)。
|
|
8806
9030
|
*/
|
|
8807
|
-
async attachKnowledge(hits) {
|
|
9031
|
+
async attachKnowledge(hits, snapshot) {
|
|
8808
9032
|
if (hits.length === 0)
|
|
8809
9033
|
return;
|
|
8810
9034
|
const store = await this.storeHandle();
|
|
@@ -8819,7 +9043,7 @@ export class KnowledgeRuntime {
|
|
|
8819
9043
|
* 一次召回十来张卡,而库里可能有上千张。
|
|
8820
9044
|
*/
|
|
8821
9045
|
try {
|
|
8822
|
-
placements = store.placementsOf([...new Set(hits.map(hit => hit.entityId))]);
|
|
9046
|
+
placements = snapshot?.placements.length ? new Map(snapshot.placements) : store.placementsOf([...new Set(hits.map(hit => hit.entityId))]);
|
|
8823
9047
|
}
|
|
8824
9048
|
catch {
|
|
8825
9049
|
return;
|
|
@@ -8841,7 +9065,14 @@ export class KnowledgeRuntime {
|
|
|
8841
9065
|
const wanted = new Set(perHit.flat().map(row => row.knowledgeId));
|
|
8842
9066
|
if (wanted.size === 0)
|
|
8843
9067
|
return;
|
|
8844
|
-
const rows =
|
|
9068
|
+
const rows = snapshot && snapshot.knowledge.length > 0
|
|
9069
|
+
? new Map(snapshot.knowledge.filter(row => wanted.has(row.id) && knowledge !== null)
|
|
9070
|
+
.flatMap(row => {
|
|
9071
|
+
// Deleted facts never reappear through a historical snapshot.
|
|
9072
|
+
const live = store.raw('SELECT status FROM knowledge WHERE id = ?').get(row.id);
|
|
9073
|
+
return live ? [[row.id, recallKnowledgeOf(assessKnowledgeEvidence({ ...row, status: live.status }, store), '')]] : [];
|
|
9074
|
+
}))
|
|
9075
|
+
: await this.readKnowledgeRows(knowledge, [...wanted]);
|
|
8845
9076
|
if (rows.size === 0)
|
|
8846
9077
|
return;
|
|
8847
9078
|
hits.forEach((hit, index) => {
|
|
@@ -8872,12 +9103,12 @@ export class KnowledgeRuntime {
|
|
|
8872
9103
|
if (row.mergedInto !== '')
|
|
8873
9104
|
followup.set(row.mergedInto, row.id);
|
|
8874
9105
|
else
|
|
8875
|
-
out.set(row.id, recallKnowledgeOf(row, ''));
|
|
9106
|
+
out.set(row.id, recallKnowledgeOf(assessKnowledgeEvidence(row, (await this.storeHandle())), ''));
|
|
8876
9107
|
}
|
|
8877
9108
|
if (followup.size > 0) {
|
|
8878
9109
|
rows = await store.list({ ids: [...followup.keys()], limit: 500 });
|
|
8879
9110
|
for (const row of rows.knowledge)
|
|
8880
|
-
out.set(row.id, recallKnowledgeOf(row, ''));
|
|
9111
|
+
out.set(row.id, recallKnowledgeOf(assessKnowledgeEvidence(row, (await this.storeHandle())), ''));
|
|
8881
9112
|
// 目标也没了(例如它的来源被删了):那条落位就是悬空的,如实**不显示**,
|
|
8882
9113
|
// 而不是把一条已经不存在的事实塞进上下文。
|
|
8883
9114
|
}
|
|
@@ -8927,27 +9158,68 @@ export class KnowledgeRuntime {
|
|
|
8927
9158
|
return failure(empty(), 'LIMIT_INVALID', '召回 top_k 必须是 1 到 50 的整数', false);
|
|
8928
9159
|
if (!Number.isInteger(requestedTopN) || requestedTopN < 1 || (options.contextTopN !== undefined && requestedTopN > recallTopK))
|
|
8929
9160
|
return failure(empty(), 'CONTEXT_TOP_N_INVALID', `上下文 top_n 必须是 1 到 ${recallTopK} 的整数`, false);
|
|
8930
|
-
if (this.units.length === 0) {
|
|
8931
|
-
await this.loadCompiledState();
|
|
8932
|
-
if (this.units.length === 0) {
|
|
8933
|
-
return failure(empty(), 'INDEX_NOT_READY', '知识索引尚未构建,请先编译知识库', true, '先执行 rebuild_knowledge,再进行召回。');
|
|
8934
|
-
}
|
|
8935
|
-
}
|
|
8936
|
-
const eventLineage = withRun(lineage, 'recall');
|
|
8937
|
-
const indexVersion = (await this.loadVectorIndex())?.manifest.version;
|
|
8938
|
-
let compileRunId;
|
|
8939
|
-
let knowledgeIndexVersion;
|
|
8940
9161
|
try {
|
|
8941
|
-
|
|
8942
|
-
if (typeof manifest.compileRunId === 'string' && manifest.compileRunId.trim())
|
|
8943
|
-
compileRunId = manifest.compileRunId;
|
|
8944
|
-
if (typeof manifest.version === 'number' && Number.isInteger(manifest.version))
|
|
8945
|
-
knowledgeIndexVersion = manifest.version;
|
|
9162
|
+
await this.environmentGuard?.();
|
|
8946
9163
|
}
|
|
8947
|
-
catch {
|
|
8948
|
-
|
|
9164
|
+
catch (cause) {
|
|
9165
|
+
return failure(empty(), 'ENVIRONMENT_NOT_READY', cause instanceof Error ? cause.message : String(cause), true, '在 Tita 设置中完成环境准备并重新自检。');
|
|
9166
|
+
}
|
|
9167
|
+
const derived = await this.derivedHandle();
|
|
9168
|
+
const liveCards = await this.loadCards();
|
|
9169
|
+
const liveKeys = new Set(liveCards.map(card => cardKey(card)));
|
|
9170
|
+
const snapshotHash = derived?.artifactHash(COMPILE_SNAPSHOT);
|
|
9171
|
+
const cached = snapshotHash && this.recallSnapshotCache?.hash === snapshotHash ? this.recallSnapshotCache : undefined;
|
|
9172
|
+
const raw = cached ? undefined : derived?.artifact(COMPILE_SNAPSHOT);
|
|
9173
|
+
let snapshot;
|
|
9174
|
+
if (cached)
|
|
9175
|
+
snapshot = cached.snapshot;
|
|
9176
|
+
else if (raw)
|
|
9177
|
+
snapshot = JSON.parse(raw);
|
|
9178
|
+
else {
|
|
9179
|
+
// Upgrade path for stores built before compiled snapshots existed.
|
|
9180
|
+
const manifest = JSON.parse(derived?.artifact('build-manifest.json') ?? '{}');
|
|
9181
|
+
snapshot = {
|
|
9182
|
+
version: manifest.version ?? 0, compileRunId: manifest.compileRunId,
|
|
9183
|
+
notes: JSON.parse(derived?.artifact(WIKI_NOTES) ?? derived?.artifact(LEGACY_UNITS) ?? '[]'),
|
|
9184
|
+
cards: liveCards, graph: JSON.parse(derived?.artifact('knowledge_graph.json') ?? '{"nodes":[],"edges":[]}'),
|
|
9185
|
+
diagnosisGraph: JSON.parse(derived?.artifact(DIAGNOSIS_GRAPH) ?? '{"nodes":[],"edges":[]}'),
|
|
9186
|
+
placements: [], knowledge: [],
|
|
9187
|
+
};
|
|
9188
|
+
}
|
|
9189
|
+
if (snapshot.notes.length === 0)
|
|
9190
|
+
return failure(empty(), 'INDEX_NOT_READY', '知识索引尚未构建,请先编译知识库', true, '先执行 rebuild_knowledge,再进行召回。');
|
|
9191
|
+
const capturedFts = { ready: derived?.hasFts() ?? false, hits: derived?.searchFts(query, candidatePool) ?? [] };
|
|
9192
|
+
const capturedVector = cached ? cached.vector : decodeVectorIndex(derived?.vectorIndex(), ENCODER_FINGERPRINT);
|
|
9193
|
+
if (this.environmentGuard && recallMode !== 'fts' && (!capturedVector
|
|
9194
|
+
|| capturedVector.manifest.provider !== this.config.embeddingProvider
|
|
9195
|
+
|| capturedVector.manifest.dimension !== this.config.embeddingDimension
|
|
9196
|
+
|| capturedVector.manifest.model !== this.embeddingModelId
|
|
9197
|
+
|| capturedVector.manifest.modelFingerprint !== this.embeddingFingerprint())) {
|
|
9198
|
+
return failure(empty(), 'INDEX_NOT_READY', '环境已准备,但当前模型的向量索引尚未构建,请重新编译知识库', true, '执行 rebuild_knowledge,完成后再召回。');
|
|
9199
|
+
}
|
|
9200
|
+
if (snapshotHash)
|
|
9201
|
+
this.recallSnapshotCache = { hash: snapshotHash, snapshot, vector: capturedVector };
|
|
9202
|
+
const eventLineage = withRun(lineage, 'recall');
|
|
9203
|
+
const indexVersion = capturedVector?.manifest.version;
|
|
9204
|
+
const compileRunId = snapshot.compileRunId;
|
|
9205
|
+
const knowledgeIndexVersion = snapshot.version;
|
|
9206
|
+
const truthAtRead = await this.storeHandle();
|
|
9207
|
+
const liveSourceIds = new Set(truthAtRead?.raw('SELECT entity_id FROM source').all()?.map(row => row.entity_id) ?? []);
|
|
9208
|
+
const snapshotCardByKey = new Map(snapshot.cards.map(card => [cardKey(card), card]));
|
|
9209
|
+
for (const key of liveKeys) {
|
|
9210
|
+
if (snapshotCardByKey.get(key)?.sourceRefs.some(ref => !liveSourceIds.has(ref)))
|
|
9211
|
+
liveKeys.delete(key);
|
|
9212
|
+
}
|
|
9213
|
+
let publishedGraph = visibleGraph(snapshot.graph, truthAtRead ? liveGraphKeys(truthAtRead) : new Set());
|
|
9214
|
+
const currentByKey = new Map(liveCards.map(card => [cardKey(card), card]));
|
|
9215
|
+
let cards = snapshot.cards.filter(card => liveKeys.has(cardKey(card))).map(card => {
|
|
9216
|
+
const current = currentByKey.get(cardKey(card));
|
|
9217
|
+
return { ...card, status: current.status, annotations: current.annotations, review: current.review,
|
|
9218
|
+
metadata: { ...card.metadata, ...(current.metadata.evidence_needs_review === true ? { evidence_needs_review: true } : {}) } };
|
|
9219
|
+
});
|
|
9220
|
+
const eligible = snapshot.notes.filter(unit => liveKeys.has(`${unit.entityType}:${unit.entityId}`)
|
|
9221
|
+
&& (includeProblems || currentByKey.get(`${unit.entityType}:${unit.entityId}`)?.status !== 'problem'));
|
|
8949
9222
|
const eligibleIds = new Set(eligible.map(unit => unit.unitId));
|
|
8950
|
-
const cards = this.cards.length > 0 ? this.cards : await this.loadCards();
|
|
8951
9223
|
const queryTerms = [...new Set(segment(query))];
|
|
8952
9224
|
const warnings = [];
|
|
8953
9225
|
// 一份 SourceRaw 语料,两处要用:关键词路的**查询词覆盖率**必须对着「建索引时那份
|
|
@@ -8977,16 +9249,19 @@ export class KnowledgeRuntime {
|
|
|
8977
9249
|
const relationMs = performance.now() - relationStarted;
|
|
8978
9250
|
const ftsStarted = performance.now();
|
|
8979
9251
|
const ftsOutcome = useFts
|
|
8980
|
-
? await this.ftsRoute(query, eligible, candidatePool, this.config.keywordThreshold, titleOf)
|
|
9252
|
+
? await this.ftsRoute(query, eligible, candidatePool, this.config.keywordThreshold, titleOf, capturedFts)
|
|
8981
9253
|
: { ranking: [], rows: [], diagnostic: { route: 'fts', status: 'skipped', reason: '当前模式未启用关键词路' } };
|
|
8982
9254
|
const ftsMs = performance.now() - ftsStarted;
|
|
8983
9255
|
const vectorStarted = performance.now();
|
|
8984
9256
|
const vectorOutcome = useVector
|
|
8985
|
-
? await this.vectorRoute(query, eligibleIds, candidatePool, this.config.vectorThreshold, knowledgeIndexVersion)
|
|
9257
|
+
? await this.vectorRoute(query, eligibleIds, candidatePool, this.config.vectorThreshold, knowledgeIndexVersion, capturedVector)
|
|
8986
9258
|
: { ranking: [], rows: [], diagnostic: { route: 'vector', status: 'skipped', reason: '当前模式未启用向量路' }, health: { ok: false, reason: '当前模式未启用向量路' } };
|
|
9259
|
+
if (this.environmentGuard && useVector && vectorOutcome.diagnostic.status === 'degraded') {
|
|
9260
|
+
return failure(empty(), 'ENVIRONMENT_NOT_READY', `向量推理失败:${vectorOutcome.diagnostic.reason ?? '未知原因'}`, true, '在 Tita 设置中重新自检环境后再召回。');
|
|
9261
|
+
}
|
|
8987
9262
|
const vectorMs = performance.now() - vectorStarted;
|
|
8988
9263
|
const graphStarted = performance.now();
|
|
8989
|
-
const graphRanking = useGraphBoost ? await this.graphBoostRanking(query, eligible, includeProblems) : [];
|
|
9264
|
+
const graphRanking = useGraphBoost ? await this.graphBoostRanking(query, eligible, includeProblems, publishedGraph, snapshot.diagnosisGraph) : [];
|
|
8990
9265
|
const graphMs = performance.now() - graphStarted;
|
|
8991
9266
|
if (coordinateForRecall.diagnostic.status === 'degraded' && coordinateForRecall.diagnostic.reason)
|
|
8992
9267
|
warnings.push(coordinateForRecall.diagnostic.reason);
|
|
@@ -9057,10 +9332,18 @@ export class KnowledgeRuntime {
|
|
|
9057
9332
|
if (reranked.warning)
|
|
9058
9333
|
warnings.push(reranked.warning);
|
|
9059
9334
|
}
|
|
9335
|
+
if (this.environmentGuard && rerank.status === 'unavailable') {
|
|
9336
|
+
return failure(empty(), 'ENVIRONMENT_NOT_READY', `重排推理失败:${rerank.error ?? '未知原因'}`, true, '在 Tita 设置中重新自检环境后再召回。');
|
|
9337
|
+
}
|
|
9060
9338
|
// 重排换的是相关性顺序,评分加权仍然要压在最上面 —— 否则「review 过的排前面一点」
|
|
9061
9339
|
// 只在关掉重排时成立,而重排默认是开着的。
|
|
9062
9340
|
hits = [...hits].sort((left, right) => rankKey(right) - rankKey(left) || left.unitId.localeCompare(right.unitId));
|
|
9063
9341
|
// --- R4 · 证据片段 -----------------------------------------------------
|
|
9342
|
+
const truth = await this.storeHandle();
|
|
9343
|
+
cards = cards.filter(card => truth?.card(card.entityId) !== undefined && card.sourceRefs.every(ref => truth?.source(ref) !== undefined));
|
|
9344
|
+
const finalKeys = new Set(cards.map(card => cardKey(card)));
|
|
9345
|
+
publishedGraph = visibleGraph(publishedGraph, truth ? liveGraphKeys(truth) : new Set());
|
|
9346
|
+
hits = hits.filter(hit => finalKeys.has(`${hit.entityType}:${hit.entityId}`));
|
|
9064
9347
|
await this.attachEvidence(hits, queryTerms, corpus);
|
|
9065
9348
|
/**
|
|
9066
9349
|
* --- R4.5 · 支撑这一段的知识(0069)--------------------------------------
|
|
@@ -9068,7 +9351,13 @@ export class KnowledgeRuntime {
|
|
|
9068
9351
|
* 排在证据**之后**、去重**之前**:它挂在每条 note 上,按卡片去重时自然跟着
|
|
9069
9352
|
* 被选中的那一条走(`ContextItem.knowledge` 读的就是 `notes[0].knowledge`)。
|
|
9070
9353
|
*/
|
|
9071
|
-
await this.attachKnowledge(hits);
|
|
9354
|
+
await this.attachKnowledge(hits, snapshot);
|
|
9355
|
+
if (cards.some(card => card.metadata.evidence_needs_review === true))
|
|
9356
|
+
warnings.push('部分卡片缺少知识落位记录,删除来源后无法核对每段正文的归属,需要人工复核。');
|
|
9357
|
+
if (hits.some(hit => hit.knowledge?.some(row => row.requiresReview)))
|
|
9358
|
+
warnings.push('部分知识依据历史或未记录版本的证据,需人工复核;证据变化不表示知识已失效。');
|
|
9359
|
+
if (hits.some(hit => hit.knowledge?.length))
|
|
9360
|
+
warnings.push('引用核验仅检查来源定位;引用完整不代表陈述已通过语义核验。');
|
|
9072
9361
|
// --- R5 · 弃答 ---------------------------------------------------------
|
|
9073
9362
|
// 一路都没过阈值 = 这个知识库里没有能回答这个问题的东西。与其凑满 topN 让模型编,
|
|
9074
9363
|
// 不如明确说「没有」:这是产品诚实性的一部分,也是 eval 里「不应召回」样本的落点。
|
|
@@ -9136,21 +9425,21 @@ export class KnowledgeRuntime {
|
|
|
9136
9425
|
.map(([key, notes]) => ({ key, notes, card: cardByKey.get(key) }))
|
|
9137
9426
|
.slice(0, contextTopN);
|
|
9138
9427
|
const wiki = hitCards.map(item => item.card).filter((card) => card !== undefined);
|
|
9139
|
-
const graph = this.oneHopGraph(new Set(hitCards.map(item => item.key)), includeProblems);
|
|
9428
|
+
const graph = this.oneHopGraph(new Set(hitCards.map(item => item.key)), includeProblems, publishedGraph);
|
|
9140
9429
|
/**
|
|
9141
9430
|
* **在去重之后**做 1 跳扩展(0050 的第二半)。
|
|
9142
9431
|
*
|
|
9143
9432
|
* 反过来(先扩展再去重)会把邻居与被命中的卡片混在一起,分不清「这是召回来的」
|
|
9144
9433
|
* 还是「这是邻居」—— 而这两件事在上下文里必须能分开,前者是证据,后者是延伸。
|
|
9145
9434
|
*/
|
|
9146
|
-
const expansion = this.expandWiki(hitCards.map(item => item.key), cardByKey, includeProblems);
|
|
9435
|
+
const expansion = this.expandWiki(hitCards.map(item => item.key), cardByKey, includeProblems, publishedGraph);
|
|
9147
9436
|
/** 上下文条目:先命中的卡片(带它们的 note),再扩展来的卡片。 */
|
|
9148
9437
|
const items = [
|
|
9149
9438
|
...hitCards.map(item => ({
|
|
9150
9439
|
wiki: item.card ?? null,
|
|
9151
9440
|
notes: item.notes,
|
|
9152
9441
|
note: item.notes[0],
|
|
9153
|
-
graph: this.oneHopGraph(new Set([item.key]), includeProblems),
|
|
9442
|
+
graph: this.oneHopGraph(new Set([item.key]), includeProblems, publishedGraph),
|
|
9154
9443
|
reason: 'hit',
|
|
9155
9444
|
...(item.notes[0].evidence ? { evidence: item.notes[0].evidence } : {}),
|
|
9156
9445
|
...(item.notes[0].knowledge ? { knowledge: item.notes[0].knowledge } : {}),
|
|
@@ -9163,7 +9452,7 @@ export class KnowledgeRuntime {
|
|
|
9163
9452
|
return {
|
|
9164
9453
|
wiki: card,
|
|
9165
9454
|
notes: [],
|
|
9166
|
-
graph: this.oneHopGraph(new Set([key]), includeProblems),
|
|
9455
|
+
graph: this.oneHopGraph(new Set([key]), includeProblems, publishedGraph),
|
|
9167
9456
|
reason: 'expansion',
|
|
9168
9457
|
...(from === undefined ? {} : { via: { entityType: from.entityType, entityId: from.entityId, title: from.title, relation: relation ?? '' } }),
|
|
9169
9458
|
};
|
|
@@ -9271,11 +9560,11 @@ export class KnowledgeRuntime {
|
|
|
9271
9560
|
* 2. **量纲**:返回**原始余弦**,不再做 `(cos+1)/2` 的映射 —— 那个映射让 `0.15` 这类阈值
|
|
9272
9561
|
* 失去意义(余弦 0 → 0.5,轻松过线)。原始余弦是绝对量,可以直接用于弃答。
|
|
9273
9562
|
*/
|
|
9274
|
-
async vectorRecall(query, eligibleIds, candidateLimit) {
|
|
9275
|
-
const index = await this.loadVectorIndex();
|
|
9563
|
+
async vectorRecall(query, eligibleIds, candidateLimit, captured) {
|
|
9564
|
+
const index = captured ?? await this.loadVectorIndex();
|
|
9276
9565
|
if (!index || index.manifest.itemCount === 0)
|
|
9277
9566
|
throw new Error('VECTOR_INDEX_NOT_READY');
|
|
9278
|
-
if (index.manifest.provider !== this.config.embeddingProvider || index.manifest.model !== this.embeddingModelId || index.manifest.dimension !== this.config.embeddingDimension)
|
|
9567
|
+
if (index.manifest.provider !== this.config.embeddingProvider || index.manifest.model !== this.embeddingModelId || index.manifest.modelFingerprint !== this.embeddingFingerprint() || index.manifest.dimension !== this.config.embeddingDimension)
|
|
9279
9568
|
throw new Error('VECTOR_INDEX_CONFIG_MISMATCH');
|
|
9280
9569
|
// 查询编码单独计时:模型没加载时这一段是 8 秒,加载之后是 0.3 秒 —— 这一段的
|
|
9281
9570
|
// 数字就是「测试为什么慢」的答案本身,混进扫描时间里就看不出来了。
|
|
@@ -9327,6 +9616,44 @@ export class KnowledgeRuntime {
|
|
|
9327
9616
|
embeddingModelId: this.embeddingModelId,
|
|
9328
9617
|
};
|
|
9329
9618
|
}
|
|
9619
|
+
/** Installed resources become available without restarting the host. */
|
|
9620
|
+
reloadResources() {
|
|
9621
|
+
this.embeddingWorker?.close();
|
|
9622
|
+
this.embeddingWorker = null;
|
|
9623
|
+
this.rerankWorker?.close();
|
|
9624
|
+
this.rerankWorker = null;
|
|
9625
|
+
this.encoderHealth = undefined;
|
|
9626
|
+
this.rerankUnavailable = undefined;
|
|
9627
|
+
this.updateConfig({ python: this.config.python, embeddingModel: this.config.embeddingModel, rerankModel: this.config.rerankModel });
|
|
9628
|
+
}
|
|
9629
|
+
async environmentProbe(kind) {
|
|
9630
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
9631
|
+
if (kind === 'embedding') {
|
|
9632
|
+
this.encoderHealth = undefined;
|
|
9633
|
+
const health = await this.checkEncoder();
|
|
9634
|
+
return { ok: health.ok, message: health.ok ? `已完成 ${this.config.embeddingDimension} 维向量推理,未发现零向量或相同向量退化` : health.reason ?? '向量推理自检失败' };
|
|
9635
|
+
}
|
|
9636
|
+
if (this.config.rerankProvider === 'none')
|
|
9637
|
+
return { ok: false, message: '重排模型未启用' };
|
|
9638
|
+
try {
|
|
9639
|
+
if (!this.rerankWorker)
|
|
9640
|
+
this.rerankWorker = this.spawnWorker('RERANK');
|
|
9641
|
+
const response = await this.rerankWorker.request({ task: 'rerank', model: this.rerankModelPath, device: this.config.embeddingDevice,
|
|
9642
|
+
query: '销售额按签收时间统计', documents: ['销售额按签收时间统计,签收后计入销售额。', '烹饪意大利面需要煮沸清水并加入面条。'] }, this.config.rerankTimeoutMs);
|
|
9643
|
+
const scores = response?.scores;
|
|
9644
|
+
if (!Array.isArray(scores) || scores.length !== 2 || scores.some(value => typeof value !== 'number' || !Number.isFinite(value)))
|
|
9645
|
+
throw new Error('重排自检返回分数无效');
|
|
9646
|
+
if (Number(response.vocabSize) < 1000 || !Number.isFinite(Number(response.vocabSize)))
|
|
9647
|
+
throw new Error('重排模型分词器词表缺失或退化');
|
|
9648
|
+
if (scores[0] <= scores[1])
|
|
9649
|
+
throw new Error('重排模型未能把直接相关文本排在无关文本前');
|
|
9650
|
+
this.rerankUnavailable = undefined;
|
|
9651
|
+
return { ok: true, message: '分词器与实际重排推理通过自检' };
|
|
9652
|
+
}
|
|
9653
|
+
catch (cause) {
|
|
9654
|
+
return { ok: false, message: cause instanceof Error ? cause.message : String(cause) };
|
|
9655
|
+
}
|
|
9656
|
+
}
|
|
9330
9657
|
async embeddingHealth() {
|
|
9331
9658
|
const index = await this.loadVectorIndex();
|
|
9332
9659
|
return success({
|
|
@@ -9437,9 +9764,12 @@ export class KnowledgeRuntime {
|
|
|
9437
9764
|
* 「盘上还没有编译产物」(`needsBuild`),要不要编由界面和 Agent 自己决定 ——
|
|
9438
9765
|
* 与 `recall` 同一条规矩:先读盘,读不到再说。
|
|
9439
9766
|
*/
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
const
|
|
9767
|
+
const derived = await this.derivedHandle();
|
|
9768
|
+
const snapshot = derived?.artifact(COMPILE_SNAPSHOT);
|
|
9769
|
+
const rawGraph = snapshot ? JSON.parse(snapshot).graph : JSON.parse(derived?.artifact('knowledge_graph.json') ?? '{"nodes":[],"edges":[]}');
|
|
9770
|
+
const truth = await this.storeHandle();
|
|
9771
|
+
const graph = visibleGraph(rawGraph, truth ? liveGraphKeys(truth) : new Set());
|
|
9772
|
+
const needsBuild = !snapshot && graph.nodes.length === 0;
|
|
9443
9773
|
const attachLineage = (data) => eventLineage ? { ...data, lineage: eventLineage } : data;
|
|
9444
9774
|
/** 空图 + 一句为什么空:调用方(尤其是 Agent)不能把「还没编译」读成「这个库没有知识」。 */
|
|
9445
9775
|
const attachBuild = (data) => needsBuild ? { ...data, needsBuild: true } : data;
|
|
@@ -9452,11 +9782,11 @@ export class KnowledgeRuntime {
|
|
|
9452
9782
|
return success(attachLineage({ types: this.wikiSchema().types.filter(def => def.enabled).map(def => def.id), relationTypes: ['supported_by', RELATES_TO] }));
|
|
9453
9783
|
const includeProblem = options.includeProblem === true;
|
|
9454
9784
|
const visible = (node) => includeProblem || node.status !== 'problem';
|
|
9455
|
-
const allNodes =
|
|
9785
|
+
const allNodes = graph.nodes
|
|
9456
9786
|
.filter(visible)
|
|
9457
9787
|
.filter(node => !options.status || node.status === options.status);
|
|
9458
9788
|
const nodes = allNodes.filter(node => !entityType || node.type === entityType);
|
|
9459
|
-
const edges =
|
|
9789
|
+
const edges = graph.edges.filter(edge => allNodes.some(node => `${node.type}:${node.id}` === String(edge.from)) && allNodes.some(node => `${node.type}:${node.id}` === String(edge.to)));
|
|
9460
9790
|
if (action === 'list')
|
|
9461
9791
|
return success(attachLineage(attachBuild({ nodes: nodes.slice(0, limit), edges: edges.slice(0, limit) })));
|
|
9462
9792
|
const nodeKey = (rawId, rawType) => {
|