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.
Files changed (252) hide show
  1. package/README.md +33 -16
  2. package/lib/client.js +1042 -2097
  3. package/lib/contract-refs.d.ts +0 -1
  4. package/lib/doc-page.js +1 -1
  5. package/lib/pages.js +2489 -1060
  6. package/lib/types/agent-behavior-rules.d.ts.map +1 -1
  7. package/lib/types/agent-behavior-rules.js +3 -2
  8. package/lib/types/agent-behavior-rules.js.map +1 -1
  9. package/lib/types/axis-contract.js +1 -1
  10. package/lib/types/axis-contract.js.map +1 -1
  11. package/lib/types/card-organize.d.ts +2 -1
  12. package/lib/types/card-organize.d.ts.map +1 -1
  13. package/lib/types/card-organize.js +5 -3
  14. package/lib/types/card-organize.js.map +1 -1
  15. package/lib/types/config-schema.d.ts +1 -1
  16. package/lib/types/config-schema.js +1 -1
  17. package/lib/types/convert/host.d.ts +2 -3
  18. package/lib/types/convert/host.d.ts.map +1 -1
  19. package/lib/types/convert/host.js +11 -2
  20. package/lib/types/convert/host.js.map +1 -1
  21. package/lib/types/convert/ingest.d.ts +14 -20
  22. package/lib/types/convert/ingest.d.ts.map +1 -1
  23. package/lib/types/convert/ingest.js +8 -5
  24. package/lib/types/convert/ingest.js.map +1 -1
  25. package/lib/types/diagnosis/store.d.ts +2 -2
  26. package/lib/types/diagnosis/store.d.ts.map +1 -1
  27. package/lib/types/diagnosis/store.js +10 -2
  28. package/lib/types/diagnosis/store.js.map +1 -1
  29. package/lib/types/diagnosis/types.d.ts +1 -1
  30. package/lib/types/diagnosis/types.js +1 -1
  31. package/lib/types/extract-model.d.ts +3 -0
  32. package/lib/types/extract-model.d.ts.map +1 -1
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/images.js +1 -1
  35. package/lib/types/index.d.ts +0 -4
  36. package/lib/types/index.d.ts.map +1 -1
  37. package/lib/types/index.js +99 -91
  38. package/lib/types/index.js.map +1 -1
  39. package/lib/types/knowledge-bases.d.ts +3 -0
  40. package/lib/types/knowledge-bases.d.ts.map +1 -1
  41. package/lib/types/knowledge-bases.js +22 -2
  42. package/lib/types/knowledge-bases.js.map +1 -1
  43. package/lib/types/knowledge-contract.d.ts +6 -0
  44. package/lib/types/knowledge-contract.d.ts.map +1 -1
  45. package/lib/types/knowledge-contract.js +6 -2
  46. package/lib/types/knowledge-contract.js.map +1 -1
  47. package/lib/types/layout.d.ts +9 -26
  48. package/lib/types/layout.d.ts.map +1 -1
  49. package/lib/types/layout.js +10 -28
  50. package/lib/types/layout.js.map +1 -1
  51. package/lib/types/materialize-snapshot.d.ts +5 -0
  52. package/lib/types/materialize-snapshot.d.ts.map +1 -1
  53. package/lib/types/migrate.d.ts +3 -1
  54. package/lib/types/migrate.d.ts.map +1 -1
  55. package/lib/types/migrate.js +3 -1
  56. package/lib/types/migrate.js.map +1 -1
  57. package/lib/types/model-text.d.ts +7 -0
  58. package/lib/types/model-text.d.ts.map +1 -1
  59. package/lib/types/model-text.js +31 -9
  60. package/lib/types/model-text.js.map +1 -1
  61. package/lib/types/organize-contract.d.ts.map +1 -1
  62. package/lib/types/organize-contract.js +2 -1
  63. package/lib/types/organize-contract.js.map +1 -1
  64. package/lib/types/pipeline/engine.d.ts +28 -4
  65. package/lib/types/pipeline/engine.d.ts.map +1 -1
  66. package/lib/types/pipeline/engine.js +68 -15
  67. package/lib/types/pipeline/engine.js.map +1 -1
  68. package/lib/types/pipeline/progress.d.ts +12 -0
  69. package/lib/types/pipeline/progress.d.ts.map +1 -0
  70. package/lib/types/pipeline/progress.js +37 -0
  71. package/lib/types/pipeline/progress.js.map +1 -0
  72. package/lib/types/pipeline/resumable-extract.d.ts +45 -0
  73. package/lib/types/pipeline/resumable-extract.d.ts.map +1 -0
  74. package/lib/types/pipeline/resumable-extract.js +262 -0
  75. package/lib/types/pipeline/resumable-extract.js.map +1 -0
  76. package/lib/types/pipeline/store.d.ts +8 -2
  77. package/lib/types/pipeline/store.d.ts.map +1 -1
  78. package/lib/types/pipeline/store.js +27 -5
  79. package/lib/types/pipeline/store.js.map +1 -1
  80. package/lib/types/pipeline/types.d.ts +56 -0
  81. package/lib/types/pipeline/types.d.ts.map +1 -1
  82. package/lib/types/pipeline/types.js +18 -1
  83. package/lib/types/pipeline/types.js.map +1 -1
  84. package/lib/types/present.d.ts +1 -1
  85. package/lib/types/present.js +1 -1
  86. package/lib/types/preset-ids.d.ts +0 -1
  87. package/lib/types/preset-ids.d.ts.map +1 -1
  88. package/lib/types/preset-ids.js +0 -3
  89. package/lib/types/preset-ids.js.map +1 -1
  90. package/lib/types/query-analysis.d.ts +0 -5
  91. package/lib/types/query-analysis.d.ts.map +1 -1
  92. package/lib/types/query-analysis.js +0 -16
  93. package/lib/types/query-analysis.js.map +1 -1
  94. package/lib/types/recall-delivery.d.ts +36 -0
  95. package/lib/types/recall-delivery.d.ts.map +1 -0
  96. package/lib/types/recall-delivery.js +16 -0
  97. package/lib/types/recall-delivery.js.map +1 -0
  98. package/lib/types/retrieval/scope.d.ts +17 -0
  99. package/lib/types/retrieval/scope.d.ts.map +1 -0
  100. package/lib/types/retrieval/scope.js +11 -0
  101. package/lib/types/retrieval/scope.js.map +1 -0
  102. package/lib/types/runtime.d.ts +75 -14
  103. package/lib/types/runtime.d.ts.map +1 -1
  104. package/lib/types/runtime.js +454 -198
  105. package/lib/types/runtime.js.map +1 -1
  106. package/lib/types/settings-volatile.d.ts +0 -7
  107. package/lib/types/settings-volatile.d.ts.map +1 -1
  108. package/lib/types/settings-volatile.js +0 -7
  109. package/lib/types/settings-volatile.js.map +1 -1
  110. package/lib/types/source-identity.d.ts +34 -0
  111. package/lib/types/source-identity.d.ts.map +1 -0
  112. package/lib/types/source-identity.js +85 -0
  113. package/lib/types/source-identity.js.map +1 -0
  114. package/lib/types/source-read.d.ts +18 -0
  115. package/lib/types/source-read.d.ts.map +1 -0
  116. package/lib/types/source-read.js +49 -0
  117. package/lib/types/source-read.js.map +1 -0
  118. package/lib/types/store/axis-references.d.ts.map +1 -1
  119. package/lib/types/store/axis-references.js +5 -3
  120. package/lib/types/store/axis-references.js.map +1 -1
  121. package/lib/types/store/knowledge-db.d.ts +5 -29
  122. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  123. package/lib/types/store/knowledge-db.js +57 -2
  124. package/lib/types/store/knowledge-db.js.map +1 -1
  125. package/lib/types/store/source-identity.d.ts +8 -0
  126. package/lib/types/store/source-identity.d.ts.map +1 -0
  127. package/lib/types/store/source-identity.js +56 -0
  128. package/lib/types/store/source-identity.js.map +1 -0
  129. package/lib/types/types.d.ts +46 -5
  130. package/lib/types/types.d.ts.map +1 -1
  131. package/lib/types/types.js +6 -3
  132. package/lib/types/types.js.map +1 -1
  133. package/lib/types/wiki-availability.d.ts +11 -0
  134. package/lib/types/wiki-availability.d.ts.map +1 -0
  135. package/lib/types/wiki-availability.js +39 -0
  136. package/lib/types/wiki-availability.js.map +1 -0
  137. package/lib/types/wiki-finalize.d.ts +4 -2
  138. package/lib/types/wiki-finalize.d.ts.map +1 -1
  139. package/lib/types/wiki-finalize.js +9 -27
  140. package/lib/types/wiki-finalize.js.map +1 -1
  141. package/lib/types/wiki-links.d.ts +35 -0
  142. package/lib/types/wiki-links.d.ts.map +1 -0
  143. package/lib/types/wiki-links.js +129 -0
  144. package/lib/types/wiki-links.js.map +1 -0
  145. package/lib/types/wiki-trial-types.d.ts +3 -0
  146. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  147. package/lib/types/wiki-trial.d.ts +3 -3
  148. package/lib/types/wiki-trial.d.ts.map +1 -1
  149. package/lib/types/wiki-trial.js +14 -3
  150. package/lib/types/wiki-trial.js.map +1 -1
  151. package/package.json +16 -16
  152. package/scripts/doctor.mjs +0 -7
  153. package/scripts/setup-embedding.sh +0 -0
  154. package/src/agent-behavior-rules.ts +3 -2
  155. package/src/axis-contract.ts +1 -1
  156. package/src/card-organize.ts +5 -3
  157. package/src/client/AttributionEventGuide.tsx +3 -3
  158. package/src/client/AxisMatrix.tsx +86 -19
  159. package/src/client/AxisRegistrySection.tsx +1 -1
  160. package/src/client/CompileSettingsView.tsx +473 -0
  161. package/src/client/DirectoryPicker.tsx +0 -2
  162. package/src/client/ExtractionTasksPage.tsx +557 -423
  163. package/src/client/GuideArchitectureCanvas.tsx +5 -12
  164. package/src/client/ImportFilesDialog.tsx +1 -0
  165. package/src/client/InfoHint.tsx +1 -1
  166. package/src/client/KnowledgeBaseTab.tsx +141 -148
  167. package/src/client/KnowledgePanel.tsx +16 -9
  168. package/src/client/OperationLogTab.tsx +1 -1
  169. package/src/client/PipelineRunView.tsx +173 -0
  170. package/src/client/RecallPanel.tsx +5 -16
  171. package/src/client/TabGuide.tsx +3 -7
  172. package/src/client/WikiFlowPanel.tsx +10 -11
  173. package/src/client/arch/NodeLogicPanel.tsx +3 -3
  174. package/src/client/arch/model.ts +5 -113
  175. package/src/client/architecture.css +1 -1
  176. package/src/client/architecture.tsx +13 -27
  177. package/src/client/attribution/AttributionTab.tsx +141 -10
  178. package/src/client/pages.css +305 -180
  179. package/src/client/pages.tsx +7 -12
  180. package/src/client/styles.css +15 -96
  181. package/src/config-schema.ts +1 -1
  182. package/src/convert/host.ts +13 -5
  183. package/src/convert/ingest.ts +20 -7
  184. package/src/diagnosis/store.ts +7 -2
  185. package/src/diagnosis/types.ts +1 -1
  186. package/src/extract-model.ts +3 -0
  187. package/src/images.ts +1 -1
  188. package/src/index.ts +92 -97
  189. package/src/knowledge-bases.ts +23 -3
  190. package/src/knowledge-contract.ts +7 -2
  191. package/src/layout.ts +10 -32
  192. package/src/materialize-snapshot.ts +1 -0
  193. package/src/migrate.ts +3 -1
  194. package/src/model-text.ts +33 -11
  195. package/src/organize-contract.ts +2 -1
  196. package/src/pipeline/engine.ts +85 -22
  197. package/src/pipeline/progress.ts +40 -0
  198. package/src/pipeline/resumable-extract.ts +233 -0
  199. package/src/pipeline/store.ts +28 -6
  200. package/src/pipeline/types.ts +47 -2
  201. package/src/present.ts +1 -1
  202. package/src/preset-ids.ts +0 -4
  203. package/src/query-analysis.ts +0 -20
  204. package/src/recall-delivery.ts +19 -0
  205. package/src/retrieval/scope.ts +10 -0
  206. package/src/runtime.ts +412 -206
  207. package/src/settings-volatile.ts +0 -7
  208. package/src/source-identity.ts +85 -0
  209. package/src/source-read.ts +47 -0
  210. package/src/store/axis-references.ts +5 -3
  211. package/src/store/knowledge-db.ts +54 -2
  212. package/src/store/source-identity.ts +42 -0
  213. package/src/types.ts +18 -5
  214. package/src/wiki-availability.ts +37 -0
  215. package/src/wiki-finalize.ts +9 -25
  216. package/src/wiki-links.ts +120 -0
  217. package/src/wiki-trial-types.ts +3 -2
  218. package/src/wiki-trial.ts +17 -6
  219. package/lib/types/intent/engine.d.ts +0 -122
  220. package/lib/types/intent/engine.d.ts.map +0 -1
  221. package/lib/types/intent/engine.js +0 -488
  222. package/lib/types/intent/engine.js.map +0 -1
  223. package/lib/types/intent/host.d.ts +0 -68
  224. package/lib/types/intent/host.d.ts.map +0 -1
  225. package/lib/types/intent/host.js +0 -249
  226. package/lib/types/intent/host.js.map +0 -1
  227. package/lib/types/intent/scope.d.ts +0 -25
  228. package/lib/types/intent/scope.d.ts.map +0 -1
  229. package/lib/types/intent/scope.js +0 -116
  230. package/lib/types/intent/scope.js.map +0 -1
  231. package/lib/types/intent/store.d.ts +0 -33
  232. package/lib/types/intent/store.d.ts.map +0 -1
  233. package/lib/types/intent/store.js +0 -239
  234. package/lib/types/intent/store.js.map +0 -1
  235. package/lib/types/intent/tools.d.ts +0 -62
  236. package/lib/types/intent/tools.d.ts.map +0 -1
  237. package/lib/types/intent/tools.js +0 -130
  238. package/lib/types/intent/tools.js.map +0 -1
  239. package/lib/types/intent/types.d.ts +0 -170
  240. package/lib/types/intent/types.d.ts.map +0 -1
  241. package/lib/types/intent/types.js +0 -56
  242. package/lib/types/intent/types.js.map +0 -1
  243. package/src/client/IntentLogic.tsx +0 -192
  244. package/src/client/IntentTab.tsx +0 -343
  245. package/src/client/TypeDefinitionDrawer.tsx +0 -195
  246. package/src/intent/README.md +0 -42
  247. package/src/intent/engine.ts +0 -515
  248. package/src/intent/host.ts +0 -315
  249. package/src/intent/scope.ts +0 -125
  250. package/src/intent/store.ts +0 -218
  251. package/src/intent/tools.ts +0 -154
  252. package/src/intent/types.ts +0 -229
@@ -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 byCoordinate = new Map(cards.filter(row => row.card.coordinate).map(row => [cardCoordinateKey(row.card.coordinate!), row]))
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 = 7
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
- * 是这三个轴实体 id。加前缀是为了把「坐标主键」和「未绑定坐标的存量卡片」分开。
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
- /** Stage 3: 融合(+ 重排)之后的 wikiNotes,最多 `recallTopK`。 */
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
- /** 查询侧的意图与 OAS 解析结果,供诊断和召回解释使用。 */
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
+ }
@@ -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
- * 与纯函数测试保留,但新编译不再把它写进正文;正文里的 wiki 链接仍由断链检查识别。
28
+ * 与纯函数测试保留。新编译使用稳定 `[[类型/实体ID|显示名称]]`,正文里的有效链接
29
+ * 由断链检查识别;代码与 URL 内的示例不作为链接。
28
30
  *
29
31
  * @module
30
32
  */
31
33
 
32
- import { buildAliasIndex, candidateKey, normalizeCandidateName } from './extract.js'
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
- const targets: string[] = []
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
- const resolved = index ?? buildAliasIndex(cards)
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
- for (let at = target.indexOf('/'); at > 0; at = target.indexOf('/', at + 1)) {
291
- const hit = resolved.get(candidateKey({ type: target.slice(0, at), name: target.slice(at + 1) }))
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) {