dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.14
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 +84 -52
- package/cordis.patch.yml +21 -17
- package/eval/benchmark-extraction-format.mjs +20 -0
- package/lib/client.js +2846 -3872
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +1299 -1660
- package/lib/types/axis-registry.d.ts +6 -0
- package/lib/types/axis-registry.d.ts.map +1 -1
- package/lib/types/axis-registry.js +45 -0
- package/lib/types/axis-registry.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 +8 -16
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +41 -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/service.d.ts +4 -3
- package/lib/types/environment/service.d.ts.map +1 -1
- package/lib/types/environment/service.js +91 -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 +3 -0
- package/lib/types/extraction-runs.d.ts.map +1 -1
- package/lib/types/extraction-runs.js +6 -0
- package/lib/types/extraction-runs.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +196 -44
- 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/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +3 -2
- 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 +12 -3
- package/lib/types/knowledge.js.map +1 -1
- 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/pipeline/engine.d.ts +57 -0
- package/lib/types/pipeline/engine.d.ts.map +1 -0
- package/lib/types/pipeline/engine.js +268 -0
- package/lib/types/pipeline/engine.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +56 -0
- package/lib/types/pipeline/store.d.ts.map +1 -0
- package/lib/types/pipeline/store.js +115 -0
- package/lib/types/pipeline/store.js.map +1 -0
- package/lib/types/pipeline/types.d.ts +82 -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/runtime.d.ts +20 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +389 -112
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/source-evidence.d.ts.map +1 -1
- package/lib/types/source-evidence.js +2 -0
- 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/migrate.js +1 -1
- package/lib/types/store/migrate.js.map +1 -1
- 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-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 +21 -17
- package/src/axis-registry.ts +38 -0
- package/src/client/AttributionEventGuide.tsx +10 -9
- package/src/client/EnvironmentSetup.tsx +35 -15
- package/src/client/ExtractionTasksPage.tsx +129 -176
- package/src/client/KnowledgeBaseTab.tsx +31 -76
- package/src/client/NodeSettings.tsx +2 -3
- package/src/client/PipelineNotice.tsx +26 -0
- package/src/client/TabGuide.tsx +4 -4
- package/src/client/TypeDefinitionDrawer.tsx +23 -8
- 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 +6 -6
- 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/graph-options.ts +2 -2
- package/src/client/pages.tsx +2 -2
- 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 +47 -68
- package/src/diagnosis/types.ts +10 -16
- package/src/environment/service.ts +79 -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 +6 -0
- package/src/index.ts +156 -45
- package/src/intent/scope.ts +1 -0
- package/src/knowledge-bases.ts +3 -2
- package/src/knowledge.ts +13 -3
- package/src/model-text.ts +5 -3
- package/src/pipeline/engine.ts +238 -0
- package/src/pipeline/store.ts +119 -0
- package/src/pipeline/types.ts +74 -0
- package/src/present.ts +10 -4
- package/src/runtime.ts +350 -110
- package/src/source-evidence.ts +1 -0
- package/src/store/axis-references.ts +3 -3
- package/src/store/migrate.ts +1 -1
- package/src/types.ts +3 -4
- package/src/wiki-materialize.ts +3 -14
- package/src/wiki-trial-types.ts +3 -0
- package/src/wiki-trial.ts +26 -7
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
|
// 查询变成「把整库的知识塞进这次召回」。
|
|
@@ -1516,6 +1525,7 @@ function rowToKnowledge(row: Record<string, unknown>, sourceRows: Array<Record<s
|
|
|
1516
1525
|
author: String(source.author ?? ''),
|
|
1517
1526
|
statement: String(source.statement ?? ''),
|
|
1518
1527
|
...(source.to_version === null || source.to_version === undefined ? {} : { toVersion: Number(source.to_version) }),
|
|
1528
|
+
...(Number(source.withdrawn) === 1 ? { withdrawn: true } : {}),
|
|
1519
1529
|
createdAt: String(source.created_at),
|
|
1520
1530
|
})),
|
|
1521
1531
|
}
|
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) {
|
|
@@ -0,0 +1,238 @@
|
|
|
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 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 PipelineHost {
|
|
17
|
+
capture(): PipelineContext
|
|
18
|
+
serialize<T>(work: () => Promise<T>): Promise<T>
|
|
19
|
+
sources(): Promise<PipelineSource[]>
|
|
20
|
+
fingerprint(source: PipelineSource, context: PipelineContext): string
|
|
21
|
+
extract(source: PipelineSource, context: PipelineContext, output: (text: string) => void): Promise<TrialReport>
|
|
22
|
+
normalize(source: PipelineSource, checkpoint: ExtractionCheckpoint, context: PipelineContext, force: boolean, output: (text: string) => void): Promise<{ reused: boolean; warnings: string[] }>
|
|
23
|
+
groups(context: PipelineContext): Promise<{ groups: MaterializationGroup[]; blocked: number }>
|
|
24
|
+
materialize(group: MaterializationGroup, context: PipelineContext, output: (text: string) => void): Promise<void>
|
|
25
|
+
build(taskId: string, checkpoint: () => Promise<void>): Promise<string>
|
|
26
|
+
record(checkpoint: ExtractionCheckpoint): Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class TaskCancelled extends Error {}
|
|
30
|
+
interface Control { pause: boolean; cancel: boolean; resume?: () => void }
|
|
31
|
+
|
|
32
|
+
/** One writer queue per base, shared with the legacy index builder. */
|
|
33
|
+
export class KnowledgePipeline {
|
|
34
|
+
private closed = false
|
|
35
|
+
private controls = new Map<string, Control>()
|
|
36
|
+
constructor(readonly store: PipelineStore, private readonly host: PipelineHost) {}
|
|
37
|
+
|
|
38
|
+
start(stage: PipelineStage, selection: PipelineSelection = {}): { task: PipelineTask; completion: Promise<PipelineTask> } {
|
|
39
|
+
if (this.closed) throw new Error('知识处理已关闭')
|
|
40
|
+
if (!['extract', 'normalize', 'materialize', 'build', 'all'].includes(stage)) throw new Error('不支持的知识处理阶段')
|
|
41
|
+
for (const value of [selection.sourceIds, selection.groupKeys]) {
|
|
42
|
+
if (value !== undefined && (!Array.isArray(value) || value.length === 0 || value.some(id => typeof id !== 'string' || !id.trim()))) throw new Error('选择范围不能为空;不传范围表示处理当前阶段的全部待办')
|
|
43
|
+
}
|
|
44
|
+
const context = this.host.capture()
|
|
45
|
+
const { call: _call, why: _why, ...model } = context.model
|
|
46
|
+
const task: PipelineTask = {
|
|
47
|
+
id: `pipeline_${randomUUID()}`, stage, state: 'queued', selection: structuredClone(selection), model,
|
|
48
|
+
queuedAt: new Date().toISOString(), done: 0, total: 0, reused: 0, failed: 0,
|
|
49
|
+
summary: '等待运行', warnings: [],
|
|
50
|
+
}
|
|
51
|
+
this.store.putTask(task)
|
|
52
|
+
this.controls.set(task.id, { pause: false, cancel: false })
|
|
53
|
+
const completion = this.host.serialize(() => this.execute(task.id, context))
|
|
54
|
+
// Starting a background task must not create an unhandled rejection.
|
|
55
|
+
void completion.catch(() => undefined)
|
|
56
|
+
return { task, completion }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close(): void {
|
|
60
|
+
this.closed = true
|
|
61
|
+
for (const control of this.controls.values()) { control.cancel = true; control.pause = false; control.resume?.() }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
control(id: string, action: PipelineAction): PipelineTask {
|
|
65
|
+
const task = this.store.task(id)
|
|
66
|
+
const control = this.controls.get(id)
|
|
67
|
+
if (!task || !control || !PIPELINE_ACTIVE_STATES.includes(task.state)) throw new Error('任务已结束;请重新运行所需阶段')
|
|
68
|
+
if (action === 'pause') {
|
|
69
|
+
control.pause = true
|
|
70
|
+
this.patch(id, { state: task.state === 'queued' ? 'paused' : 'pausing', summary: '当前文档或卡片处理完成后暂停' })
|
|
71
|
+
} else if (action === 'resume') {
|
|
72
|
+
if (!control.pause) throw new Error('任务没有暂停')
|
|
73
|
+
control.pause = false
|
|
74
|
+
this.patch(id, { state: task.startedAt ? 'running' : 'queued' })
|
|
75
|
+
control.resume?.()
|
|
76
|
+
control.resume = undefined
|
|
77
|
+
} else {
|
|
78
|
+
control.cancel = true
|
|
79
|
+
control.pause = false
|
|
80
|
+
this.patch(id, { state: 'cancelling', summary: '当前文档或卡片处理完成后停止' })
|
|
81
|
+
control.resume?.()
|
|
82
|
+
}
|
|
83
|
+
return this.store.task(id)!
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private patch(id: string, patch: Partial<PipelineTask>): PipelineTask {
|
|
87
|
+
const task = { ...this.store.task(id)!, ...patch }
|
|
88
|
+
this.store.putTask(task)
|
|
89
|
+
return task
|
|
90
|
+
}
|
|
91
|
+
private finishStage(id: string): void {
|
|
92
|
+
const task = this.store.task(id)!
|
|
93
|
+
if (task.currentStage) this.patch(id, { results: { ...task.results, [task.currentStage]: { done: task.done, total: task.total, reused: task.reused, failed: task.failed } } })
|
|
94
|
+
}
|
|
95
|
+
private async checkpoint(id: string): Promise<void> {
|
|
96
|
+
const control = this.controls.get(id)
|
|
97
|
+
while (control) {
|
|
98
|
+
if (control.cancel) throw new TaskCancelled('任务已停止,已完成的文档与阶段会保留')
|
|
99
|
+
if (!control.pause) return
|
|
100
|
+
this.patch(id, { state: 'paused', summary: '已暂停,可继续运行' })
|
|
101
|
+
await new Promise<void>(resolve => { control.resume = resolve })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
private output(id: string, sourceId?: string): { append: (text: string) => void; flush: () => void } {
|
|
105
|
+
let buffer = ''
|
|
106
|
+
let last = Date.now()
|
|
107
|
+
let bytes = 0
|
|
108
|
+
let capped = false
|
|
109
|
+
const flush = (): void => {
|
|
110
|
+
if (!buffer) return
|
|
111
|
+
this.store.event(id, 'output', buffer, sourceId)
|
|
112
|
+
buffer = ''
|
|
113
|
+
last = Date.now()
|
|
114
|
+
}
|
|
115
|
+
let failure: unknown
|
|
116
|
+
const timer = setInterval(() => { try { flush() } catch (cause) { failure = cause; clearInterval(timer) } }, 250)
|
|
117
|
+
timer.unref()
|
|
118
|
+
return {
|
|
119
|
+
append: text => {
|
|
120
|
+
if (failure) throw failure
|
|
121
|
+
bytes += Buffer.byteLength(text)
|
|
122
|
+
if (bytes > 4 * 1024 * 1024) {
|
|
123
|
+
if (!capped) { flush(); this.store.event(id, 'warning', '实时预览超过 4 MB;完整返回保存在本次抽取报告中。', sourceId); capped = true }
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
buffer += text
|
|
127
|
+
if (buffer.length >= 1024 || Date.now() - last >= 250) flush()
|
|
128
|
+
}, flush: () => { clearInterval(timer); if (failure) throw failure; flush() },
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
private async execute(id: string, context: PipelineContext): Promise<PipelineTask> {
|
|
132
|
+
const task = this.store.task(id)!
|
|
133
|
+
const stages = task.stage === 'all' ? ['extract', 'normalize', 'materialize', 'build'] as const : [task.stage]
|
|
134
|
+
try {
|
|
135
|
+
await this.checkpoint(id)
|
|
136
|
+
this.patch(id, { state: 'running', startedAt: new Date().toISOString() })
|
|
137
|
+
const allSources = await this.host.sources()
|
|
138
|
+
const sourceIds = task.selection.sourceIds === undefined ? undefined : new Set(task.selection.sourceIds)
|
|
139
|
+
if (sourceIds && [...sourceIds].some(sourceId => !allSources.some(source => source.sourceId === sourceId))) throw new Error('选择范围中有已删除或不存在的来源')
|
|
140
|
+
const sources = allSources.filter(source => !sourceIds || sourceIds.has(source.sourceId))
|
|
141
|
+
for (const stage of stages) {
|
|
142
|
+
await this.checkpoint(id)
|
|
143
|
+
this.patch(id, { currentStage: stage, done: 0, total: 0, reused: 0, failed: 0, summary: PIPELINE_STAGE_LABEL[stage] })
|
|
144
|
+
this.store.event(id, 'progress', PIPELINE_STAGE_LABEL[stage])
|
|
145
|
+
if (stage === 'build') {
|
|
146
|
+
const summary = await this.host.build(id, () => this.checkpoint(id))
|
|
147
|
+
this.patch(id, { done: 1, total: 1, summary })
|
|
148
|
+
this.finishStage(id)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (stage === 'materialize') {
|
|
152
|
+
const { groups } = await this.host.groups(context)
|
|
153
|
+
const selected = task.selection.groupKeys ? new Set(task.selection.groupKeys) : undefined
|
|
154
|
+
if (selected && [...selected].some(key => !groups.some(group => group.key === key))) throw new Error('选择的卡片分组已变化,请刷新待物化列表')
|
|
155
|
+
const affectedSources = task.stage === 'all' && sourceIds ? new Set(sources.map(source => source.sourceId)) : undefined
|
|
156
|
+
const pending = groups.filter(group => (!selected || selected.has(group.key)) && (!affectedSources || group.sourceIds.some(sourceId => affectedSources.has(sourceId))) && (task.selection.force || group.state !== 'current'))
|
|
157
|
+
this.patch(id, { total: pending.length })
|
|
158
|
+
for (const group of pending) {
|
|
159
|
+
await this.checkpoint(id)
|
|
160
|
+
const output = this.output(id)
|
|
161
|
+
try { await this.host.materialize(group, context, output.append) }
|
|
162
|
+
catch (cause) {
|
|
163
|
+
if (cause instanceof TaskCancelled) throw cause
|
|
164
|
+
const message = `${group.title}:${cause instanceof Error ? cause.message : String(cause)}`
|
|
165
|
+
const current = this.store.task(id)!
|
|
166
|
+
this.patch(id, { failed: current.failed + 1, warnings: [...current.warnings, message] })
|
|
167
|
+
this.store.event(id, 'warning', message)
|
|
168
|
+
}
|
|
169
|
+
finally { output.flush() }
|
|
170
|
+
const current = this.store.task(id)!
|
|
171
|
+
this.patch(id, { done: current.done + 1, summary: `已物化 ${current.done + 1}/${pending.length} 张:${group.title}` })
|
|
172
|
+
}
|
|
173
|
+
this.finishStage(id)
|
|
174
|
+
const current = this.store.task(id)!
|
|
175
|
+
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};已完成卡片保留,可重试。` })
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
this.patch(id, { total: sources.length })
|
|
179
|
+
for (const source of sources) {
|
|
180
|
+
await this.checkpoint(id)
|
|
181
|
+
this.store.event(id, 'progress', `${PIPELINE_STAGE_LABEL[stage]}:${source.title}`, source.sourceId)
|
|
182
|
+
const output = this.output(id, source.sourceId)
|
|
183
|
+
let reused = false
|
|
184
|
+
try {
|
|
185
|
+
if (stage === 'extract') {
|
|
186
|
+
const fingerprint = this.host.fingerprint(source, context)
|
|
187
|
+
const cached = task.selection.force ? undefined : this.store.cached(source.sourceId, source.version, fingerprint)
|
|
188
|
+
if (cached) { this.store.selectExtraction(source.sourceId, cached.id); reused = true }
|
|
189
|
+
else {
|
|
190
|
+
const report = await this.host.extract(source, context, output.append)
|
|
191
|
+
if (this.closed) throw new TaskCancelled('知识库已关闭,已完成结果保留')
|
|
192
|
+
const checkpoint: ExtractionCheckpoint = { id: `extract_${randomUUID()}`, sourceId: source.sourceId, sourceVersion: source.version, sourceHash: source.sourceHash, fingerprint, report, createdAt: new Date().toISOString() }
|
|
193
|
+
// The host checks the captured source version again before activation.
|
|
194
|
+
const current = (await this.host.sources()).find(row => row.sourceId === source.sourceId)
|
|
195
|
+
if (!current || current.version !== source.version || current.sourceHash !== source.sourceHash) {
|
|
196
|
+
await this.host.record(checkpoint)
|
|
197
|
+
this.store.event(id, 'report', checkpoint.id, source.sourceId)
|
|
198
|
+
throw new Error('来源在抽取期间已变化;返回已保留在历史中,请抽取最新版本')
|
|
199
|
+
}
|
|
200
|
+
await this.host.record(checkpoint)
|
|
201
|
+
this.store.event(id, 'report', checkpoint.id, source.sourceId)
|
|
202
|
+
if (report.model.parsed !== true) throw new Error(report.model.why ?? '抽取返回未通过校验')
|
|
203
|
+
this.store.saveExtraction(checkpoint)
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
const checkpoint = this.store.head(source.sourceId).extracted
|
|
207
|
+
if (!checkpoint) throw new Error('尚无抽取结果,请先运行抽取')
|
|
208
|
+
if (checkpoint.sourceVersion !== source.version || checkpoint.sourceHash !== source.sourceHash) throw new Error('抽取结果依据旧版来源,请先抽取最新版本')
|
|
209
|
+
const result = await this.host.normalize(source, checkpoint, context, task.selection.force === true, output.append)
|
|
210
|
+
reused = result.reused
|
|
211
|
+
if (result.warnings.length) this.patch(id, { warnings: [...this.store.task(id)!.warnings, ...result.warnings.map(warning => `${source.title}:${warning}`)] })
|
|
212
|
+
for (const warning of result.warnings) this.store.event(id, 'warning', warning, source.sourceId)
|
|
213
|
+
}
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
if (cause instanceof TaskCancelled) throw cause
|
|
216
|
+
const message = `${source.title}:${cause instanceof Error ? cause.message : String(cause)}`
|
|
217
|
+
const current = this.store.task(id)!
|
|
218
|
+
this.patch(id, { failed: current.failed + 1, warnings: [...current.warnings, message] })
|
|
219
|
+
this.store.event(id, 'warning', message, source.sourceId)
|
|
220
|
+
} finally { output.flush() }
|
|
221
|
+
const current = this.store.task(id)!
|
|
222
|
+
this.patch(id, { done: current.done + 1, reused: current.reused + Number(reused), summary: `${PIPELINE_STAGE_LABEL[stage]} ${current.done + 1}/${sources.length}${reused ? ' · 复用已有结果' : ''}` })
|
|
223
|
+
}
|
|
224
|
+
this.finishStage(id)
|
|
225
|
+
if (this.store.task(id)!.failed > 0) {
|
|
226
|
+
const current = this.store.task(id)!
|
|
227
|
+
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};已完成结果保留,可重试此阶段。` })
|
|
228
|
+
return this.store.task(id)!
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await this.checkpoint(id)
|
|
232
|
+
const current = this.store.task(id)!
|
|
233
|
+
return this.patch(id, { state: '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} 项` })
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
return this.patch(id, { state: cause instanceof TaskCancelled ? 'cancelled' : 'failed', finishedAt: new Date().toISOString(), summary: cause instanceof Error ? cause.message : String(cause) })
|
|
236
|
+
} finally { this.controls.delete(id) }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { SqliteDatabase } from '../fts.js'
|
|
2
|
+
import type { TrialReport } from '../wiki-trial-types.js'
|
|
3
|
+
import type { PipelineEvent, PipelineTask } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface ExtractionCheckpoint {
|
|
6
|
+
id: string
|
|
7
|
+
sourceId: string
|
|
8
|
+
sourceVersion: number
|
|
9
|
+
sourceHash: string
|
|
10
|
+
fingerprint: string
|
|
11
|
+
report: TrialReport
|
|
12
|
+
createdAt: string
|
|
13
|
+
normalized?: { report: TrialReport; knowledgeIds: Record<string, string>; eventIds: Record<string, string>; fingerprint: string; unresolvedKnowledgeIds?: string[] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Business checkpoints live with truth; execution history lives with extraction audits. */
|
|
17
|
+
export class PipelineStore {
|
|
18
|
+
constructor(readonly truth: SqliteDatabase, private readonly audit: SqliteDatabase) {
|
|
19
|
+
truth.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS pipeline_extractions (
|
|
21
|
+
id TEXT PRIMARY KEY, source_id TEXT NOT NULL, source_version INTEGER NOT NULL,
|
|
22
|
+
source_hash TEXT NOT NULL, fingerprint TEXT NOT NULL, report TEXT NOT NULL,
|
|
23
|
+
normalized TEXT, created_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS pipeline_extract_cache ON pipeline_extractions(source_id, source_version, fingerprint);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS pipeline_sources (source_id TEXT PRIMARY KEY, extracted_id TEXT, normalized_id TEXT);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS pipeline_materializations (group_key TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, card_id TEXT NOT NULL);
|
|
28
|
+
`)
|
|
29
|
+
audit.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS pipeline_tasks (id TEXT PRIMARY KEY, queued_at TEXT NOT NULL, state TEXT NOT NULL, payload TEXT NOT NULL);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS pipeline_tasks_time ON pipeline_tasks(queued_at DESC);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS pipeline_events (
|
|
33
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, kind TEXT NOT NULL,
|
|
34
|
+
text TEXT NOT NULL, source_id TEXT, at TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS pipeline_events_task ON pipeline_events(task_id, seq);
|
|
37
|
+
`)
|
|
38
|
+
// A new runtime never claims that an abandoned request is still running.
|
|
39
|
+
for (const task of (audit.prepare("SELECT payload FROM pipeline_tasks WHERE state IN ('queued','running','pausing','paused','cancelling')").all() as Array<{ payload: string }>).map(row => JSON.parse(row.payload) as PipelineTask)) {
|
|
40
|
+
if (['queued', 'running', 'pausing', 'paused', 'cancelling'].includes(task.state)) {
|
|
41
|
+
this.putTask({ ...task, state: 'interrupted', finishedAt: new Date().toISOString(), summary: '服务已重新启动;已完成的阶段保留,可重新运行此任务。' })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
putTask(task: PipelineTask): void {
|
|
47
|
+
this.audit.prepare('INSERT INTO pipeline_tasks(id, queued_at, state, payload) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET state=excluded.state, payload=excluded.payload')
|
|
48
|
+
.run(task.id, task.queuedAt, task.state, JSON.stringify(task))
|
|
49
|
+
}
|
|
50
|
+
task(id: string): PipelineTask | undefined {
|
|
51
|
+
const row = this.audit.prepare('SELECT payload FROM pipeline_tasks WHERE id=?').get(id) as { payload: string } | undefined
|
|
52
|
+
return row ? JSON.parse(row.payload) as PipelineTask : undefined
|
|
53
|
+
}
|
|
54
|
+
tasks(limit = 20, offset = 0): { tasks: PipelineTask[]; total: number } {
|
|
55
|
+
const rows = this.audit.prepare("SELECT payload FROM pipeline_tasks ORDER BY CASE WHEN state IN ('queued','running','pausing','paused','cancelling') THEN 0 ELSE 1 END, queued_at DESC LIMIT ? OFFSET ?")
|
|
56
|
+
.all(Math.max(1, Math.min(10000, limit)), Math.max(0, offset)) as Array<{ payload: string }>
|
|
57
|
+
const total = Number((this.audit.prepare('SELECT COUNT(*) AS n FROM pipeline_tasks').get() as { n: number }).n)
|
|
58
|
+
return { tasks: rows.map(row => JSON.parse(row.payload) as PipelineTask), total }
|
|
59
|
+
}
|
|
60
|
+
event(taskId: string, kind: PipelineEvent['kind'], text: string, sourceId?: string): void {
|
|
61
|
+
this.audit.prepare('INSERT INTO pipeline_events(task_id,kind,text,source_id,at) VALUES (?,?,?,?,?)')
|
|
62
|
+
.run(taskId, kind, text, sourceId ?? null, new Date().toISOString())
|
|
63
|
+
}
|
|
64
|
+
events(taskId: string, after = 0, limit = 100): { events: PipelineEvent[]; next: number; hasMore: boolean } {
|
|
65
|
+
const take = Number.isFinite(limit) ? Math.max(1, Math.min(500, Math.trunc(limit))) : 100
|
|
66
|
+
after = Number.isFinite(after) ? Math.max(0, Math.trunc(after)) : 0
|
|
67
|
+
const rows = this.audit.prepare('SELECT * FROM pipeline_events WHERE task_id=? AND seq>? ORDER BY seq LIMIT ?')
|
|
68
|
+
.all(taskId, after, take + 1) as Array<Record<string, unknown>>
|
|
69
|
+
const hasMore = rows.length > take
|
|
70
|
+
const events = rows.slice(0, take).map(row => ({ seq: Number(row.seq), taskId: String(row.task_id), kind: String(row.kind) as PipelineEvent['kind'], text: String(row.text), ...(row.source_id ? { sourceId: String(row.source_id) } : {}), at: String(row.at) }))
|
|
71
|
+
return { events, next: events.at(-1)?.seq ?? after, hasMore }
|
|
72
|
+
}
|
|
73
|
+
checkpoint(id: string): ExtractionCheckpoint | undefined {
|
|
74
|
+
const row = this.truth.prepare('SELECT * FROM pipeline_extractions WHERE id=?').get(id) as Record<string, unknown> | undefined
|
|
75
|
+
return row ? { id: String(row.id), sourceId: String(row.source_id), sourceVersion: Number(row.source_version), sourceHash: String(row.source_hash), fingerprint: String(row.fingerprint), report: JSON.parse(String(row.report)), createdAt: String(row.created_at), ...(row.normalized ? { normalized: JSON.parse(String(row.normalized)) } : {}) } : undefined
|
|
76
|
+
}
|
|
77
|
+
cached(sourceId: string, version: number, fingerprint: string): ExtractionCheckpoint | undefined {
|
|
78
|
+
const row = this.truth.prepare('SELECT id FROM pipeline_extractions WHERE source_id=? AND source_version=? AND fingerprint=? ORDER BY created_at DESC, rowid DESC LIMIT 1')
|
|
79
|
+
.get(sourceId, version, fingerprint) as { id: string } | undefined
|
|
80
|
+
return row ? this.checkpoint(row.id) : undefined
|
|
81
|
+
}
|
|
82
|
+
head(sourceId: string): { extracted?: ExtractionCheckpoint; normalized?: ExtractionCheckpoint } {
|
|
83
|
+
const row = this.truth.prepare('SELECT extracted_id,normalized_id FROM pipeline_sources WHERE source_id=?').get(sourceId) as { extracted_id?: string; normalized_id?: string } | undefined
|
|
84
|
+
return { extracted: row?.extracted_id ? this.checkpoint(row.extracted_id) : undefined, normalized: row?.normalized_id ? this.checkpoint(row.normalized_id) : undefined }
|
|
85
|
+
}
|
|
86
|
+
saveExtraction(value: ExtractionCheckpoint): void {
|
|
87
|
+
this.truth.exec('SAVEPOINT pipeline_extract')
|
|
88
|
+
try {
|
|
89
|
+
this.truth.prepare('INSERT INTO pipeline_extractions(id,source_id,source_version,source_hash,fingerprint,report,created_at) VALUES (?,?,?,?,?,?,?)')
|
|
90
|
+
.run(value.id, value.sourceId, value.sourceVersion, value.sourceHash, value.fingerprint, JSON.stringify(value.report), value.createdAt)
|
|
91
|
+
this.selectExtraction(value.sourceId, value.id)
|
|
92
|
+
this.truth.exec('RELEASE pipeline_extract')
|
|
93
|
+
} catch (cause) { this.truth.exec('ROLLBACK TO pipeline_extract; RELEASE pipeline_extract'); throw cause }
|
|
94
|
+
}
|
|
95
|
+
selectExtraction(sourceId: string, id: string): void {
|
|
96
|
+
this.truth.prepare('INSERT INTO pipeline_sources(source_id,extracted_id) VALUES (?,?) ON CONFLICT(source_id) DO UPDATE SET extracted_id=excluded.extracted_id').run(sourceId, id)
|
|
97
|
+
}
|
|
98
|
+
saveNormalization(value: ExtractionCheckpoint, normalized: NonNullable<ExtractionCheckpoint['normalized']>): void {
|
|
99
|
+
this.truth.prepare('UPDATE pipeline_extractions SET normalized=? WHERE id=?').run(JSON.stringify(normalized), value.id)
|
|
100
|
+
this.truth.prepare('INSERT INTO pipeline_sources(source_id,normalized_id) VALUES (?,?) ON CONFLICT(source_id) DO UPDATE SET normalized_id=excluded.normalized_id').run(value.sourceId, value.id)
|
|
101
|
+
}
|
|
102
|
+
unresolvedKnowledge(): { ids: Set<string>; sources: Set<string> } {
|
|
103
|
+
const rows = this.truth.prepare('SELECT s.source_id, e.normalized FROM pipeline_sources s JOIN pipeline_extractions e ON e.id=s.normalized_id WHERE e.normalized IS NOT NULL').all() as Array<{ source_id: string; normalized: string }>
|
|
104
|
+
const ids = new Set<string>(); const sources = new Set<string>()
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
const pending = (JSON.parse(row.normalized) as NonNullable<ExtractionCheckpoint['normalized']>).unresolvedKnowledgeIds ?? []
|
|
107
|
+
if (pending.length) sources.add(row.source_id)
|
|
108
|
+
for (const id of pending) ids.add(id)
|
|
109
|
+
}
|
|
110
|
+
return { ids, sources }
|
|
111
|
+
}
|
|
112
|
+
materialization(key: string): { fingerprint: string; cardId: string } | undefined {
|
|
113
|
+
const row = this.truth.prepare('SELECT fingerprint,card_id FROM pipeline_materializations WHERE group_key=?').get(key) as { fingerprint: string; card_id: string } | undefined
|
|
114
|
+
return row ? { fingerprint: row.fingerprint, cardId: row.card_id } : undefined
|
|
115
|
+
}
|
|
116
|
+
materialized(key: string, fingerprint: string, cardId: string): void {
|
|
117
|
+
this.truth.prepare('INSERT INTO pipeline_materializations(group_key,fingerprint,card_id) VALUES (?,?,?) ON CONFLICT(group_key) DO UPDATE SET fingerprint=excluded.fingerprint,card_id=excluded.card_id').run(key, fingerprint, cardId)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Shared, browser-safe contracts for independently runnable knowledge stages. */
|
|
2
|
+
export type PipelineStage = 'extract' | 'normalize' | 'materialize' | 'build' | 'all'
|
|
3
|
+
export type PipelineState = 'queued' | 'running' | 'pausing' | 'paused' | 'cancelling' | 'succeeded' | 'partial' | 'failed' | 'cancelled' | 'interrupted'
|
|
4
|
+
export type PipelineAction = 'pause' | 'resume' | 'cancel'
|
|
5
|
+
|
|
6
|
+
export interface PipelineSelection {
|
|
7
|
+
sourceIds?: string[]
|
|
8
|
+
groupKeys?: string[]
|
|
9
|
+
force?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PipelineModel {
|
|
13
|
+
provider?: string
|
|
14
|
+
model?: string
|
|
15
|
+
reasoningEffort?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PipelineTask {
|
|
19
|
+
id: string
|
|
20
|
+
stage: PipelineStage
|
|
21
|
+
state: PipelineState
|
|
22
|
+
selection: PipelineSelection
|
|
23
|
+
model: PipelineModel
|
|
24
|
+
queuedAt: string
|
|
25
|
+
startedAt?: string
|
|
26
|
+
finishedAt?: string
|
|
27
|
+
currentStage?: Exclude<PipelineStage, 'all'>
|
|
28
|
+
done: number
|
|
29
|
+
total: number
|
|
30
|
+
reused: number
|
|
31
|
+
failed: number
|
|
32
|
+
summary: string
|
|
33
|
+
warnings: string[]
|
|
34
|
+
results?: Partial<Record<Exclude<PipelineStage, 'all'>, { done: number; total: number; reused: number; failed: number }>>
|
|
35
|
+
legacy?: boolean
|
|
36
|
+
reportId?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PipelineEvent {
|
|
40
|
+
seq: number
|
|
41
|
+
taskId: string
|
|
42
|
+
kind: 'output' | 'progress' | 'warning' | 'report'
|
|
43
|
+
text: string
|
|
44
|
+
sourceId?: string
|
|
45
|
+
at: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MaterializationGroup {
|
|
49
|
+
key: string
|
|
50
|
+
title: string
|
|
51
|
+
coordinate: { objectId: string; actionId: string; sceneId: string }
|
|
52
|
+
knowledgeCount: number
|
|
53
|
+
eventCount: number
|
|
54
|
+
sourceIds: string[]
|
|
55
|
+
cardId?: string
|
|
56
|
+
state: 'new' | 'changed' | 'current' | 'withdrawn' | 'blocked'
|
|
57
|
+
reason?: string
|
|
58
|
+
fingerprint: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PipelineStatus {
|
|
62
|
+
sources: Array<{ id: string; title: string; version: number; extractionId?: string; normalizedId?: string; state: 'pending' | 'extracted' | 'normalized' | 'outdated' }>
|
|
63
|
+
groups: MaterializationGroup[]
|
|
64
|
+
blocked: number
|
|
65
|
+
pendingNew: number
|
|
66
|
+
pendingUpdated: number
|
|
67
|
+
pendingWithdrawn: number
|
|
68
|
+
indexPending: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const PIPELINE_STAGE_LABEL: Record<PipelineStage, string> = {
|
|
72
|
+
extract: '抽取 Knowledge / Event', normalize: '归一合并', materialize: '物化 WikiCard', build: '构建检索索引', all: '全链路运行',
|
|
73
|
+
}
|
|
74
|
+
export const PIPELINE_ACTIVE_STATES: readonly PipelineState[] = ['queued', 'running', 'pausing', 'paused', 'cancelling']
|
package/src/present.ts
CHANGED
|
@@ -54,6 +54,7 @@ function short(text: string, max = 48): string {
|
|
|
54
54
|
export interface ImportMetaSource { sourceId: string; title?: string; bytes: number }
|
|
55
55
|
export interface ImportMetaFailure { index: number; title?: string; code: string; message: string }
|
|
56
56
|
export interface ImportMetaCompile {
|
|
57
|
+
taskId?: string; state?: string
|
|
57
58
|
cardsCreated: number; cardsUpdated: number; cardsReused?: number
|
|
58
59
|
notesReused?: number; notesRecompiled?: number
|
|
59
60
|
sourcesExtracted?: number; sourcesSkipped?: number; cardsRemoved?: number
|
|
@@ -101,7 +102,7 @@ export function importCallView(args: { sources?: unknown[]; compile?: boolean })
|
|
|
101
102
|
return {
|
|
102
103
|
card: 'generic',
|
|
103
104
|
kind: 'edit',
|
|
104
|
-
title: `导入 ${count} 份来源${args?.compile ===
|
|
105
|
+
title: `导入 ${count} 份来源${args?.compile === true ? '并处理知识' : ''}`,
|
|
105
106
|
rawInput: titles.length > 0 ? titles.map(title => short(title)) : { 份数: count },
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -112,10 +113,15 @@ export function importResultMarkdown(meta: ImportMeta): string {
|
|
|
112
113
|
for (const item of meta.written) lines.push(`- ${item.title ? `**${short(item.title)}**` : item.sourceId} · ${item.bytes} 字符`)
|
|
113
114
|
for (const item of meta.failed) lines.push(`- 第 ${item.index + 1} 份${item.title ? `「${short(item.title)}」` : ''}失败:${item.code} ${item.message}`)
|
|
114
115
|
if (meta.compile) {
|
|
115
|
-
const { cardsCreated, cardsUpdated, cardsReused, notesReused, notesRecompiled, embedding, version } = meta.compile
|
|
116
|
+
const { cardsCreated, cardsUpdated, cardsReused, notesReused, notesRecompiled, embedding, version, state, taskId } = meta.compile
|
|
117
|
+
if (state) {
|
|
118
|
+
const label = state === 'succeeded' ? '已完成' : state === 'partial' ? '部分完成' : state === 'cancelled' ? '已停止' : '未完成'
|
|
119
|
+
lines.push('', `知识处理${label}:新建 ${cardsCreated} 张、更新 ${cardsUpdated} 张。${taskId ? `任务:${taskId}。` : ''}详情请打开知识处理与任务历史。`)
|
|
120
|
+
return lines.join('\n')
|
|
121
|
+
}
|
|
116
122
|
lines.push('', `编译:新建 ${cardsCreated} 张、并入 ${cardsUpdated} 张、复用 ${cardsReused ?? 0} 张;召回片段复用 ${notesReused ?? 0} 条、重编 ${notesRecompiled ?? 0} 条;索引 v${version ?? '?'}${embedding ? `(向量 ${embedding})` : ''}。`)
|
|
117
123
|
} else {
|
|
118
|
-
lines.push('', '
|
|
124
|
+
lines.push('', '这次只保存来源(默认 `compile: false`)。下一步使用 `run_knowledge_pipeline` 分别抽取、归一、物化,最后构建检索索引。')
|
|
119
125
|
}
|
|
120
126
|
return lines.join('\n')
|
|
121
127
|
}
|
|
@@ -314,7 +320,7 @@ export function addSourceCallView(args: { title?: string; content?: string }): {
|
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
export function addSourceResultMarkdown(meta: AddSourceMeta): string {
|
|
317
|
-
return `**来源已写入** —— 「${meta.title ? short(meta.title) : meta.sourceId}」(${meta.bytes} 字符)。\n\n下一步:\`
|
|
323
|
+
return `**来源已写入** —— 「${meta.title ? short(meta.title) : meta.sourceId}」(${meta.bytes} 字符)。\n\n下一步:\`run_knowledge_pipeline\` 分别抽取、归一和物化,最后构建检索索引。`
|
|
318
324
|
}
|
|
319
325
|
|
|
320
326
|
export interface WriteCardsMeta {
|