dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-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 (180) hide show
  1. package/README.md +147 -19
  2. package/cordis.patch.yml +25 -10
  3. package/lib/client.js +6400 -3326
  4. package/lib/doc-page.js +7761 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +63 -11
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/convert/ingest.d.ts +7 -4
  9. package/lib/types/convert/ingest.d.ts.map +1 -1
  10. package/lib/types/convert/ingest.js +52 -14
  11. package/lib/types/convert/ingest.js.map +1 -1
  12. package/lib/types/convert/types.d.ts +2 -2
  13. package/lib/types/convert/types.d.ts.map +1 -1
  14. package/lib/types/convert/types.js +2 -2
  15. package/lib/types/convert/types.js.map +1 -1
  16. package/lib/types/diagnosis/cards.d.ts +8 -7
  17. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  18. package/lib/types/diagnosis/cards.js +119 -49
  19. package/lib/types/diagnosis/cards.js.map +1 -1
  20. package/lib/types/diagnosis/run.d.ts +57 -0
  21. package/lib/types/diagnosis/run.d.ts.map +1 -0
  22. package/lib/types/diagnosis/run.js +245 -0
  23. package/lib/types/diagnosis/run.js.map +1 -0
  24. package/lib/types/diagnosis/schema.d.ts +25 -2
  25. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  26. package/lib/types/diagnosis/schema.js +64 -25
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +19 -2
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +32 -42
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +187 -51
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +95 -25
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +297 -166
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +168 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js +1 -1
  43. package/lib/types/diagnosis/types.js.map +1 -1
  44. package/lib/types/document-page.d.ts +39 -0
  45. package/lib/types/document-page.d.ts.map +1 -0
  46. package/lib/types/document-page.js +43 -0
  47. package/lib/types/document-page.js.map +1 -0
  48. package/lib/types/extract-model.d.ts +67 -27
  49. package/lib/types/extract-model.d.ts.map +1 -1
  50. package/lib/types/extract-model.js +225 -97
  51. package/lib/types/extract-model.js.map +1 -1
  52. package/lib/types/extract.d.ts +16 -0
  53. package/lib/types/extract.d.ts.map +1 -1
  54. package/lib/types/extract.js +19 -2
  55. package/lib/types/extract.js.map +1 -1
  56. package/lib/types/fs-atomic.d.ts +11 -0
  57. package/lib/types/fs-atomic.d.ts.map +1 -0
  58. package/lib/types/fs-atomic.js +41 -0
  59. package/lib/types/fs-atomic.js.map +1 -0
  60. package/lib/types/index.d.ts.map +1 -1
  61. package/lib/types/index.js +466 -63
  62. package/lib/types/index.js.map +1 -1
  63. package/lib/types/intent/types.js +1 -1
  64. package/lib/types/intent/types.js.map +1 -1
  65. package/lib/types/knowledge-bases.d.ts +23 -12
  66. package/lib/types/knowledge-bases.d.ts.map +1 -1
  67. package/lib/types/knowledge-bases.js +37 -44
  68. package/lib/types/knowledge-bases.js.map +1 -1
  69. package/lib/types/layout.d.ts +3 -3
  70. package/lib/types/layout.d.ts.map +1 -1
  71. package/lib/types/layout.js +4 -4
  72. package/lib/types/layout.js.map +1 -1
  73. package/lib/types/migrate.d.ts +1 -1
  74. package/lib/types/migrate.js +2 -2
  75. package/lib/types/migrate.js.map +1 -1
  76. package/lib/types/model-text.d.ts +99 -0
  77. package/lib/types/model-text.d.ts.map +1 -0
  78. package/lib/types/model-text.js +103 -0
  79. package/lib/types/model-text.js.map +1 -0
  80. package/lib/types/present.d.ts +239 -0
  81. package/lib/types/present.d.ts.map +1 -0
  82. package/lib/types/present.js +255 -0
  83. package/lib/types/present.js.map +1 -0
  84. package/lib/types/recall-rank.d.ts +37 -0
  85. package/lib/types/recall-rank.d.ts.map +1 -1
  86. package/lib/types/recall-rank.js +41 -1
  87. package/lib/types/recall-rank.js.map +1 -1
  88. package/lib/types/runtime.d.ts +564 -31
  89. package/lib/types/runtime.d.ts.map +1 -1
  90. package/lib/types/runtime.js +2645 -292
  91. package/lib/types/runtime.js.map +1 -1
  92. package/lib/types/settings-schema.d.ts +4 -2
  93. package/lib/types/settings-schema.d.ts.map +1 -1
  94. package/lib/types/settings-schema.js +2 -0
  95. package/lib/types/settings-schema.js.map +1 -1
  96. package/lib/types/source-images.d.ts +118 -0
  97. package/lib/types/source-images.d.ts.map +1 -0
  98. package/lib/types/source-images.js +288 -0
  99. package/lib/types/source-images.js.map +1 -0
  100. package/lib/types/types.d.ts +356 -28
  101. package/lib/types/types.d.ts.map +1 -1
  102. package/lib/types/types.js +112 -18
  103. package/lib/types/types.js.map +1 -1
  104. package/lib/types/wiki-finalize.d.ts +140 -0
  105. package/lib/types/wiki-finalize.d.ts.map +1 -0
  106. package/lib/types/wiki-finalize.js +269 -0
  107. package/lib/types/wiki-finalize.js.map +1 -0
  108. package/lib/types/wiki-types.d.ts +10 -2
  109. package/lib/types/wiki-types.d.ts.map +1 -1
  110. package/lib/types/wiki-types.js +27 -30
  111. package/lib/types/wiki-types.js.map +1 -1
  112. package/package.json +4 -3
  113. package/preset/tita-data-agent/agent.cordis.yml +25 -10
  114. package/src/client/DirectoryPicker.tsx +12 -12
  115. package/src/client/DrawerSection.tsx +18 -0
  116. package/src/client/InfoHint.tsx +21 -8
  117. package/src/client/IntentTab.tsx +20 -26
  118. package/src/client/KnowledgeBaseTab.tsx +458 -158
  119. package/src/client/KnowledgePanel.tsx +44 -10
  120. package/src/client/KnowledgeStepRail.tsx +3 -0
  121. package/src/client/KnowledgeToolRow.tsx +331 -0
  122. package/src/client/NodeSettings.tsx +20 -22
  123. package/src/client/PreviewSections.tsx +178 -43
  124. package/src/client/RecallPanel.tsx +418 -127
  125. package/src/client/TypeDefinitionDrawer.tsx +154 -54
  126. package/src/client/WikiCardReview.tsx +266 -0
  127. package/src/client/WikiFlowCard.tsx +57 -95
  128. package/src/client/WikiTypeEditor.tsx +197 -95
  129. package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
  130. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  131. package/src/client/arch/model.ts +164 -26
  132. package/src/client/architecture.css +25 -10
  133. package/src/client/architecture.tsx +66 -41
  134. package/src/client/asset-images.ts +125 -0
  135. package/src/client/attribution/AttributionGraph.tsx +481 -34
  136. package/src/client/attribution/AttributionTab.tsx +501 -106
  137. package/src/client/attribution/CaseView.tsx +9 -14
  138. package/src/client/attribution/GraphStage.tsx +25 -18
  139. package/src/client/attribution/RunView.tsx +27 -9
  140. package/src/client/attribution/StageChrome.tsx +280 -140
  141. package/src/client/attribution/graph-options.ts +611 -47
  142. package/src/client/attribution/model.ts +559 -30
  143. package/src/client/convert/ConvertPanel.tsx +36 -4
  144. package/src/client/doc-page.css +134 -0
  145. package/src/client/doc-page.tsx +242 -0
  146. package/src/client/index.ts +18 -0
  147. package/src/client/panel-state.ts +105 -0
  148. package/src/client/problem-domains.ts +1 -1
  149. package/src/client/request.ts +36 -0
  150. package/src/client/styles.css +397 -54
  151. package/src/client/useFitScale.ts +15 -2
  152. package/src/client/wiki-type-groups.ts +86 -0
  153. package/src/convert/host.ts +68 -9
  154. package/src/convert/ingest.ts +61 -18
  155. package/src/convert/types.ts +2 -2
  156. package/src/diagnosis/cards.ts +120 -50
  157. package/src/diagnosis/run.ts +251 -0
  158. package/src/diagnosis/schema.ts +67 -26
  159. package/src/diagnosis/seed-source.ts +29 -4
  160. package/src/diagnosis/seed.ts +187 -85
  161. package/src/diagnosis/store.ts +310 -173
  162. package/src/diagnosis/types.ts +181 -16
  163. package/src/document-page.ts +51 -0
  164. package/src/extract-model.ts +254 -99
  165. package/src/extract.ts +35 -3
  166. package/src/fs-atomic.ts +41 -0
  167. package/src/index.ts +499 -62
  168. package/src/intent/types.ts +1 -1
  169. package/src/knowledge-bases.ts +50 -49
  170. package/src/layout.ts +4 -4
  171. package/src/migrate.ts +2 -2
  172. package/src/model-text.ts +126 -0
  173. package/src/present.ts +367 -0
  174. package/src/recall-rank.ts +57 -2
  175. package/src/runtime.ts +2802 -293
  176. package/src/settings-schema.ts +2 -0
  177. package/src/source-images.ts +326 -0
  178. package/src/types.ts +446 -45
  179. package/src/wiki-finalize.ts +310 -0
  180. package/src/wiki-types.ts +28 -32
@@ -16,6 +16,16 @@ import { useEffect, useState, type RefObject } from 'react'
16
16
  /** 窄栏下的两档。`fit` = 整图缩到容器,`actual` = 1:1(滚动查看)。 */
17
17
  export type ZoomMode = 'fit' | 'actual'
18
18
 
19
+ /**
20
+ * 适应哪一条轴。
21
+ *
22
+ * - `both`(默认):宽高都缩进容器,缩完就是容器的整块面积 —— 知识图谱那一栏用它。
23
+ * - `width`:只按宽度缩,高度允许溢出 —— 概览画布用它。概览是一块**自持视口**的画布
24
+ * (见 `architecture.css` 的 `.tita-archScrollStage`),纵向溢出由空格 + 拖拽平移看,
25
+ * 而不是把整块板缩到读不出字。
26
+ */
27
+ export type FitAxis = 'both' | 'width'
28
+
19
29
  export interface FitScale {
20
30
  scale: number
21
31
  /** 缩放后的板高;容器高度就用它(`fit` 档下正好没有可滚的余量)。 */
@@ -31,6 +41,7 @@ export interface FitScale {
31
41
  * @param enabled 容器不可见时不要量(隐藏容器尺寸为 0,算出来的比例是 0)
32
42
  * @param fullscreen 全屏时两个方向都按容器算;窄栏只有宽度可用
33
43
  * @param mode 缩放档
44
+ * @param fit 适应哪条轴;`width` 时高度永不算进比例(溢出交由平移)
34
45
  */
35
46
  export function useFitScale(
36
47
  scroller: RefObject<HTMLDivElement | null>,
@@ -39,6 +50,7 @@ export function useFitScale(
39
50
  enabled: boolean,
40
51
  fullscreen: boolean,
41
52
  mode: ZoomMode,
53
+ fit: FitAxis = 'both',
42
54
  ): FitScale {
43
55
  /**
44
56
  * 可视尺寸。用 `offsetWidth/Height` 而不是 `clientWidth/clientHeight`:后者会随
@@ -60,9 +72,10 @@ export function useFitScale(
60
72
  }, [enabled, fullscreen])
61
73
 
62
74
  // 窄栏只有宽度是可用的:高度由板高自己决定,拿它去算比例就成了一条回路,
63
- // 所以那里高度按设计高度算(即「只按宽度适应」)。
75
+ // 所以那里高度按设计高度算(即「只按宽度适应」)。`fit: 'width'` 把这条规则
76
+ // 从「窄栏的例外」提升成「这一栏的常态」—— 全屏下也不再把高度算进比例。
64
77
  const availW = viewport.w || contentW
65
- const availH = fullscreen ? (viewport.h || contentH) : contentH
78
+ const availH = fit === 'width' ? contentH : fullscreen ? (viewport.h || contentH) : contentH
66
79
  const scale = mode === 'actual' ? 1 : Math.min(1, availW / contentW, availH / contentH)
67
80
  return { scale, boardH: contentH * scale }
68
81
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Wiki 类型列表的**分组与排布计划**(0040 §4.3)。
3
+ *
4
+ * 抽成独立模块的理由是**它出过一次错,而且是静默的**:把「列表怎么摆」直接写在 JSX 的
5
+ * 三元里时,`prefix === ''` 那一支只渲染了 `members[0]` —— 于是六个没有斜杠的通用类型
6
+ * (concept / metric / table / event / playbook / case)在界面上只剩第一个,
7
+ * 另外五个**不见了**,而且没有任何报错。这里把「摆成几行、哪几个进折叠组」
8
+ * 变成一个可以单独断言的纯函数:它没有 React、没有样式、没有 DOM,输入是注册表、
9
+ * 输出是行清单。
10
+ *
11
+ * 三条规则(都是呈现层行为,不改注册表结构、不改 `entity_type`):
12
+ *
13
+ * 1. **没有斜杠的 id 各自平铺成一行。** 它们不构成一组 —— `concept` 与 `case` 之间
14
+ * 除了「都没有命名空间」之外没有任何共同点,把它们折成一组只是把六个类型藏进一个
15
+ * 名字叫空字符串的组里。
16
+ * 2. **同一个前缀的成员聚成一组,组内保持类型定义顺序**,组与组按前缀第一次出现的
17
+ * 位置排。注册表的顺序仍然是唯一真源,这里不重排、不排序。
18
+ * 3. **前缀下只有一个成员时不折叠**(折成只装一项的组,多一次点击、少一次直达),
19
+ * 那个成员直接平铺回一级列表。
20
+ */
21
+
22
+ export interface WikiTypeGroup<T> {
23
+ prefix: string
24
+ label: string
25
+ /** 组内保持注册表里的定义顺序。 */
26
+ members: T[]
27
+ }
28
+
29
+ /** 一行要画什么:一个平铺的类型,还是一个可折叠的组。 */
30
+ export type WikiTypeRow<T> =
31
+ | { kind: 'row'; type: T }
32
+ | { kind: 'group'; prefix: string; label: string; members: T[] }
33
+
34
+ /**
35
+ * 组名默认取前缀本身 —— 只有真正需要翻译的才写进这张表。
36
+ *
37
+ * 写死五个 `diagnosis/*` 的 id 是不行的:自定义类型也允许带斜杠,
38
+ * `diagnosis` 前缀下的类型是可增删的。
39
+ */
40
+ export const GROUP_LABELS: Record<string, string> = { diagnosis: '归因图谱' }
41
+
42
+ export const idPrefix = (id: string): string => {
43
+ const at = id.indexOf('/')
44
+ return at > 0 ? id.slice(0, at) : ''
45
+ }
46
+
47
+ export const groupLabel = (prefix: string): string => GROUP_LABELS[prefix] ?? prefix
48
+
49
+ /** 按 id 前缀把类型折成组。顺序 = 前缀第一次出现的位置。 */
50
+ export function groupWikiTypes<T extends { id: string }>(types: T[]): Array<WikiTypeGroup<T>> {
51
+ const groups: Array<WikiTypeGroup<T>> = []
52
+ const byPrefix = new Map<string, WikiTypeGroup<T>>()
53
+ for (const type of types) {
54
+ const prefix = idPrefix(type.id)
55
+ const existing = byPrefix.get(prefix)
56
+ if (existing) { existing.members.push(type); continue }
57
+ const created: WikiTypeGroup<T> = { prefix, label: groupLabel(prefix), members: [type] }
58
+ byPrefix.set(prefix, created)
59
+ groups.push(created)
60
+ }
61
+ return groups
62
+ }
63
+
64
+ /**
65
+ * 类型注册表 → 界面上的行清单。
66
+ *
67
+ * **不丢任何一个类型**是它的核心承诺:`rows` 里所有 `type` 加起来必须与输入一一对应。
68
+ * 测试钉住了这一条(见 `tests/wiki-type-groups.test.mjs`)—— 上一次出错时
69
+ * 界面少了五个类型,而所有既有测试都是绿的。
70
+ */
71
+ export function wikiTypeRows<T extends { id: string }>(types: T[]): Array<WikiTypeRow<T>> {
72
+ const rows: Array<WikiTypeRow<T>> = []
73
+ for (const group of groupWikiTypes(types)) {
74
+ // 没有命名空间的照旧平铺 —— **每一个成员各占一行**,不是只画第一个。
75
+ if (group.prefix === '') {
76
+ for (const type of group.members) rows.push({ kind: 'row', type })
77
+ continue
78
+ }
79
+ if (group.members.length === 1) {
80
+ rows.push({ kind: 'row', type: group.members[0]! })
81
+ continue
82
+ }
83
+ rows.push({ kind: 'group', prefix: group.prefix, label: group.label, members: group.members })
84
+ }
85
+ return rows
86
+ }
@@ -156,7 +156,7 @@ function send(res: ServerResponse, status: number, body: unknown): void {
156
156
  }
157
157
 
158
158
  /** Compact task view for tools — the full ingest payload never enters context. */
159
- function taskView(task: ConvertTask): Record<string, unknown> {
159
+ function taskView(kernel: ConvertService, task: ConvertTask): Record<string, unknown> {
160
160
  const manifest = task.manifest
161
161
  return {
162
162
  taskId: task.id,
@@ -171,6 +171,8 @@ function taskView(task: ConvertTask): Record<string, unknown> {
171
171
  error: task.error ?? null,
172
172
  warnings: task.warnings.map(warning => warning.code),
173
173
  attempts: task.attempts,
174
+ /** 与服务端 retry() 的守卫同源(`canRetryTask`):Agent 不该去点一枚注定失败的按钮。 */
175
+ canRetry: kernel.canRetry(task).ok,
174
176
  deliveryDir: task.deliveryDir,
175
177
  assetCount: manifest?.assets.length ?? 0,
176
178
  wordCount: manifest?.wordCount ?? 0,
@@ -307,10 +309,24 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
307
309
  config = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
308
310
  void resetService()
309
311
 
312
+ /**
313
+ * 写操作要用户点头。
314
+ *
315
+ * `convert_task` 是**一个混合工具**:`list` / `get` / `markdown` / `manifest` 只读,
316
+ * `cancel` / `retry` 会改队列。所以它不能整只进 `WRITE_TOOLS`(那会让「看一眼队列」
317
+ * 也要审批),也不能整只留在外面 —— 那样一个自称只读的工具就能取消别人的任务。
318
+ * 判据落在 `action` 上。
319
+ */
310
320
  ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
311
321
  if ((WRITE_TOOLS as readonly string[]).includes(exec.name)) {
312
322
  return Promise.resolve({ kind: 'ask', reason: '该操作会转换本地内容,并写入 Tita Knowledge Store,需要本轮用户审批。' })
313
323
  }
324
+ if (exec.name === 'convert_task') {
325
+ const action = (exec.arguments as { action?: unknown } | undefined)?.action
326
+ if (action === 'cancel' || action === 'retry') {
327
+ return Promise.resolve({ kind: 'ask', reason: `convert_task 的 ${String(action)} 会改动转换队列,需要本轮用户审批。` })
328
+ }
329
+ }
314
330
  return next()
315
331
  })
316
332
 
@@ -346,10 +362,11 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
346
362
 
347
363
  ctx.tools.register(tool(
348
364
  'convert_source',
349
- 'Convert a document (Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / text-layer PDF) or inline text into knowledge-ready Markdown + local images. Async kernel task; returns taskId, delivery dir and ingest hints (source_type candidates, external_id, content_hash). Write operation: DSH asks the user once before execution.',
365
+ 'Convert a document (Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / text-layer PDF), a web page / WeChat article (a bare URL, or a saved .html/.htm page), local audio-video-image, or inline text into knowledge-ready Markdown + local images. Async kernel task; returns taskId, delivery dir and ingest hints (source_type candidates, external_id, content_hash). Write operation: DSH asks the user once before execution.',
350
366
  {
351
- kind: { type: 'string', enum: ['file', 'text'], required: true },
367
+ kind: { type: 'string', enum: ['file', 'text', 'url'], required: true },
352
368
  path: { type: 'string' },
369
+ url: { type: 'string' },
353
370
  text: { type: 'string' },
354
371
  title: { type: 'string' },
355
372
  source_type: { type: 'string' },
@@ -373,6 +390,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
373
390
  kind,
374
391
  ...(candidates.length > 0 ? { sourceTypeCandidates: candidates } : {}),
375
392
  ...(typeof args.text === 'string' ? { text: args.text } : {}),
393
+ ...(typeof args.url === 'string' ? { url: args.url } : {}),
376
394
  ...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
377
395
  ...(typeof args.source_type === 'string' && args.source_type.trim() ? { sourceType: args.source_type.trim(), sourceTypeSource: 'agent' as const } : {}),
378
396
  ...(typeof args.adapter === 'string' && args.adapter.trim() ? { adapter: args.adapter.trim() } : {}),
@@ -399,10 +417,33 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
399
417
  if (!request.title) request.title = basename(absolute)
400
418
  }
401
419
 
420
+ /*
421
+ * 链接档**必须**在宿主侧就把地址检查掉。
422
+ *
423
+ * 内核的 `resolveAdapter` 见到 `kind=url` 会直接交给 web 适配器,而适配器只认
424
+ * `request.url` —— 少了这个字段,报出来的是一句和用户输入毫无关系的 `URL_REQUIRED`。
425
+ * 这里挡住并给出「你要给的是哪一种」。
426
+ */
427
+ if (kind === 'url') {
428
+ const raw = typeof args.url === 'string' ? args.url.trim() : ''
429
+ if (!raw) {
430
+ return failure({ taskId: null }, 'URL_REQUIRED', 'kind=url 必须提供 http(s) 地址 url。', false, '例如 https://mp.weixin.qq.com/s?__biz=...(公众号文章直接粘整条链接即可)。')
431
+ }
432
+ try {
433
+ const parsed = new URL(raw)
434
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
435
+ return failure({ taskId: null }, 'URL_INVALID', `只支持 http/https:${parsed.protocol}`, false, '本机文件请用 kind=file + path。')
436
+ }
437
+ } catch {
438
+ return failure({ taskId: null }, 'URL_INVALID', `不是合法的地址:${raw}`, false, '整条链接一起粘过来,包含 https:// 前缀。')
439
+ }
440
+ request.url = raw
441
+ }
442
+
402
443
  const submitted = await kernel.submit(request)
403
444
  const shouldWait = args.wait !== false
404
445
  const finished = shouldWait ? await waitForTask(kernel, submitted.id, config.toolWaitMs) : submitted
405
- const view = taskView(finished ?? submitted)
446
+ const view = taskView(kernel, finished ?? submitted)
406
447
  return success({
407
448
  ...view,
408
449
  waited: shouldWait,
@@ -413,7 +454,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
413
454
 
414
455
  ctx.tools.register(tool(
415
456
  'convert_task',
416
- 'Inspect the conversion queue: list, get, cancel, retry, or read the delivered Markdown/manifest of a task. Read-only.',
457
+ 'Inspect and steer the conversion queue. list / get / markdown / manifest are read-only; cancel and retry change the queue (both need user approval). retry only works on a failed or cancelled task whose staged input is still in tmp/ — a delivered task cannot be retried, because its input is cleaned up when it is delivered; submit the file again instead.',
417
458
  {
418
459
  action: { type: 'string', enum: ['list', 'get', 'cancel', 'retry', 'markdown', 'manifest'], required: true },
419
460
  task_id: { type: 'string' },
@@ -425,12 +466,30 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
425
466
  const action = String(args.action)
426
467
  if (action === 'list') {
427
468
  const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(100, Number(args.limit))) : 20
428
- return success({ tasks: kernel.list().slice(0, limit).map(taskView) })
469
+ return success({ tasks: kernel.list().slice(0, limit).map(task => taskView(kernel, task)) })
429
470
  }
430
471
  const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : ''
431
472
  if (!taskId) return failure({ taskId: null }, 'TASK_ID_REQUIRED', `${action} 需要 task_id。`)
432
- if (action === 'cancel') return success(taskView(kernel.cancel(taskId)))
433
- if (action === 'retry') return success(taskView(await kernel.retry(taskId)))
473
+ /*
474
+ * `cancel` / `retry` **会改队列**,所以它们不把 `AdapterError` 抛出去让宿主兜,
475
+ * 而是把 code 与 suggestion 如实放进 envelope:Agent 读到的应该是「为什么不能」
476
+ * 与「那该怎么办」(例如:已交付的任务要**重新提交文件**,而不是重试 ——
477
+ * 它的输入在交付后就清理了)。
478
+ */
479
+ if (action === 'cancel' || action === 'retry') {
480
+ try {
481
+ return success(taskView(kernel, action === 'cancel' ? kernel.cancel(taskId) : await kernel.retry(taskId)))
482
+ } catch (cause) {
483
+ const error = cause as { code?: unknown; message?: unknown; suggestion?: unknown }
484
+ return failure(
485
+ { taskId },
486
+ typeof error.code === 'string' ? error.code : 'CONVERT_TASK_FAILED',
487
+ typeof error.message === 'string' ? error.message : String(cause),
488
+ false,
489
+ typeof error.suggestion === 'string' ? error.suggestion : undefined,
490
+ )
491
+ }
492
+ }
434
493
  if (action === 'markdown') {
435
494
  const file = await kernel.readDeliveryFile(taskId, 'index.md')
436
495
  if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 index.md(未成功或已被清理)。')
@@ -443,7 +502,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
443
502
  }
444
503
  const task = kernel.get(taskId)
445
504
  if (!task) return failure({ taskId }, 'TASK_NOT_FOUND', `任务不存在:${taskId}`)
446
- return success(taskView(task))
505
+ return success(taskView(kernel, task))
447
506
  },
448
507
  ))
449
508
 
@@ -25,7 +25,7 @@ import { cp, mkdir, stat } from 'node:fs/promises'
25
25
  import { basename, join, resolve, sep } from 'node:path'
26
26
  import type { ConvertService, IngestPayload } from 'dsh-convert-core'
27
27
  import type { LineageContext } from 'dsh-data-contracts/refs'
28
- import type { SourceType, SourceTypeDef } from '../types.js'
28
+ import type { SourceProvenance, SourceType, SourceTypeDef } from '../types.js'
29
29
  import {
30
30
  failure,
31
31
  KNOWLEDGE_ASSET_DIR,
@@ -48,6 +48,8 @@ export interface IngestTarget {
48
48
  metadata?: Record<string, unknown>
49
49
  sourceId?: string
50
50
  lineage?: LineageContext
51
+ /** OKF v0.2 溯源信息(0048):写成 front matter 的顶层键,不塞进 metadata。 */
52
+ provenance?: SourceProvenance
51
53
  }): Promise<{
52
54
  ok: boolean
53
55
  data: { sourceId: string; path: string }
@@ -98,7 +100,7 @@ export interface IngestOutcome {
98
100
  sourceTypeSupplied: boolean
99
101
  sourceTypeCandidates: string[]
100
102
  assets: PlacedAsset[]
101
- /** Path to the source file relative to the knowledge root, e.g. `01_source_truth/x.md`. */
103
+ /** Path to the source file relative to the knowledge root, e.g. `01_source_raw/x.md`. */
102
104
  relativeSourcePath?: string
103
105
  cardHints: CardHints
104
106
  notes: string[]
@@ -111,8 +113,9 @@ export interface CardHints {
111
113
  title: string
112
114
  summary: string
113
115
  /**
114
- * Card body with image references already re-based to `02_wiki/<entityType>/`,
115
- * so the first image actually reaches the multimodal embedder.
116
+ * Card body with image references re-based to the canonical
117
+ * knowledge-root-relative form (`01_source_raw/_assets/<bucket>/<file>`), so
118
+ * the first image actually reaches the multimodal embedder.
116
119
  */
117
120
  contentWithImages: string
118
121
  /** The image references as they must appear inside a card body. */
@@ -137,12 +140,17 @@ function rewriteImageRefs(markdown: string, mapping: Map<string, string>): strin
137
140
  return result
138
141
  }
139
142
 
140
- function cardImageRef(hash: string, rel: string, entityType: string): string {
141
- // A card lives at 02_wiki/<entityType>/<slug>.md; the asset lives at
142
- // 01_source_truth/_assets/<hash>/<file>.
143
- const depth = 2
144
- const up = Array.from({ length: depth }, () => '..').join('/')
145
- return `${up}/${KNOWLEDGE_SOURCE_DIR}/${KNOWLEDGE_ASSET_DIR}/${hash}/${basename(rel)}`
143
+ /**
144
+ * 图片引用写成**知识库根相对**的规范形:`01_source_raw/_assets/<bucket>/<file>`。
145
+ *
146
+ * 为什么不写成「相对引用它的那个文件」:同一份资产会被三处引用 —— SourceRaw 在
147
+ * `01_source_raw/<type>/`、卡片在 `02_wiki/<type>/`、还有面板拼出来的 URL。写死
148
+ * 「往上两级」在深度相同的地方凑巧成立,换个深度就错,而且错了不会报错,只会让
149
+ * 向量编码器读到不存在的文件(0047 实机:`FileNotFoundError: …/02_wiki/concept/_assets/…`)。
150
+ * 根相对只有一种解释,运行时按 {@link assetRefCandidates} 解析。
151
+ */
152
+ function assetImageRef(hash: string, rel: string): string {
153
+ return `${KNOWLEDGE_SOURCE_DIR}/${KNOWLEDGE_ASSET_DIR}/${hash}/${basename(rel)}`
146
154
  }
147
155
 
148
156
  export function buildCardHints(input: {
@@ -152,12 +160,11 @@ export function buildCardHints(input: {
152
160
  assetMapping: Map<string, string>
153
161
  entityType?: string
154
162
  }): CardHints {
155
- const entityType = input.entityType && ENTITY_TYPES.includes(input.entityType) ? input.entityType : 'concept'
156
163
  const content = rewriteImageRefs(
157
164
  input.payload.content,
158
- new Map([...input.assetMapping].map(([from, to]) => [from, cardImageRef(input.assetBucket, to, entityType)])),
165
+ new Map([...input.assetMapping].map(([from, to]) => [from, assetImageRef(input.assetBucket, to)])),
159
166
  )
160
- const imageRefs = input.payload.assets.map(asset => cardImageRef(input.assetBucket, asset.rel, entityType))
167
+ const imageRefs = input.payload.assets.map(asset => assetImageRef(input.assetBucket, asset.rel))
161
168
  const summary = input.payload.content
162
169
  .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
163
170
  .replace(/[#>*`_\-[\]()]/g, ' ')
@@ -249,9 +256,17 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
249
256
 
250
257
  const sourceContent = rewriteImageRefs(
251
258
  payload.content,
252
- new Map([...assetMapping].map(([from, to]) => [from, `${KNOWLEDGE_ASSET_DIR}/${bucket}/${basename(to)}`])),
259
+ new Map([...assetMapping].map(([from, to]) => [from, assetImageRef(bucket, to)])),
253
260
  )
254
261
 
262
+ /**
263
+ * `metadata` 只剩**类型专属的扩展键**(0048)。
264
+ *
265
+ * 原先链接、解析器、格式、哈希、资产清单全挤在这一个 JSON 字符串里 —— 人读不了、
266
+ * `grep` 不到,等于没有元数据。OKF v0.2 的那一组(`description` / `resource` /
267
+ * `generated` / `sources[]`)现在走 `provenance`,写成真正的顶层键;这里留下的是
268
+ * OKF 没有对应概念、但下游确实要用的运行信息。
269
+ */
255
270
  const metadata: Record<string, unknown> = {
256
271
  ...(options.metadata ?? {}),
257
272
  source_format: payload.sourceFormat,
@@ -259,9 +274,10 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
259
274
  content_hash: payload.contentHash,
260
275
  convert_task_id: taskId,
261
276
  convert_contract_version: 1,
262
- ...(payload.sourceUrl ? { source_url: payload.sourceUrl } : {}),
263
- ...(payload.sourcePath ? { source_path: payload.sourcePath } : {}),
264
- assets: placed.map(asset => `${KNOWLEDGE_ASSET_DIR}/${bucket}/${basename(asset.rel)}`),
277
+ ...(payload.original
278
+ ? { original: { rel: payload.original.rel, bytes: payload.original.bytes, sha256: payload.original.sha256 } }
279
+ : {}),
280
+ assets: placed.map(asset => assetImageRef(bucket, asset.rel)),
265
281
  }
266
282
 
267
283
  const sourceType = options.sourceType?.trim() || 'document'
@@ -273,6 +289,29 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
273
289
  const title = options.title?.trim() || payload.title
274
290
  const externalId = payload.externalId
275
291
 
292
+ /**
293
+ * OKF v0.2 的溯源信息。
294
+ *
295
+ * `generated.at` 就是**采集时间**:OKF 里没有「抓取时间」这个概念,而「这份文件
296
+ * 什么时候被谁写出来的」正好是它。`sources[]` 记的是这份文件的来源 —— 对网页就是
297
+ * 那个页面本身(地址、标题、作者、发布时间),对上传的文件就是文件名。
298
+ */
299
+ const provenance: SourceProvenance = {
300
+ ...(payload.document?.description ? { description: payload.document.description } : {}),
301
+ ...(payload.sourceUrl ? { resource: payload.sourceUrl } : {}),
302
+ generatedBy: `convert/${payload.parser}`,
303
+ generatedAt: new Date().toISOString(),
304
+ sources: [{
305
+ id: 'origin',
306
+ resource: payload.sourceUrl ?? payload.original?.rel ?? payload.sourcePath ?? title,
307
+ title,
308
+ ...(payload.document?.siteName ? { author: payload.document.siteName } : {}),
309
+ ...(!payload.document?.siteName && payload.document?.author ? { author: payload.document.author } : {}),
310
+ ...(payload.document?.publishedAt ? { lastModified: payload.document.publishedAt } : {}),
311
+ }],
312
+ }
313
+
314
+
276
315
  if (!options.planOnly) {
277
316
  const result = await options.runtime.addSource({
278
317
  sourceType: sourceType as SourceType,
@@ -280,6 +319,7 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
280
319
  content: sourceContent,
281
320
  externalId,
282
321
  metadata,
322
+ provenance,
283
323
  ...(options.lineage ? { lineage: options.lineage as LineageContext } : {}),
284
324
  })
285
325
 
@@ -297,7 +337,10 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
297
337
  const sourceId = result.data.sourceId
298
338
  const sourcePath = result.data.path
299
339
  const cardHints = buildCardHints({ payload, sourceId, assetBucket: bucket, assetMapping })
300
- notes.push('SourceRaw 已写入;图片要进入多模态召回,还需要一张引用它们的 wiki 卡片(见 cardHints)。')
340
+ // 0046 起编译会把来源里的图**自动**挂进对应卡片正文(按「图前那段话」定位),
341
+ // 所以 cardHints 不再是让图进多模态召回的**唯一**办法,只是「想立刻手工建一张图卡」
342
+ // 时的现成素材。
343
+ notes.push('SourceRaw 已写入;图片已随正文落地,下一次编译会把它们挂到对应卡片上(cardHints 是手工建图卡时的现成素材)。')
301
344
 
302
345
  return success<IngestOutcome>(
303
346
  {
@@ -12,9 +12,9 @@ import { join } from 'node:path'
12
12
  export const CONVERT_SETTINGS_NAMESPACE = 'tita-data-agent-convert'
13
13
 
14
14
  /** Knowledge store layout the convert seam cooperates with (issue 0013). */
15
- export const KNOWLEDGE_SOURCE_DIR = '01_source_truth'
15
+ export const KNOWLEDGE_SOURCE_DIR = '01_source_raw'
16
16
  export const KNOWLEDGE_WIKI_DIR = '02_wiki'
17
- /** Sub-directory inside `01_source_truth` that holds converted images. */
17
+ /** Sub-directory inside `01_source_raw` that holds converted images. */
18
18
  export const KNOWLEDGE_ASSET_DIR = '_assets'
19
19
 
20
20
  export interface ConvertPluginConfig {