dsh-data-domain-knowledge 0.1.1-alpha.2 → 0.1.1-alpha.4
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 +33 -16
- package/lib/client.js +1042 -2097
- package/lib/contract-refs.d.ts +0 -1
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +2489 -1060
- package/lib/types/agent-behavior-rules.d.ts.map +1 -1
- package/lib/types/agent-behavior-rules.js +3 -2
- package/lib/types/agent-behavior-rules.js.map +1 -1
- package/lib/types/axis-contract.js +1 -1
- package/lib/types/axis-contract.js.map +1 -1
- package/lib/types/card-organize.d.ts +2 -1
- package/lib/types/card-organize.d.ts.map +1 -1
- package/lib/types/card-organize.js +5 -3
- package/lib/types/card-organize.js.map +1 -1
- package/lib/types/config-schema.d.ts +1 -1
- package/lib/types/config-schema.js +1 -1
- package/lib/types/convert/host.d.ts +2 -3
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +11 -2
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +14 -20
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +8 -5
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +2 -2
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +10 -2
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/extract-model.d.ts +3 -0
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/images.js +1 -1
- package/lib/types/index.d.ts +0 -4
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +99 -91
- 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 +22 -2
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge-contract.d.ts +6 -0
- package/lib/types/knowledge-contract.d.ts.map +1 -1
- package/lib/types/knowledge-contract.js +6 -2
- package/lib/types/knowledge-contract.js.map +1 -1
- package/lib/types/layout.d.ts +9 -26
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +10 -28
- package/lib/types/layout.js.map +1 -1
- package/lib/types/materialize-snapshot.d.ts +5 -0
- package/lib/types/materialize-snapshot.d.ts.map +1 -1
- package/lib/types/migrate.d.ts +3 -1
- package/lib/types/migrate.d.ts.map +1 -1
- package/lib/types/migrate.js +3 -1
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +7 -0
- package/lib/types/model-text.d.ts.map +1 -1
- package/lib/types/model-text.js +31 -9
- package/lib/types/model-text.js.map +1 -1
- package/lib/types/organize-contract.d.ts.map +1 -1
- package/lib/types/organize-contract.js +2 -1
- package/lib/types/organize-contract.js.map +1 -1
- package/lib/types/pipeline/engine.d.ts +28 -4
- package/lib/types/pipeline/engine.d.ts.map +1 -1
- package/lib/types/pipeline/engine.js +68 -15
- package/lib/types/pipeline/engine.js.map +1 -1
- package/lib/types/pipeline/progress.d.ts +12 -0
- package/lib/types/pipeline/progress.d.ts.map +1 -0
- package/lib/types/pipeline/progress.js +37 -0
- package/lib/types/pipeline/progress.js.map +1 -0
- package/lib/types/pipeline/resumable-extract.d.ts +45 -0
- package/lib/types/pipeline/resumable-extract.d.ts.map +1 -0
- package/lib/types/pipeline/resumable-extract.js +262 -0
- package/lib/types/pipeline/resumable-extract.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +8 -2
- package/lib/types/pipeline/store.d.ts.map +1 -1
- package/lib/types/pipeline/store.js +27 -5
- package/lib/types/pipeline/store.js.map +1 -1
- package/lib/types/pipeline/types.d.ts +56 -0
- package/lib/types/pipeline/types.d.ts.map +1 -1
- package/lib/types/pipeline/types.js +18 -1
- package/lib/types/pipeline/types.js.map +1 -1
- package/lib/types/present.d.ts +1 -1
- package/lib/types/present.js +1 -1
- package/lib/types/preset-ids.d.ts +0 -1
- package/lib/types/preset-ids.d.ts.map +1 -1
- package/lib/types/preset-ids.js +0 -3
- package/lib/types/preset-ids.js.map +1 -1
- package/lib/types/query-analysis.d.ts +0 -5
- package/lib/types/query-analysis.d.ts.map +1 -1
- package/lib/types/query-analysis.js +0 -16
- package/lib/types/query-analysis.js.map +1 -1
- package/lib/types/recall-delivery.d.ts +36 -0
- package/lib/types/recall-delivery.d.ts.map +1 -0
- package/lib/types/recall-delivery.js +16 -0
- package/lib/types/recall-delivery.js.map +1 -0
- package/lib/types/retrieval/scope.d.ts +17 -0
- package/lib/types/retrieval/scope.d.ts.map +1 -0
- package/lib/types/retrieval/scope.js +11 -0
- package/lib/types/retrieval/scope.js.map +1 -0
- package/lib/types/runtime.d.ts +75 -14
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +454 -198
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-volatile.d.ts +0 -7
- package/lib/types/settings-volatile.d.ts.map +1 -1
- package/lib/types/settings-volatile.js +0 -7
- package/lib/types/settings-volatile.js.map +1 -1
- package/lib/types/source-identity.d.ts +34 -0
- package/lib/types/source-identity.d.ts.map +1 -0
- package/lib/types/source-identity.js +85 -0
- package/lib/types/source-identity.js.map +1 -0
- package/lib/types/source-read.d.ts +18 -0
- package/lib/types/source-read.d.ts.map +1 -0
- package/lib/types/source-read.js +49 -0
- package/lib/types/source-read.js.map +1 -0
- package/lib/types/store/axis-references.d.ts.map +1 -1
- package/lib/types/store/axis-references.js +5 -3
- package/lib/types/store/axis-references.js.map +1 -1
- package/lib/types/store/knowledge-db.d.ts +5 -29
- package/lib/types/store/knowledge-db.d.ts.map +1 -1
- package/lib/types/store/knowledge-db.js +57 -2
- package/lib/types/store/knowledge-db.js.map +1 -1
- package/lib/types/store/source-identity.d.ts +8 -0
- package/lib/types/store/source-identity.d.ts.map +1 -0
- package/lib/types/store/source-identity.js +56 -0
- package/lib/types/store/source-identity.js.map +1 -0
- package/lib/types/types.d.ts +46 -5
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +6 -3
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-availability.d.ts +11 -0
- package/lib/types/wiki-availability.d.ts.map +1 -0
- package/lib/types/wiki-availability.js +39 -0
- package/lib/types/wiki-availability.js.map +1 -0
- package/lib/types/wiki-finalize.d.ts +4 -2
- package/lib/types/wiki-finalize.d.ts.map +1 -1
- package/lib/types/wiki-finalize.js +9 -27
- package/lib/types/wiki-finalize.js.map +1 -1
- package/lib/types/wiki-links.d.ts +35 -0
- package/lib/types/wiki-links.d.ts.map +1 -0
- package/lib/types/wiki-links.js +129 -0
- package/lib/types/wiki-links.js.map +1 -0
- package/lib/types/wiki-trial-types.d.ts +3 -0
- package/lib/types/wiki-trial-types.d.ts.map +1 -1
- package/lib/types/wiki-trial.d.ts +3 -3
- package/lib/types/wiki-trial.d.ts.map +1 -1
- package/lib/types/wiki-trial.js +14 -3
- package/lib/types/wiki-trial.js.map +1 -1
- package/package.json +16 -16
- package/scripts/doctor.mjs +0 -7
- package/scripts/setup-embedding.sh +0 -0
- package/src/agent-behavior-rules.ts +3 -2
- package/src/axis-contract.ts +1 -1
- package/src/card-organize.ts +5 -3
- package/src/client/AttributionEventGuide.tsx +3 -3
- package/src/client/AxisMatrix.tsx +86 -19
- package/src/client/AxisRegistrySection.tsx +1 -1
- package/src/client/CompileSettingsView.tsx +473 -0
- package/src/client/DirectoryPicker.tsx +0 -2
- package/src/client/ExtractionTasksPage.tsx +557 -423
- package/src/client/GuideArchitectureCanvas.tsx +5 -12
- package/src/client/ImportFilesDialog.tsx +1 -0
- package/src/client/InfoHint.tsx +1 -1
- package/src/client/KnowledgeBaseTab.tsx +141 -148
- package/src/client/KnowledgePanel.tsx +16 -9
- package/src/client/OperationLogTab.tsx +1 -1
- package/src/client/PipelineRunView.tsx +173 -0
- package/src/client/RecallPanel.tsx +5 -16
- package/src/client/TabGuide.tsx +3 -7
- package/src/client/WikiFlowPanel.tsx +10 -11
- package/src/client/arch/NodeLogicPanel.tsx +3 -3
- package/src/client/arch/model.ts +5 -113
- package/src/client/architecture.css +1 -1
- package/src/client/architecture.tsx +13 -27
- package/src/client/attribution/AttributionTab.tsx +141 -10
- package/src/client/pages.css +305 -180
- package/src/client/pages.tsx +7 -12
- package/src/client/styles.css +15 -96
- package/src/config-schema.ts +1 -1
- package/src/convert/host.ts +13 -5
- package/src/convert/ingest.ts +20 -7
- package/src/diagnosis/store.ts +7 -2
- package/src/diagnosis/types.ts +1 -1
- package/src/extract-model.ts +3 -0
- package/src/images.ts +1 -1
- package/src/index.ts +92 -97
- package/src/knowledge-bases.ts +23 -3
- package/src/knowledge-contract.ts +7 -2
- package/src/layout.ts +10 -32
- package/src/materialize-snapshot.ts +1 -0
- package/src/migrate.ts +3 -1
- package/src/model-text.ts +33 -11
- package/src/organize-contract.ts +2 -1
- package/src/pipeline/engine.ts +85 -22
- package/src/pipeline/progress.ts +40 -0
- package/src/pipeline/resumable-extract.ts +233 -0
- package/src/pipeline/store.ts +28 -6
- package/src/pipeline/types.ts +47 -2
- package/src/present.ts +1 -1
- package/src/preset-ids.ts +0 -4
- package/src/query-analysis.ts +0 -20
- package/src/recall-delivery.ts +19 -0
- package/src/retrieval/scope.ts +10 -0
- package/src/runtime.ts +412 -206
- package/src/settings-volatile.ts +0 -7
- package/src/source-identity.ts +85 -0
- package/src/source-read.ts +47 -0
- package/src/store/axis-references.ts +5 -3
- package/src/store/knowledge-db.ts +54 -2
- package/src/store/source-identity.ts +42 -0
- package/src/types.ts +18 -5
- package/src/wiki-availability.ts +37 -0
- package/src/wiki-finalize.ts +9 -25
- package/src/wiki-links.ts +120 -0
- package/src/wiki-trial-types.ts +3 -2
- package/src/wiki-trial.ts +17 -6
- package/lib/types/intent/engine.d.ts +0 -122
- package/lib/types/intent/engine.d.ts.map +0 -1
- package/lib/types/intent/engine.js +0 -488
- package/lib/types/intent/engine.js.map +0 -1
- package/lib/types/intent/host.d.ts +0 -68
- package/lib/types/intent/host.d.ts.map +0 -1
- package/lib/types/intent/host.js +0 -249
- package/lib/types/intent/host.js.map +0 -1
- package/lib/types/intent/scope.d.ts +0 -25
- package/lib/types/intent/scope.d.ts.map +0 -1
- package/lib/types/intent/scope.js +0 -116
- package/lib/types/intent/scope.js.map +0 -1
- package/lib/types/intent/store.d.ts +0 -33
- package/lib/types/intent/store.d.ts.map +0 -1
- package/lib/types/intent/store.js +0 -239
- package/lib/types/intent/store.js.map +0 -1
- package/lib/types/intent/tools.d.ts +0 -62
- package/lib/types/intent/tools.d.ts.map +0 -1
- package/lib/types/intent/tools.js +0 -130
- package/lib/types/intent/tools.js.map +0 -1
- package/lib/types/intent/types.d.ts +0 -170
- package/lib/types/intent/types.d.ts.map +0 -1
- package/lib/types/intent/types.js +0 -56
- package/lib/types/intent/types.js.map +0 -1
- package/src/client/IntentLogic.tsx +0 -192
- package/src/client/IntentTab.tsx +0 -343
- package/src/client/TypeDefinitionDrawer.tsx +0 -195
- package/src/intent/README.md +0 -42
- package/src/intent/engine.ts +0 -515
- package/src/intent/host.ts +0 -315
- package/src/intent/scope.ts +0 -125
- package/src/intent/store.ts +0 -218
- package/src/intent/tools.ts +0 -154
- package/src/intent/types.ts +0 -229
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { chooseSourceId } from './store/source-identity.js'
|
|
2
|
+
import { readSourceExact, SourceReadError, type SourceReadInput } from './source-read.js'
|
|
3
|
+
import { matchesScope, scopeError, type KnowledgeScope } from './retrieval/scope.js'
|
|
4
|
+
import { runResumableExtraction, EXTRACTION_PLAN_VERSION } from './pipeline/resumable-extract.js'
|
|
5
|
+
import { compileWikiNavigation, scanWikiLinks, stripWikiNavigation, wikiIndexContent, wikiLinkCandidates, wikiReference, type WikiReference } from './wiki-links.js'
|
|
6
|
+
import { wikiProjection, wikiGroupKey, procedureText, type ProcedureContent, type WikiKind } from './knowledge-contract.js'
|
|
7
|
+
import { wikiUnavailableReason } from './wiki-availability.js'
|
|
8
|
+
import { SourceIdentityError, resolveSourceIdentities, normalizeSourceUrl, type SourceIdentityInput, type SourceIdentity, isSourceId } from './source-identity.js'
|
|
9
|
+
import { agentContextItem } from './recall-delivery.js'
|
|
2
10
|
import { buildDeriveSystem, DERIVE_CONTRACT_VERSION } from './derive-contract.js'
|
|
3
11
|
import { createHash, randomUUID } from 'node:crypto'
|
|
4
12
|
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
@@ -34,13 +42,13 @@ import { buildAliasIndex, candidateEntityId, extractCandidates, normalizeCandida
|
|
|
34
42
|
import { skeletonBody, type ModelCall } from './extract-model.js'
|
|
35
43
|
import { applyTrialAxisResolution, cardNameFor, normalizeKnowledge, runWikiTrial, trialSummary, locateQuote, type AxisPromptBlock, type TrialAxisSet, type TrialAxisSetResolution } from './wiki-trial.js'
|
|
36
44
|
import { EXTRACTION_CONTRACT_VERSION } from './extract-table.js'
|
|
37
|
-
import { KnowledgePipeline, type PipelineContext, type PipelineSource } from './pipeline/engine.js'
|
|
45
|
+
import { KnowledgePipeline, type PipelineContext, type PipelineDeriveProgress, type PipelineSource } from './pipeline/engine.js'
|
|
38
46
|
import { PipelineStore, type ExtractionCheckpoint } from './pipeline/store.js'
|
|
39
|
-
import type
|
|
47
|
+
import { normalizeExtractionTimeoutMinutes, type ExtractionTimeoutMinutes, type MaterializationGroup, type PipelineItemState, type PipelineStatus } from './pipeline/types.js'
|
|
40
48
|
import type { MaterializationSnapshot } from './materialize-snapshot.js'
|
|
41
49
|
import { candidatesFromReport, coordinateIdsOf, coordinateIsEmpty, type MaterializedCard } from './wiki-materialize.js'
|
|
42
50
|
import {
|
|
43
|
-
sectionRecords,
|
|
51
|
+
sectionRecords, ORGANIZE_VERSION,
|
|
44
52
|
buildOrganizePrompt, buildOrganizeSystem, checkOrganize, citationsFrom, layoutHash, layoutOf,
|
|
45
53
|
parseOrganize, placementsOf, renderFootnoteDefinitions, renderOrganizeBody,
|
|
46
54
|
type CardCitationRecord, type CardLayoutRecord, type OrganizeInput, type OrganizePlan,
|
|
@@ -597,7 +605,9 @@ function recallKnowledgeOf(row: Knowledge, section: string): RecallKnowledge {
|
|
|
597
605
|
...(first?.toVersion === undefined ? {} : { sourceVersion: first.toVersion }),
|
|
598
606
|
...(first?.currentVersion === undefined ? {} : { currentSourceVersion: first.currentVersion }),
|
|
599
607
|
...(first?.quote ? { quote: first.quote } : {}),
|
|
600
|
-
...(first?.documentId ? { sourceId: first.documentId
|
|
608
|
+
...(first?.documentId ? { sourceId: first.documentId, readRef: { source_id: first.documentId,
|
|
609
|
+
...(first.toVersion === undefined ? {} : { version: first.toVersion }),
|
|
610
|
+
...(first.locator ? { start: first.locator.start, end: first.locator.end } : {}), quote: first.quote } } : {}),
|
|
601
611
|
...(first && first.headingPath.length > 0 ? { headingPath: first.headingPath } : {}),
|
|
602
612
|
}
|
|
603
613
|
}
|
|
@@ -651,6 +661,11 @@ const NO_DELETION: DocumentDeletion = {
|
|
|
651
661
|
}
|
|
652
662
|
|
|
653
663
|
export interface ImportedSourceReport {
|
|
664
|
+
reusedSource?: boolean
|
|
665
|
+
proposedSourceId?: string
|
|
666
|
+
sourceIdentity?: SourceIdentity[]
|
|
667
|
+
version?: number
|
|
668
|
+
readRef?: { kb?: string; source_id: string; version?: number; content_hash: string }
|
|
654
669
|
index: number
|
|
655
670
|
sourceId: string
|
|
656
671
|
title?: string
|
|
@@ -661,6 +676,7 @@ export interface ImportedSourceReport {
|
|
|
661
676
|
|
|
662
677
|
/** 批量导入里失败的一条。**带着原因回来**,因为局部失败要求调用方能只重试那几条。 */
|
|
663
678
|
export interface ImportFailure {
|
|
679
|
+
candidates?: string[]
|
|
664
680
|
index: number
|
|
665
681
|
title?: string
|
|
666
682
|
code: string
|
|
@@ -1181,7 +1197,7 @@ function parseLineage(value: unknown): LineageContext | undefined {
|
|
|
1181
1197
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
1182
1198
|
const record = value as Record<string, unknown>
|
|
1183
1199
|
const lineage: LineageContext = {}
|
|
1184
|
-
for (const key of ['sessionId', 'turnId', '
|
|
1200
|
+
for (const key of ['sessionId', 'turnId', 'taskId', 'subtaskId', 'parentSubtaskId', 'runId', 'traceId', 'domain'] as const) {
|
|
1185
1201
|
if (typeof record[key] === 'string' && record[key].trim()) lineage[key] = record[key].trim().slice(0, 160)
|
|
1186
1202
|
}
|
|
1187
1203
|
return Object.keys(lineage).length > 0 ? lineage : undefined
|
|
@@ -1311,7 +1327,7 @@ export function splitWikiNotes(card: Pick<KnowledgeCard, 'summary' | 'content'>)
|
|
|
1311
1327
|
const sections: Array<{ heading: string; lines: string[] }> = []
|
|
1312
1328
|
let current: { heading: string; lines: string[] } | undefined
|
|
1313
1329
|
let fence: string | undefined
|
|
1314
|
-
for (const line of card.content.trim().split('\n')) {
|
|
1330
|
+
for (const line of stripWikiNavigation(card.content).trim().split('\n')) {
|
|
1315
1331
|
const code = line.match(/^\s*(?:(?:\d+[.)]|[-+*])\s+)?(`{3,}|~{3,})(.*)$/)
|
|
1316
1332
|
if (code) {
|
|
1317
1333
|
if (!fence) fence = code[1]!
|
|
@@ -1776,6 +1792,7 @@ export interface KnowledgeRuntimeOptions {
|
|
|
1776
1792
|
* 省略时算没有备注 —— 独立运行时与单元测试不必先造一个注册表。
|
|
1777
1793
|
*/
|
|
1778
1794
|
extractionNote?: () => string
|
|
1795
|
+
extractionTimeoutMinutes?: () => ExtractionTimeoutMinutes
|
|
1779
1796
|
/**
|
|
1780
1797
|
* 0029:家当目录(`<agentRoot>/resource`)。
|
|
1781
1798
|
*
|
|
@@ -1831,6 +1848,7 @@ export interface ModelSeam {
|
|
|
1831
1848
|
provider?: string
|
|
1832
1849
|
model?: string
|
|
1833
1850
|
reasoningEffort?: string
|
|
1851
|
+
maxTokens?: number
|
|
1834
1852
|
}
|
|
1835
1853
|
|
|
1836
1854
|
/**
|
|
@@ -1862,6 +1880,7 @@ export interface CompileStepView {
|
|
|
1862
1880
|
/** 这一段的计数事实,一行一条。第一条通常是进度(`读取文档 3/40`)。 */
|
|
1863
1881
|
facts: string[]
|
|
1864
1882
|
}
|
|
1883
|
+
type CompileActivityView = Omit<CompileStepView, 'id'> & { id: CompileStepId | 'publish' }
|
|
1865
1884
|
|
|
1866
1885
|
export type CompileTaskState = 'queued' | 'running' | 'pausing' | 'paused' | 'cancelling' | 'succeeded' | 'failed' | 'cancelled'
|
|
1867
1886
|
export type CompileTaskAction = 'pause' | 'resume' | 'cancel'
|
|
@@ -1997,6 +2016,7 @@ export class KnowledgeRuntime {
|
|
|
1997
2016
|
readonly rootDir: string
|
|
1998
2017
|
/** 本库的坐标提取备注(0068)。见 {@link KnowledgeRuntimeOptions.extractionNote}。 */
|
|
1999
2018
|
private readonly extractionNoteProvider?: () => string
|
|
2019
|
+
private readonly extractionTimeoutProvider?: () => ExtractionTimeoutMinutes
|
|
2000
2020
|
/**
|
|
2001
2021
|
* Id of the knowledge base this runtime serves. `KnowledgeBases` sets it when
|
|
2002
2022
|
* it creates/caches a runtime; asset URLs are built from it. Standalone
|
|
@@ -2165,6 +2185,7 @@ export class KnowledgeRuntime {
|
|
|
2165
2185
|
// 运行时实际在 B」这种两处说法。
|
|
2166
2186
|
this.config = { ...DEFAULT_KNOWLEDGE_CONFIG, ...config }
|
|
2167
2187
|
this.extractionNoteProvider = options.extractionNote
|
|
2188
|
+
this.extractionTimeoutProvider = options.extractionTimeoutMinutes
|
|
2168
2189
|
this.memoryDir = options.memoryRoot ? resolve(options.memoryRoot) : join(this.rootDir, '04_memory')
|
|
2169
2190
|
this.staticModelCall = options.modelCall
|
|
2170
2191
|
this.environmentGuard = options.environmentGuard
|
|
@@ -3179,8 +3200,8 @@ export class KnowledgeRuntime {
|
|
|
3179
3200
|
* 每条的原因仍然在 `sources[].message` 里。
|
|
3180
3201
|
*/
|
|
3181
3202
|
async importSources(input: {
|
|
3182
|
-
sources: Array<{ title?: string; content: string; externalId?: string; metadata?: Record<string, unknown>; allowDuplicateTitle?: boolean }>
|
|
3183
|
-
/**
|
|
3203
|
+
sources: Array<{ sourceId?: string; expectedVersion?: number; sourceUrl?: string; sourceIdentity?: SourceIdentityInput; title?: string; content: string; externalId?: string; metadata?: Record<string, unknown>; allowDuplicateTitle?: boolean }>
|
|
3204
|
+
/** 默认只保存 Source Raw;显式 true 才运行完整处理流程。 */
|
|
3184
3205
|
compile?: boolean
|
|
3185
3206
|
lineage?: LineageContext
|
|
3186
3207
|
}): Promise<Envelope<ImportSourcesResult>> {
|
|
@@ -3198,9 +3219,20 @@ export class KnowledgeRuntime {
|
|
|
3198
3219
|
const written: ImportedSourceReport[] = []
|
|
3199
3220
|
const failed: ImportFailure[] = []
|
|
3200
3221
|
const warnings: string[] = []
|
|
3222
|
+
const seenSourceIds = new Set<string>()
|
|
3201
3223
|
for (const [index, item] of items.entries()) {
|
|
3224
|
+
if (item?.sourceId && seenSourceIds.has(item.sourceId)) {
|
|
3225
|
+
failed.push({ index, title: item.title, code: 'SOURCE_ID_DUPLICATED_IN_BATCH', message: '同一批次中的独立输入必须使用不同 source_id。' })
|
|
3226
|
+
warnings.push(`第 ${index + 1} 份来源重复使用 source_id,未写入。`)
|
|
3227
|
+
continue
|
|
3228
|
+
}
|
|
3229
|
+
if (item?.sourceId) seenSourceIds.add(item.sourceId)
|
|
3202
3230
|
const title = item?.title?.trim() || undefined
|
|
3203
3231
|
const result = await this.addSource({
|
|
3232
|
+
...(item?.sourceId ? { sourceId: item.sourceId } : {}),
|
|
3233
|
+
...(item?.expectedVersion === undefined ? {} : { expectedVersion: item.expectedVersion }),
|
|
3234
|
+
...(item?.sourceUrl === undefined ? {} : { sourceUrl: item.sourceUrl }),
|
|
3235
|
+
...(item?.sourceIdentity === undefined ? {} : { sourceIdentity: item.sourceIdentity }),
|
|
3204
3236
|
...(title ? { title } : {}),
|
|
3205
3237
|
content: item?.content,
|
|
3206
3238
|
...(item?.externalId ? { externalId: item.externalId } : {}),
|
|
@@ -3210,7 +3242,7 @@ export class KnowledgeRuntime {
|
|
|
3210
3242
|
})
|
|
3211
3243
|
if (result.ok === false) {
|
|
3212
3244
|
const message = result.error?.message ?? '写入失败'
|
|
3213
|
-
failed.push({ index, ...(title ? { title } : {}), code: result.error?.code ?? 'SOURCE_WRITE_FAILED', message })
|
|
3245
|
+
failed.push({ index, ...(title ? { title } : {}), code: result.error?.code ?? 'SOURCE_WRITE_FAILED', message, candidates: result.data.candidates })
|
|
3214
3246
|
// 逐条失败也要出现在 warnings 里:`ImportSourcesResult.failed` 是结构化账本,
|
|
3215
3247
|
// 而 warnings 是不管谁在读都必须看见的那一句。
|
|
3216
3248
|
warnings.push(`第 ${index + 1} 份来源${title ? `「${title}」` : ''}没有写入:${message}`)
|
|
@@ -3218,7 +3250,7 @@ export class KnowledgeRuntime {
|
|
|
3218
3250
|
}
|
|
3219
3251
|
written.push({
|
|
3220
3252
|
index,
|
|
3221
|
-
sourceId: result.data.sourceId,
|
|
3253
|
+
sourceId: result.data.sourceId, version: result.data.version, readRef: result.data.readRef, reusedSource: result.data.reusedSource, proposedSourceId: result.data.proposedSourceId, sourceIdentity: result.data.sourceIdentity,
|
|
3222
3254
|
...(title ? { title } : {}),
|
|
3223
3255
|
bytes: typeof item?.content === 'string' ? item.content.length : 0,
|
|
3224
3256
|
})
|
|
@@ -3317,14 +3349,17 @@ export class KnowledgeRuntime {
|
|
|
3317
3349
|
externalId?: string
|
|
3318
3350
|
metadata?: Record<string, unknown>
|
|
3319
3351
|
sourceId?: string
|
|
3352
|
+
expectedVersion?: number
|
|
3353
|
+
sourceUrl?: string
|
|
3354
|
+
sourceIdentity?: SourceIdentityInput
|
|
3320
3355
|
lineage?: LineageContext
|
|
3321
3356
|
provenance?: SourceProvenance
|
|
3322
3357
|
/**
|
|
3323
3358
|
* 允许与已有的来源同标题(0051)。默认 `false`:重名是错误,拒收并要求改标题。
|
|
3324
|
-
* 传 `true`
|
|
3359
|
+
* 传 `true` 时按各自 Source Raw ID 并存,并回一条 warning。
|
|
3325
3360
|
*/
|
|
3326
3361
|
allowDuplicateTitle?: boolean
|
|
3327
|
-
}): Promise<Envelope<{ sourceId: string; lineage?: LineageContext }>> {
|
|
3362
|
+
}): Promise<Envelope<{ sourceId: string; proposedSourceId?: string; reusedSource?: boolean; sourceIdentity?: SourceIdentity[]; candidates?: string[]; possibleDuplicateIds?: string[]; version?: number; readRef?: { kb?: string; source_id: string; version?: number; content_hash: string }; lineage?: LineageContext }>> {
|
|
3328
3363
|
if (!this.config.allowSourceTruth) return failure({ sourceId: '' }, 'SOURCE_TRUTH_DISABLED', '当前配置禁止写入 SourceTruth', false)
|
|
3329
3364
|
if (typeof input.content !== 'string' || input.content.trim().length === 0) return failure({ sourceId: '' }, 'CONTENT_REQUIRED', 'content 不能为空', true)
|
|
3330
3365
|
/**
|
|
@@ -3344,86 +3379,50 @@ export class KnowledgeRuntime {
|
|
|
3344
3379
|
if (db === null) {
|
|
3345
3380
|
return failure({ sourceId: '' }, 'STORE_UNAVAILABLE', this.storeProblem ?? '知识库不可用', true, '检查 Node 版本(正文进库之后需要 22.5+ 的 node:sqlite)与知识库文件的属主标记。')
|
|
3346
3381
|
}
|
|
3347
|
-
const
|
|
3348
|
-
if (!
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
* 重名(0051):**默认拒收**。
|
|
3352
|
-
*
|
|
3353
|
-
* 判据从「目标路径被谁占着」换成「库里有没有人用这个标题」。文件名没有
|
|
3354
|
-
* 之后看起来这条该消失,其实不该 —— 它要解决的问题不是「文件会互相覆盖」,而是
|
|
3355
|
-
* 「库里有两份同名材料」这件事**必须让人知道**。所以三条出路原样保留:
|
|
3356
|
-
*
|
|
3357
|
-
* 1. 同一个 `sourceId`(同一份东西重复导入)→ 就是它自己,不冲突;
|
|
3358
|
-
* 2. 另一个 `sourceId` 用了这个标题 → **拒收**,报错里带上「被谁占着」;
|
|
3359
|
-
* 3. 调用方明确要留两份同名材料 → 传 `allowDuplicateTitle`,回一条 warning。
|
|
3360
|
-
*
|
|
3361
|
-
* 为什么不静默加后缀:那样人永远不知道库里有俩「复盘」。也不静默覆盖:那等于直接
|
|
3362
|
-
* 丢一份知识。
|
|
3363
|
-
*/
|
|
3382
|
+
const proposedSourceId = input.sourceId
|
|
3383
|
+
if (proposedSourceId !== undefined && !isSourceId(proposedSourceId)) return failure({ sourceId: '' }, 'SOURCE_ID_INVALID', 'source_id 含有非法字符', false)
|
|
3384
|
+
let sourceId = proposedSourceId ?? '', reusedSource = false
|
|
3385
|
+
let record!: SourceRaw
|
|
3364
3386
|
let duplicateNote: string | undefined
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
if (input.allowDuplicateTitle !== true) {
|
|
3368
|
-
return failure(
|
|
3369
|
-
{ sourceId: '' },
|
|
3370
|
-
'SOURCE_TITLE_DUPLICATED',
|
|
3371
|
-
`标题「${title}」已被来源 ${owner} 占用,改一个标题再来`,
|
|
3372
|
-
true,
|
|
3373
|
-
'换一个有区分度的标题(例如带上日期或版本),或显式传 allow_duplicate_title: true 让两份并存。',
|
|
3374
|
-
)
|
|
3375
|
-
}
|
|
3376
|
-
duplicateNote = `标题「${title}」与来源 ${owner} 重复,已按 allow_duplicate_title 并存;建议改成有区分度的标题。`
|
|
3377
|
-
}
|
|
3378
|
-
/**
|
|
3379
|
-
* 写入交给存储层:它在**一个事务**里做三件事 —— 正文进 `blob`、加一行 `revision`、
|
|
3380
|
-
* 更新当前态。而「正文没变就不造版本」也在那里(幂等重入的常态,回滚可回滚的依据)。
|
|
3381
|
-
*
|
|
3382
|
-
* 0062 之前这三件事散在三处,其中「版本快照」是单独写一个文件的 —— 于是崩在中间
|
|
3383
|
-
* 就是「文件变了、历史没记」,只能靠启动对账兜底。
|
|
3384
|
-
*/
|
|
3385
|
-
const existing = db.source(sourceId)
|
|
3386
|
-
const record: SourceRaw = {
|
|
3387
|
-
sourceId,
|
|
3388
|
-
title,
|
|
3389
|
-
content: input.content,
|
|
3390
|
-
...(input.externalId ? { externalId: input.externalId } : {}),
|
|
3391
|
-
metadata: input.metadata ?? {},
|
|
3392
|
-
...(input.lineage ? { lineage: input.lineage } : {}),
|
|
3393
|
-
createdAt: existing?.record.createdAt ?? new Date().toISOString(),
|
|
3394
|
-
updatedAt: new Date().toISOString(),
|
|
3395
|
-
}
|
|
3396
|
-
/**
|
|
3397
|
-
* 溯源:调用方给了就用它;没给就**从正文自己声明的地址**补一条最小的;再没有才退回
|
|
3398
|
-
* 外部标识(`upload:<文件名>:<mtime>` 这种,它带着文件的名字)。
|
|
3399
|
-
*
|
|
3400
|
-
* 补进去的都是**已经知道的事实**,不含猜测 —— 但少了这一步,「这份材料从哪来」的
|
|
3401
|
-
* 答案就取决于它当初是走哪条路进来的:转换内核那条路写 `resource` 与 `sources[]`,
|
|
3402
|
-
* 直接读上传这条什么都不写。
|
|
3403
|
-
*/
|
|
3404
|
-
const declaredOrigin = declaredOriginOf(record.content)
|
|
3405
|
-
const provenance: SourceProvenance = input.provenance ?? {
|
|
3406
|
-
...(declaredOrigin === undefined ? {} : { resource: declaredOrigin }),
|
|
3407
|
-
sources: [{
|
|
3408
|
-
id: 'origin',
|
|
3409
|
-
resource: declaredOrigin ?? input.externalId?.trim() ?? record.title,
|
|
3410
|
-
title: record.title,
|
|
3411
|
-
}],
|
|
3412
|
-
}
|
|
3413
|
-
const frontMatter = sourceFrontMatter(record, provenance)
|
|
3414
|
-
/**
|
|
3415
|
-
* 底稿(0067):调用方这一趟带进来的优先,没有就**沿用库里已有的那一份**。
|
|
3416
|
-
*
|
|
3417
|
-
* 沿用是必须的:`addSource` 也是「只改记账字段」那条路(补溯源、改标题、修 front
|
|
3418
|
-
* matter),而底稿是正文之外的资产 —— 在这条路上把它清掉,等于让人改一次标题就丢一次
|
|
3419
|
-
* 重跑的依据。清空底稿只有一条路:连同这份来源一起删掉。
|
|
3420
|
-
*/
|
|
3421
|
-
const original = sourceOriginalOf(input.metadata) ?? existing?.original
|
|
3387
|
+
let possibleDuplicateIds: string[] = []
|
|
3388
|
+
let identities: SourceIdentity[] = []
|
|
3422
3389
|
try {
|
|
3423
|
-
|
|
3390
|
+
if (input.sourceUrl !== undefined && input.sourceIdentity?.url !== undefined && normalizeSourceUrl(input.sourceUrl).href !== normalizeSourceUrl(input.sourceIdentity.url).href) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', 'source_url 与 source_identity.url 不一致。')
|
|
3391
|
+
if (input.sourceIdentity !== undefined || input.sourceUrl !== undefined) identities = resolveSourceIdentities({ ...input.sourceIdentity, ...(input.sourceUrl === undefined ? {} : { url: input.sourceUrl }) })
|
|
3392
|
+
// transact writes the operation row first: identity lookup, source write and binding share one writer lock.
|
|
3393
|
+
db.transact({ opId: operationId(), actor: 'panel', summary: `写入来源「${input.title ?? proposedSourceId ?? '新来源'}」` }, tx => {
|
|
3394
|
+
const choice = chooseSourceId(db, proposedSourceId, identities)
|
|
3395
|
+
sourceId = choice.sourceId; reusedSource = choice.reused
|
|
3396
|
+
const existing = db.source(sourceId)
|
|
3397
|
+
if (!existing) possibleDuplicateIds = (db.raw("SELECT s.entity_id FROM source s JOIN revision r ON r.entity_kind='source' AND r.entity_id=s.entity_id AND r.version=s.head_version WHERE r.blob_hash=? AND s.entity_id<>? ORDER BY s.entity_id LIMIT 20")
|
|
3398
|
+
.all(createHash('sha256').update(input.content).digest('hex'), sourceId) as Array<{ entity_id: string }>).map(row => row.entity_id)
|
|
3399
|
+
if (input.expectedVersion !== undefined && (!Number.isInteger(input.expectedVersion) || input.expectedVersion < 0 || (existing?.version ?? 0) !== input.expectedVersion)) throw new SourceIdentityError('SOURCE_VERSION_CONFLICT', '来源版本已变化,请按 source_id 读取当前版本后再更新。', [sourceId])
|
|
3400
|
+
const title = input.title?.trim() || existing?.record.title || sourceId
|
|
3401
|
+
const owner = db.sourceIdByTitle(title)
|
|
3402
|
+
if (owner !== undefined && owner !== sourceId && existing?.record.title !== title) {
|
|
3403
|
+
if (input.allowDuplicateTitle !== true) throw new SourceIdentityError('SOURCE_TITLE_DUPLICATED', `标题「${title}」已被来源 ${owner} 占用;请改标题或显式允许同名并存。`, [owner])
|
|
3404
|
+
duplicateNote = `标题「${title}」与来源 ${owner} 重复,已按 allow_duplicate_title 并存。`
|
|
3405
|
+
}
|
|
3406
|
+
record = {
|
|
3407
|
+
sourceId, title, content: input.content,
|
|
3408
|
+
...((input.externalId ?? existing?.record.externalId) ? { externalId: input.externalId ?? existing?.record.externalId } : {}),
|
|
3409
|
+
metadata: input.metadata ?? existing?.record.metadata ?? {},
|
|
3410
|
+
...(input.lineage ? { lineage: input.lineage } : {}),
|
|
3411
|
+
createdAt: existing?.record.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
3412
|
+
}
|
|
3413
|
+
// Body-declared URLs remain provenance only. They never claim or redirect source identity.
|
|
3414
|
+
const origin = input.sourceUrl ?? input.sourceIdentity?.url ?? declaredOriginOf(record.content)
|
|
3415
|
+
?? (typeof existing?.frontmatter.resource === 'string' ? existing.frontmatter.resource : undefined)
|
|
3416
|
+
const provenance: SourceProvenance = input.provenance ?? {
|
|
3417
|
+
...(origin ? { resource: origin } : {}), sources: [{ id: 'origin', resource: origin ?? input.externalId?.trim() ?? title, title }],
|
|
3418
|
+
}
|
|
3419
|
+
const frontMatter = { ...(existing?.frontmatter ?? {}), ...sourceFrontMatter(record, provenance) }
|
|
3420
|
+
const original = sourceOriginalOf(input.metadata) ?? existing?.original
|
|
3424
3421
|
tx.putSource(record, frontMatter, { actor: 'user', kind: 'named', summary: `写入来源「${title}」` }, original)
|
|
3422
|
+
for (const identity of identities) tx.bindSourceIdentity(identity, sourceId)
|
|
3425
3423
|
})
|
|
3426
3424
|
} catch (cause) {
|
|
3425
|
+
if (cause instanceof SourceIdentityError) return failure({ sourceId, candidates: cause.candidates }, cause.code, cause.message, false)
|
|
3427
3426
|
return failure({ sourceId: '' }, 'STORE_WRITE_FAILED', `写进知识库失败:${cause instanceof Error ? cause.message : String(cause)}`, true)
|
|
3428
3427
|
}
|
|
3429
3428
|
/**
|
|
@@ -3447,7 +3446,7 @@ export class KnowledgeRuntime {
|
|
|
3447
3446
|
this.sourceIndex = undefined
|
|
3448
3447
|
return success(
|
|
3449
3448
|
{
|
|
3450
|
-
sourceId,
|
|
3449
|
+
sourceId, proposedSourceId, reusedSource, possibleDuplicateIds, sourceIdentity: db.sourceIdentities(sourceId), version: db.source(sourceId)?.version, readRef: { kb: this.baseId || 'default', source_id: sourceId, version: db.source(sourceId)?.version, content_hash: createHash('sha256').update(record.content).digest('hex') },
|
|
3451
3450
|
...(record.lineage ? { lineage: record.lineage } : {}),
|
|
3452
3451
|
assets: { refs: imageRefs.length, missing: missingImages.length },
|
|
3453
3452
|
},
|
|
@@ -4386,7 +4385,8 @@ export class KnowledgeRuntime {
|
|
|
4386
4385
|
|
|
4387
4386
|
const listed = input.knowledge ? { knowledge: input.knowledge, total: input.knowledge.length } : await store.list({
|
|
4388
4387
|
objectId: input.coordinate.objectId,
|
|
4389
|
-
|
|
4388
|
+
actionId: input.coordinate.actionId, sceneId: input.coordinate.sceneId,
|
|
4389
|
+
...(input.existing?.metadata.wiki_kind ? { kind: input.existing.metadata.wiki_kind === 'way' ? 'procedure' as const : 'fact' as const } : {}),
|
|
4390
4390
|
excludeWithdrawn: true,
|
|
4391
4391
|
limit: MAX_ORGANIZE_KNOWLEDGE,
|
|
4392
4392
|
})
|
|
@@ -4438,7 +4438,7 @@ export class KnowledgeRuntime {
|
|
|
4438
4438
|
hash,
|
|
4439
4439
|
layout,
|
|
4440
4440
|
reuse,
|
|
4441
|
-
system: buildOrganizeSystem(input.
|
|
4441
|
+
system: buildOrganizeSystem(input.existing?.metadata.wiki_kind === 'way' || listed.knowledge[0]?.kind === 'procedure' ? 'way' : 'fact'),
|
|
4442
4442
|
prompt: buildOrganizePrompt(organizeInput),
|
|
4443
4443
|
}
|
|
4444
4444
|
}
|
|
@@ -4637,7 +4637,7 @@ export class KnowledgeRuntime {
|
|
|
4637
4637
|
const plan = planRelated(cards, relations, await this.projectedRelated(cards))
|
|
4638
4638
|
const entries = plan.get(cardKey(card))
|
|
4639
4639
|
if (entries === undefined || entries.length === 0) return {}
|
|
4640
|
-
return { related: entries.map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })) }
|
|
4640
|
+
return { related: entries.map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })) }
|
|
4641
4641
|
} catch {
|
|
4642
4642
|
// 相关投影是**增强**:算不出来时不给清单,于是正文里一个 `[[…]]` 都不许出现 —— 而不是把卡片卡住。
|
|
4643
4643
|
return {}
|
|
@@ -4868,7 +4868,9 @@ export class KnowledgeRuntime {
|
|
|
4868
4868
|
const createdAt = typeof existing?.frontmatter.created_at === 'string' && existing.frontmatter.created_at ? existing.frontmatter.created_at : updatedAt
|
|
4869
4869
|
const provenance = [...(input.provenance ?? []), ...(input.lineage ? [{ ...input.lineage, role: 'created' as const, refType: 'knowledge_card', refId: input.entityId }] : [])]
|
|
4870
4870
|
// 旧版本曾把 schema_issues 写入 metadata。重写卡片时主动摘掉它,避免旧的待补结果继续出现。
|
|
4871
|
-
const { schema_issues: _staleIssues, ...metadata } = input.metadata ?? {}
|
|
4871
|
+
const { schema_issues: _staleIssues, ...metadata } = input.metadata ?? previous?.metadata ?? {}
|
|
4872
|
+
if (status === 'deprecated' && (input.origin === 'review' || input.origin === 'manual' || (input.origin === undefined && previous?.status !== 'deprecated'))) metadata.wiki_deprecation_origin = 'manual'
|
|
4873
|
+
if (status !== 'deprecated') delete metadata.wiki_deprecation_origin
|
|
4872
4874
|
void _staleIssues
|
|
4873
4875
|
// 备注与 review **默认继承库里已有的**:不传不等于清空(见入参注释)。
|
|
4874
4876
|
const annotations = input.annotations ?? previousAnnotations ?? []
|
|
@@ -4949,7 +4951,106 @@ export class KnowledgeRuntime {
|
|
|
4949
4951
|
return success({ entityId: input.entityId, ...(input.lineage ? { lineage: input.lineage } : {}) })
|
|
4950
4952
|
}
|
|
4951
4953
|
|
|
4952
|
-
/**
|
|
4954
|
+
/** Shared by direct reads, index publication and recall; always reads live contributions. */
|
|
4955
|
+
private async wikiUnavailability(cards: readonly KnowledgeCard[], suppliedPlacements?: Map<string, CardKnowledgeRow[]>): Promise<Map<string, string>> {
|
|
4956
|
+
const truth = await this.storeHandle()
|
|
4957
|
+
const knowledge = await this.knowledgeStore()
|
|
4958
|
+
if (!truth || !knowledge) return new Map(cards.map(card => [cardKey(card), '知识真源不可用,无法核验 Wiki。']))
|
|
4959
|
+
const placements = suppliedPlacements ?? truth.placementsOf(cards.map(card => card.entityId))
|
|
4960
|
+
const ids = [...new Set(cards.flatMap(card => [...(placements.get(card.entityId) ?? []).map(row => row.knowledgeId), ...Object.keys(card.metadata.knowledge_versions as object ?? {})]))]
|
|
4961
|
+
const rows = new Map<string, Knowledge>()
|
|
4962
|
+
for (let offset = 0; offset < ids.length; offset += 500) {
|
|
4963
|
+
const page = await knowledge.list({ ids: ids.slice(offset, offset + 500), includeMerged: true, limit: 500 })
|
|
4964
|
+
for (const row of page.knowledge) rows.set(row.id, row)
|
|
4965
|
+
}
|
|
4966
|
+
const sources = new Map((truth.raw('SELECT entity_id,head_version FROM source').all() as Array<{ entity_id: string; head_version: number }>).map(row => [row.entity_id, row.head_version]))
|
|
4967
|
+
const cases = new Map<string, number | undefined>()
|
|
4968
|
+
const result = new Map<string, string>()
|
|
4969
|
+
for (const card of cards) {
|
|
4970
|
+
const reason = wikiUnavailableReason(card, placements.get(card.entityId) ?? [], { knowledge: rows, sources, caseVersion: id => {
|
|
4971
|
+
if (!cases.has(id)) cases.set(id, truth.event(id)?.version)
|
|
4972
|
+
return cases.get(id)
|
|
4973
|
+
} })
|
|
4974
|
+
if (reason) result.set(cardKey(card), reason)
|
|
4975
|
+
}
|
|
4976
|
+
return result
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
/** Read a stable Wiki link or browse an Object's coordinates without flattening its files. */
|
|
4980
|
+
async readWiki(input: KnowledgeScope & { target?: string; object?: string; includeContent?: boolean; limit?: number; offset?: number; maxChars?: number; contentOffset?: number; contentMaxChars?: number; expectedVersion?: number; linkLimit?: number; linkOffset?: number } = {}): Promise<Envelope<Record<string, unknown>>> {
|
|
4981
|
+
const empty = { wikis: [], total: 0, offset: input.offset ?? 0, hasMore: false, outgoing: [], backlinks: [] }
|
|
4982
|
+
const limit = input.limit ?? 20, offset = input.offset ?? 0, maxChars = input.maxChars ?? (this.config.contextMaxChars || 20000)
|
|
4983
|
+
const linkLimit = input.linkLimit ?? 20, linkOffset = input.linkOffset ?? 0
|
|
4984
|
+
const pageChars = input.contentMaxChars ?? Math.min(20000, maxChars)
|
|
4985
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100 || !Number.isInteger(offset) || offset < 0) return failure(empty, 'WIKI_PAGE_INVALID', 'limit 必须为 1–100,offset 必须为非负整数。', false)
|
|
4986
|
+
if (!Number.isInteger(maxChars) || maxChars < 1 || maxChars > 200000 || !Number.isInteger(pageChars) || pageChars < 1 || pageChars > 40000 || !Number.isInteger(linkLimit) || linkLimit < 1 || linkLimit > 100 || !Number.isInteger(linkOffset) || linkOffset < 0) return failure(empty, 'WIKI_BUDGET_INVALID', 'max_chars 为 1–200000,content_max_chars 为 1–40000,link_limit 为 1–100,link_offset 为非负整数。', false)
|
|
4987
|
+
if (scopeError(input)) return failure(empty, 'SCOPE_INVALID', scopeError(input)!, false)
|
|
4988
|
+
if (input.contentOffset !== undefined && (!input.target || !Number.isInteger(input.contentOffset) || input.contentOffset < 0)) return failure(empty, 'WIKI_CONTENT_OFFSET_INVALID', '正文分页需要唯一 target 和非负整数 content_offset。', false)
|
|
4989
|
+
if (input.expectedVersion !== undefined && (!input.target || !Number.isInteger(input.expectedVersion) || input.expectedVersion < 1)) return failure(empty, 'WIKI_VERSION_INVALID', 'expected_version 需要唯一 target 和正整数版本号。', false)
|
|
4990
|
+
if (!input.target?.trim() && !input.objectId?.trim() && !input.object?.trim()) return failure(empty, 'WIKI_SCOPE_REQUIRED', '请提供双链 target、object_id 或明确的 Object 名称。', false)
|
|
4991
|
+
const cards = await this.loadCards()
|
|
4992
|
+
let objectId = input.objectId
|
|
4993
|
+
if (input.object !== undefined) {
|
|
4994
|
+
const name = input.object.trim().toLocaleLowerCase()
|
|
4995
|
+
const objects = (await this.listAxisEntities('object') ?? []).filter(object => object.status !== 'deprecated' && object.status !== 'merged'
|
|
4996
|
+
&& [object.id, object.name, ...object.aliases].some(value => value.toLocaleLowerCase() === name))
|
|
4997
|
+
if (objects.length !== 1) return failure({ ...empty, candidates: objects.map(object => ({ id: object.id, name: object.name })) }, objects.length ? 'OBJECT_AMBIGUOUS' : 'OBJECT_NOT_FOUND', 'Object 名称未唯一解析,请使用明确的 object_id。', false)
|
|
4998
|
+
if (objectId !== undefined && objectId !== objects[0]!.id) return failure(empty, 'OBJECT_SCOPE_CONFLICT', 'object 与 object_id 指向不同对象。', false)
|
|
4999
|
+
objectId = objects[0]!.id
|
|
5000
|
+
}
|
|
5001
|
+
let selected = cards.filter(card => matchesScope(card, { ...input, objectId }))
|
|
5002
|
+
if (input.target?.trim()) {
|
|
5003
|
+
selected = wikiLinkCandidates(selected, input.target)
|
|
5004
|
+
if (selected.length !== 1) return failure({ ...empty, candidates: selected.map(card => ({ ...wikiReference(card), coordinate: card.coordinate, wikiKind: card.metadata.wiki_kind })) }, selected.length ? 'WIKI_LINK_AMBIGUOUS' : 'WIKI_NOT_FOUND', '双链未唯一指向 Wiki,请使用稳定 target。', false)
|
|
5005
|
+
if (selected[0]!.status === 'deprecated') return failure(empty, 'WIKI_UNAVAILABLE', '目标 Wiki 已停用,请按 Object 查找当前文件。', false)
|
|
5006
|
+
}
|
|
5007
|
+
selected = selected.filter(card => card.status !== 'deprecated').sort((a, b) => wikiReference(a).target.localeCompare(wikiReference(b).target))
|
|
5008
|
+
const page = selected.slice(offset, offset + limit), includeContent = input.includeContent ?? Boolean(input.target)
|
|
5009
|
+
const truth = await this.storeHandle()
|
|
5010
|
+
const unavailableByKey = await this.wikiUnavailability(page, truth?.placementsOf(page.map(card => card.entityId)))
|
|
5011
|
+
const wikis: Array<Record<string, unknown>> = []
|
|
5012
|
+
let usedChars = 0
|
|
5013
|
+
for (const card of page) {
|
|
5014
|
+
const unavailable = unavailableByKey.get(cardKey(card)), version = truth?.card(card.entityId)?.version
|
|
5015
|
+
if (input.expectedVersion !== undefined && input.expectedVersion !== version) return failure(empty, 'WIKI_VERSION_CHANGED', 'Wiki 已更新,请重新读取,避免拼接不同版本的正文。', false)
|
|
5016
|
+
const ref = wikiReference(card)
|
|
5017
|
+
const entry: Record<string, unknown> = { ...ref, coordinate: card.coordinate, wikiKind: card.metadata.wiki_kind, status: card.status, version, available: !unavailable,
|
|
5018
|
+
...(unavailable ? { reason: unavailable } : {}), ...(card.coordinate ? { coordinateLabels: await this.coordinateNames(card.coordinate) } : {}), contentDelivered: false }
|
|
5019
|
+
if (includeContent && !unavailable) {
|
|
5020
|
+
if (input.contentOffset !== undefined) {
|
|
5021
|
+
if (input.contentOffset > card.content.length) return failure(empty, 'WIKI_CONTENT_OFFSET_INVALID', 'content_offset 超过正文长度。', false)
|
|
5022
|
+
const end = Math.min(card.content.length, input.contentOffset + Math.min(pageChars, maxChars - usedChars))
|
|
5023
|
+
const content = card.content.slice(input.contentOffset, end), truncated = end < card.content.length
|
|
5024
|
+
entry.contentPage = { content, offset: input.contentOffset, totalChars: card.content.length, returnedChars: content.length, truncated, nextOffset: truncated ? end : null }
|
|
5025
|
+
entry.contentDelivered = true; entry.contentComplete = input.contentOffset === 0 && !truncated
|
|
5026
|
+
usedChars += content.length
|
|
5027
|
+
if (truncated) entry.nextRead = { tool: 'read_knowledge_wiki', args: { kb: this.baseId || 'default', target: ref.target, expected_version: version, content_offset: end, content_max_chars: pageChars, max_chars: maxChars } }
|
|
5028
|
+
} else {
|
|
5029
|
+
const cost = JSON.stringify(card).length
|
|
5030
|
+
if (usedChars + cost <= maxChars) { entry.wiki = card; entry.contentDelivered = true; entry.contentComplete = true; usedChars += cost }
|
|
5031
|
+
else { entry.requiredChars = cost; entry.contentComplete = false; entry.nextRead = { tool: 'read_knowledge_wiki', args: { kb: this.baseId || 'default', target: ref.target, expected_version: version, content_offset: 0, content_max_chars: pageChars, max_chars: maxChars } } }
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
wikis.push(entry)
|
|
5035
|
+
}
|
|
5036
|
+
const first = input.target ? page[0] : undefined, live = cards.filter(card => card.status !== 'deprecated')
|
|
5037
|
+
const outgoing = first ? [...new Map(scanWikiLinks(first.content).flatMap(link => {
|
|
5038
|
+
const targets = wikiLinkCandidates(live, link.target)
|
|
5039
|
+
return targets.length === 1 && targets[0]!.entityId !== first.entityId ? [[targets[0]!.entityId, wikiReference(targets[0]!)] as const] : []
|
|
5040
|
+
})).values()].sort((a, b) => a.target.localeCompare(b.target)) : []
|
|
5041
|
+
const backlinks = first ? live.filter(card => card.entityId !== first.entityId && scanWikiLinks(card.content).some(link => {
|
|
5042
|
+
const targets = wikiLinkCandidates(live, link.target)
|
|
5043
|
+
return targets.length === 1 && targets[0]!.entityId === first.entityId
|
|
5044
|
+
})).map(wikiReference).sort((a, b) => a.target.localeCompare(b.target)) : []
|
|
5045
|
+
const data = { wikis, total: selected.length, offset, hasMore: offset + page.length < selected.length, nextOffset: offset + page.length < selected.length ? offset + page.length : null,
|
|
5046
|
+
outgoing: outgoing.slice(linkOffset, linkOffset + linkLimit), backlinks: backlinks.slice(linkOffset, linkOffset + linkLimit),
|
|
5047
|
+
links: { offset: linkOffset, limit: linkLimit, outgoingTotal: outgoing.length, backlinksTotal: backlinks.length, nextOffset: linkOffset + linkLimit < Math.max(outgoing.length, backlinks.length) ? linkOffset + linkLimit : null },
|
|
5048
|
+
budget: { maxChars, usedChars, omittedCards: wikis.filter(item => includeContent && item.available && !item.contentDelivered).length } }
|
|
5049
|
+
await this.audit('wiki.read', { target: input.target, objectId, includeContent, returned: page.length, usedChars })
|
|
5050
|
+
return success(data)
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
/** Resolve one card by its stable ID, optionally constrained to a type. */
|
|
4953
5054
|
async readCard(entityId: string, entityType?: SchemaType): Promise<KnowledgeCard | undefined> {
|
|
4954
5055
|
const db = await this.storeHandle()
|
|
4955
5056
|
if (db === null) return undefined
|
|
@@ -5023,7 +5124,43 @@ export class KnowledgeRuntime {
|
|
|
5023
5124
|
async readCase(id: string, version?: number): Promise<EventEntity | undefined> {
|
|
5024
5125
|
const db = await this.storeHandle()
|
|
5025
5126
|
if (version !== undefined && (!Number.isInteger(version) || version < 1)) return undefined
|
|
5026
|
-
|
|
5127
|
+
if (!db?.event(id)) return undefined
|
|
5128
|
+
return version === undefined ? db.event(id) : db.caseVersion(id, version)
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
async queryCases(input: KnowledgeScope & { ids?: string[]; version?: number; query?: string; limit?: number; offset?: number; includeContent?: boolean; maxChars?: number; contentOffset?: number }): Promise<Envelope<Record<string, unknown>>> {
|
|
5132
|
+
const empty = { cases: [], missing_ids: [], total: 0, hasMore: false }
|
|
5133
|
+
const limit = input.limit ?? 20, offset = input.offset ?? 0, maxChars = input.maxChars ?? 20000
|
|
5134
|
+
if (scopeError(input)) return failure(empty, 'SCOPE_INVALID', scopeError(input)!, false)
|
|
5135
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50 || !Number.isInteger(offset) || offset < 0 || !Number.isInteger(maxChars) || maxChars < 1 || maxChars > 100000) return failure(empty, 'CASE_PAGE_INVALID', 'limit 为 1–50,offset 为非负整数,max_chars 为 1–100000。', false)
|
|
5136
|
+
if (input.ids !== undefined && (!Array.isArray(input.ids) || !input.ids.length || input.ids.length > 50 || input.ids.some(id => typeof id !== 'string' || !id.trim()))) return failure(empty, 'CASE_IDS_INVALID', 'case_ids 必须含 1–50 个有效 ID。', false)
|
|
5137
|
+
const ids = input.ids ? [...new Set(input.ids)] : undefined
|
|
5138
|
+
if (input.version !== undefined && (!ids || ids.length !== 1 || !Number.isInteger(input.version) || input.version < 1)) return failure(empty, 'CASE_VERSION_INVALID', '历史版本读取要求一个 Case ID 和正整数 version。', false)
|
|
5139
|
+
if (input.contentOffset !== undefined && (!ids || ids.length !== 1 || !Number.isInteger(input.contentOffset) || input.contentOffset < 0)) return failure(empty, 'CASE_CONTENT_OFFSET_INVALID', '正文分页需要一个 Case ID 和非负整数 content_offset。', false)
|
|
5140
|
+
if (input.query !== undefined && (typeof input.query !== 'string' || !input.query.trim() || ids)) return failure(empty, 'CASE_QUERY_INVALID', 'query 必须非空,且不与 case_ids 同传。', false)
|
|
5141
|
+
if (!ids && !input.query?.trim() && ![input.objectId, input.actionId, input.sceneId].some(value => value !== undefined)) return failure(empty, 'CASE_QUERY_REQUIRED', '请提供 case_ids、当前子问题 query 或坐标范围。', false)
|
|
5142
|
+
const read = ids ? await Promise.all(ids.map(id => this.readCase(id, input.version))) : await this.loadEvents()
|
|
5143
|
+
const terms = input.query ? [...new Set(segment(input.query))] : []
|
|
5144
|
+
const matched = read.filter((item): item is EventEntity => !!item)
|
|
5145
|
+
.filter(item => ![input.objectId, input.actionId, input.sceneId].some(value => value !== undefined) || item.records.some(record => matchesScope({ coordinate: record.coordinateIds }, input)))
|
|
5146
|
+
.map(item => ({ item, score: terms.length ? terms.filter(term => JSON.stringify(item).toLowerCase().includes(term.toLowerCase())).length / terms.length : 1 }))
|
|
5147
|
+
.filter(row => row.score > 0).sort((a, b) => b.score - a.score || a.item.id.localeCompare(b.item.id))
|
|
5148
|
+
const cases: Array<Record<string, unknown>> = [], includeContent = input.includeContent ?? Boolean(ids)
|
|
5149
|
+
let usedChars = 0
|
|
5150
|
+
for (const { item, score } of matched.slice(offset, offset + limit)) {
|
|
5151
|
+
const raw = JSON.stringify(item), version = item.version ?? 1
|
|
5152
|
+
const sourceReads = item.sources.map(source => ({ kb: this.baseId || 'default', source_id: source.documentId, version: source.sourceVersion, ...(source.locator ? { start: source.locator.start, end: source.locator.end } : {}), quote: source.quote, historicalVersionKnown: source.sourceVersion !== undefined }))
|
|
5153
|
+
const ref = { id: item.id, title: item.title, version, sourceReads, coordinate: item.coordinateIds, occurredAt: item.occurredAt, historicalEvidence: true, score }
|
|
5154
|
+
if (includeContent && input.contentOffset !== undefined) {
|
|
5155
|
+
if (input.contentOffset > raw.length) return failure(empty, 'CASE_CONTENT_OFFSET_INVALID', 'content_offset 超过 Case 正文长度。', false)
|
|
5156
|
+
const end = Math.min(raw.length, input.contentOffset + maxChars), content = raw.slice(input.contentOffset, end)
|
|
5157
|
+
cases.push({ ...ref, contentPage: { format: 'json', content, offset: input.contentOffset, totalChars: raw.length, truncated: end < raw.length },
|
|
5158
|
+
...(end < raw.length ? { nextRead: { tool: 'get_knowledge_cases', args: { kb: this.baseId || 'default', case_ids: [item.id], version, content_offset: end, max_chars: maxChars } } } : {}) }); usedChars += content.length
|
|
5159
|
+
} else if (includeContent && usedChars + raw.length <= maxChars) { cases.push({ ...item, sourceReads, historicalEvidence: true, contentDelivered: true }); usedChars += raw.length }
|
|
5160
|
+
else cases.push({ ...ref, contentDelivered: false, requiredChars: raw.length, nextRead: { tool: 'get_knowledge_cases', args: { kb: this.baseId || 'default', case_ids: [item.id], version, ...(raw.length > maxChars ? { content_offset: 0 } : {}), max_chars: maxChars } } })
|
|
5161
|
+
}
|
|
5162
|
+
await this.audit('case.read', { ids, query: input.query, returned: cases.length, usedChars })
|
|
5163
|
+
return success({ cases, missing_ids: ids?.filter((_, index) => read[index] === undefined) ?? [], total: matched.length, offset, hasMore: offset + limit < matched.length, nextOffset: offset + limit < matched.length ? offset + limit : null, retrieval: ids ? 'id' : 'keyword', budget: { maxChars, usedChars } })
|
|
5027
5164
|
}
|
|
5028
5165
|
|
|
5029
5166
|
async loadEvents(): Promise<EventEntity[]> {
|
|
@@ -5040,7 +5177,7 @@ export class KnowledgeRuntime {
|
|
|
5040
5177
|
if (card.status === 'problem' && status !== 'unreviewed') return failure({ entityId, status }, 'STATUS_TRANSITION_INVALID', 'problem 不能直接恢复为其他状态,请先改为 unreviewed', false)
|
|
5041
5178
|
const eventLineage = withRun(lineage, 'knowledge-status')
|
|
5042
5179
|
const provenance = [...(card.provenance ?? []), ...(eventLineage ? [{ ...eventLineage, role: 'executed' as const, refType: 'knowledge_status', refId: entityId }] : [])]
|
|
5043
|
-
const written = await this.writeCard({ ...card, status, sourceRefs: card.sourceRefs, provenance, metadata: { ...card.metadata, ...(problemNote?.trim() ? { problem_note: problemNote.trim() } : {}) } })
|
|
5180
|
+
const written = await this.writeCard({ ...card, status, sourceRefs: card.sourceRefs, provenance, origin: 'review', metadata: { ...card.metadata, ...(problemNote?.trim() ? { problem_note: problemNote.trim() } : {}) } })
|
|
5044
5181
|
if (written.ok === false) return failure({ entityId, status }, written.error.code, written.error.message, written.error.retryable, written.error.suggestion)
|
|
5045
5182
|
return success({ entityId, status, ...(eventLineage ? { lineage: eventLineage } : {}) })
|
|
5046
5183
|
}
|
|
@@ -5087,7 +5224,7 @@ export class KnowledgeRuntime {
|
|
|
5087
5224
|
* `metadata.related_cards`;这里继续剥离旧版本正文里的标记,保证存量卡片迁移期间
|
|
5088
5225
|
* 也不会把导航送进召回。
|
|
5089
5226
|
*/
|
|
5090
|
-
const recallBody = stripEventReferenceSection(stripRelatedSection(card.content))
|
|
5227
|
+
const recallBody = wikiIndexContent(stripEventReferenceSection(stripRelatedSection(card.content)))
|
|
5091
5228
|
for (const [index, chunk] of splitWikiNotes({ ...card, content: recallBody }).entries()) {
|
|
5092
5229
|
const baseId = `${card.entityType}:${card.entityId}`
|
|
5093
5230
|
const stableSection = layoutOf(card.metadata)?.sections?.find(section => section.title === chunk.heading)
|
|
@@ -5649,7 +5786,7 @@ export class KnowledgeRuntime {
|
|
|
5649
5786
|
existing: card,
|
|
5650
5787
|
...(input.imagePlan.byCard.get(key) === undefined ? {} : { images: input.imagePlan.byCard.get(key) }),
|
|
5651
5788
|
...(relatedPlan.get(key) === undefined ? {} : {
|
|
5652
|
-
related: (relatedPlan.get(key) ?? []).map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })),
|
|
5789
|
+
related: (relatedPlan.get(key) ?? []).map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })),
|
|
5653
5790
|
}),
|
|
5654
5791
|
call: input.call,
|
|
5655
5792
|
})
|
|
@@ -6211,12 +6348,11 @@ export class KnowledgeRuntime {
|
|
|
6211
6348
|
// ------------------------------------------- Finalize · 索引 / 断链 / 相关(0049)
|
|
6212
6349
|
|
|
6213
6350
|
/**
|
|
6214
|
-
*
|
|
6215
|
-
*
|
|
6351
|
+
* 编译收尾:生成类型化 Wiki 的稳定双链与导航,更新 `metadata.related_cards`,
|
|
6352
|
+
* 清理旧相关块,并把断链统计交给编译报告与索引页。
|
|
6216
6353
|
*
|
|
6217
|
-
*
|
|
6218
|
-
*
|
|
6219
|
-
* `<!-- compiled:related -->` 只在这里做一次兼容清理。
|
|
6354
|
+
* 导航只放链接,不复制目标正文;面板使用结构化元数据,检索排除生成的导航块。
|
|
6355
|
+
* 旧卡片上的 `<!-- compiled:related -->` 在这里兼容清理。
|
|
6220
6356
|
*
|
|
6221
6357
|
* 三段都是确定性的,所以这个函数可以重复跑:正文和结构化相关数据都没变化时不写盘。
|
|
6222
6358
|
*/
|
|
@@ -6230,11 +6366,24 @@ export class KnowledgeRuntime {
|
|
|
6230
6366
|
* 它是**算出来的**,不是谁声明的:这张卡里的知识引用了哪个轴实体,而那个轴自己
|
|
6231
6367
|
* 有一张卡,就有了链接。模型抽出来的关系(`relations`)是第二层。
|
|
6232
6368
|
*/
|
|
6233
|
-
const
|
|
6234
|
-
const
|
|
6235
|
-
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6369
|
+
const axes = (await this.listAxisEntities() ?? []).filter(axis => axis.status !== 'deprecated' && axis.status !== 'merged')
|
|
6370
|
+
const objects = axes.filter(axis => axis.kind === 'object')
|
|
6371
|
+
const names = new Map(axes.map(axis => [axis.id, axis.name]))
|
|
6372
|
+
const labeled = cards.map(card => card.metadata.wiki_kind && card.coordinate ? { ...card, metadata: { ...card.metadata,
|
|
6373
|
+
wiki_coordinate_labels: { object: names.get(card.coordinate.objectId) ?? card.coordinate.objectId, action: names.get(card.coordinate.actionId) ?? card.coordinate.actionId, scene: names.get(card.coordinate.sceneId) ?? card.coordinate.sceneId },
|
|
6374
|
+
} } : card)
|
|
6375
|
+
const live = labeled.filter(card => card.status !== 'deprecated')
|
|
6376
|
+
const nextCards = labeled.map(card => {
|
|
6377
|
+
const base = { ...card, content: stripEventReferenceSection(stripRelatedSection(card.content)) }
|
|
6378
|
+
if (!card.metadata.wiki_kind || card.status === 'deprecated') return base
|
|
6379
|
+
const navigation = compileWikiNavigation(base, live, objects)
|
|
6380
|
+
return { ...base, content: navigation.content, metadata: { ...base.metadata,
|
|
6381
|
+
wiki_links: navigation.outgoing, wiki_inline_targets: navigation.inlineTargets, wiki_object_refs: navigation.objectIds,
|
|
6382
|
+
} }
|
|
6383
|
+
})
|
|
6384
|
+
const projected = await this.projectedRelated(nextCards)
|
|
6385
|
+
const related = planRelated(live, relations, projected)
|
|
6386
|
+
const deadLinks = deadLinksOf(nextCards.filter(card => card.status !== 'deprecated'))
|
|
6238
6387
|
let rewritten = 0
|
|
6239
6388
|
for (const [index, card] of cards.entries()) {
|
|
6240
6389
|
const key = cardKey(card)
|
|
@@ -6242,7 +6391,7 @@ export class KnowledgeRuntime {
|
|
|
6242
6391
|
const entries = related.get(key) ?? []
|
|
6243
6392
|
const previousEntries = relatedEntriesOf(card.metadata)
|
|
6244
6393
|
const contentChanged = nextCard.content !== card.content
|
|
6245
|
-
const relatedChanged = JSON.stringify(previousEntries) !== JSON.stringify(entries)
|
|
6394
|
+
const relatedChanged = JSON.stringify(previousEntries) !== JSON.stringify(entries) || JSON.stringify(card.metadata.wiki_coordinate_labels) !== JSON.stringify(nextCard.metadata.wiki_coordinate_labels) || JSON.stringify(card.metadata.wiki_links) !== JSON.stringify(nextCard.metadata.wiki_links) || JSON.stringify(card.metadata.wiki_inline_targets) !== JSON.stringify(nextCard.metadata.wiki_inline_targets) || JSON.stringify(card.metadata.wiki_object_refs) !== JSON.stringify(nextCard.metadata.wiki_object_refs)
|
|
6246
6395
|
if (!contentChanged && !relatedChanged) continue
|
|
6247
6396
|
/**
|
|
6248
6397
|
* 没有来源的卡片写不进去(`writeCard` 要求 `source_refs` 非空),而这是**唯一**
|
|
@@ -6263,7 +6412,7 @@ export class KnowledgeRuntime {
|
|
|
6263
6412
|
status: card.status,
|
|
6264
6413
|
...(card.owner ? { owner: card.owner } : {}),
|
|
6265
6414
|
...(card.provenance ? { provenance: card.provenance } : {}),
|
|
6266
|
-
metadata: { ...
|
|
6415
|
+
metadata: { ...nextCard.metadata, [RELATED_METADATA_KEY]: entries },
|
|
6267
6416
|
origin: 'compile',
|
|
6268
6417
|
})
|
|
6269
6418
|
if (written.ok === false) {
|
|
@@ -6387,13 +6536,9 @@ export class KnowledgeRuntime {
|
|
|
6387
6536
|
}
|
|
6388
6537
|
}
|
|
6389
6538
|
|
|
6390
|
-
/**
|
|
6391
|
-
private contextCost(item: ContextItem
|
|
6392
|
-
|
|
6393
|
-
if (card === null) return 0
|
|
6394
|
-
const shallow = card.title.length + (card.summary?.length ?? 0)
|
|
6395
|
-
const knowledge = (item.knowledge ?? []).reduce((sum, row) => sum + row.text.length + row.state.length + 8, 0)
|
|
6396
|
-
return knowledge + shallow + card.content.length
|
|
6539
|
+
/** 与 Agent 实际收到的上下文条目使用同一序列化形状,包含完整 Wiki、依据和引用。 */
|
|
6540
|
+
private contextCost(item: ContextItem): number {
|
|
6541
|
+
return JSON.stringify(agentContextItem(item)).length
|
|
6397
6542
|
}
|
|
6398
6543
|
|
|
6399
6544
|
/** 溢出目录:**系统临时目录**,不随知识库走(0050 的取舍,见 issue)。 */
|
|
@@ -6421,17 +6566,14 @@ export class KnowledgeRuntime {
|
|
|
6421
6566
|
const { items, maxChars, recallId } = input
|
|
6422
6567
|
const hitCards = items.filter(item => item.reason === 'hit').length
|
|
6423
6568
|
const expandedCards = items.length - hitCards
|
|
6424
|
-
const totalChars = items.reduce((sum, item) => sum + this.contextCost(item
|
|
6569
|
+
const totalChars = items.reduce((sum, item) => sum + this.contextCost(item), 0)
|
|
6425
6570
|
if (!Number.isFinite(maxChars) || maxChars <= 0) {
|
|
6426
6571
|
return { items, report: { maxChars: 0, usedChars: totalChars, omittedCards: 0, omittedChars: 0, hitCards, expandedCards } }
|
|
6427
6572
|
}
|
|
6428
6573
|
const keptItems: ContextItem[] = []
|
|
6429
6574
|
let used = 0
|
|
6430
6575
|
for (const item of items) {
|
|
6431
|
-
const
|
|
6432
|
-
const shallow = this.contextCost(item, false)
|
|
6433
|
-
// 命中条目按整条算(它的正文就是证据);扩展条目可以先按「浅」那一档进。
|
|
6434
|
-
const cost = item.reason === 'hit' ? full : shallow
|
|
6576
|
+
const cost = this.contextCost(item)
|
|
6435
6577
|
if (used + cost > maxChars) continue
|
|
6436
6578
|
used += cost
|
|
6437
6579
|
keptItems.push(item)
|
|
@@ -6442,17 +6584,18 @@ export class KnowledgeRuntime {
|
|
|
6442
6584
|
maxChars,
|
|
6443
6585
|
usedChars: used,
|
|
6444
6586
|
omittedCards: omitted.length,
|
|
6445
|
-
omittedChars: omitted.reduce((sum, item) => sum + this.contextCost(item
|
|
6587
|
+
omittedChars: omitted.reduce((sum, item) => sum + this.contextCost(item), 0),
|
|
6446
6588
|
hitCards: keptItems.filter(item => item.reason === 'hit').length,
|
|
6447
6589
|
expandedCards: keptItems.filter(item => item.reason === 'expansion').length,
|
|
6448
6590
|
}
|
|
6449
6591
|
if (omitted.length === 0) return { items: keptItems, report }
|
|
6450
6592
|
const overflowPath = await this.writeRecallOverflow({ items, recallId, usedChars: used, maxChars })
|
|
6451
6593
|
report.overflowPath = overflowPath
|
|
6594
|
+
report.omitted = omitted.flatMap(item => item.wiki ? [{ target: `${item.wiki.entityType}/${item.wiki.entityId}`, title: item.wiki.title, requiredChars: this.contextCost(item), read: { tool: 'read_knowledge_wiki', args: { kb: this.baseId || 'default', target: `${item.wiki.entityType}/${item.wiki.entityId}` } } }] : [])
|
|
6452
6595
|
return {
|
|
6453
6596
|
items: keptItems,
|
|
6454
6597
|
report,
|
|
6455
|
-
notice: `还有 ${report.omittedCards} 张卡片共 ${report.omittedChars} 字符没有放进上下文,完整内容在 ${overflowPath}
|
|
6598
|
+
notice: `还有 ${report.omittedCards} 张卡片共 ${report.omittedChars} 字符没有放进上下文,完整内容在 ${overflowPath};可按 budget.omitted 中的稳定 target 调用 read_knowledge_wiki 补读;此文件仅供本机诊断,不保证长期存在。`,
|
|
6456
6599
|
}
|
|
6457
6600
|
}
|
|
6458
6601
|
|
|
@@ -6554,13 +6697,17 @@ export class KnowledgeRuntime {
|
|
|
6554
6697
|
* 而不是报错:这一层是**增强**,它坏了不该让编译失败。
|
|
6555
6698
|
*/
|
|
6556
6699
|
private async projectedRelated(cards: KnowledgeCard[]): Promise<Array<{ fromKey: string; toKey: string }>> {
|
|
6700
|
+
const links = cards.flatMap(card => scanWikiLinks(card.content).flatMap(link => {
|
|
6701
|
+
const candidates = wikiLinkCandidates(cards.filter(target => target.status !== 'deprecated'), link.target)
|
|
6702
|
+
return candidates.length === 1 && candidates[0]!.entityId !== card.entityId ? [{ fromKey: cardKey(card), toKey: cardKey(candidates[0]!) }] : []
|
|
6703
|
+
}))
|
|
6557
6704
|
const knowledge = await this.knowledgeStore()
|
|
6558
|
-
if (knowledge === null) return
|
|
6705
|
+
if (knowledge === null) return links
|
|
6559
6706
|
const db = await this.storeHandle()
|
|
6560
|
-
if (db === null) return
|
|
6707
|
+
if (db === null) return links
|
|
6561
6708
|
let placements: Map<string, CardKnowledgeRow[]>
|
|
6562
|
-
try { placements = db.placements() } catch { return
|
|
6563
|
-
if (placements.size === 0) return
|
|
6709
|
+
try { placements = db.placements() } catch { return links }
|
|
6710
|
+
if (placements.size === 0) return links
|
|
6564
6711
|
/**
|
|
6565
6712
|
* 单轴卡:坐标里恰好只有一个分量非空。
|
|
6566
6713
|
*
|
|
@@ -6585,11 +6732,11 @@ export class KnowledgeRuntime {
|
|
|
6585
6732
|
bucket.push(key)
|
|
6586
6733
|
byAxis.set(axis, bucket)
|
|
6587
6734
|
}
|
|
6588
|
-
if (byAxis.size === 0) return
|
|
6735
|
+
if (byAxis.size === 0) return links
|
|
6589
6736
|
const ids = [...new Set([...placements.values()].flat().map(row => row.knowledgeId))]
|
|
6590
|
-
if (ids.length === 0) return
|
|
6737
|
+
if (ids.length === 0) return links
|
|
6591
6738
|
let axes: Map<string, { objectId: string; actionId: string; sceneId: string }>
|
|
6592
|
-
try { axes = await knowledge.axesOf(ids) } catch { return
|
|
6739
|
+
try { axes = await knowledge.axesOf(ids) } catch { return links }
|
|
6593
6740
|
const pairs = new Map<string, { fromKey: string; toKey: string }>()
|
|
6594
6741
|
for (const [cardId, rows] of placements) {
|
|
6595
6742
|
const fromKeys = keyById.get(cardId) ?? []
|
|
@@ -6609,7 +6756,7 @@ export class KnowledgeRuntime {
|
|
|
6609
6756
|
}
|
|
6610
6757
|
}
|
|
6611
6758
|
}
|
|
6612
|
-
return [...pairs.values()]
|
|
6759
|
+
return [...pairs.values(), ...links]
|
|
6613
6760
|
}
|
|
6614
6761
|
|
|
6615
6762
|
/**
|
|
@@ -8127,7 +8274,7 @@ export class KnowledgeRuntime {
|
|
|
8127
8274
|
: card.metadata.wiki_kind
|
|
8128
8275
|
? events.filter(event => {
|
|
8129
8276
|
const c = event.coordinateIds
|
|
8130
|
-
return c
|
|
8277
|
+
return coordinateKeyOf(c) === coordinateKeyOf(card.coordinate!)
|
|
8131
8278
|
}).map(event => event.id).sort()
|
|
8132
8279
|
: (eventsByCoordinate.get(coordinateKeyOf(card.coordinate)) ?? []).sort()
|
|
8133
8280
|
const oldRaw = card.metadata.event_refs
|
|
@@ -8229,7 +8376,7 @@ export class KnowledgeRuntime {
|
|
|
8229
8376
|
|
|
8230
8377
|
for (const event of input.events) {
|
|
8231
8378
|
const seenEvidence = new Set<string>()
|
|
8232
|
-
const evidence = [...event.sources, ...event.timeline.flatMap(step => step.sources)].filter(source => {
|
|
8379
|
+
const evidence = [...event.sources, ...event.timeline.flatMap(step => step.sources)].map(source => ({ ...source, sourceVersion: db.source(source.documentId)?.version })).filter(source => {
|
|
8233
8380
|
if (!source.found) return false
|
|
8234
8381
|
const key = source.documentId + '|' + source.quote
|
|
8235
8382
|
if (seenEvidence.has(key)) return false
|
|
@@ -8273,7 +8420,7 @@ export class KnowledgeRuntime {
|
|
|
8273
8420
|
symptom: event.symptom,
|
|
8274
8421
|
coordinate: { object: event.object, action: event.action, scene: event.scene },
|
|
8275
8422
|
coordinateIds,
|
|
8276
|
-
timeline: event.timeline,
|
|
8423
|
+
timeline: event.timeline.map(step => ({ ...step, sources: step.sources.map(source => ({ ...source, sourceVersion: db.source(source.documentId)?.version })) })),
|
|
8277
8424
|
rootCause: event.rootCause,
|
|
8278
8425
|
rootCauseStatus: event.rootCauseStatus,
|
|
8279
8426
|
outcome: event.outcome,
|
|
@@ -8338,7 +8485,7 @@ export class KnowledgeRuntime {
|
|
|
8338
8485
|
if (!db || !audit || !await this.knowledgeStore()) throw new Error('知识库或任务历史不可用,无法启动处理任务')
|
|
8339
8486
|
const store = new PipelineStore(db.connection(), audit.connection())
|
|
8340
8487
|
const engine = new KnowledgePipeline(store, {
|
|
8341
|
-
capture: () => ({ model: this.modelSeamNow(), note: this.extractionNote(), profile: structuredClone(this.organizeProfile()), organize: this.config.cardOrganize, format: 'table' }),
|
|
8488
|
+
capture: () => ({ model: this.modelSeamNow(), note: this.extractionNote(), extractionTimeoutMinutes: normalizeExtractionTimeoutMinutes(this.extractionTimeoutProvider?.()), profile: structuredClone(this.organizeProfile()), organize: this.config.cardOrganize, format: 'table' }),
|
|
8342
8489
|
serialize: work => {
|
|
8343
8490
|
this.compilePending += 1
|
|
8344
8491
|
const run = this.compileTail.then(work, work)
|
|
@@ -8359,25 +8506,33 @@ export class KnowledgeRuntime {
|
|
|
8359
8506
|
* 指纹,否则归一自己学到的实体(它们本来就出现在这份文档里)会让它永远无法复用。
|
|
8360
8507
|
*/
|
|
8361
8508
|
fingerprint: (source, context) => this.pipelineExtractionFingerprint(source, context),
|
|
8362
|
-
extract: async (source, context, onDelta) => {
|
|
8509
|
+
extract: async (source, context, onDelta, execution) => {
|
|
8363
8510
|
if (!context.model.call) throw new Error(`抽取模型不可用:${context.model.why ?? '请在编译设置中选择模型'}`)
|
|
8364
8511
|
const axes = await this.axisPromptBlock(source.content)
|
|
8365
|
-
return
|
|
8512
|
+
return runResumableExtraction({ title: source.title, text: source.content, sourceId: source.sourceId, store, ...execution, recoveryTimeoutMs: normalizeExtractionTimeoutMinutes(context.extractionTimeoutMinutes) * 60_000, fingerprint: `${execution.fingerprint}:v${source.version}`, axes, note: context.note, call: context.model.call, modelRequest: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort, maxTokens: context.model.maxTokens }, outputFormat: context.format, sessionId: `knowledge:${this.baseId || 'default'}`, onDelta })
|
|
8366
8513
|
},
|
|
8367
8514
|
normalize: (source, checkpoint, context, force, output) => this.normalizePipelineSource(store, source, checkpoint, context, force, output),
|
|
8368
8515
|
/**
|
|
8369
8516
|
* 引擎只把 `snapshot` 原样传回来,所以那边看到的是 `unknown`;这里把它取回成
|
|
8370
8517
|
* 真实类型(见 `materialize-snapshot.ts` 里为什么不放在 `pipeline/types.ts`)。
|
|
8371
8518
|
*/
|
|
8372
|
-
derive: (sourceIds, context, force, output, checkpoint) => this.deriveProcedures(sourceIds, context, force, output, checkpoint),
|
|
8519
|
+
derive: (sourceIds, context, force, output, checkpoint, progress) => this.deriveProcedures(sourceIds, context, force, output, checkpoint, progress),
|
|
8373
8520
|
groups: async context => {
|
|
8374
8521
|
const snapshot = await this.materializationSnapshot(store, context)
|
|
8375
|
-
return { groups: snapshot.groups, blocked: snapshot.blocked, blockedAmbiguous: snapshot.blockedAmbiguous, blockedAwaitingNormalize: snapshot.blockedAwaitingNormalize, snapshot }
|
|
8522
|
+
return { groups: snapshot.groups, blocked: snapshot.blocked, blockedAmbiguous: snapshot.blockedAmbiguous, blockedAwaitingNormalize: snapshot.blockedAwaitingNormalize, blockedItems: snapshot.blockedItems, snapshot }
|
|
8376
8523
|
},
|
|
8377
8524
|
materialize: (group, context, output, snapshot) => this.materializePipelineGroup(store, group, context, output, snapshot as MaterializationSnapshot),
|
|
8378
|
-
|
|
8525
|
+
link: async () => {
|
|
8526
|
+
const warnings: string[] = []
|
|
8527
|
+
const finalized = await this.runFinalizeStage(await this.loadCards(), Object.values(await this.readExtractedRelations()).flat(), warnings)
|
|
8528
|
+
if (warnings.length) throw new Error(warnings.join(';'))
|
|
8529
|
+
return `已更新 Wiki 双链与导航:${finalized.rewritten} 份`
|
|
8530
|
+
},
|
|
8531
|
+
build: async (taskId, checkpoint, progress) => {
|
|
8379
8532
|
let result: Envelope<RebuildReport>
|
|
8380
|
-
try { result = await this.compileOnce(taskId, 'all', undefined, { extract: false, checkpoint
|
|
8533
|
+
try { result = await this.compileOnce(taskId, 'all', undefined, { extract: false, checkpoint, onProgress: step => {
|
|
8534
|
+
if ((step.id === 'index' || step.id === 'build' || step.id === 'publish') && step.state === 'running') progress?.(step.facts.join(';'))
|
|
8535
|
+
} }) }
|
|
8381
8536
|
catch (cause) { await this.settleProgressFailure(taskId, cause); throw cause }
|
|
8382
8537
|
this.settleProgressReport(taskId, result)
|
|
8383
8538
|
if (!result.ok) throw new Error(result.error.message)
|
|
@@ -8443,7 +8598,7 @@ export class KnowledgeRuntime {
|
|
|
8443
8598
|
.filter(entry => [entry.name, ...entry.aliases].map(normalizeAxisName).some(value => value.length > 0 && text.includes(value)))
|
|
8444
8599
|
.map(entry => ({ id: entry.id, name: entry.name, aliases: entry.aliases, description: entry.description, guidance: entry.guidance }))
|
|
8445
8600
|
.sort((left, right) => left.id.localeCompare(right.id)))
|
|
8446
|
-
return contentHashOf(JSON.stringify({ source: source.sourceHash, title: source.title, contract: EXTRACTION_CONTRACT_VERSION, note: context.note, format: context.format, axes: relevant, model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } }))
|
|
8601
|
+
return contentHashOf(JSON.stringify({ source: source.sourceHash, title: source.title, contract: EXTRACTION_CONTRACT_VERSION, plan: EXTRACTION_PLAN_VERSION, note: context.note, format: context.format, axes: relevant, model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } }))
|
|
8447
8602
|
}
|
|
8448
8603
|
|
|
8449
8604
|
private async normalizationFingerprint(checkpoint: ExtractionCheckpoint, context: PipelineContext): Promise<string> {
|
|
@@ -8568,14 +8723,14 @@ export class KnowledgeRuntime {
|
|
|
8568
8723
|
const cards = await this.loadCards()
|
|
8569
8724
|
const relations = Object.values(await this.readExtractedRelations()).flat()
|
|
8570
8725
|
for (const [key, entries] of planRelated(cards, relations, await this.projectedRelated(cards))) {
|
|
8571
|
-
if (entries.length > 0) plan.set(key, entries.map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })))
|
|
8726
|
+
if (entries.length > 0) plan.set(key, entries.map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })))
|
|
8572
8727
|
}
|
|
8573
8728
|
} catch { /* 增强项:算不出来就不给清单 */ }
|
|
8574
8729
|
return plan
|
|
8575
8730
|
}
|
|
8576
8731
|
|
|
8577
8732
|
/** Independent, checkpointed Case -> Procedure compilation. No execution is inferred. */
|
|
8578
|
-
private async deriveProcedures(sourceIds: string[] | undefined, context: PipelineContext, force: boolean, output: (text: string) => void, checkpoint: () => Promise<void
|
|
8733
|
+
private async deriveProcedures(sourceIds: string[] | undefined, context: PipelineContext, force: boolean, output: (text: string) => void, checkpoint: () => Promise<void>, progress?: (value: PipelineDeriveProgress) => void): Promise<{ done: number; total: number; reused: number; summary: string }> {
|
|
8579
8734
|
const db = await this.storeHandle()
|
|
8580
8735
|
const knowledge = await this.knowledgeStore()
|
|
8581
8736
|
if (!db || !knowledge) throw new Error('Knowledge 存储不可用')
|
|
@@ -8584,24 +8739,29 @@ export class KnowledgeRuntime {
|
|
|
8584
8739
|
const selected = sourceIds ? new Set(sourceIds) : undefined
|
|
8585
8740
|
const cases = (await this.loadEvents()).filter(item => !selected || item.records.some(record => selected.has(record.sourceId)))
|
|
8586
8741
|
let done = 0, reused = 0
|
|
8742
|
+
progress?.({ done, total: cases.length, reused, phase: 'preparing', summary: `已读取 ${cases.length} 个 Case` })
|
|
8587
8743
|
const strings = (value: unknown): string[] => Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string').map(v => v.trim()).filter(Boolean) : []
|
|
8588
8744
|
for (const item of cases) {
|
|
8589
8745
|
await checkpoint()
|
|
8746
|
+
const observe = (phase: PipelineDeriveProgress['phase'], state: PipelineItemState['state'], summary: string, failed = 0): void => progress?.({ done: done + failed, total: cases.length, reused, failed, phase, summary, item: { id: item.id, title: item.title, state } })
|
|
8747
|
+
observe('processing', 'processing', `正在检查 Case:${item.title}`)
|
|
8590
8748
|
const caseVersion = item.version ?? 1
|
|
8591
8749
|
const evidence = [...item.sources, ...item.timeline.flatMap(step => step.sources)]
|
|
8592
8750
|
const sourceVersions = new Map(evidence.map(e => [e.documentId, db.source(e.documentId)?.version]))
|
|
8593
8751
|
const fingerprint = contentHashOf(JSON.stringify({ contract: DERIVE_CONTRACT_VERSION, item: { ...item, updatedAt: undefined }, sourceVersions: [...sourceVersions], model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } }))
|
|
8594
|
-
const receipt = db.raw('SELECT fingerprint, knowledge_ids FROM pipeline_derivations WHERE case_id=?').get(item.id) as { fingerprint: string; knowledge_ids: string } | undefined
|
|
8595
|
-
|
|
8752
|
+
const receipt = db.raw('SELECT fingerprint, response, knowledge_ids FROM pipeline_derivations WHERE case_id=?').get(item.id) as { fingerprint: string; response: string; knowledge_ids: string } | undefined
|
|
8753
|
+
// Replay validated cached output through ingest too: normalization may have withdrawn its support.
|
|
8754
|
+
const cachedResponse = !force && receipt?.fingerprint === fingerprint ? receipt.response : undefined
|
|
8596
8755
|
const auditId = randomUUID()
|
|
8597
8756
|
const request = { system: buildDeriveSystem(), prompt: JSON.stringify({ case: { ...item, version: caseVersion }, sourceEvidence: evidence, sourceVersions: [...sourceVersions] }) }
|
|
8598
|
-
let response = '{"procedures":[],"skipped_cases":[]}'
|
|
8757
|
+
let response = cachedResponse ?? '{"procedures":[],"skipped_cases":[]}'
|
|
8599
8758
|
db.raw('INSERT INTO pipeline_derivation_runs VALUES (?,?,?,?,?,?,?,?,?)').run(auditId, item.id, caseVersion, fingerprint, JSON.stringify(request), '', 'running', null, new Date().toISOString())
|
|
8600
8759
|
try {
|
|
8601
|
-
if (item.timeline.some(step => step.type === 'check' || step.type === 'action')) {
|
|
8760
|
+
if (cachedResponse === undefined && item.timeline.some(step => step.type === 'check' || step.type === 'action')) {
|
|
8602
8761
|
if (!context.model.call) throw new Error(`提炼模型不可用:${context.model.why ?? '请选择模型'}`)
|
|
8762
|
+
observe('waiting-model', 'processing', `正在提炼 Case:${item.title}`)
|
|
8603
8763
|
response = await context.model.call({ ...request, sessionId: `knowledge:${this.baseId || 'default'}:derive`, onDelta: output })
|
|
8604
|
-
} else output(`Case ${item.id} 没有已执行过程,不反推 Procedure。\n`)
|
|
8764
|
+
} else if (cachedResponse === undefined) output(`Case ${item.id} 没有已执行过程,不反推 Procedure。\n`)
|
|
8605
8765
|
const parsed = JSON.parse(response.trim().replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '')) as { procedures?: Array<Record<string, unknown>> }
|
|
8606
8766
|
if (!Array.isArray(parsed.procedures)) throw new Error('Case 提炼返回缺少 procedures 数组')
|
|
8607
8767
|
if (this.closed || db.event(item.id)?.version !== caseVersion || [...sourceVersions].some(([id, version]) => db.source(id)?.version !== version)) throw new Error('Case 或来源在提炼期间变化,请重试')
|
|
@@ -8636,7 +8796,10 @@ export class KnowledgeRuntime {
|
|
|
8636
8796
|
db.raw("UPDATE pipeline_derivation_runs SET response=?,state='succeeded' WHERE id=?").run(response, auditId)
|
|
8637
8797
|
output(`Case ${item.id}:保存 ${ids.length} 条候选 Procedure。\n`)
|
|
8638
8798
|
done++
|
|
8799
|
+
if (cachedResponse !== undefined) reused++
|
|
8800
|
+
observe(cachedResponse !== undefined ? 'reused' : 'completed', cachedResponse !== undefined ? 'reused' : 'succeeded', `${item.title}:已保存 ${ids.length} 条候选方法`)
|
|
8639
8801
|
} catch (cause) {
|
|
8802
|
+
observe('failed', 'failed', `${item.title}:${cause instanceof Error ? cause.message : String(cause)}`, 1)
|
|
8640
8803
|
db.raw("UPDATE pipeline_derivation_runs SET response=?,state='failed',error=? WHERE id=?").run(response, cause instanceof Error ? cause.message : String(cause), auditId)
|
|
8641
8804
|
throw cause
|
|
8642
8805
|
}
|
|
@@ -8651,7 +8814,7 @@ export class KnowledgeRuntime {
|
|
|
8651
8814
|
* 两处必须用同一个表达式,否则「什么算变化」就有两套说法。
|
|
8652
8815
|
*/
|
|
8653
8816
|
private pipelineGroupFingerprint(knowledge: Knowledge[], context: PipelineContext): string {
|
|
8654
|
-
return contentHashOf(JSON.stringify({ knowledge: knowledge.map(row => ({ id: row.id, kind: row.kind ?? 'fact', data: row.data, text: row.text, status: row.status, sources: row.sources.filter(source => !source.withdrawn) })).sort((a, b) => a.id.localeCompare(b.id)), profile: context.profile, organize: context.organize, model: context.organize ? { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } : undefined }))
|
|
8817
|
+
return contentHashOf(JSON.stringify({ contract: 'coordinate-wiki-v2', organizeVersion: context.organize ? ORGANIZE_VERSION : undefined, knowledge: knowledge.map(row => ({ id: row.id, kind: row.kind ?? 'fact', data: row.data, text: row.text, status: row.status, sources: row.sources.filter(source => !source.withdrawn) })).sort((a, b) => a.id.localeCompare(b.id)), profile: context.profile, organize: context.organize, model: context.organize ? { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } : undefined }))
|
|
8655
8818
|
}
|
|
8656
8819
|
|
|
8657
8820
|
/**
|
|
@@ -8688,12 +8851,12 @@ export class KnowledgeRuntime {
|
|
|
8688
8851
|
].filter(Boolean).map(text => `- ${text}[^${row.id}]`).join('\n')
|
|
8689
8852
|
}
|
|
8690
8853
|
|
|
8691
|
-
private async freshGroupFingerprint(coordinate: CardCoordinate, context: PipelineContext, db: KnowledgeDb): Promise<string> {
|
|
8854
|
+
private async freshGroupFingerprint(coordinate: CardCoordinate, wikiKind: WikiKind, context: PipelineContext, db: KnowledgeDb): Promise<string> {
|
|
8692
8855
|
const store = await this.knowledgeStore()
|
|
8693
8856
|
if (!store) return ''
|
|
8694
8857
|
const raw: Knowledge[] = []
|
|
8695
8858
|
for (let offset = 0;; offset += 500) {
|
|
8696
|
-
const page = await store.list({ objectId: coordinate.objectId, kind:
|
|
8859
|
+
const page = await store.list({ objectId: coordinate.objectId, kind: wikiKind === 'way' ? 'procedure' : 'fact', actionId: coordinate.actionId, sceneId: coordinate.sceneId, excludeWithdrawn: true, limit: 500, offset })
|
|
8697
8860
|
raw.push(...page.knowledge)
|
|
8698
8861
|
if (offset + page.knowledge.length >= page.total) break
|
|
8699
8862
|
}
|
|
@@ -8705,7 +8868,7 @@ export class KnowledgeRuntime {
|
|
|
8705
8868
|
if (source) bodies.set(id, { version: source.version, content: source.record.content })
|
|
8706
8869
|
}
|
|
8707
8870
|
const rows = raw
|
|
8708
|
-
.filter(row => row.status !== 'deprecated' && (row.kind !== 'procedure' || (row.data?.procedure?.completeness === 'complete' && row.data.procedure.method.length > 0 && row.status !== 'disputed' && row.data.procedure.caseRefs.every(ref => db.event(ref.id)?.version === ref.version) && !row.sources.some(source => source.toVersion !== undefined && db.source(source.documentId)?.version !== source.toVersion))))
|
|
8871
|
+
.filter(row => row.status !== 'deprecated' && (row.kind !== 'procedure' || (row.data?.procedure?.completeness === 'complete' && row.data.procedure.method.length > 0 && row.status !== 'disputed' && row.data.procedure.caseRefs.every(ref => db.event(ref.id)?.version === ref.version) && !row.sources.some(source => !source.withdrawn && source.toVersion !== undefined && db.source(source.documentId)?.version !== source.toVersion))))
|
|
8709
8872
|
.map(row => assessKnowledgeEvidence(row, db, bodies))
|
|
8710
8873
|
.map(row => ({ ...row, sources: row.sources.filter(source => !source.withdrawn && source.evidenceStatus !== 'missing') }))
|
|
8711
8874
|
.filter(row => row.sources.length > 0)
|
|
@@ -8726,12 +8889,13 @@ export class KnowledgeRuntime {
|
|
|
8726
8889
|
*/
|
|
8727
8890
|
const plans = options.plans !== false
|
|
8728
8891
|
const unresolved = store.unresolvedKnowledge()
|
|
8729
|
-
const grouped = new Map<string, { coordinate: CardCoordinate; knowledge: Knowledge[] }>()
|
|
8892
|
+
const grouped = new Map<string, { coordinate: CardCoordinate; wikiKind: WikiKind; knowledge: Knowledge[] }>()
|
|
8730
8893
|
let blockedAmbiguous = 0
|
|
8731
8894
|
let blockedAwaitingNormalize = 0
|
|
8732
|
-
const
|
|
8733
|
-
|
|
8734
|
-
|
|
8895
|
+
const blockedItems: MaterializationSnapshot['blockedItems'] = []
|
|
8896
|
+
const groupFor = (coordinate: CardCoordinate, wikiKind: WikiKind) => {
|
|
8897
|
+
const key = wikiGroupKey(wikiKind, coordinate)
|
|
8898
|
+
if (!grouped.has(key)) grouped.set(key, { coordinate, wikiKind, knowledge: [] })
|
|
8735
8899
|
return grouped.get(key)!
|
|
8736
8900
|
}
|
|
8737
8901
|
const caseStore = await this.storeHandle()
|
|
@@ -8739,24 +8903,29 @@ export class KnowledgeRuntime {
|
|
|
8739
8903
|
if (row.kind === 'case') continue
|
|
8740
8904
|
const coordinate = wikiProjection(row.kind ?? 'fact', row)
|
|
8741
8905
|
const method = row.data?.procedure
|
|
8742
|
-
if (!coordinate || (row.kind === 'procedure' && (!method || method.completeness !== 'complete' || !method.method.length || row.status === 'disputed' || method.caseRefs.some(ref => caseStore?.event(ref.id)?.version !== ref.version) || row.sources.some(source => source.toVersion !== undefined && caseStore?.source(source.documentId)?.version !== source.toVersion)))) {
|
|
8743
|
-
|
|
8906
|
+
if (!coordinate || (row.kind === 'procedure' && (!method || method.completeness !== 'complete' || !method.method.length || row.status === 'disputed' || method.caseRefs.some(ref => caseStore?.event(ref.id)?.version !== ref.version) || row.sources.some(source => source.toVersion !== undefined && caseStore?.source(source.documentId)?.version !== source.toVersion)))) {
|
|
8907
|
+
blockedAmbiguous += 1
|
|
8908
|
+
blockedItems.push({ knowledgeId: row.id, sourceIds: [...new Set(row.sources.map(source => source.documentId).filter(Boolean))], ...(coordinate ? { groupKey: wikiGroupKey(row.kind === 'procedure' ? 'way' : 'fact', coordinate) } : {}) })
|
|
8909
|
+
continue
|
|
8910
|
+
}
|
|
8911
|
+
groupFor(coordinate, row.kind === 'procedure' ? 'way' : 'fact').knowledge.push(row)
|
|
8744
8912
|
}
|
|
8745
8913
|
const db = await this.storeHandle()
|
|
8746
8914
|
const events = new Map<string, EventEntity[]>()
|
|
8747
8915
|
for (const event of db?.events() ?? []) {
|
|
8748
8916
|
const coordinate = event.coordinateIds
|
|
8749
8917
|
if (!coordinate.objectId) continue
|
|
8750
|
-
for (const
|
|
8751
|
-
|
|
8752
|
-
const key = coordinateKeyOf(projection)
|
|
8918
|
+
for (const kind of ['fact', 'way'] as const) {
|
|
8919
|
+
const key = wikiGroupKey(kind, coordinate)
|
|
8753
8920
|
const bucket = events.get(key) ?? []
|
|
8754
8921
|
bucket.push(event)
|
|
8755
8922
|
events.set(key, bucket)
|
|
8756
8923
|
}
|
|
8757
8924
|
}
|
|
8758
8925
|
const cards = await this.loadCards()
|
|
8759
|
-
const
|
|
8926
|
+
const groupKeyOf = (card: KnowledgeCard): string => card.coordinate && (card.metadata.wiki_kind === 'fact' || card.metadata.wiki_kind === 'way')
|
|
8927
|
+
? wikiGroupKey(card.metadata.wiki_kind, card.coordinate) : card.coordinate ? coordinateKeyOf(card.coordinate) : ''
|
|
8928
|
+
const byCoordinate = new Map(cards.filter(card => card.coordinate).map(card => [groupKeyOf(card), card]))
|
|
8760
8929
|
const groups: MaterializationGroup[] = []
|
|
8761
8930
|
const knowledge = new Map<string, Knowledge[]>()
|
|
8762
8931
|
for (const [key, value] of grouped) {
|
|
@@ -8770,11 +8939,13 @@ export class KnowledgeRuntime {
|
|
|
8770
8939
|
const needsNormalization = this.needsNormalizationForGroup(store, value.knowledge, unresolved, db)
|
|
8771
8940
|
if (needsNormalization) blockedAwaitingNormalize += value.knowledge.length
|
|
8772
8941
|
knowledge.set(key, value.knowledge)
|
|
8773
|
-
groups.push({ key, wikiKind: value.
|
|
8942
|
+
groups.push({ key, wikiKind: value.wikiKind, title: existing?.title ?? `${names.object}${names.action ? ` · ${names.action}` : ''}${names.scene ? ` · ${names.scene}` : ''} · ${value.wikiKind === 'way' ? 'Wiki Way' : 'Wiki Fact'}`, coordinate: value.coordinate, knowledgeCount: value.knowledge.length, eventCount: events.get(key)?.length ?? 0, sourceIds: contributingSources, ...(existing ? { cardId: existing.entityId } : {}), state: needsNormalization ? 'blocked' : current ? 'current' : existing ? 'changed' : 'new', ...(needsNormalization ? { reason: '分组中的来源已变化、新抽取待归一或坐标有歧义,请先完成归一' } : {}), fingerprint })
|
|
8774
8943
|
}
|
|
8775
8944
|
for (const card of cards) {
|
|
8776
|
-
|
|
8777
|
-
|
|
8945
|
+
const key = groupKeyOf(card)
|
|
8946
|
+
const legacyReceipt = card.coordinate ? store.materialization(coordinateKeyOf(card.coordinate)) : undefined
|
|
8947
|
+
if (!card.coordinate || grouped.has(key) || (!store.materialization(key) && !legacyReceipt && card.metadata.pipeline_materialization !== true) || card.status === 'deprecated') continue
|
|
8948
|
+
groups.push({ key, wikiKind: card.metadata.wiki_kind === 'way' ? 'way' : 'fact', title: card.title, coordinate: card.coordinate, knowledgeCount: 0, eventCount: 0, sourceIds: card.sourceRefs, cardId: card.entityId, state: card.sourceRefs.some(id => unresolved.sources.has(id)) ? 'blocked' : 'withdrawn', ...(card.sourceRefs.some(id => unresolved.sources.has(id)) ? { reason: '来源中的坐标仍有歧义,请先完成归一' } : {}), fingerprint: 'withdrawn' })
|
|
8778
8949
|
}
|
|
8779
8950
|
const sources = plans ? (await this.loadSources()).map(source => ({ sourceId: source.sourceId, content: source.content })) : []
|
|
8780
8951
|
const imagePlan = plans
|
|
@@ -8805,7 +8976,7 @@ export class KnowledgeRuntime {
|
|
|
8805
8976
|
return {
|
|
8806
8977
|
groups: groups.sort((a, b) => a.title.localeCompare(b.title)),
|
|
8807
8978
|
blocked: blockedAmbiguous + blockedAwaitingNormalize,
|
|
8808
|
-
blockedAmbiguous, blockedAwaitingNormalize,
|
|
8979
|
+
blockedAmbiguous, blockedAwaitingNormalize, blockedItems,
|
|
8809
8980
|
knowledge, events, sources, images: imagePlan.byCard, related: relatedByKey,
|
|
8810
8981
|
}
|
|
8811
8982
|
}
|
|
@@ -8836,7 +9007,7 @@ export class KnowledgeRuntime {
|
|
|
8836
9007
|
const statusBeforeDeprecate = (await this.readCard(existing.entityId))?.status
|
|
8837
9008
|
if (statusBeforeDeprecate === undefined) throw new Error('卡片在处理期间已被删除,本次物化未提交,请刷新后重试')
|
|
8838
9009
|
if (statusBeforeDeprecate !== existing.status) throw new Error(`卡片在处理期间被人工改动(状态 ${existing.status} → ${statusBeforeDeprecate}),本次物化未提交,请重试`)
|
|
8839
|
-
const written = await this.writeCard({ ...existing, status: 'deprecated', origin: 'compile' })
|
|
9010
|
+
const written = await this.writeCard({ ...existing, metadata: { ...existing.metadata, wiki_deprecation_origin: existing.status === 'deprecated' && existing.metadata.wiki_deprecation_origin !== 'withdrawal' ? 'manual' : 'withdrawal' }, status: 'deprecated', origin: 'compile' })
|
|
8840
9011
|
if (!written.ok) throw new Error(written.error.message)
|
|
8841
9012
|
store.materialized(group.key, group.fingerprint, existing.entityId)
|
|
8842
9013
|
// 停用也是一次物化:卡片从可召回里退出,这一步发生过,就该留一条轨迹。
|
|
@@ -8855,7 +9026,7 @@ export class KnowledgeRuntime {
|
|
|
8855
9026
|
let content = knowledge.map(row => `## ${row.data?.procedure?.question ?? row.text.split('\n')[0]!.slice(0, 60)}\n\n${this.organizeKnowledgeText(row)}[^${row.id}]${boundaries.get(row.id) ? `\n\n### 适用条件与边界\n\n${boundaries.get(row.id)}` : ''}`).join('\n\n')
|
|
8856
9027
|
let summary = knowledge[0]?.text ?? ''
|
|
8857
9028
|
let placements: CardKnowledgeRow[] = knowledge.map((row, index) => ({ knowledgeId: row.id, section: row.data?.procedure?.question ?? row.text.split('\n')[0]!.slice(0, 60), ordinal: index + 1, origin: 'compiler' }))
|
|
8858
|
-
let metadata: Record<string, unknown> = { ...(existing?.metadata ?? {}), pipeline_materialization: true, wiki_kind: group.wikiKind ?? (
|
|
9029
|
+
let metadata: Record<string, unknown> = { ...(existing?.metadata ?? {}), pipeline_materialization: true, wiki_kind: group.wikiKind ?? 'fact', wiki_coordinate_version: 2, knowledge_versions: Object.fromEntries(knowledge.map(row => [row.id, row.version])) }
|
|
8859
9030
|
const entityId = existing?.entityId ?? `coord-${hash(group.key)}`
|
|
8860
9031
|
const images = snapshot.images.get(group.key) ?? []
|
|
8861
9032
|
const related = existing && snapshot.related.has(group.key) ? { related: snapshot.related.get(group.key)! } : {}
|
|
@@ -8880,7 +9051,7 @@ export class KnowledgeRuntime {
|
|
|
8880
9051
|
* 「只有知识行变了」;这条按坐标重算整组指纹,能把那种变化也挡住。
|
|
8881
9052
|
*/
|
|
8882
9053
|
if (this.needsNormalizationForGroup(store, knowledge, store.unresolvedKnowledge(), db)) throw new Error('卡片组织期间事实发生变化,结果未提交,请重试')
|
|
8883
|
-
if (await this.freshGroupFingerprint(group.coordinate, context, db) !== group.fingerprint) throw new Error('卡片分组在处理期间事实发生了变化,结果未提交,请重试')
|
|
9054
|
+
if (await this.freshGroupFingerprint(group.coordinate, group.wikiKind ?? 'fact', context, db) !== group.fingerprint) throw new Error('卡片分组在处理期间事实发生了变化,结果未提交,请重试')
|
|
8884
9055
|
if (this.closed) throw new Error('知识库已关闭,本次物化未提交')
|
|
8885
9056
|
/**
|
|
8886
9057
|
* 状态必须**在写之前**重读,并且和读到的旧值比对。
|
|
@@ -8896,7 +9067,8 @@ export class KnowledgeRuntime {
|
|
|
8896
9067
|
if (statusAtWrite !== existing?.status) {
|
|
8897
9068
|
throw new Error(`卡片在处理期间被人工改动(状态 ${existing?.status ?? '未知'} → ${statusAtWrite ?? '未知'}),本次物化未提交,请重试`)
|
|
8898
9069
|
}
|
|
8899
|
-
const
|
|
9070
|
+
const nextStatus = statusAtWrite === 'deprecated' && existing?.metadata.wiki_deprecation_origin === 'withdrawal' ? 'unreviewed' : statusAtWrite ?? 'unreviewed'
|
|
9071
|
+
const written = await this.writeCard({ entityType, entityId, title, content, summary, sourceRefs: group.sourceIds, status: nextStatus, coordinate: group.coordinate, placements, metadata, origin: 'compile' })
|
|
8900
9072
|
if (!written.ok) throw new Error(written.error.message)
|
|
8901
9073
|
db.setCardEvents(entityId, eventIds)
|
|
8902
9074
|
store.materialized(group.key, group.fingerprint, entityId)
|
|
@@ -8915,7 +9087,7 @@ export class KnowledgeRuntime {
|
|
|
8915
9087
|
card: {
|
|
8916
9088
|
id: entityId,
|
|
8917
9089
|
title,
|
|
8918
|
-
status:
|
|
9090
|
+
status: nextStatus,
|
|
8919
9091
|
characters: content.length,
|
|
8920
9092
|
events: eventIds.length,
|
|
8921
9093
|
images: images.length,
|
|
@@ -8939,7 +9111,9 @@ export class KnowledgeRuntime {
|
|
|
8939
9111
|
const db = await this.derivedHandle()
|
|
8940
9112
|
const manifest = db?.artifact('build-manifest.json')
|
|
8941
9113
|
const hashes = manifest ? (JSON.parse(manifest) as { cardHashes?: Record<string, string> }).cardHashes ?? {} : {}
|
|
8942
|
-
const
|
|
9114
|
+
const allCards = await this.loadCards()
|
|
9115
|
+
const unavailable = await this.wikiUnavailability(allCards)
|
|
9116
|
+
const cards = allCards.filter(card => !card.metadata.wiki_kind || !unavailable.has(cardKey(card)))
|
|
8943
9117
|
const indexPending = cards.some(card => hashes[`${card.entityType}:${card.entityId}`] !== this.cardHashOf(card)) || Object.keys(hashes).length !== cards.length
|
|
8944
9118
|
return { sources: rows, groups, blocked, pendingNew: groups.filter(group => group.state === 'new').length, pendingUpdated: groups.filter(group => group.state === 'changed').length, pendingWithdrawn: groups.filter(group => group.state === 'withdrawn').length, indexPending }
|
|
8945
9119
|
}
|
|
@@ -9131,7 +9305,7 @@ export class KnowledgeRuntime {
|
|
|
9131
9305
|
* 跑一趟编译的**实体**(0066)。它不做排队,也不对外承诺「你 await 它就等于你发起了
|
|
9132
9306
|
* 它」—— 那两件事分别在 `enqueueCompile()` 与 `rebuild()` / `startCompile()` 里。
|
|
9133
9307
|
*/
|
|
9134
|
-
private async compileOnce(runId: string, only: 'all' | 'schema' | 'wiki' | 'embeddings' = 'all', lineage?: LineageContext, options: { extract?: boolean; forceExtract?: boolean; checkpoint?: () => Promise<void
|
|
9308
|
+
private async compileOnce(runId: string, only: 'all' | 'schema' | 'wiki' | 'embeddings' = 'all', lineage?: LineageContext, options: { extract?: boolean; forceExtract?: boolean; checkpoint?: () => Promise<void>; onProgress?: (step: CompileActivityView) => void } = {}): Promise<Envelope<RebuildReport>> {
|
|
9135
9309
|
if (only === 'all' || only === 'embeddings') await this.environmentGuard?.('embedding')
|
|
9136
9310
|
this.publication = { artifacts: new Map() }
|
|
9137
9311
|
const configAtStart = JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() })
|
|
@@ -9165,18 +9339,23 @@ export class KnowledgeRuntime {
|
|
|
9165
9339
|
}
|
|
9166
9340
|
this.publication.artifacts.set(COMPILE_SNAPSHOT, JSON.stringify(snapshot))
|
|
9167
9341
|
await this.checkpointCompileTask(runId)
|
|
9342
|
+
const changedWiki = await this.wikiUnavailability(snapshot.cards, new Map(snapshot.placements))
|
|
9343
|
+
if (snapshot.cards.some(card => card.metadata.wiki_kind && changedWiki.has(cardKey(card)))) throw new Error('WIKI_CHANGED_DURING_BUILD: Wiki 的支撑知识在构建期间变化,请重新组织并构建索引')
|
|
9168
9344
|
if (JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() }) !== configAtStart) throw new Error('COMPILE_CONFIG_CHANGED: 编译期间设置已变化,请重新编译')
|
|
9169
9345
|
await options.checkpoint?.()
|
|
9346
|
+
options.onProgress?.({ id: 'publish', state: 'running', facts: ['正在统一发布检索索引版本'] })
|
|
9170
9347
|
db.publish(this.publication)
|
|
9171
9348
|
this.vectorIndex = null
|
|
9172
9349
|
}
|
|
9173
9350
|
return result
|
|
9174
9351
|
} finally {
|
|
9175
9352
|
this.publication = undefined
|
|
9353
|
+
// Compilation filters unavailable projections; the browsing cache still represents all saved Wiki.
|
|
9354
|
+
this.cards = await this.loadCards()
|
|
9176
9355
|
}
|
|
9177
9356
|
}
|
|
9178
9357
|
|
|
9179
|
-
private async prepareCompile(runId: string, only: 'all' | 'schema' | 'wiki' | 'embeddings' = 'all', lineage?: LineageContext, options: { extract?: boolean; forceExtract?: boolean } = {}): Promise<Envelope<RebuildReport>> {
|
|
9358
|
+
private async prepareCompile(runId: string, only: 'all' | 'schema' | 'wiki' | 'embeddings' = 'all', lineage?: LineageContext, options: { extract?: boolean; forceExtract?: boolean; onProgress?: (step: CompileActivityView) => void } = {}): Promise<Envelope<RebuildReport>> {
|
|
9180
9359
|
/**
|
|
9181
9360
|
* 进度记录**先占位**:`runId` 一落下去,这一趟就有了名字 —— 之后无论从哪个出口结束
|
|
9182
9361
|
* (三种 return,或者抛异常),收口都能认出这条记录属于谁。
|
|
@@ -9258,6 +9437,7 @@ export class KnowledgeRuntime {
|
|
|
9258
9437
|
const setStep = (id: CompileStepId, state: CompileStepView['state'], facts: string[]): void => {
|
|
9259
9438
|
const current = this.compileProgress ?? blankProgress(runId, only, startedAt, 'running', queuedAt)
|
|
9260
9439
|
this.writeCompileProgress({ ...current, steps: current.steps.map(step => step.id === id ? { ...step, state, facts } : step) })
|
|
9440
|
+
options.onProgress?.({ id, state, facts })
|
|
9261
9441
|
}
|
|
9262
9442
|
this.cards = await this.loadCards()
|
|
9263
9443
|
const sources = await this.loadSources()
|
|
@@ -9436,6 +9616,9 @@ export class KnowledgeRuntime {
|
|
|
9436
9616
|
await this.checkpointCompileTask(runId)
|
|
9437
9617
|
// 相关 metadata 或旧正文清理刚落盘:stage 2 必须看到最新卡片。
|
|
9438
9618
|
if (finalize.rewritten > 0) this.cards = await this.loadCards()
|
|
9619
|
+
const unavailableWiki = await this.wikiUnavailability(this.cards)
|
|
9620
|
+
for (const card of this.cards) if (card.metadata.wiki_kind && unavailableWiki.has(cardKey(card))) warnings.push(`Wiki「${card.title}」未进入索引:${unavailableWiki.get(cardKey(card))}`)
|
|
9621
|
+
this.cards = this.cards.filter(card => !card.metadata.wiki_kind || !unavailableWiki.has(cardKey(card)))
|
|
9439
9622
|
/**
|
|
9440
9623
|
* 索引页也是这次编译的产物,所以它变了也算「语料变了」。
|
|
9441
9624
|
*
|
|
@@ -9848,12 +10031,11 @@ export class KnowledgeRuntime {
|
|
|
9848
10031
|
}
|
|
9849
10032
|
|
|
9850
10033
|
/**
|
|
9851
|
-
* Coordinate
|
|
10034
|
+
* Coordinate 路:解析 Query 中出现的 Object / Action / Scene,再把命中的卡片拆成
|
|
9852
10035
|
* Section 候选。它和 FTS / Vector 使用同一个 `unitId` 粒度,后面才能统一 RRF 与重排。
|
|
9853
10036
|
*
|
|
9854
10037
|
* 这里不让模型自由生成实体名:注册表负责 canonical / alias 命中,只有真实存在的
|
|
9855
|
-
* 坐标 id
|
|
9856
|
-
* 后续接入模型消歧时不需要改变召回合同。
|
|
10038
|
+
* 坐标 id 才能进入候选。未解析的原文继续交给关键词、向量路线与统一重排。
|
|
9857
10039
|
*/
|
|
9858
10040
|
private async coordinateRoute(
|
|
9859
10041
|
query: string,
|
|
@@ -10243,7 +10425,8 @@ export class KnowledgeRuntime {
|
|
|
10243
10425
|
for (const ref of hit.sourceRefs) {
|
|
10244
10426
|
const source = corpus.get(ref)
|
|
10245
10427
|
if (!source) continue
|
|
10246
|
-
|
|
10428
|
+
const version = (await this.storeHandle())?.source(ref)?.version
|
|
10429
|
+
evidence.push(...evidenceExcerpts(ref, source.title, source.content, queryTerms).map(excerpt => ({ ...excerpt, readRef: { kb: this.baseId || 'default', source_id: ref, version, content_hash: createHash('sha256').update(source.content).digest('hex'), quote: excerpt.excerpt.replace(/…$/, '') } })))
|
|
10247
10430
|
}
|
|
10248
10431
|
if (evidence.length > 0) {
|
|
10249
10432
|
hit.evidence = evidence
|
|
@@ -10317,7 +10500,7 @@ export class KnowledgeRuntime {
|
|
|
10317
10500
|
const collected = new Map<string, RecallKnowledge>()
|
|
10318
10501
|
for (const row of perHit[index] ?? []) {
|
|
10319
10502
|
const resolved = rows.get(row.knowledgeId)
|
|
10320
|
-
if (resolved !== undefined) collected.set(resolved.id, resolved)
|
|
10503
|
+
if (resolved !== undefined) collected.set(resolved.id, { ...resolved, ...(resolved.readRef ? { readRef: { ...resolved.readRef, kb: this.baseId || 'default' } } : {}) })
|
|
10321
10504
|
}
|
|
10322
10505
|
if (collected.size > 0) {
|
|
10323
10506
|
hit.knowledge = [...collected.values()]
|
|
@@ -10356,7 +10539,7 @@ export class KnowledgeRuntime {
|
|
|
10356
10539
|
includeProblems = false,
|
|
10357
10540
|
mode?: RecallMode,
|
|
10358
10541
|
lineage?: LineageContext,
|
|
10359
|
-
options: { contextTopN?: number; rerank?: boolean; contextMaxChars?: number; detail?: boolean } = {},
|
|
10542
|
+
options: KnowledgeScope & { contextTopN?: number; rerank?: boolean; contextMaxChars?: number; detail?: boolean } = {},
|
|
10360
10543
|
): Promise<Envelope<RecallResult>> {
|
|
10361
10544
|
const recallId = generatedId('recall')
|
|
10362
10545
|
/**
|
|
@@ -10395,6 +10578,8 @@ export class KnowledgeRuntime {
|
|
|
10395
10578
|
if (mode !== undefined && !['fts', 'vector', 'hybrid'].includes(String(mode))) {
|
|
10396
10579
|
return failure(empty(), 'MODE_INVALID', '召回 mode 只支持 fts、vector、hybrid;graph 不是召回模式', false)
|
|
10397
10580
|
}
|
|
10581
|
+
if (scopeError(options)) return failure(empty(), 'SCOPE_INVALID', scopeError(options)!, false)
|
|
10582
|
+
if (options.contextMaxChars !== undefined && (!Number.isInteger(options.contextMaxChars) || options.contextMaxChars < 0)) return failure(empty(), 'CONTEXT_BUDGET_INVALID', 'context_max_chars 必须为非负整数。', false)
|
|
10398
10583
|
if (!query?.trim()) return failure(empty(), 'QUERY_REQUIRED', 'query 不能为空', true)
|
|
10399
10584
|
if (!Number.isInteger(recallTopK) || recallTopK < 1 || recallTopK > 50) return failure(empty(), 'LIMIT_INVALID', '召回 top_k 必须是 1 到 50 的整数', false)
|
|
10400
10585
|
if (!Number.isInteger(requestedTopN) || requestedTopN < 1 || (options.contextTopN !== undefined && requestedTopN > recallTopK)) return failure(empty(), 'CONTEXT_TOP_N_INVALID', `上下文 top_n 必须是 1 到 ${recallTopK} 的整数`, false)
|
|
@@ -10443,26 +10628,17 @@ export class KnowledgeRuntime {
|
|
|
10443
10628
|
for (const key of liveKeys) {
|
|
10444
10629
|
if (snapshotCardByKey.get(key)?.sourceRefs.some(ref => !liveSourceIds.has(ref))) liveKeys.delete(key)
|
|
10445
10630
|
}
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
const unsafeProcedures = new Set(snapshot.knowledge.filter(row => {
|
|
10449
|
-
if (row.kind !== 'procedure') return false
|
|
10450
|
-
const current = truthAtRead?.raw('SELECT status,head_version FROM knowledge WHERE id=?').get(row.id) as { status: string; head_version: number } | undefined
|
|
10451
|
-
return !current || current.status === 'deprecated' || current.status === 'disputed' || current.head_version !== row.version
|
|
10452
|
-
|| !row.data?.procedure || row.data.procedure.completeness !== 'complete'
|
|
10453
|
-
|| row.data.procedure.caseRefs.some(ref => truthAtRead?.event(ref.id)?.version !== ref.version)
|
|
10454
|
-
|| row.sources.some(source => source.toVersion !== undefined && truthAtRead?.source(source.documentId)?.version !== source.toVersion)
|
|
10455
|
-
}).map(row => row.id))
|
|
10456
|
-
const unsafeCardIds = new Set(snapshot.placements.filter(([, placements]) => placements.some(p => unsafeProcedures.has(p.knowledgeId))).map(([id]) => id))
|
|
10457
|
-
for (const key of liveKeys) if (unsafeCardIds.has(snapshotCardByKey.get(key)?.entityId ?? '')) liveKeys.delete(key)
|
|
10631
|
+
const unavailableWiki = await this.wikiUnavailability(snapshot.cards, new Map(snapshot.placements))
|
|
10632
|
+
for (const key of unavailableWiki.keys()) liveKeys.delete(key)
|
|
10458
10633
|
let publishedGraph = visibleGraph(snapshot.graph, truthAtRead ? liveGraphKeys(truthAtRead) : new Set())
|
|
10459
10634
|
const currentByKey = new Map(liveCards.map(card => [cardKey(card), card]))
|
|
10460
10635
|
let cards: KnowledgeCard[] = snapshot.cards.filter(card => liveKeys.has(cardKey(card))).map(card => {
|
|
10461
10636
|
const current = currentByKey.get(cardKey(card))!
|
|
10462
10637
|
return { ...card, status: current.status, annotations: current.annotations, review: current.review,
|
|
10463
10638
|
metadata: { ...card.metadata, ...(current.metadata.evidence_needs_review === true ? { evidence_needs_review: true } : {}) } }
|
|
10464
|
-
})
|
|
10465
|
-
const
|
|
10639
|
+
}).filter(card => matchesScope(card, options))
|
|
10640
|
+
const scopedKeys = new Set(cards.map(cardKey))
|
|
10641
|
+
const eligible = snapshot.notes.filter(unit => scopedKeys.has(`${unit.entityType}:${unit.entityId}`)
|
|
10466
10642
|
&& (includeProblems || currentByKey.get(`${unit.entityType}:${unit.entityId}`)?.status !== 'problem'))
|
|
10467
10643
|
const eligibleIds = new Set(eligible.map(unit => unit.unitId))
|
|
10468
10644
|
/**
|
|
@@ -10571,13 +10747,9 @@ export class KnowledgeRuntime {
|
|
|
10571
10747
|
let rerankScored: Array<{ unitId: string; logit: number; rerankScore: number; kept: boolean }> = []
|
|
10572
10748
|
if (options.rerank === false || this.config.rerankProvider === 'none') {
|
|
10573
10749
|
rerank = { status: 'skipped', provider: this.config.rerankProvider }
|
|
10574
|
-
hits = hits.slice(0, recallTopK)
|
|
10575
10750
|
} else {
|
|
10576
10751
|
const rerankQuery = [
|
|
10577
10752
|
query,
|
|
10578
|
-
...(queryAnalysis.intent.id !== 'unknown' && queryAnalysis.intent.confidence >= 0.7
|
|
10579
|
-
? [`意图:${queryAnalysis.intent.id}`]
|
|
10580
|
-
: []),
|
|
10581
10753
|
...(['object', 'action', 'scene'] as const).flatMap(kind => {
|
|
10582
10754
|
const values = queryAnalysis.axes[kind].map(item => item.name).slice(0, 3)
|
|
10583
10755
|
return values.length > 0 ? [`${kind}:${values.join('、')}`] : []
|
|
@@ -10594,7 +10766,15 @@ export class KnowledgeRuntime {
|
|
|
10594
10766
|
}
|
|
10595
10767
|
// 重排换的是相关性顺序,评分加权仍然要压在最上面 —— 否则「review 过的排前面一点」
|
|
10596
10768
|
// 只在关掉重排时成立,而重排默认是开着的。
|
|
10597
|
-
hits = [...hits].sort((left, right) => rankKey(right) - rankKey(left) || left.unitId.localeCompare(right.unitId))
|
|
10769
|
+
hits = [...hits].sort((left, right) => rankKey(right) - rankKey(left) || left.unitId.localeCompare(right.unitId))
|
|
10770
|
+
const rankedSections = hits
|
|
10771
|
+
// Select complete Wiki identities before applying top_k. Each Wiki contributes its best section.
|
|
10772
|
+
const selectedWikiKeys = new Set<string>()
|
|
10773
|
+
hits = rankedSections.filter(hit => {
|
|
10774
|
+
const key = `${hit.entityType}:${hit.entityId}`
|
|
10775
|
+
if (selectedWikiKeys.has(key) || selectedWikiKeys.size >= recallTopK) return false
|
|
10776
|
+
selectedWikiKeys.add(key); return true
|
|
10777
|
+
})
|
|
10598
10778
|
|
|
10599
10779
|
// --- R4 · 证据片段 -----------------------------------------------------
|
|
10600
10780
|
const truth = await this.storeHandle()
|
|
@@ -10664,7 +10844,7 @@ export class KnowledgeRuntime {
|
|
|
10664
10844
|
* 不再各自占一个上下文名额。
|
|
10665
10845
|
*/
|
|
10666
10846
|
const notesByWiki = new Map<string, RecallHit[]>()
|
|
10667
|
-
for (const hit of
|
|
10847
|
+
for (const hit of rankedSections.filter(hit => selectedWikiKeys.has(wikiKey(hit)) && finalKeys.has(wikiKey(hit)))) {
|
|
10668
10848
|
const key = wikiKey(hit)
|
|
10669
10849
|
const bucket = notesByWiki.get(key)
|
|
10670
10850
|
if (bucket) bucket.push(hit)
|
|
@@ -10720,8 +10900,7 @@ export class KnowledgeRuntime {
|
|
|
10720
10900
|
/**
|
|
10721
10901
|
* 上下文预算(0050):装箱 + 溢出。
|
|
10722
10902
|
*
|
|
10723
|
-
* 让位顺序写死在 `packContext`
|
|
10724
|
-
* 再给扩展卡片的标题 + 摘要 + 关系,最后才给扩展卡片的正文。
|
|
10903
|
+
* 让位顺序写死在 `packContext` 里:优先交付命中的完整 Wiki,再交付扩展 Wiki。
|
|
10725
10904
|
*/
|
|
10726
10905
|
/**
|
|
10727
10906
|
* 每次调用可以覆盖预算(0050):指路行里就写着「读不到就调大预算重新召回」,
|
|
@@ -10729,6 +10908,14 @@ export class KnowledgeRuntime {
|
|
|
10729
10908
|
*/
|
|
10730
10909
|
const maxChars = options.contextMaxChars ?? this.config.contextMaxChars
|
|
10731
10910
|
const packed = await this.packContext({ items, maxChars, recallId })
|
|
10911
|
+
const deliveredKeys = new Set(packed.items.flatMap(item => item.wiki ? [cardKey(item.wiki)] : []))
|
|
10912
|
+
// Diagnostic HTTP requests may inspect unbudgeted candidate text. Normal delivery cannot leak it.
|
|
10913
|
+
const deliveredHits = options.detail ? hits : hits.map(hit => {
|
|
10914
|
+
if (deliveredKeys.has(wikiKey(hit))) return hit
|
|
10915
|
+
const { evidence: _evidence, knowledge: _knowledge, ...reference } = hit
|
|
10916
|
+
return { ...reference, content: '' }
|
|
10917
|
+
})
|
|
10918
|
+
const deliveredWiki = options.detail ? wiki : wiki.filter(card => deliveredKeys.has(cardKey(card)))
|
|
10732
10919
|
/** 哪些扩展卡片真的进了上下文(被预算挡住的也如实报出来)。 */
|
|
10733
10920
|
expansion.included = packed.items
|
|
10734
10921
|
.filter(item => item.reason === 'expansion' && item.wiki !== null)
|
|
@@ -10789,9 +10976,11 @@ export class KnowledgeRuntime {
|
|
|
10789
10976
|
}
|
|
10790
10977
|
|
|
10791
10978
|
const data: RecallResult = {
|
|
10792
|
-
hits, context: packed.items, wiki, graph, backend, routes: ranRoutes, recallMode, rerank, recallTopK, candidatePool, fusedCount, contextTopN,
|
|
10979
|
+
hits: deliveredHits, context: packed.items, wiki: deliveredWiki, graph, backend, routes: ranRoutes, recallMode, rerank, recallTopK, candidatePool, fusedCount, contextTopN,
|
|
10793
10980
|
queryAnalysis,
|
|
10794
|
-
insufficientEvidence
|
|
10981
|
+
insufficientEvidence: insufficientEvidence || !packed.items.some(item => item.reason === 'hit' && item.wiki),
|
|
10982
|
+
foundCandidates: hits.length > 0, contentDelivered: packed.items.some(item => item.reason === 'hit' && item.wiki),
|
|
10983
|
+
diagnostics, recallId, timings,
|
|
10795
10984
|
expansion, budget: packed.report,
|
|
10796
10985
|
...(ledger === undefined ? {} : { ledger }),
|
|
10797
10986
|
...(indexVersion === undefined ? {} : { indexVersion }),
|
|
@@ -10936,6 +11125,20 @@ export class KnowledgeRuntime {
|
|
|
10936
11125
|
* 路径是知识库里的真实文件,读它与读这里的内容是同一份数据,而且没有配额。这样
|
|
10937
11126
|
* 「一次读不完」就不再等于「读不到」,也不会出现「以为拿到全文、其实只有前 4 万字符」。
|
|
10938
11127
|
*/
|
|
11128
|
+
async readSource(input: SourceReadInput, lineage?: LineageContext): Promise<Envelope<Record<string, unknown>>> {
|
|
11129
|
+
if (!this.config.allowSourceTruth) return failure({}, 'SOURCE_TRUTH_DISABLED', '当前配置禁止访问 Source Raw。', false)
|
|
11130
|
+
const db = await this.storeHandle()
|
|
11131
|
+
if (!db) return failure({}, 'STORE_UNAVAILABLE', '来源存储不可用。', true)
|
|
11132
|
+
try {
|
|
11133
|
+
const data = readSourceExact(db, input)
|
|
11134
|
+
await this.audit('source_truth.read_exact', { sourceId: input.sourceId, version: data.version, start: data.start, end: data.end, lineage })
|
|
11135
|
+
return success({ ...data, sourceIdentity: db.sourceIdentities(input.sourceId), readRef: { ...(data.readRef as object), kb: this.baseId || 'default' }, nextRead: data.nextRead ? { ...(data.nextRead as Record<string, unknown>), args: { ...((data.nextRead as any).args), kb: this.baseId || 'default' } } : undefined })
|
|
11136
|
+
} catch (error) {
|
|
11137
|
+
if (error instanceof SourceReadError) return failure({}, error.code, error.message, false)
|
|
11138
|
+
throw error
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
11141
|
+
|
|
10939
11142
|
async sourceTruth(query: string, limit = 20, maxChars = 40000, lineage?: LineageContext, offset = 0, sourceOffset = 0): Promise<Envelope<{ sources: SourceRawRead[]; sourceReadId: string; maxChars: number; offset: number; returnedChars: number; truncated: boolean; hasMoreInCurrentPage: boolean; totalMatchedSources: number; sourceOffset: number; hasMoreSources: boolean; nextSourceOffset?: number; lineage?: LineageContext }>> {
|
|
10940
11143
|
const sourceReadId = generatedId('source-read')
|
|
10941
11144
|
const empty = { sources: [] as SourceRawRead[], sourceReadId, maxChars, offset, returnedChars: 0, truncated: false, hasMoreInCurrentPage: false, totalMatchedSources: 0, sourceOffset, hasMoreSources: false }
|
|
@@ -10944,6 +11147,7 @@ export class KnowledgeRuntime {
|
|
|
10944
11147
|
if (!Number.isInteger(offset) || offset < 0) return failure(empty, 'SOURCE_OFFSET_INVALID', 'offset 必须是不小于 0 的整数;它接着上一页的 returnedChars 往下读', false)
|
|
10945
11148
|
if (!Number.isInteger(sourceOffset) || sourceOffset < 0) return failure(empty, 'SOURCE_OFFSET_INVALID', 'source_offset 必须是不小于 0 的整数;需要读更多来源时使用返回的 nextSourceOffset', false)
|
|
10946
11149
|
const queryTokens = tokens(query)
|
|
11150
|
+
const sourceDb = await this.storeHandle()
|
|
10947
11151
|
const matchedSources = (await this.loadSources()).filter(item => !queryTokens.length || queryTokens.some(token => `${item.title}\n${item.content}`.toLocaleLowerCase().includes(token)))
|
|
10948
11152
|
const sources = matchedSources.slice(sourceOffset, sourceOffset + limit)
|
|
10949
11153
|
const hasMoreSources = sourceOffset + sources.length < matchedSources.length
|
|
@@ -10981,6 +11185,8 @@ export class KnowledgeRuntime {
|
|
|
10981
11185
|
if (content.length === 0 && source.content.length > 0) continue
|
|
10982
11186
|
bounded.push({
|
|
10983
11187
|
...source,
|
|
11188
|
+
version: sourceDb?.source(source.sourceId)?.version,
|
|
11189
|
+
readRef: { kb: this.baseId || 'default', source_id: source.sourceId, version: sourceDb?.source(source.sourceId)?.version, content_hash: createHash('sha256').update(source.content).digest('hex') },
|
|
10984
11190
|
content,
|
|
10985
11191
|
/*
|
|
10986
11192
|
* 这里原先回一个**磁盘绝对路径**,工具描述让 Agent「截断了就去读那个文件」。
|