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/settings-volatile.ts
CHANGED
|
@@ -93,13 +93,6 @@ export function unwrapVolatile<T>(value: unknown): T | undefined {
|
|
|
93
93
|
* `knowledge` / `convert` / `behaviorRules` 三组),而只解一层的结果是「组本身还是原始对象、
|
|
94
94
|
* 组里的字段还是引用」。
|
|
95
95
|
*
|
|
96
|
-
* 0029 实测到的故障就是这样来的:`intent` 组被原样交给 `applyIntent`,于是
|
|
97
|
-
* `engine.settings.predicates` 是一个 `{get, write}` 引用而不是字符串数组,
|
|
98
|
-
* `routingPrompt()` 里那句 `predicates.join('、')` 抛
|
|
99
|
-
* `this.settings.predicates.join is not a function`。抛出点在 **preset 的挂载行**上,
|
|
100
|
-
* 所以整个 preset 组合不出会话 —— 宿主平面没事(它在 apply 时不读谓词),一个字段
|
|
101
|
-
* 的类型错误把 preset 打挂。
|
|
102
|
-
*
|
|
103
96
|
* ## 为什么是两层而不是把 `unwrapVolatile` 写成递归
|
|
104
97
|
*
|
|
105
98
|
* 多出来的那一层是**已知的、唯一的一层**:组 → 字段。递归会把「数组里的对象恰好
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Source IDs are opaque, scoped by knowledge base, and independent of names, locations and content. */
|
|
2
|
+
export function isSourceId(value: unknown): value is string {
|
|
3
|
+
return typeof value === 'string' && /^[a-zA-Z0-9._:-]{1,160}$/.test(value)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Allocate once when an input joins the import queue; retries and conversions keep this ID. */
|
|
7
|
+
export function newSourceId(): string {
|
|
8
|
+
return `src_${globalThis.crypto.randomUUID()}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Explicit acquisition identity; never inferred from document body or an arbitrary external_id. */
|
|
12
|
+
export interface SourceIdentityInput {
|
|
13
|
+
url?: string
|
|
14
|
+
system?: string
|
|
15
|
+
scope?: string
|
|
16
|
+
documentType?: string
|
|
17
|
+
documentId?: string
|
|
18
|
+
/** Snake-case names accepted by Agent/HTTP callers. */
|
|
19
|
+
document_id?: string
|
|
20
|
+
document_type?: string
|
|
21
|
+
}
|
|
22
|
+
export interface SourceIdentity {
|
|
23
|
+
system: string
|
|
24
|
+
scope: string
|
|
25
|
+
documentType: string
|
|
26
|
+
documentId: string
|
|
27
|
+
rule: 'native-v1' | 'google-docs-v1' | 'feishu-v1' | 'url-v1'
|
|
28
|
+
url?: string
|
|
29
|
+
}
|
|
30
|
+
export class SourceIdentityError extends Error {
|
|
31
|
+
constructor(readonly code: string, message: string, readonly candidates: string[] = []) { super(message) }
|
|
32
|
+
}
|
|
33
|
+
export function sourceIdentityKey(value: SourceIdentity): string {
|
|
34
|
+
return JSON.stringify([value.system, value.scope, value.documentType, value.documentId])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** WHATWG URL only: preserve query order, fragments, trailing slashes and protocol. No network/redirect inference. */
|
|
38
|
+
export function normalizeSourceUrl(value: string): URL {
|
|
39
|
+
let url: URL
|
|
40
|
+
try { url = new URL(value) } catch { throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', '来源 URL 必须是完整的 HTTP(S) 地址。') }
|
|
41
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || value.length > 8192) throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', '来源 URL 必须为 HTTP(S),且不能包含用户名或密码。')
|
|
42
|
+
return url
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Known document links only. Unknown query parameters / fragments fall back to exact URL identity. */
|
|
46
|
+
function platformIdentity(url: URL, scope?: string): SourceIdentity | undefined {
|
|
47
|
+
if (url.hash || url.port) return undefined
|
|
48
|
+
// These explicit tracking parameters do not select a document or its version. All others are retained via URL fallback.
|
|
49
|
+
if ([...url.searchParams.keys()].some(key => !['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].includes(key))) return undefined
|
|
50
|
+
if (url.hostname === 'docs.google.com') {
|
|
51
|
+
const match = /^\/document\/(?:u\/\d+\/)?d\/([a-zA-Z0-9_-]+)(?:\/(?:edit|view|preview))?\/?$/.exec(url.pathname)
|
|
52
|
+
if (match) return { system: 'google-docs', scope: scope ?? url.origin, documentType: 'document', documentId: match[1]!, rule: 'google-docs-v1', url: url.href }
|
|
53
|
+
}
|
|
54
|
+
if (url.hostname.endsWith('.feishu.cn') || url.hostname.endsWith('.larksuite.com')) {
|
|
55
|
+
const match = /^\/(docx|docs|wiki)\/([a-zA-Z0-9]+)\/?$/.exec(url.pathname)
|
|
56
|
+
if (match) return { system: url.hostname.endsWith('.feishu.cn') ? 'feishu' : 'lark', scope: scope ?? url.origin,
|
|
57
|
+
documentType: match[1] === 'wiki' ? 'wiki-node' : match[1] === 'docs' ? 'doc' : 'docx', documentId: match[2]!, rule: 'feishu-v1', url: url.href }
|
|
58
|
+
}
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** First entry is the primary identity; additional entries are verified URL aliases of that same identity. */
|
|
63
|
+
export function resolveSourceIdentities(input: SourceIdentityInput): SourceIdentity[] {
|
|
64
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', 'source_identity 必须为对象。')
|
|
65
|
+
if ((input.documentId !== undefined && input.document_id !== undefined && input.documentId !== input.document_id) || (input.documentType !== undefined && input.document_type !== undefined && input.documentType !== input.document_type)) throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', '平台文档身份的同名参数不能冲突。')
|
|
66
|
+
input = { ...input, documentId: input.documentId ?? input.document_id, documentType: input.documentType ?? input.document_type }
|
|
67
|
+
for (const key of ['url', 'system', 'scope', 'documentType', 'documentId'] as const) {
|
|
68
|
+
const value = input[key]
|
|
69
|
+
if (value !== undefined && (typeof value !== 'string' || !value.trim() || value !== value.trim() || value.length > (key === 'url' ? 8192 : 2048))) throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', `source_identity.${key} 必须为非空、无首尾空格的字符串。`)
|
|
70
|
+
}
|
|
71
|
+
const url = input.url ? normalizeSourceUrl(input.url) : undefined
|
|
72
|
+
const parsed = url ? platformIdentity(url, input.scope) : undefined
|
|
73
|
+
const native = input.documentId !== undefined || input.system !== undefined || input.documentType !== undefined
|
|
74
|
+
const urlIdentity = url ? { system: 'web', scope: input.scope ?? url.origin, documentType: 'url', documentId: url.href, rule: 'url-v1' as const, url: url.href } : undefined
|
|
75
|
+
if (native) {
|
|
76
|
+
if (!input.system || !input.scope || !input.documentId) throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', '平台身份必须同时提供 system、scope 和 document_id。')
|
|
77
|
+
const identity: SourceIdentity = { system: input.system.toLowerCase(), scope: input.scope, documentType: input.documentType ?? 'document', documentId: input.documentId, rule: 'native-v1', ...(url ? { url: url.href } : {}) }
|
|
78
|
+
if (parsed && sourceIdentityKey(identity) !== sourceIdentityKey(parsed)) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', '平台文档 ID 与链接解析结果不一致。')
|
|
79
|
+
// An unknown URL is provenance only: it does not prove an alias for a native resource.
|
|
80
|
+
return parsed && urlIdentity ? [identity, urlIdentity] : [identity]
|
|
81
|
+
}
|
|
82
|
+
if (parsed && urlIdentity) return [parsed, urlIdentity]
|
|
83
|
+
if (urlIdentity) return [urlIdentity]
|
|
84
|
+
throw new SourceIdentityError('SOURCE_IDENTITY_INVALID', '请提供 url,或完整的平台文档身份。')
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import type { KnowledgeDb } from './store/knowledge-db.js'
|
|
3
|
+
import { isSourceId } from './source-identity.js'
|
|
4
|
+
import { locateQuote } from './wiki-trial.js'
|
|
5
|
+
|
|
6
|
+
export interface SourceReadInput {
|
|
7
|
+
sourceId: string
|
|
8
|
+
version?: number
|
|
9
|
+
contentHash?: string
|
|
10
|
+
start?: number
|
|
11
|
+
end?: number
|
|
12
|
+
quote?: string
|
|
13
|
+
offset?: number
|
|
14
|
+
maxChars?: number
|
|
15
|
+
}
|
|
16
|
+
export class SourceReadError extends Error {
|
|
17
|
+
constructor(readonly code: string, message: string) { super(message) }
|
|
18
|
+
}
|
|
19
|
+
/** Source ID identifies the document; the integer version pins its body; ranges use UTF-16, end exclusive. */
|
|
20
|
+
export function readSourceExact(db: KnowledgeDb, input: SourceReadInput): Record<string, unknown> {
|
|
21
|
+
const fail = (code: string, message: string): never => { throw new SourceReadError(code, message) }
|
|
22
|
+
if (!isSourceId(input.sourceId)) return fail('SOURCE_ID_INVALID', 'source_id 必须为已返回的完整合法 ID。')
|
|
23
|
+
const current = db.source(input.sourceId)
|
|
24
|
+
if (!current) return fail('SOURCE_NOT_FOUND', 'source_id 不存在或已删除;不会回退到标题或正文搜索。')
|
|
25
|
+
const version = input.version ?? current.version
|
|
26
|
+
if (!Number.isInteger(version) || version < 1) return fail('SOURCE_VERSION_INVALID', 'version 必须为正整数。')
|
|
27
|
+
const revision = db.revisionBody('source', input.sourceId, version)
|
|
28
|
+
if (!revision) return fail('SOURCE_VERSION_NOT_FOUND', '指定来源版本不可用;不会改读最新版。')
|
|
29
|
+
const text = revision.body
|
|
30
|
+
const contentHash = createHash('sha256').update(text).digest('hex')
|
|
31
|
+
if (input.contentHash !== undefined && input.contentHash !== contentHash && input.contentHash !== contentHash.slice(0, 24)) return fail('SOURCE_HASH_MISMATCH', '指定版本的正文哈希与引用不一致。')
|
|
32
|
+
if (input.quote !== undefined && (typeof input.quote !== 'string' || !input.quote.trim())) return fail('SOURCE_QUOTE_INVALID', 'quote 必须为非空原文引文。')
|
|
33
|
+
const located = input.quote === undefined ? undefined : locateQuote(text, input.quote)
|
|
34
|
+
if (located && !located.found) return fail('SOURCE_QUOTE_MISMATCH', '引文不在指定版本中。')
|
|
35
|
+
const start = input.start ?? (input.end === undefined ? located?.locator?.start : undefined) ?? 0
|
|
36
|
+
const end = input.end ?? (input.start === undefined ? located?.locator?.end : undefined) ?? text.length
|
|
37
|
+
const offset = input.offset ?? 0, maxChars = input.maxChars ?? 20000
|
|
38
|
+
if (![start, end, offset, maxChars].every(Number.isInteger) || start < 0 || end < start || end > text.length || offset < 0 || offset > end - start || maxChars < 1 || maxChars > 40000) return fail('SOURCE_RANGE_INVALID', '范围必须满足 0 ≤ start ≤ end ≤ 正文长度;offset 为范围内偏移,max_chars 为 1–40000。')
|
|
39
|
+
if (input.quote && !locateQuote(text.slice(start, end), input.quote).found) return fail('SOURCE_QUOTE_MISMATCH', '指定范围与引文不匹配。')
|
|
40
|
+
const from = start + offset, to = Math.min(end, from + maxChars), truncated = to < end
|
|
41
|
+
const readRef = { source_id: input.sourceId, version, content_hash: contentHash, start, end }
|
|
42
|
+
return { sourceId: input.sourceId, title: String(revision.frontmatter.title ?? current.record.title), version, currentVersion: current.version,
|
|
43
|
+
historical: version !== current.version, contentHash, content: text.slice(from, to), start: from, end: to, rangeStart: start, rangeEnd: end,
|
|
44
|
+
offset, returnedChars: to - from, totalChars: text.length, truncated, nextOffset: truncated ? offset + to - from : null, readRef,
|
|
45
|
+
...(truncated ? { nextRead: { tool: 'query_source_truth', args: { ...readRef, offset: offset + to - from, max_chars: maxChars } } } : {}),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -15,13 +15,14 @@ export function redirectAxisReferences(db: KnowledgeDb, kind: AxisKind, fromId:
|
|
|
15
15
|
if (hasKnowledge) db.raw(`UPDATE knowledge SET ${column} = ?, updated_at = ? WHERE ${column} = ?`).run(intoId, new Date().toISOString(), fromId)
|
|
16
16
|
const placements = db.placements()
|
|
17
17
|
const cards = db.cards()
|
|
18
|
-
const
|
|
18
|
+
const wikiKind = (card: typeof cards[number]['card']) => card.metadata.wiki_kind === 'fact' || card.metadata.wiki_kind === 'way' ? card.metadata.wiki_kind : undefined
|
|
19
|
+
const byCoordinate = new Map(cards.filter(row => row.card.coordinate).map(row => [cardCoordinateKey(row.card.coordinate!, wikiKind(row.card)), row]))
|
|
19
20
|
db.transact({ opId: `axis-merge:${fromId}:${intoId}:${Date.now()}`, actor: 'user', summary: '合并轴实体并迁移知识、卡片与事件引用' }, tx => {
|
|
20
21
|
for (const row of cards) {
|
|
21
22
|
const coordinate = row.card.coordinate
|
|
22
23
|
if (!coordinate || coordinate[field] !== fromId) continue
|
|
23
24
|
const next = redirect(coordinate)
|
|
24
|
-
const key = cardCoordinateKey(next)
|
|
25
|
+
const key = cardCoordinateKey(next, wikiKind(row.card))
|
|
25
26
|
const target = byCoordinate.get(key)
|
|
26
27
|
const base = target && target.card.entityId !== row.card.entityId ? target : row
|
|
27
28
|
const merged = base !== row
|
|
@@ -32,6 +33,7 @@ export function redirectAxisReferences(db: KnowledgeDb, kind: AxisKind, fromId:
|
|
|
32
33
|
sourceRefs: [...new Set([...base.card.sourceRefs, ...row.card.sourceRefs])],
|
|
33
34
|
annotations: [...new Map([...(base.card.annotations ?? []), ...(row.card.annotations ?? [])].map(annotation => [annotation.id, annotation])).values()],
|
|
34
35
|
metadata: { ...base.card.metadata, layout: undefined, citations: undefined,
|
|
36
|
+
knowledge_versions: { ...(row.card.metadata.knowledge_versions as Record<string, number> ?? {}), ...(base.card.metadata.knowledge_versions as Record<string, number> ?? {}) },
|
|
35
37
|
aliases: [...new Set([...(Array.isArray(base.card.metadata.aliases) ? base.card.metadata.aliases : []), ...(Array.isArray(row.card.metadata.aliases) ? row.card.metadata.aliases : []), base.card.title, row.card.title])],
|
|
36
38
|
...(eventRefs.length ? { event_refs: JSON.stringify(eventRefs) } : {}) },
|
|
37
39
|
}
|
|
@@ -41,7 +43,7 @@ export function redirectAxisReferences(db: KnowledgeDb, kind: AxisKind, fromId:
|
|
|
41
43
|
const frontmatter = { ...base.frontmatter, coordinate: { object: next.objectId, action: next.actionId, scene: next.sceneId }, source_refs: card.sourceRefs, metadata: card.metadata, annotations: card.annotations }
|
|
42
44
|
tx.putCard(card, frontmatter, { actor: 'user', kind: 'named', summary: '轴合并后的卡片引用' }, combined, true)
|
|
43
45
|
db.setCardEvents(card.entityId, eventRefs)
|
|
44
|
-
byCoordinate.delete(cardCoordinateKey(coordinate))
|
|
46
|
+
byCoordinate.delete(cardCoordinateKey(coordinate, wikiKind(row.card)))
|
|
45
47
|
byCoordinate.set(key, { ...base, card, frontmatter })
|
|
46
48
|
placements.set(card.entityId, combined)
|
|
47
49
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SourceIdentityError, type SourceIdentity } from '../source-identity.js'
|
|
1
2
|
/**
|
|
2
3
|
* 知识库文件(`<库 id>/knowledge.db`):**正文、版本、血缘的唯一存储**。
|
|
3
4
|
*
|
|
@@ -54,7 +55,7 @@ export const KNOWLEDGE_DB_APPLICATION_ID = 0x544B4244
|
|
|
54
55
|
* 旧库会在打开时做一次**事务化就地迁移**。知识库是用户数据,不能因为存储层增加了
|
|
55
56
|
* 一列或调整了主键就要求用户重新导入;只有比当前版本更新的库才拒绝打开。
|
|
56
57
|
*/
|
|
57
|
-
export const KNOWLEDGE_DB_SCHEMA =
|
|
58
|
+
export const KNOWLEDGE_DB_SCHEMA = 9
|
|
58
59
|
|
|
59
60
|
/** 实体种类。`knowledge` 与轴注册表不走这张表(它们各有自己的表,见 §3.2)。 */
|
|
60
61
|
export type EntityKind = 'source' | 'card'
|
|
@@ -264,6 +265,16 @@ CREATE TABLE IF NOT EXISTS card_event (
|
|
|
264
265
|
CREATE INDEX IF NOT EXISTS card_event_event_idx ON card_event(event_id);
|
|
265
266
|
`
|
|
266
267
|
|
|
268
|
+
const SOURCE_IDENTITY_SCHEMA_SQL = `
|
|
269
|
+
CREATE TABLE IF NOT EXISTS source_identity (
|
|
270
|
+
system TEXT NOT NULL, scope TEXT NOT NULL, document_type TEXT NOT NULL, document_id TEXT NOT NULL,
|
|
271
|
+
source_id TEXT NOT NULL, rule TEXT NOT NULL, url TEXT,
|
|
272
|
+
PRIMARY KEY(system, scope, document_type, document_id)
|
|
273
|
+
);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS source_identity_source ON source_identity(source_id);
|
|
275
|
+
CREATE UNIQUE INDEX IF NOT EXISTS source_identity_primary ON source_identity(source_id) WHERE rule <> 'url-v1';
|
|
276
|
+
`
|
|
277
|
+
|
|
267
278
|
function schemaSql(): string {
|
|
268
279
|
return `
|
|
269
280
|
CREATE TABLE IF NOT EXISTS blob (
|
|
@@ -286,6 +297,8 @@ CREATE TABLE IF NOT EXISTS source (
|
|
|
286
297
|
original_bytes INTEGER
|
|
287
298
|
);
|
|
288
299
|
|
|
300
|
+
${SOURCE_IDENTITY_SCHEMA_SQL}
|
|
301
|
+
|
|
289
302
|
CREATE TABLE IF NOT EXISTS card (
|
|
290
303
|
-- 坐标是卡片身份。entity_id 是旧接口、版本和关联表继续使用的稳定别名。
|
|
291
304
|
coordinate_key TEXT PRIMARY KEY NOT NULL,
|
|
@@ -566,6 +579,12 @@ function migrateSchema(db: SqliteDatabase, fromVersion: number): void {
|
|
|
566
579
|
|
|
567
580
|
db.exec('BEGIN IMMEDIATE')
|
|
568
581
|
try {
|
|
582
|
+
if (fromVersion === 8) {
|
|
583
|
+
db.exec(SOURCE_IDENTITY_SCHEMA_SQL)
|
|
584
|
+
db.exec(`PRAGMA user_version = ${KNOWLEDGE_DB_SCHEMA}`)
|
|
585
|
+
db.exec('COMMIT')
|
|
586
|
+
return
|
|
587
|
+
}
|
|
569
588
|
addColumnIfMissing(db, 'blob', 'bytes', '"bytes" INTEGER NOT NULL DEFAULT 0')
|
|
570
589
|
rebuildSourceTableForSchemaMigration(db)
|
|
571
590
|
addColumnIfMissing(db, 'source', 'original_rel', '"original_rel" TEXT')
|
|
@@ -581,6 +600,18 @@ function migrateSchema(db: SqliteDatabase, fromVersion: number): void {
|
|
|
581
600
|
|
|
582
601
|
// 其他模块可能已经先建了自己的表;schemaSql 使用 IF NOT EXISTS,可安全补齐剩余结构和索引。
|
|
583
602
|
db.exec(schemaSql())
|
|
603
|
+
// Rekey typed Wiki rows without changing entity IDs, content, reviews or revisions.
|
|
604
|
+
const wikiRows = db.prepare('SELECT entity_id,coordinate_key,object_id,action_id,scene_id,frontmatter FROM card').all() as Array<Record<string, unknown>>
|
|
605
|
+
for (const row of wikiRows) {
|
|
606
|
+
const metadata = parseFrontmatter(String(row.frontmatter)).metadata as Record<string, unknown> | undefined
|
|
607
|
+
const kind = metadata?.wiki_kind
|
|
608
|
+
const coordinate = coordinateOfRow(row)
|
|
609
|
+
if (!coordinate || (kind !== 'fact' && kind !== 'way')) continue
|
|
610
|
+
const key = cardCoordinateKey(coordinate, kind)
|
|
611
|
+
const holder = db.prepare('SELECT entity_id FROM card WHERE coordinate_key=?').get(key) as { entity_id: string } | undefined
|
|
612
|
+
if (holder && holder.entity_id !== row.entity_id) continue
|
|
613
|
+
db.prepare('UPDATE card SET coordinate_key=? WHERE entity_id=?').run(key, String(row.entity_id))
|
|
614
|
+
}
|
|
584
615
|
db.exec(`PRAGMA application_id = ${KNOWLEDGE_DB_APPLICATION_ID}`)
|
|
585
616
|
db.exec(`PRAGMA user_version = ${KNOWLEDGE_DB_SCHEMA}`)
|
|
586
617
|
db.exec('COMMIT')
|
|
@@ -719,6 +750,14 @@ export class KnowledgeTx {
|
|
|
719
750
|
})
|
|
720
751
|
}
|
|
721
752
|
|
|
753
|
+
bindSourceIdentity(identity: SourceIdentity, sourceId: string): void {
|
|
754
|
+
if (!this.db.source(sourceId)) throw new SourceIdentityError('SOURCE_ORIGIN_UNAVAILABLE', '身份绑定的来源必须存在。', [sourceId])
|
|
755
|
+
const owner = this.db.sourceIdByIdentity(identity)
|
|
756
|
+
if (owner && owner !== sourceId) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', '来源身份已绑定另一份文档。', [owner, sourceId])
|
|
757
|
+
this.raw('INSERT INTO source_identity(system,scope,document_type,document_id,source_id,rule,url) VALUES (?,?,?,?,?,?,?) ON CONFLICT(system,scope,document_type,document_id) DO NOTHING')
|
|
758
|
+
.run(identity.system, identity.scope, identity.documentType, identity.documentId, sourceId, identity.rule, identity.url ?? null)
|
|
759
|
+
}
|
|
760
|
+
|
|
722
761
|
/**
|
|
723
762
|
* 写一张卡片。判据同样是正文哈希。
|
|
724
763
|
*
|
|
@@ -736,7 +775,7 @@ export class KnowledgeTx {
|
|
|
736
775
|
const storedCoordinate = card.coordinate ?? (existing === undefined ? undefined : coordinateOfRow(existing))
|
|
737
776
|
const coordinateKey = storedCoordinate === undefined
|
|
738
777
|
? legacyCardStorageKey(card.entityId)
|
|
739
|
-
: cardCoordinateKey(storedCoordinate)
|
|
778
|
+
: cardCoordinateKey(storedCoordinate, card.metadata.wiki_kind === 'fact' || card.metadata.wiki_kind === 'way' ? card.metadata.wiki_kind : undefined)
|
|
740
779
|
const oldKey = typeof existing?.coordinate_key === 'string' ? existing.coordinate_key : undefined
|
|
741
780
|
if (existing !== undefined) {
|
|
742
781
|
if (!allowCoordinateMove && oldKey !== undefined && oldKey !== coordinateKey && !oldKey.startsWith('legacy:')) {
|
|
@@ -840,6 +879,8 @@ export class KnowledgeTx {
|
|
|
840
879
|
/** 删实体本体。**`revision` 行与 `blob` 不动** —— 删除即灭史是 0062 之前的缺陷 5。 */
|
|
841
880
|
removeEntity(kind: EntityKind, id: string): void {
|
|
842
881
|
this.raw(`DELETE FROM ${kind === 'source' ? 'source' : 'card'} WHERE entity_id = ?`).run(id)
|
|
882
|
+
if (kind === 'source') this.raw('DELETE FROM source_identity WHERE source_id=?').run(id)
|
|
883
|
+
if (kind === 'source' && tableExists(this.db.connection(), 'pipeline_chunks')) this.raw('DELETE FROM pipeline_chunks WHERE source_id=?').run(id)
|
|
843
884
|
this.raw('DELETE FROM entity_asset WHERE entity_kind = ? AND entity_id = ?').run(kind, id)
|
|
844
885
|
/*
|
|
845
886
|
* 卡片的知识落位跟着走(0069)。这里没有外键,所以它是**代码责任** —— 漏掉的话,
|
|
@@ -890,6 +931,7 @@ export class KnowledgeDb {
|
|
|
890
931
|
let db: SqliteDatabase
|
|
891
932
|
try {
|
|
892
933
|
db = new DatabaseSync(file)
|
|
934
|
+
db.exec('PRAGMA busy_timeout = 5000')
|
|
893
935
|
} catch (cause) {
|
|
894
936
|
return { ok: false, code: 'OPEN_FAILED', message: `打开失败:${cause instanceof Error ? cause.message : String(cause)}` }
|
|
895
937
|
}
|
|
@@ -1217,6 +1259,16 @@ export class KnowledgeDb {
|
|
|
1217
1259
|
return typeof row?.entity_id === 'string' ? row.entity_id : undefined
|
|
1218
1260
|
}
|
|
1219
1261
|
|
|
1262
|
+
sourceIdByIdentity(identity: SourceIdentity): string | undefined {
|
|
1263
|
+
const row = this.raw('SELECT source_id FROM source_identity WHERE system=? AND scope=? AND document_type=? AND document_id=?')
|
|
1264
|
+
.get(identity.system, identity.scope, identity.documentType, identity.documentId) as { source_id: string } | undefined
|
|
1265
|
+
return row?.source_id
|
|
1266
|
+
}
|
|
1267
|
+
sourceIdentities(sourceId: string): SourceIdentity[] {
|
|
1268
|
+
const rows = this.raw('SELECT * FROM source_identity WHERE source_id=? ORDER BY system,scope,document_type,document_id').all(sourceId) as Array<Record<string, string>>
|
|
1269
|
+
return rows.map(row => ({ system: row.system!, scope: row.scope!, documentType: row.document_type!, documentId: row.document_id!, rule: row.rule as SourceIdentity['rule'], ...(row.url ? { url: row.url } : {}) }))
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1220
1272
|
card(id: string): CardRecord | undefined {
|
|
1221
1273
|
return (this.readEntities('card', id) as CardRecord[])[0]
|
|
1222
1274
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SourceIdentityError, newSourceId, resolveSourceIdentities, sourceIdentityKey, type SourceIdentity } from '../source-identity.js'
|
|
2
|
+
import type { KnowledgeDb } from './knowledge-db.js'
|
|
3
|
+
|
|
4
|
+
/** Old conversion URLs may already be lossy canonical URLs; they are candidates, not automatic identity proof. */
|
|
5
|
+
function legacyIdentities(db: KnowledgeDb, id: string): SourceIdentity[] {
|
|
6
|
+
const source = db.source(id)
|
|
7
|
+
if (!source) return []
|
|
8
|
+
const stored = source.frontmatter
|
|
9
|
+
const metadata = source.record.metadata
|
|
10
|
+
const generated = stored.generated as { by?: unknown } | undefined
|
|
11
|
+
if (metadata.source_format !== 'web_page' || typeof generated?.by !== 'string' || !generated.by.startsWith('convert/') || typeof stored.resource !== 'string') return []
|
|
12
|
+
try { return resolveSourceIdentities({ url: stored.resource }) } catch { return [] }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Called inside KnowledgeDb.transact, after its first write has acquired SQLite's writer lock. */
|
|
16
|
+
export function chooseSourceId(db: KnowledgeDb, proposedId: string | undefined, incoming: SourceIdentity[]): { sourceId: string; reused: boolean } {
|
|
17
|
+
if (!incoming.length) return { sourceId: proposedId ?? newSourceId(), reused: proposedId !== undefined && !!db.source(proposedId) }
|
|
18
|
+
const primary = incoming[0]!, primaryKey = sourceIdentityKey(primary)
|
|
19
|
+
const owners = new Set(incoming.map(identity => db.sourceIdByIdentity(identity)).filter((id): id is string => !!id))
|
|
20
|
+
const proposed = proposedId === undefined ? undefined : db.source(proposedId)
|
|
21
|
+
if (owners.size > 1) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', '平台 ID 与 URL 映射到不同来源,未自动合并。', [...owners])
|
|
22
|
+
if (proposed) {
|
|
23
|
+
const registered = db.sourceIdentities(proposedId!)
|
|
24
|
+
const primaryBindings = registered.filter(identity => identity.rule !== 'url-v1')
|
|
25
|
+
const samePrimary = registered.some(identity => sourceIdentityKey(identity) === primaryKey)
|
|
26
|
+
const sameAlias = registered.some(identity => incoming.some(value => sourceIdentityKey(value) === sourceIdentityKey(identity)))
|
|
27
|
+
if (!samePrimary && (primaryBindings.length > 0 || (registered.some(identity => identity.rule === 'url-v1') && !sameAlias))) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', 'source_id 已属于另一份文档,不能改变来源身份。', [proposedId!])
|
|
28
|
+
if (owners.size && !owners.has(proposedId!)) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', '已知 source_id 与来源映射冲突。', [proposedId!, ...owners])
|
|
29
|
+
}
|
|
30
|
+
if (!owners.size) {
|
|
31
|
+
// Existing IDs and content are never rewritten by migration. An explicit known ID can confirm a legacy candidate.
|
|
32
|
+
const rows = db.raw("SELECT entity_id FROM source WHERE json_extract(frontmatter,'$.metadata.source_format')='web_page' AND entity_id NOT IN (SELECT source_id FROM source_identity)").all() as Array<{ entity_id: string }>
|
|
33
|
+
const candidates = rows.filter(row => legacyIdentities(db, row.entity_id).some(identity => sourceIdentityKey(identity) === primaryKey)).map(row => row.entity_id)
|
|
34
|
+
if (proposed && proposedId && candidates.includes(proposedId)) owners.add(proposedId)
|
|
35
|
+
else if (candidates.length > 1) throw new SourceIdentityError('SOURCE_ORIGIN_AMBIGUOUS', '多份历史来源使用同一文档地址,请明确选择已有 source_id。', candidates)
|
|
36
|
+
else if (candidates[0]) throw new SourceIdentityError('SOURCE_ORIGIN_UNCONFIRMED', '历史地址尚无已确认映射;请核对候选后明确传其 source_id,不能仅凭旧 canonical 地址自动覆盖。', candidates)
|
|
37
|
+
}
|
|
38
|
+
const owner = [...owners][0]
|
|
39
|
+
if (owner && proposedId !== undefined && owner !== proposedId && (proposed || db.versions('source', proposedId).length)) throw new SourceIdentityError('SOURCE_ORIGIN_CONFLICT', '已有 source_id 与来源映射指向不同文档。', [proposedId, owner])
|
|
40
|
+
if (owner && !db.source(owner)) throw new SourceIdentityError('SOURCE_ORIGIN_UNAVAILABLE', '来源映射对应的文档已不可用。', [owner])
|
|
41
|
+
return { sourceId: owner ?? proposedId ?? newSourceId(), reused: !!owner || !!proposed }
|
|
42
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { wikiGroupKey, type WikiKind } from './knowledge-contract.js'
|
|
1
2
|
import { DIAGNOSIS_ENTITY_TYPES } from './diagnosis/schema.js'
|
|
3
|
+
import type { ExtractionTimeoutMinutes } from './pipeline/types.js'
|
|
2
4
|
|
|
3
5
|
/** Wiki Card 自己的生命周期。它描述卡片是否可展示、是否被审核或是否有编译问题。 */
|
|
4
6
|
export type CardStatus = 'unreviewed' | 'draft' | 'reviewed' | 'active' | 'deprecated' | 'problem'
|
|
@@ -27,6 +29,8 @@ export type RerankProvider = 'bert-python' | 'none'
|
|
|
27
29
|
export type CompileModelMode = 'default' | 'fixed'
|
|
28
30
|
export interface CompileModelSelection {
|
|
29
31
|
reasoningEffort?: string
|
|
32
|
+
/** Per-request output cap in tokens. Omitted means the selected model's default. */
|
|
33
|
+
maxTokens?: number
|
|
30
34
|
mode: CompileModelMode
|
|
31
35
|
provider?: string
|
|
32
36
|
model?: string
|
|
@@ -298,6 +302,8 @@ export interface SourceRaw {
|
|
|
298
302
|
* **真实文件**,读它与读这里的内容是同一份数据。
|
|
299
303
|
*/
|
|
300
304
|
export interface SourceRawRead extends SourceRaw {
|
|
305
|
+
version?: number
|
|
306
|
+
readRef?: { kb?: string; source_id: string; version?: number; content_hash?: string }
|
|
301
307
|
/**
|
|
302
308
|
* 这份来源在磁盘上的绝对路径。
|
|
303
309
|
*
|
|
@@ -382,10 +388,11 @@ export interface CardCoordinate {
|
|
|
382
388
|
/**
|
|
383
389
|
* 卡片的存储主键。
|
|
384
390
|
*
|
|
385
|
-
* `entityId`
|
|
386
|
-
*
|
|
391
|
+
* `entityId` 是读取与版本表使用的稳定 ID。类型化 Wiki 由 Wiki 类型和完整三轴共同
|
|
392
|
+
* 确定身份;未指定类型时保留旧坐标键,供存量卡片兼容使用。知识库由所在数据库隔离。
|
|
387
393
|
*/
|
|
388
|
-
export function cardCoordinateKey(coordinate: CardCoordinate): string {
|
|
394
|
+
export function cardCoordinateKey(coordinate: CardCoordinate, kind?: WikiKind): string {
|
|
395
|
+
if (kind) return wikiGroupKey(kind, coordinate)
|
|
389
396
|
return `coord:${coordinate.objectId}\u0001${coordinate.actionId}\u0001${coordinate.sceneId}`
|
|
390
397
|
}
|
|
391
398
|
|
|
@@ -527,6 +534,7 @@ export interface SearchHit extends WikiNote {
|
|
|
527
534
|
* 成立,而不是靠模型自觉。
|
|
528
535
|
*/
|
|
529
536
|
export interface RecallEvidence {
|
|
537
|
+
readRef?: { kb?: string; source_id: string; version?: number; content_hash?: string; quote?: string }
|
|
530
538
|
sourceId: string
|
|
531
539
|
title?: string
|
|
532
540
|
/** 覆盖查询词最多的那一段原文(已按上限截断)。 */
|
|
@@ -575,6 +583,7 @@ export interface RecallKnowledge {
|
|
|
575
583
|
/** 出处:原文里原样复制的那一段。 */
|
|
576
584
|
quote?: string
|
|
577
585
|
sourceId?: string
|
|
586
|
+
readRef?: { kb?: string; source_id: string; version?: number; start?: number; end?: number; quote?: string }
|
|
578
587
|
headingPath?: string[]
|
|
579
588
|
/** 已经被并进哪一条。空串或缺省 = 它自己就是保留的那条。 */
|
|
580
589
|
mergedInto?: string
|
|
@@ -788,10 +797,13 @@ export interface ContextBudgetReport {
|
|
|
788
797
|
expandedCards: number
|
|
789
798
|
/** 溢出文件的绝对路径(有溢出时才有;它在系统临时目录里,不随库走)。 */
|
|
790
799
|
overflowPath?: string
|
|
800
|
+
omitted?: Array<{ target: string; title: string; requiredChars: number; read: { tool: string; args: { kb?: string; target: string } } }>
|
|
791
801
|
}
|
|
792
802
|
|
|
793
803
|
export interface RecallResult {
|
|
794
|
-
|
|
804
|
+
foundCandidates?: boolean
|
|
805
|
+
contentDelivered?: boolean
|
|
806
|
+
/** Stage 3: 每篇 Wiki 的最佳章节,最多 `recallTopK` 篇;其他匹配章节在 context[].notes。 */
|
|
795
807
|
hits: RecallHit[]
|
|
796
808
|
/** Stage 4: the `contextTopN` notes expanded to their wiki + 1-hop graph. */
|
|
797
809
|
context: ContextItem[]
|
|
@@ -839,7 +851,7 @@ export interface RecallResult {
|
|
|
839
851
|
/** 各阶段耗时(毫秒)。它很小,所以不藏在 `detail` 后面 —— Agent 也会问「为什么慢」。 */
|
|
840
852
|
timings?: RecallTimings
|
|
841
853
|
recallId: string
|
|
842
|
-
/**
|
|
854
|
+
/** 查询侧的 OAS 坐标解析结果,供诊断和召回解释使用。 */
|
|
843
855
|
queryAnalysis?: import('./query-analysis.js').QueryAnalysis
|
|
844
856
|
indexVersion?: number
|
|
845
857
|
compileRunId?: string
|
|
@@ -871,6 +883,7 @@ export interface KnowledgeBase {
|
|
|
871
883
|
extractionNote: string
|
|
872
884
|
/** 编译模型路由:默认跟随 DSH,或固定到模型目录中的一项。 */
|
|
873
885
|
compileModel: CompileModelSelection
|
|
886
|
+
extractionTimeoutMinutes: ExtractionTimeoutMinutes
|
|
874
887
|
createdAt: string
|
|
875
888
|
updatedAt: string
|
|
876
889
|
sourceCount: number
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Knowledge } from './knowledge.js'
|
|
2
|
+
import type { CardKnowledgeRow } from './store/knowledge-db.js'
|
|
3
|
+
import type { KnowledgeCard } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface WikiTruth {
|
|
6
|
+
knowledge: ReadonlyMap<string, Knowledge>
|
|
7
|
+
sources: ReadonlyMap<string, number>
|
|
8
|
+
caseVersion(id: string): number | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The saved Wiki generation, rather than the index build time, binds its supporting versions. */
|
|
12
|
+
export function wikiUnavailableReason(card: KnowledgeCard, placements: readonly CardKnowledgeRow[], truth: WikiTruth): string | undefined {
|
|
13
|
+
if (card.status === 'deprecated') return '该 Wiki 已停用。'
|
|
14
|
+
if (card.sourceRefs.some(id => !truth.sources.has(id))) return '支撑该 Wiki 的 Source Raw 已缺失。'
|
|
15
|
+
const kind = card.metadata.wiki_kind
|
|
16
|
+
if (kind !== 'fact' && kind !== 'way') return undefined
|
|
17
|
+
if (card.metadata.wiki_coordinate_version !== 2) return '该 Wiki 仍使用旧聚合规则,请先重新组织 Wiki。'
|
|
18
|
+
const versions = card.metadata.knowledge_versions as Record<string, number> | undefined
|
|
19
|
+
const ids = new Set([...placements.map(item => item.knowledgeId), ...Object.keys(versions ?? {})])
|
|
20
|
+
if (kind === 'way' && ids.size === 0) return '方法缺少知识落位与版本依据,请重新组织 Wiki。'
|
|
21
|
+
for (const id of ids) {
|
|
22
|
+
const row = truth.knowledge.get(id)
|
|
23
|
+
if (!row || row.mergedInto || row.status === 'deprecated') return '支撑知识已撤回、合并或停用,请重新组织 Wiki。'
|
|
24
|
+
if ((row.kind ?? 'fact') !== (kind === 'way' ? 'procedure' : 'fact')) return 'Wiki 类型与支撑知识不一致,请重新组织 Wiki。'
|
|
25
|
+
if ((versions?.[id] !== undefined && versions[id] !== row.version) || (kind === 'way' && versions?.[id] === undefined)) return '支撑知识内容版本已变化,请重新组织 Wiki。'
|
|
26
|
+
if (card.coordinate && (row.objectId !== card.coordinate.objectId || row.actionId !== card.coordinate.actionId || row.sceneId !== card.coordinate.sceneId)) return '支撑知识坐标已变化,请重新组织 Wiki。'
|
|
27
|
+
const active = row.sources.filter(source => !source.withdrawn && (!source.documentId || truth.sources.has(source.documentId)))
|
|
28
|
+
if (!active.length) return '支撑知识的来源贡献已撤回,请重新组织 Wiki。'
|
|
29
|
+
if (kind === 'way') {
|
|
30
|
+
const method = row.data?.procedure
|
|
31
|
+
if (row.status === 'disputed' || !method || method.completeness !== 'complete' || !method.method.length
|
|
32
|
+
|| method.caseRefs.some(ref => truth.caseVersion(ref.id) !== ref.version)
|
|
33
|
+
|| active.some(source => source.documentId && source.toVersion !== undefined && truth.sources.get(source.documentId) !== source.toVersion)) return '支撑方法或证据版本已变化,请重新组织 Wiki。'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
package/src/wiki-finalize.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 1. **相关卡片**(`planRelated`)—— 卡片之间原来只有图上的边,卡片正文里什么都没有,
|
|
11
11
|
* 人打开一张卡片看不到它跟谁有关系。现在把关系存进卡片的 `metadata.related_cards`,
|
|
12
|
-
*
|
|
12
|
+
* 面板和索引页按这份结构化数据展示。类型化 Wiki 的正文双链和导航另由
|
|
13
|
+
* `wiki-links.ts` 生成,索引前剥离导航。
|
|
13
14
|
* 2. **断链统计**(`deadLinksOf`)—— 为编译报告与索引页提供关系图的可见性,不修改卡片
|
|
14
15
|
* 内容,也不写入卡片 metadata。
|
|
15
16
|
* 3. **索引页**(`buildWikiIndex`)—— 知识库原来没有「这里有什么」的落盘入口:
|
|
@@ -24,12 +25,13 @@
|
|
|
24
25
|
* ## 为什么只认 wiki 链接语法
|
|
25
26
|
*
|
|
26
27
|
* 旧相关块里写的是 `[[类型/标题|标题]]`,不是 markdown 相对链接。这个语法仍由迁移
|
|
27
|
-
*
|
|
28
|
+
* 与纯函数测试保留。新编译使用稳定 `[[类型/实体ID|显示名称]]`,正文里的有效链接
|
|
29
|
+
* 由断链检查识别;代码与 URL 内的示例不作为链接。
|
|
28
30
|
*
|
|
29
31
|
* @module
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
|
-
import {
|
|
34
|
+
import { scanWikiLinks, wikiLinkCandidates } from './wiki-links.js'
|
|
33
35
|
import type { KnowledgeCard } from './types.js'
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -89,17 +91,9 @@ export function relatedEntriesOf(metadata: Record<string, unknown> | undefined):
|
|
|
89
91
|
})
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
/** 正文里的 wiki 链接:`[[类型/名称]]`、`[[类型/名称|显示文字]]`、`[[名称]]`。 */
|
|
93
|
-
const WIKI_LINK = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g
|
|
94
|
-
|
|
95
94
|
/** 卡片正文里出现的 wiki 链接目标(原样,未归一)。 */
|
|
96
95
|
export function wikiLinkTargets(content: string): string[] {
|
|
97
|
-
|
|
98
|
-
for (const match of content.matchAll(WIKI_LINK)) {
|
|
99
|
-
const target = (match[1] ?? '').trim()
|
|
100
|
-
if (target) targets.push(target)
|
|
101
|
-
}
|
|
102
|
-
return targets
|
|
96
|
+
return scanWikiLinks(content).map(link => link.target)
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
/**
|
|
@@ -278,20 +272,10 @@ export interface DeadLink {
|
|
|
278
272
|
* 会被整批报成断链。这里从最长的类型前缀开始试(即从左往右逐个 `/` 切)。
|
|
279
273
|
*/
|
|
280
274
|
export function deadLinksOf(cards: KnowledgeCard[], index?: Map<string, KnowledgeCard>): DeadLink[] {
|
|
281
|
-
|
|
282
|
-
const byName = new Map<string, KnowledgeCard>()
|
|
283
|
-
for (const card of cards) {
|
|
284
|
-
for (const name of [card.entityId, card.title, ...aliasesOf(card)]) {
|
|
285
|
-
const normalized = normalizeCandidateName(name)
|
|
286
|
-
if (normalized && !byName.has(normalized)) byName.set(normalized, card)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
275
|
+
void index // Retained for compatibility; all targets must resolve uniquely.
|
|
289
276
|
const resolveTarget = (target: string): KnowledgeCard | undefined => {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (hit) return hit
|
|
293
|
-
}
|
|
294
|
-
return byName.get(normalizeCandidateName(target))
|
|
277
|
+
const candidates = wikiLinkCandidates(cards, target)
|
|
278
|
+
return candidates.length === 1 ? candidates[0] : undefined
|
|
295
279
|
}
|
|
296
280
|
const dead: DeadLink[] = []
|
|
297
281
|
for (const card of cards) {
|