dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.15
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 +89 -56
- package/cordis.patch.yml +36 -129
- package/eval/benchmark-extraction-format.mjs +20 -0
- package/lib/client.js +3262 -4142
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +1147 -364
- package/lib/types/axis-registry.d.ts +8 -1
- package/lib/types/axis-registry.d.ts.map +1 -1
- package/lib/types/axis-registry.js +62 -7
- package/lib/types/axis-registry.js.map +1 -1
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +3 -0
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +9 -0
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +68 -9
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +7 -3
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +13 -8
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +4 -13
- package/lib/types/diagnosis/run.d.ts.map +1 -1
- package/lib/types/diagnosis/run.js +26 -16
- package/lib/types/diagnosis/run.js.map +1 -1
- package/lib/types/diagnosis/schema.js +2 -2
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +1 -1
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +5 -4
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +4 -4
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +6 -8
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +18 -17
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +94 -66
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +10 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/environment/dsh-python.d.ts +77 -0
- package/lib/types/environment/dsh-python.d.ts.map +1 -0
- package/lib/types/environment/dsh-python.js +139 -0
- package/lib/types/environment/dsh-python.js.map +1 -0
- package/lib/types/environment/install.d.ts +9 -1
- package/lib/types/environment/install.d.ts.map +1 -1
- package/lib/types/environment/install.js +149 -24
- package/lib/types/environment/install.js.map +1 -1
- package/lib/types/environment/model-catalog.d.ts +34 -2
- package/lib/types/environment/model-catalog.d.ts.map +1 -1
- package/lib/types/environment/model-catalog.js +40 -2
- package/lib/types/environment/model-catalog.js.map +1 -1
- package/lib/types/environment/service.d.ts +13 -3
- package/lib/types/environment/service.d.ts.map +1 -1
- package/lib/types/environment/service.js +127 -23
- package/lib/types/environment/service.js.map +1 -1
- package/lib/types/environment/types.d.ts +9 -0
- package/lib/types/environment/types.d.ts.map +1 -1
- package/lib/types/events.d.ts +2 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +13 -0
- package/lib/types/events.js.map +1 -1
- package/lib/types/extract-contract.d.ts +1 -1
- package/lib/types/extract-contract.d.ts.map +1 -1
- package/lib/types/extract-contract.js +34 -31
- package/lib/types/extract-contract.js.map +1 -1
- package/lib/types/extract-model.d.ts +2 -0
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract-table-contract.d.ts +3 -0
- package/lib/types/extract-table-contract.d.ts.map +1 -0
- package/lib/types/extract-table-contract.js +30 -0
- package/lib/types/extract-table-contract.js.map +1 -0
- package/lib/types/extract-table.d.ts +8 -0
- package/lib/types/extract-table.d.ts.map +1 -0
- package/lib/types/extract-table.js +151 -0
- package/lib/types/extract-table.js.map +1 -0
- package/lib/types/extraction-runs.d.ts +30 -6
- package/lib/types/extraction-runs.d.ts.map +1 -1
- package/lib/types/extraction-runs.js +69 -8
- package/lib/types/extraction-runs.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +0 -7
- package/lib/types/fs-atomic.d.ts.map +1 -1
- package/lib/types/fs-atomic.js +17 -2
- package/lib/types/fs-atomic.js.map +1 -1
- package/lib/types/images.d.ts +1 -1
- package/lib/types/images.d.ts.map +1 -1
- package/lib/types/images.js +2 -2
- package/lib/types/images.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +232 -50
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/scope.d.ts.map +1 -1
- package/lib/types/intent/scope.js +1 -0
- package/lib/types/intent/scope.js.map +1 -1
- package/lib/types/intent/tools.d.ts +19 -0
- package/lib/types/intent/tools.d.ts.map +1 -1
- package/lib/types/intent/tools.js +37 -1
- package/lib/types/intent/tools.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +5 -16
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge.d.ts +3 -0
- package/lib/types/knowledge.d.ts.map +1 -1
- package/lib/types/knowledge.js +21 -5
- package/lib/types/knowledge.js.map +1 -1
- package/lib/types/materialize-snapshot.d.ts +49 -0
- package/lib/types/materialize-snapshot.d.ts.map +1 -0
- package/lib/types/materialize-snapshot.js +2 -0
- package/lib/types/materialize-snapshot.js.map +1 -0
- package/lib/types/model-text.d.ts +1 -1
- package/lib/types/model-text.d.ts.map +1 -1
- package/lib/types/model-text.js +5 -4
- package/lib/types/model-text.js.map +1 -1
- package/lib/types/oplog.d.ts.map +1 -1
- package/lib/types/oplog.js +7 -0
- package/lib/types/oplog.js.map +1 -1
- package/lib/types/pipeline/engine.d.ts +60 -0
- package/lib/types/pipeline/engine.d.ts.map +1 -0
- package/lib/types/pipeline/engine.js +338 -0
- package/lib/types/pipeline/engine.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +73 -0
- package/lib/types/pipeline/store.d.ts.map +1 -0
- package/lib/types/pipeline/store.js +143 -0
- package/lib/types/pipeline/store.js.map +1 -0
- package/lib/types/pipeline/types.d.ts +101 -0
- package/lib/types/pipeline/types.d.ts.map +1 -0
- package/lib/types/pipeline/types.js +5 -0
- package/lib/types/pipeline/types.js.map +1 -0
- package/lib/types/present.d.ts +2 -0
- package/lib/types/present.d.ts.map +1 -1
- package/lib/types/present.js +9 -4
- package/lib/types/present.js.map +1 -1
- package/lib/types/retention.d.ts +39 -0
- package/lib/types/retention.d.ts.map +1 -0
- package/lib/types/retention.js +42 -0
- package/lib/types/retention.js.map +1 -0
- package/lib/types/runtime.d.ts +115 -5
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +862 -132
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/source-evidence.d.ts +9 -1
- package/lib/types/source-evidence.d.ts.map +1 -1
- package/lib/types/source-evidence.js +13 -5
- package/lib/types/source-evidence.js.map +1 -1
- package/lib/types/store/axis-references.js +3 -3
- package/lib/types/store/axis-references.js.map +1 -1
- package/lib/types/store/derived.d.ts +1 -1
- package/lib/types/store/derived.d.ts.map +1 -1
- package/lib/types/store/derived.js +41 -7
- package/lib/types/store/derived.js.map +1 -1
- package/lib/types/store/knowledge-db.d.ts +18 -0
- package/lib/types/store/knowledge-db.d.ts.map +1 -1
- package/lib/types/store/knowledge-db.js +111 -6
- package/lib/types/store/knowledge-db.js.map +1 -1
- package/lib/types/store/migrate.js +1 -1
- package/lib/types/store/migrate.js.map +1 -1
- package/lib/types/trajectory-types.d.ts +172 -0
- package/lib/types/trajectory-types.d.ts.map +1 -0
- package/lib/types/trajectory-types.js +36 -0
- package/lib/types/trajectory-types.js.map +1 -0
- package/lib/types/types.d.ts +3 -4
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +16 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -1
- package/lib/types/wiki-finalize.js +26 -0
- package/lib/types/wiki-finalize.js.map +1 -1
- package/lib/types/wiki-materialize.d.ts.map +1 -1
- package/lib/types/wiki-materialize.js +3 -15
- package/lib/types/wiki-materialize.js.map +1 -1
- 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 +2 -0
- package/lib/types/wiki-trial.d.ts.map +1 -1
- package/lib/types/wiki-trial.js +31 -7
- package/lib/types/wiki-trial.js.map +1 -1
- package/package.json +6 -3
- package/preset/tita-data-agent/agent.cordis.yml +36 -129
- package/src/axis-registry.ts +57 -9
- package/src/client/AttributionEventGuide.tsx +10 -9
- package/src/client/CompileDialogs.tsx +10 -2
- package/src/client/EnvironmentSetup.tsx +76 -16
- package/src/client/ExpansionGraph.tsx +13 -7
- package/src/client/ExtractionTasksPage.tsx +543 -156
- package/src/client/ImportFilesDialog.tsx +2 -13
- package/src/client/IntentTab.tsx +3 -1
- package/src/client/KnowledgeBaseTab.tsx +139 -122
- package/src/client/KnowledgeLayerSection.tsx +19 -2
- package/src/client/KnowledgePanel.tsx +17 -3
- package/src/client/ListPager.tsx +15 -5
- package/src/client/MaterializeTrajectoryView.tsx +124 -0
- package/src/client/NodeSettings.tsx +90 -25
- package/src/client/NormalizeTrajectoryView.tsx +136 -0
- package/src/client/PipelineNotice.tsx +26 -0
- package/src/client/RecallPanel.tsx +55 -25
- package/src/client/TabGuide.tsx +4 -4
- package/src/client/TrialReportView.tsx +61 -16
- package/src/client/TypeDefinitionDrawer.tsx +33 -9
- package/src/client/WikiCardPreview.tsx +43 -10
- package/src/client/WikiFlowPanel.tsx +4 -4
- package/src/client/architecture.tsx +1 -1
- package/src/client/attribution/AttributionTab.tsx +37 -36
- package/src/client/attribution/CaseView.tsx +3 -1
- package/src/client/attribution/EventView.tsx +14 -4
- package/src/client/attribution/RunView.tsx +2 -2
- package/src/client/attribution/ViewSwitch.tsx +20 -5
- package/src/client/attribution/graph-options.ts +38 -31
- package/src/client/attribution/model.ts +16 -9
- package/src/client/graph/palette.ts +165 -0
- package/src/client/pages.css +224 -28
- package/src/client/pages.tsx +33 -11
- package/src/client/styles.css +61 -6
- package/src/client/wiki-trial.ts +30 -1
- package/src/convert/host.ts +3 -0
- package/src/convert/ingest.ts +73 -9
- package/src/diagnosis/cards.ts +13 -8
- package/src/diagnosis/run.ts +27 -17
- package/src/diagnosis/schema.ts +2 -2
- package/src/diagnosis/seed-source.ts +5 -4
- package/src/diagnosis/seed.ts +6 -8
- package/src/diagnosis/store.ts +99 -69
- package/src/diagnosis/types.ts +10 -16
- package/src/environment/dsh-python.ts +156 -0
- package/src/environment/install.ts +134 -19
- package/src/environment/model-catalog.ts +45 -3
- package/src/environment/service.ts +111 -23
- package/src/environment/types.ts +6 -0
- package/src/events.ts +12 -0
- package/src/extract-contract.ts +6 -3
- package/src/extract-model.ts +2 -0
- package/src/extract-table-contract.ts +29 -0
- package/src/extract-table.ts +126 -0
- package/src/extraction-runs.ts +70 -11
- package/src/fs-atomic.ts +19 -2
- package/src/images.ts +2 -2
- package/src/index.ts +207 -72
- package/src/intent/scope.ts +1 -0
- package/src/intent/tools.ts +38 -1
- package/src/knowledge-bases.ts +4 -12
- package/src/knowledge.ts +24 -4
- package/src/materialize-snapshot.ts +46 -0
- package/src/model-text.ts +5 -3
- package/src/oplog.ts +7 -0
- package/src/pipeline/engine.ts +312 -0
- package/src/pipeline/store.ts +147 -0
- package/src/pipeline/types.ts +92 -0
- package/src/present.ts +10 -4
- package/src/retention.ts +76 -0
- package/src/runtime.ts +853 -131
- package/src/source-evidence.ts +12 -5
- package/src/store/axis-references.ts +3 -3
- package/src/store/derived.ts +40 -7
- package/src/store/knowledge-db.ts +111 -5
- package/src/store/migrate.ts +1 -1
- package/src/trajectory-types.ts +142 -0
- package/src/types.ts +3 -4
- package/src/wiki-finalize.ts +28 -0
- package/src/wiki-materialize.ts +3 -14
- package/src/wiki-trial-types.ts +3 -0
- package/src/wiki-trial.ts +26 -7
- package/src/client/convert/ConvertPanel.tsx +0 -583
- package/src/client/convert/styles.css +0 -138
- package/src/client/convert/styles.ts +0 -49
package/src/knowledge-bases.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './types.js'
|
|
31
31
|
import { defaultKnowledgeRoot, failure, KnowledgeRuntime, success, type KnowledgeRuntimeOptions } from './runtime.js'
|
|
32
32
|
import { defaultAgentRoot, knowledgeBaseRoot, memoryRoot, resourceRoot } from './layout.js'
|
|
33
|
+
import { writeTextAtomic } from './fs-atomic.js'
|
|
33
34
|
|
|
34
35
|
/** 持久化形态:配置 + 计算字段缓存分区。 */
|
|
35
36
|
interface KnowledgeBaseRecord {
|
|
@@ -118,12 +119,13 @@ function textOr(input: unknown, fallback: string): string {
|
|
|
118
119
|
|
|
119
120
|
function compileModelOf(input: unknown): CompileModelSelection {
|
|
120
121
|
const value = record(input)
|
|
121
|
-
|
|
122
|
+
const effort = typeof value.reasoningEffort === 'string' && value.reasoningEffort.trim() ? { reasoningEffort: value.reasoningEffort.trim() } : {}
|
|
123
|
+
if (value.mode !== 'fixed') return { ...DEFAULT_COMPILE_MODEL, ...effort }
|
|
122
124
|
const provider = typeof value.provider === 'string' ? value.provider.trim() : ''
|
|
123
125
|
const model = typeof value.model === 'string' ? value.model.trim() : ''
|
|
124
126
|
// 旧版本没有这个字段,或留下了半截配置时,安全地回到 DSH 默认模型。
|
|
125
127
|
if (!provider || !model) return { ...DEFAULT_COMPILE_MODEL }
|
|
126
|
-
return { mode: 'fixed', provider, model }
|
|
128
|
+
return { mode: 'fixed', provider, model, ...effort }
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
function normalizeExt(value: unknown, fallback: string): string {
|
|
@@ -142,16 +144,6 @@ function emptyBase(id: string): KnowledgeBase {
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
async function writeTextAtomic(path: string, content: string): Promise<void> {
|
|
146
|
-
const temporary = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`
|
|
147
|
-
try {
|
|
148
|
-
await writeFile(temporary, content, 'utf8')
|
|
149
|
-
await rename(temporary, path)
|
|
150
|
-
} finally {
|
|
151
|
-
try { await unlink(temporary) } catch { /* rename already consumed it */ }
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
147
|
export function defaultDataHome(): string {
|
|
156
148
|
return resourceRoot(defaultAgentRoot())
|
|
157
149
|
}
|
package/src/knowledge.ts
CHANGED
|
@@ -96,6 +96,8 @@ export interface KnowledgeSource {
|
|
|
96
96
|
currentVersion?: number
|
|
97
97
|
evidenceStatus?: 'current' | 'historical' | 'unversioned' | 'missing' | 'human'
|
|
98
98
|
quoteInCurrentVersion?: boolean
|
|
99
|
+
/** A completed newer extraction withdrew this document contribution. */
|
|
100
|
+
withdrawn?: boolean
|
|
99
101
|
createdAt: string
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -227,6 +229,7 @@ export interface KnowledgeVersion {
|
|
|
227
229
|
* 「`object_id` 这个参数不在」与「`object_id=` 空值」。
|
|
228
230
|
*/
|
|
229
231
|
export interface KnowledgeQuery {
|
|
232
|
+
excludeWithdrawn?: boolean
|
|
230
233
|
objectId?: string
|
|
231
234
|
actionId?: string
|
|
232
235
|
sceneId?: string
|
|
@@ -537,6 +540,7 @@ export class KnowledgeStore {
|
|
|
537
540
|
throw cause
|
|
538
541
|
}
|
|
539
542
|
}
|
|
543
|
+
if (!hasColumn(this.db, 'knowledge_sources', 'withdrawn')) this.db.exec('ALTER TABLE knowledge_sources ADD COLUMN withdrawn INTEGER NOT NULL DEFAULT 0')
|
|
540
544
|
if (!hasColumn(this.db, 'knowledge', 'head_version')) {
|
|
541
545
|
this.db.exec("ALTER TABLE knowledge ADD COLUMN head_version INTEGER NOT NULL DEFAULT 0")
|
|
542
546
|
}
|
|
@@ -637,7 +641,7 @@ export class KnowledgeStore {
|
|
|
637
641
|
if (existing !== null && normalizeKnowledge(existing.text) !== normalized) throw new Error('KNOWLEDGE_ID_COLLISION')
|
|
638
642
|
|
|
639
643
|
if (existing === null) {
|
|
640
|
-
this.db.exec('
|
|
644
|
+
this.db.exec('SAVEPOINT knowledge_ingest')
|
|
641
645
|
try {
|
|
642
646
|
this.db.prepare(
|
|
643
647
|
'INSERT INTO knowledge (id, text, normalized, object_id, action_id, scene_id, status, run_id, merged_into, head_version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
@@ -654,10 +658,10 @@ export class KnowledgeStore {
|
|
|
654
658
|
at: now,
|
|
655
659
|
})
|
|
656
660
|
this.db.prepare('UPDATE knowledge SET head_version = 1 WHERE id = ?').run(id)
|
|
657
|
-
this.db.exec('
|
|
661
|
+
this.db.exec('RELEASE knowledge_ingest')
|
|
658
662
|
return { knowledge: this.read(id)!, merged: false, addedSources: added }
|
|
659
663
|
} catch (cause) {
|
|
660
|
-
this.db.exec('ROLLBACK')
|
|
664
|
+
this.db.exec('ROLLBACK TO knowledge_ingest; RELEASE knowledge_ingest')
|
|
661
665
|
throw cause
|
|
662
666
|
}
|
|
663
667
|
}
|
|
@@ -744,6 +748,10 @@ export class KnowledgeStore {
|
|
|
744
748
|
const row = normalizeSource(source, now)
|
|
745
749
|
const key = sourceKey(row)
|
|
746
750
|
if (seen.has(key)) {
|
|
751
|
+
if (existingByKey.get(key)?.withdrawn) {
|
|
752
|
+
this.db!.prepare('UPDATE knowledge_sources SET withdrawn=0 WHERE knowledge_id=? AND document_id=? AND quote=? AND COALESCE(to_version,-1)=?')
|
|
753
|
+
.run(id, row.documentId, row.quote, row.toVersion ?? -1)
|
|
754
|
+
}
|
|
747
755
|
continue
|
|
748
756
|
}
|
|
749
757
|
seen.add(key)
|
|
@@ -963,6 +971,7 @@ export class KnowledgeStore {
|
|
|
963
971
|
} {
|
|
964
972
|
const where: string[] = [query.includeMerged === true ? '1 = 1' : "merged_into = ''"]
|
|
965
973
|
const params: Record<string, unknown> = {}
|
|
974
|
+
if (query.excludeWithdrawn) where.push('EXISTS (SELECT 1 FROM knowledge_sources active_source WHERE active_source.knowledge_id = knowledge.id AND active_source.withdrawn = 0)')
|
|
966
975
|
if (query.ids !== undefined) {
|
|
967
976
|
// 空数组是一个**取值**(筛出空集),不能退化成「不过滤」—— 那会让一次失败的落位
|
|
968
977
|
// 查询变成「把整库的知识塞进这次召回」。
|
|
@@ -976,7 +985,17 @@ export class KnowledgeStore {
|
|
|
976
985
|
if (!skip.has('status') && query.status !== undefined) { where.push('status = :status'); params.status = query.status }
|
|
977
986
|
if (query.runId !== undefined) { where.push('run_id = :runId'); params.runId = query.runId }
|
|
978
987
|
if (query.noCoordinate === true) where.push("object_id = '' AND action_id = '' AND scene_id = ''")
|
|
979
|
-
if (query.text) {
|
|
988
|
+
if (query.text) {
|
|
989
|
+
/**
|
|
990
|
+
* LIKE 的元字符必须转义(0070)。
|
|
991
|
+
*
|
|
992
|
+
* `normalizeKnowledge` 会保留 `%` 与 `_`,而 `?q=` 是用户能直接传的:查询 `%`
|
|
993
|
+
* 会拼成 `%%%`,命中整库并虚报 total;`a_b` 还会顺带匹配 `axb`。反斜杠自己也要
|
|
994
|
+
* 先转义 —— 否则用户输入的一个 `\` 会把后面的字符变成转义序列。
|
|
995
|
+
*/
|
|
996
|
+
where.push("normalized LIKE :text ESCAPE '\\'")
|
|
997
|
+
params.text = `%${normalizeKnowledge(query.text).replace(/[\\%_]/g, char => `\\${char}`)}%`
|
|
998
|
+
}
|
|
980
999
|
if (query.cardId !== undefined) {
|
|
981
1000
|
/*
|
|
982
1001
|
* `card_knowledge` 归**另一个模块**所有(`store/knowledge-db.ts`),而两个模块
|
|
@@ -1516,6 +1535,7 @@ function rowToKnowledge(row: Record<string, unknown>, sourceRows: Array<Record<s
|
|
|
1516
1535
|
author: String(source.author ?? ''),
|
|
1517
1536
|
statement: String(source.statement ?? ''),
|
|
1518
1537
|
...(source.to_version === null || source.to_version === undefined ? {} : { toVersion: Number(source.to_version) }),
|
|
1538
|
+
...(Number(source.withdrawn) === 1 ? { withdrawn: true } : {}),
|
|
1519
1539
|
createdAt: String(source.created_at),
|
|
1520
1540
|
})),
|
|
1521
1541
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 物化阶段的语料快照(0070)—— **只有服务端看得见**。
|
|
3
|
+
*
|
|
4
|
+
* 它刻意不放在 `pipeline/types.ts` 里:那个模块被客户端 import(`PIPELINE_ACTIVE_STATES`
|
|
5
|
+
* 这类值),而本文件的字段类型来自 `knowledge.ts` / `events.ts` / `source-images.ts` /
|
|
6
|
+
* `card-organize.ts`,它们全都 `import 'node:…'`。把服务端模块拉进客户端的类型程序,
|
|
7
|
+
* 浏览器与 Node 的类型边界就没了(这条规矩写在 `wiki-trial-types.ts` 里)。
|
|
8
|
+
* 引擎只把这个对象原样传回去,所以它在引擎那边是 `unknown`。
|
|
9
|
+
*
|
|
10
|
+
* ## 为什么是「一次阶段运行算一份」
|
|
11
|
+
*
|
|
12
|
+
* 物化是逐组的,而每一组都要用到全库的东西:别组的正文(图片分发)、全部卡片(相关推荐)、
|
|
13
|
+
* 全部事件、全部知识。每组现算一遍就是 O(组数 × 语料) —— 几百张卡时单线程跑不完。
|
|
14
|
+
*
|
|
15
|
+
* ## 快照的边界
|
|
16
|
+
*
|
|
17
|
+
* 语料在**同一条串行队列里**是不变的(归一排在物化后面)。但别的写入者(另一个进程、
|
|
18
|
+
* 同一个库的另一条路径)不受这条队列约束,所以写回之前仍然要**单独复查这一组**的
|
|
19
|
+
* 事实(`freshGroupFingerprint`);而**人工裁决**从来不被快照 —— 卡片状态在每次写之前
|
|
20
|
+
* 重读并与旧值比对(见 `materializePipelineGroup`)。
|
|
21
|
+
*/
|
|
22
|
+
import type { OrganizeRelatedCandidate } from './card-organize.js'
|
|
23
|
+
import type { EventEntity } from './events.js'
|
|
24
|
+
import type { Knowledge } from './knowledge.js'
|
|
25
|
+
import type { MaterializationGroup } from './pipeline/types.js'
|
|
26
|
+
import type { SourceImage } from './source-images.js'
|
|
27
|
+
|
|
28
|
+
export interface MaterializationSnapshot {
|
|
29
|
+
groups: MaterializationGroup[]
|
|
30
|
+
/** 坐标仍有歧义 + 等待归一,两部分的合计(分开的两个字段见下)。 */
|
|
31
|
+
blocked: number
|
|
32
|
+
/** 坐标仍有歧义的 Knowledge 条数。这些行不属于任何分组。 */
|
|
33
|
+
blockedAmbiguous: number
|
|
34
|
+
/** 来源已变化 / 新抽取尚未归一的 Knowledge 条数。 */
|
|
35
|
+
blockedAwaitingNormalize: number
|
|
36
|
+
/** key → 这一组参与成卡的知识。 */
|
|
37
|
+
knowledge: Map<string, Knowledge[]>
|
|
38
|
+
/** key → 这一组的 Event。 */
|
|
39
|
+
events: Map<string, EventEntity[]>
|
|
40
|
+
/** 图片分发:全部来源正文,阶段内只读一次。 */
|
|
41
|
+
sources: Array<{ sourceId: string; content: string }>
|
|
42
|
+
/** 图片分发计划:整库算一次,按组取。 */
|
|
43
|
+
images: Map<string, SourceImage[]>
|
|
44
|
+
/** 相关推荐计划:整库算一次,按组取(键是分组键,不是卡片身份)。 */
|
|
45
|
+
related: Map<string, OrganizeRelatedCandidate[]>
|
|
46
|
+
}
|
package/src/model-text.ts
CHANGED
|
@@ -101,6 +101,7 @@ export function buildModelRequest(input: {
|
|
|
101
101
|
export async function collectModelText(
|
|
102
102
|
stream: AsyncIterable<StreamChunkLike>,
|
|
103
103
|
onUsage?: (usage: TrialTokenUsage) => void,
|
|
104
|
+
onDelta?: (text: string) => void,
|
|
104
105
|
): Promise<string> {
|
|
105
106
|
let text = ''
|
|
106
107
|
/** 流怎么结束的。用来区分「正常但空」「失败」「中止」三种情况。 */
|
|
@@ -112,6 +113,7 @@ export async function collectModelText(
|
|
|
112
113
|
if (chunk.type === 'usage' && chunk.usage) onUsage?.(chunk.usage)
|
|
113
114
|
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
114
115
|
text += chunk.text
|
|
116
|
+
onDelta?.(chunk.text)
|
|
115
117
|
continue
|
|
116
118
|
}
|
|
117
119
|
if (chunk.type === 'finish') {
|
|
@@ -120,9 +122,9 @@ export async function collectModelText(
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
if (failure) {
|
|
124
|
-
const code = failure
|
|
125
|
-
const message = failure
|
|
125
|
+
if (failure || finish === 'error' || finish === 'aborted' || finish === 'max-tokens') {
|
|
126
|
+
const code = failure?.code?.trim() || (finish === 'max-tokens' ? 'OUTPUT_TRUNCATED' : '未知错误码')
|
|
127
|
+
const message = failure?.message?.trim() || (finish === 'max-tokens' ? '输出达到长度上限,部分返回已保留,请调整模型限制后重试' : '模型服务没有给出说明')
|
|
126
128
|
throw new Error(`模型调用失败(${code}):${message}`)
|
|
127
129
|
}
|
|
128
130
|
if (text.length === 0) {
|
package/src/oplog.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { mkdir } from 'node:fs/promises'
|
|
31
31
|
import { dirname } from 'node:path'
|
|
32
32
|
import { loadSqlite, type SqliteDatabase } from './fts.js'
|
|
33
|
+
import { applyRetention } from './retention.js'
|
|
33
34
|
import type { OperationDetail, OperationKind, OperationStatus } from './types.js'
|
|
34
35
|
|
|
35
36
|
/** 谁发起的:面板按钮,还是 Agent 的工具。 */
|
|
@@ -125,6 +126,12 @@ export class OperationLog {
|
|
|
125
126
|
this.db.prepare(
|
|
126
127
|
'INSERT INTO operations (id, kind, status, actor, kb, title, summary, started_at, finished_at, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
127
128
|
).run(entry.id, entry.kind, entry.status, entry.actor, entry.kb, entry.title, entry.summary, entry.startedAt, entry.finishedAt ?? null, JSON.stringify(entry.detail))
|
|
129
|
+
/*
|
|
130
|
+
* 处理日志的保留上限(0070)。这张表原来只增不减:每编译一次、每导入一批都加行,
|
|
131
|
+
* 而 `detail` 里还塞着整批文件清单。上限同样只作用在**水位以上**的新行 ——
|
|
132
|
+
* 升级那一刻已有的日志一条不删(见 `retention.ts`)。
|
|
133
|
+
*/
|
|
134
|
+
applyRetention(this.db, 'operations', { table: 'operations', key: 'rowid', keep: 2000 })
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
async patch(
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import type { ModelCall } from '../extract-model.js'
|
|
3
|
+
import type { SourceRaw, WikiTypeDef } from '../types.js'
|
|
4
|
+
import type { TrialReport } from '../wiki-trial-types.js'
|
|
5
|
+
import { PipelineStore, type ExtractionCheckpoint } from './store.js'
|
|
6
|
+
import { PIPELINE_ACTIVE_STATES, PIPELINE_STAGE_LABEL, type MaterializationBatch, type MaterializationGroup, type PipelineAction, type PipelineModel, type PipelineSelection, type PipelineStage, type PipelineTask } from './types.js'
|
|
7
|
+
|
|
8
|
+
export interface PipelineSource extends SourceRaw { version: number; sourceHash: string }
|
|
9
|
+
export interface PipelineContext {
|
|
10
|
+
model: PipelineModel & { call?: ModelCall; why?: string }
|
|
11
|
+
note: string
|
|
12
|
+
profile?: WikiTypeDef
|
|
13
|
+
organize: boolean
|
|
14
|
+
format: 'json' | 'table'
|
|
15
|
+
}
|
|
16
|
+
export interface PipelineBuildResult { summary: string; skipped?: boolean }
|
|
17
|
+
export interface PipelineHost {
|
|
18
|
+
capture(): PipelineContext
|
|
19
|
+
serialize<T>(work: () => Promise<T>): Promise<T>
|
|
20
|
+
sources(): Promise<PipelineSource[]>
|
|
21
|
+
/** One source by id. Same shape as an element of `sources()`. */
|
|
22
|
+
source(sourceId: string): Promise<PipelineSource | undefined>
|
|
23
|
+
fingerprint(source: PipelineSource, context: PipelineContext): Promise<string>
|
|
24
|
+
extract(source: PipelineSource, context: PipelineContext, output: (text: string) => void): Promise<TrialReport>
|
|
25
|
+
normalize(source: PipelineSource, checkpoint: ExtractionCheckpoint, context: PipelineContext, force: boolean, output: (text: string) => void): Promise<{ reused: boolean; warnings: string[] }>
|
|
26
|
+
groups(context: PipelineContext): Promise<MaterializationBatch>
|
|
27
|
+
materialize(group: MaterializationGroup, context: PipelineContext, output: (text: string) => void, snapshot: unknown): Promise<void>
|
|
28
|
+
build(taskId: string, checkpoint: () => Promise<void>): Promise<PipelineBuildResult>
|
|
29
|
+
record(checkpoint: ExtractionCheckpoint): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class TaskCancelled extends Error {}
|
|
33
|
+
interface Control { pause: boolean; cancel: boolean; resume?: () => void }
|
|
34
|
+
|
|
35
|
+
/** One writer queue per base, shared with the legacy index builder. */
|
|
36
|
+
export class KnowledgePipeline {
|
|
37
|
+
private closed = false
|
|
38
|
+
private controls = new Map<string, Control>()
|
|
39
|
+
constructor(readonly store: PipelineStore, private readonly host: PipelineHost) {}
|
|
40
|
+
|
|
41
|
+
start(stage: PipelineStage, selection: PipelineSelection = {}): { task: PipelineTask; completion: Promise<PipelineTask> } {
|
|
42
|
+
if (this.closed) throw new Error('知识处理已关闭')
|
|
43
|
+
if (!['extract', 'normalize', 'materialize', 'build', 'all'].includes(stage)) throw new Error('不支持的知识处理阶段')
|
|
44
|
+
for (const value of [selection.sourceIds, selection.groupKeys]) {
|
|
45
|
+
if (value !== undefined && (!Array.isArray(value) || value.length === 0 || value.some(id => typeof id !== 'string' || !id.trim()))) throw new Error('选择范围不能为空;不传范围表示处理当前阶段的全部待办')
|
|
46
|
+
}
|
|
47
|
+
const context = this.host.capture()
|
|
48
|
+
const { call: _call, why: _why, ...model } = context.model
|
|
49
|
+
const task: PipelineTask = {
|
|
50
|
+
id: `pipeline_${randomUUID()}`, stage, state: 'queued', selection: structuredClone(selection), model,
|
|
51
|
+
queuedAt: new Date().toISOString(), done: 0, total: 0, reused: 0, failed: 0,
|
|
52
|
+
summary: '等待运行', warnings: [],
|
|
53
|
+
}
|
|
54
|
+
this.store.putTask(task)
|
|
55
|
+
this.controls.set(task.id, { pause: false, cancel: false })
|
|
56
|
+
const completion = this.host.serialize(() => this.execute(task.id, context))
|
|
57
|
+
// Starting a background task must not create an unhandled rejection.
|
|
58
|
+
void completion.catch(() => undefined)
|
|
59
|
+
return { task, completion }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close(): void {
|
|
63
|
+
this.closed = true
|
|
64
|
+
for (const control of this.controls.values()) { control.cancel = true; control.pause = false; control.resume?.() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
control(id: string, action: PipelineAction): PipelineTask {
|
|
68
|
+
const task = this.store.task(id)
|
|
69
|
+
const control = this.controls.get(id)
|
|
70
|
+
if (!task || !control || !PIPELINE_ACTIVE_STATES.includes(task.state)) throw new Error('任务已结束;请重新运行所需阶段')
|
|
71
|
+
if (action === 'pause') {
|
|
72
|
+
control.pause = true
|
|
73
|
+
this.patch(id, { state: task.state === 'queued' ? 'paused' : 'pausing', summary: '当前文档或卡片处理完成后暂停' })
|
|
74
|
+
} else if (action === 'resume') {
|
|
75
|
+
if (!control.pause) throw new Error('任务没有暂停')
|
|
76
|
+
control.pause = false
|
|
77
|
+
this.patch(id, { state: task.startedAt ? 'running' : 'queued' })
|
|
78
|
+
control.resume?.()
|
|
79
|
+
control.resume = undefined
|
|
80
|
+
} else {
|
|
81
|
+
control.cancel = true
|
|
82
|
+
control.pause = false
|
|
83
|
+
this.patch(id, { state: 'cancelling', summary: '当前文档或卡片处理完成后停止' })
|
|
84
|
+
control.resume?.()
|
|
85
|
+
}
|
|
86
|
+
return this.store.task(id)!
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private patch(id: string, patch: Partial<PipelineTask>): PipelineTask {
|
|
90
|
+
const task = { ...this.store.task(id)!, ...patch }
|
|
91
|
+
this.store.putTask(task)
|
|
92
|
+
return task
|
|
93
|
+
}
|
|
94
|
+
private finishStage(id: string, outcome?: Pick<PipelineBuildResult, 'summary' | 'skipped'>): void {
|
|
95
|
+
const task = this.store.task(id)!
|
|
96
|
+
if (task.currentStage) this.patch(id, { results: { ...task.results, [task.currentStage]: { done: task.done, total: task.total, reused: task.reused, failed: task.failed, ...outcome } } })
|
|
97
|
+
}
|
|
98
|
+
private async checkpoint(id: string): Promise<void> {
|
|
99
|
+
const control = this.controls.get(id)
|
|
100
|
+
while (control) {
|
|
101
|
+
if (control.cancel) throw new TaskCancelled('任务已停止,已完成的文档与阶段会保留')
|
|
102
|
+
if (!control.pause) return
|
|
103
|
+
this.patch(id, { state: 'paused', summary: '已暂停,可继续运行' })
|
|
104
|
+
await new Promise<void>(resolve => { control.resume = resolve })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
private output(id: string, stage: Exclude<PipelineStage, 'all'>, sourceId?: string): { append: (text: string) => void; flush: () => void } {
|
|
108
|
+
let buffer = ''
|
|
109
|
+
let last = Date.now()
|
|
110
|
+
let bytes = 0
|
|
111
|
+
let capped = false
|
|
112
|
+
const flush = (): void => {
|
|
113
|
+
if (!buffer) return
|
|
114
|
+
this.store.event(id, 'output', buffer, sourceId, stage)
|
|
115
|
+
buffer = ''
|
|
116
|
+
last = Date.now()
|
|
117
|
+
}
|
|
118
|
+
let failure: unknown
|
|
119
|
+
const timer = setInterval(() => { try { flush() } catch (cause) { failure = cause; clearInterval(timer) } }, 250)
|
|
120
|
+
timer.unref()
|
|
121
|
+
return {
|
|
122
|
+
append: text => {
|
|
123
|
+
if (failure) throw failure
|
|
124
|
+
bytes += Buffer.byteLength(text)
|
|
125
|
+
if (bytes > 4 * 1024 * 1024) {
|
|
126
|
+
if (!capped) { flush(); this.store.event(id, 'warning', '实时预览超过 4 MB;完整返回保存在本次抽取报告中。', sourceId, stage); capped = true }
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
buffer += text
|
|
130
|
+
if (buffer.length >= 1024 || Date.now() - last >= 250) flush()
|
|
131
|
+
}, flush: () => { clearInterval(timer); if (failure) throw failure; flush() },
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
private async execute(id: string, context: PipelineContext): Promise<PipelineTask> {
|
|
135
|
+
const task = this.store.task(id)!
|
|
136
|
+
const stages = task.stage === 'all' ? ['extract', 'normalize', 'materialize', 'build'] as const : [task.stage]
|
|
137
|
+
try {
|
|
138
|
+
await this.checkpoint(id)
|
|
139
|
+
this.patch(id, { state: 'running', startedAt: new Date().toISOString() })
|
|
140
|
+
const allSources = await this.host.sources()
|
|
141
|
+
const sourceIds = task.selection.sourceIds === undefined ? undefined : new Set(task.selection.sourceIds)
|
|
142
|
+
if (sourceIds && [...sourceIds].some(sourceId => !allSources.some(source => source.sourceId === sourceId))) throw new Error('选择范围中有已删除或不存在的来源')
|
|
143
|
+
const sources = allSources.filter(source => !sourceIds || sourceIds.has(source.sourceId))
|
|
144
|
+
/**
|
|
145
|
+
* 坐标仍有歧义、等归一的 Knowledge 条数。它不是失败,所以既不进 `failed`、
|
|
146
|
+
* 也不提前结束链路(否则一个歧义 mention 就能让整库索引永不发布);
|
|
147
|
+
* 但它也不是「全部完成」,所以最终状态报 `partial`。
|
|
148
|
+
*/
|
|
149
|
+
let deferred = 0
|
|
150
|
+
for (const stage of stages) {
|
|
151
|
+
await this.checkpoint(id)
|
|
152
|
+
this.patch(id, { currentStage: stage, done: 0, total: 0, reused: 0, failed: 0, summary: PIPELINE_STAGE_LABEL[stage] })
|
|
153
|
+
this.store.event(id, 'progress', PIPELINE_STAGE_LABEL[stage], undefined, stage)
|
|
154
|
+
if (stage === 'build') {
|
|
155
|
+
const result = await this.host.build(id, () => this.checkpoint(id))
|
|
156
|
+
this.patch(id, { done: 1, total: 1, summary: result.summary })
|
|
157
|
+
this.finishStage(id, result)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (stage === 'materialize') {
|
|
161
|
+
const batch = await this.host.groups(context)
|
|
162
|
+
const { groups } = batch
|
|
163
|
+
const selected = task.selection.groupKeys ? new Set(task.selection.groupKeys) : undefined
|
|
164
|
+
if (selected && [...selected].some(key => !groups.some(group => group.key === key))) throw new Error('选择的卡片分组已变化,请刷新待物化列表')
|
|
165
|
+
/**
|
|
166
|
+
* 来源范围对**任何**阶段都生效,不只对 `all`:`start()` 会校验 `sourceIds`
|
|
167
|
+
* 非空,调用方因此有理由认为它生效了。只对 `all` 生效等于静默忽略选择。
|
|
168
|
+
*/
|
|
169
|
+
const affectedSources = sourceIds ? new Set(sources.map(source => source.sourceId)) : undefined
|
|
170
|
+
const scoped = selected !== undefined || affectedSources !== undefined
|
|
171
|
+
const inScope = (group: MaterializationGroup): boolean => (!selected || selected.has(group.key)) && (!affectedSources || group.sourceIds.some(sourceId => affectedSources.has(sourceId)))
|
|
172
|
+
/**
|
|
173
|
+
* 待归一的量要**按选择范围**数,且两个原因分开说(0070)。
|
|
174
|
+
*
|
|
175
|
+
* 「坐标有歧义」与「来源变了、抽取结果还没归一」是两件事,报同一句话会让读的人
|
|
176
|
+
* 去查错的地方。更要紧的是范围:按 `sourceIds` / `groupKeys` 缩小的那一趟,
|
|
177
|
+
* 如果拿全库的数字报 `partial`,调用方会去重试一件它明确排除的事 —— 而它其实
|
|
178
|
+
* 全都做完了。坐标有歧义的那些行不属于任何分组,无法按来源归属,所以只在
|
|
179
|
+
* 没有选择范围时才计入。
|
|
180
|
+
*/
|
|
181
|
+
const ambiguous = scoped ? 0 : batch.blockedAmbiguous
|
|
182
|
+
const awaiting = scoped
|
|
183
|
+
? groups.filter(group => group.state === 'blocked' && inScope(group)).reduce((sum, group) => sum + group.knowledgeCount, 0)
|
|
184
|
+
: batch.blockedAwaitingNormalize
|
|
185
|
+
if (ambiguous > 0 || awaiting > 0) {
|
|
186
|
+
deferred = ambiguous + awaiting
|
|
187
|
+
const parts = [
|
|
188
|
+
ambiguous > 0 ? `${ambiguous} 条 Knowledge 的坐标仍有歧义` : '',
|
|
189
|
+
awaiting > 0 ? `${awaiting} 条 Knowledge 的来源已变化或新抽取待归一` : '',
|
|
190
|
+
].filter(Boolean)
|
|
191
|
+
const message = `${parts.join(';')},暂不物化;本次不影响索引构建。`
|
|
192
|
+
this.patch(id, { warnings: [...this.store.task(id)!.warnings, message] })
|
|
193
|
+
this.store.event(id, 'warning', message, undefined, stage)
|
|
194
|
+
}
|
|
195
|
+
const pending = groups.filter(group => group.state !== 'blocked' && inScope(group) && (task.selection.force || group.state !== 'current'))
|
|
196
|
+
this.patch(id, { total: pending.length })
|
|
197
|
+
for (const group of pending) {
|
|
198
|
+
await this.checkpoint(id)
|
|
199
|
+
const output = this.output(id, stage)
|
|
200
|
+
try { await this.host.materialize(group, context, output.append, batch.snapshot) }
|
|
201
|
+
catch (cause) {
|
|
202
|
+
if (cause instanceof TaskCancelled) throw cause
|
|
203
|
+
const message = `${group.title}:${cause instanceof Error ? cause.message : String(cause)}`
|
|
204
|
+
const current = this.store.task(id)!
|
|
205
|
+
this.patch(id, { failed: current.failed + 1, warnings: [...current.warnings, message] })
|
|
206
|
+
this.store.event(id, 'warning', message, undefined, stage)
|
|
207
|
+
}
|
|
208
|
+
finally { output.flush() }
|
|
209
|
+
const current = this.store.task(id)!
|
|
210
|
+
this.patch(id, { done: current.done + 1, summary: `已物化 ${current.done + 1}/${pending.length} 张:${group.title}` })
|
|
211
|
+
}
|
|
212
|
+
const current = this.store.task(id)!
|
|
213
|
+
/**
|
|
214
|
+
* 阶段结论要给 `summary`:任务列表的四段步轨读的是 `results[stage]`,
|
|
215
|
+
* 缺了它那一段会显示成「已完成」(成功色),而任务徽标写着「部分完成」——
|
|
216
|
+
* 同一件事在半个屏幕上两种说法。
|
|
217
|
+
*/
|
|
218
|
+
this.finishStage(id, {
|
|
219
|
+
summary: `已物化 ${current.done - current.failed}/${current.total} 张${deferred > 0 ? `;${deferred} 条待归一` : ''}${affectedSources !== undefined ? `(按来源范围缩小:${affectedSources.size} 份来源)` : ''}`,
|
|
220
|
+
})
|
|
221
|
+
/**
|
|
222
|
+
* **真的**有分组失败时才提前结束(含 `build`):这里与 `blocked` 的处理刻意不同。
|
|
223
|
+
*
|
|
224
|
+
* `blocked` 是「等归一」的待办,不是失败,所以它不阻断;而这里是某一张卡没写成功
|
|
225
|
+
* (模型报错、写库失败),此时发布索引等于把一个半成品当成品推给召回 ——
|
|
226
|
+
* 而重试的代价很小:已经物化好的那些分组指纹没变,下一次会被判成 `current` 直接跳过。
|
|
227
|
+
*/
|
|
228
|
+
if (current.failed) return this.patch(id, { state: current.failed === current.total ? 'failed' : 'partial', finishedAt: new Date().toISOString(), summary: `物化结束:成功 ${current.total - current.failed}/${current.total};已完成卡片保留,可重试。` })
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
this.patch(id, { total: sources.length })
|
|
232
|
+
for (const source of sources) {
|
|
233
|
+
await this.checkpoint(id)
|
|
234
|
+
this.store.event(id, 'progress', `${PIPELINE_STAGE_LABEL[stage]}:${source.title}`, source.sourceId, stage)
|
|
235
|
+
const output = this.output(id, stage, source.sourceId)
|
|
236
|
+
let reused = false
|
|
237
|
+
try {
|
|
238
|
+
if (stage === 'extract') {
|
|
239
|
+
const fingerprint = await this.host.fingerprint(source, context)
|
|
240
|
+
const cached = task.selection.force ? undefined : this.store.cached(source.sourceId, source.version, fingerprint)
|
|
241
|
+
if (cached) { this.store.selectExtraction(source.sourceId, cached.id); reused = true }
|
|
242
|
+
else {
|
|
243
|
+
const report = await this.host.extract(source, context, output.append)
|
|
244
|
+
if (this.closed) throw new TaskCancelled('知识库已关闭,已完成结果保留')
|
|
245
|
+
const checkpoint: ExtractionCheckpoint = { id: `extract_${randomUUID()}`, sourceId: source.sourceId, sourceVersion: source.version, sourceHash: source.sourceHash, fingerprint, report, createdAt: new Date().toISOString() }
|
|
246
|
+
// The host checks the captured source version again before activation.
|
|
247
|
+
// Read that one source back, not the whole library: the old form re-hashed
|
|
248
|
+
// every document body once per document, which is O(N²) over a big base.
|
|
249
|
+
const current = await this.host.source(source.sourceId)
|
|
250
|
+
if (!current || current.version !== source.version || current.sourceHash !== source.sourceHash) {
|
|
251
|
+
await this.host.record(checkpoint)
|
|
252
|
+
this.store.event(id, 'report', checkpoint.id, source.sourceId, stage)
|
|
253
|
+
throw new Error('来源在抽取期间已变化;返回已保留在历史中,请抽取最新版本')
|
|
254
|
+
}
|
|
255
|
+
await this.host.record(checkpoint)
|
|
256
|
+
this.store.event(id, 'report', checkpoint.id, source.sourceId, stage)
|
|
257
|
+
/**
|
|
258
|
+
* 没通过校验时**要说清是哪一条不合格**(0070)。
|
|
259
|
+
*
|
|
260
|
+
* 原来只报一句 `model.why`(「抽取返回未通过校验」),"哪些引文对不上" 只留在
|
|
261
|
+
* 审计报告里,而人看不到那一层。现在把 `exceptions` 的前三条带进错误里 ——
|
|
262
|
+
* 面板上的失败行直接能读出原因。
|
|
263
|
+
*
|
|
264
|
+
* 这里**不缓存**这一份失败返回(试过,退回去了):缓存会让下一次重跑不再调模型,
|
|
265
|
+
* 而同一个阶段结束时的承诺是「可重试此阶段」——「重试」按下去什么都不发生,
|
|
266
|
+
* 比多花一次模型调用糟得多。完整返回已经由 `host.record` 收进审计库。
|
|
267
|
+
*/
|
|
268
|
+
if (report.model.parsed !== true) {
|
|
269
|
+
const detail = report.exceptions.slice(0, 3).map(item => `${item.step}:${item.message}`).join(';')
|
|
270
|
+
throw new Error(`${report.model.why ?? '抽取返回未通过校验'}${detail === '' ? '' : `(${detail})`}`)
|
|
271
|
+
}
|
|
272
|
+
this.store.saveExtraction(checkpoint)
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
const checkpoint = this.store.head(source.sourceId).extracted
|
|
276
|
+
if (!checkpoint) throw new Error('尚无抽取结果,请先运行抽取')
|
|
277
|
+
if (checkpoint.sourceVersion !== source.version || checkpoint.sourceHash !== source.sourceHash) throw new Error('抽取结果依据旧版来源,请先抽取最新版本')
|
|
278
|
+
const result = await this.host.normalize(source, checkpoint, context, task.selection.force === true, output.append)
|
|
279
|
+
reused = result.reused
|
|
280
|
+
if (result.warnings.length) this.patch(id, { warnings: [...this.store.task(id)!.warnings, ...result.warnings.map(warning => `${source.title}:${warning}`)] })
|
|
281
|
+
for (const warning of result.warnings) this.store.event(id, 'warning', warning, source.sourceId, stage)
|
|
282
|
+
}
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
if (cause instanceof TaskCancelled) throw cause
|
|
285
|
+
const message = `${source.title}:${cause instanceof Error ? cause.message : String(cause)}`
|
|
286
|
+
const current = this.store.task(id)!
|
|
287
|
+
this.patch(id, { failed: current.failed + 1, warnings: [...current.warnings, message] })
|
|
288
|
+
this.store.event(id, 'warning', message, source.sourceId, stage)
|
|
289
|
+
} finally { output.flush() }
|
|
290
|
+
const current = this.store.task(id)!
|
|
291
|
+
this.patch(id, { done: current.done + 1, reused: current.reused + Number(reused), summary: `${PIPELINE_STAGE_LABEL[stage]} ${current.done + 1}/${sources.length}${reused ? ' · 复用已有结果' : ''}` })
|
|
292
|
+
}
|
|
293
|
+
this.finishStage(id)
|
|
294
|
+
if (this.store.task(id)!.failed > 0) {
|
|
295
|
+
const current = this.store.task(id)!
|
|
296
|
+
this.patch(id, { state: current.failed === current.total ? 'failed' : 'partial', finishedAt: new Date().toISOString(), summary: `${PIPELINE_STAGE_LABEL[stage]}结束:成功 ${current.total - current.failed}/${current.total},失败 ${current.failed};已完成结果保留,可重试此阶段。` })
|
|
297
|
+
return this.store.task(id)!
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await this.checkpoint(id)
|
|
301
|
+
const current = this.store.task(id)!
|
|
302
|
+
/**
|
|
303
|
+
* `deferred > 0` 时状态是 `partial`:这一趟把能做的都做了(包括发布索引),
|
|
304
|
+
* 但还有一批分组在等归一。报 `succeeded` 会让人以为「物化完了」。
|
|
305
|
+
*/
|
|
306
|
+
const suffix = deferred > 0 ? ` · 另有 ${deferred} 条 Knowledge 待归一(坐标歧义或来源已变),暂未物化` : ''
|
|
307
|
+
return this.patch(id, { state: deferred > 0 ? 'partial' : 'succeeded', finishedAt: new Date().toISOString(), summary: (task.stage === 'all' ? `全链路完成 · 抽取 ${current.results?.extract?.done ?? 0} 份(复用 ${current.results?.extract?.reused ?? 0} 份) · 物化 ${current.results?.materialize?.done ?? 0} 张 · 索引已发布` : `${PIPELINE_STAGE_LABEL[task.stage]}完成 · 处理 ${current.done} 项 · 复用 ${current.reused} 项`) + suffix })
|
|
308
|
+
} catch (cause) {
|
|
309
|
+
return this.patch(id, { state: cause instanceof TaskCancelled ? 'cancelled' : 'failed', finishedAt: new Date().toISOString(), summary: cause instanceof Error ? cause.message : String(cause) })
|
|
310
|
+
} finally { this.controls.delete(id) }
|
|
311
|
+
}
|
|
312
|
+
}
|