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
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type ReactNode } from 'react'
2
2
  import {
3
3
  AlertCircle, Check, Copy, Download, ExternalLink, FileText, FileUp, Inbox,
4
- Package, RefreshCw, RotateCcw, ScanEye, X,
4
+ Link, Package, RefreshCw, RotateCcw, ScanEye, X,
5
5
  } from '@tita-data-agent/ui/icons'
6
6
  import type { LucideIcon } from '@tita-data-agent/ui/icons'
7
7
  import { Badge, Button, Input, Label, SelectMenu, Switch, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
@@ -121,6 +121,7 @@ const STATE_META: Record<TaskSummary['state'], { label: string; tone: 'neutral'
121
121
  const KIND_META: Record<string, { label: string; icon: LucideIcon }> = {
122
122
  file: { label: '文件', icon: FileText },
123
123
  text: { label: '文本', icon: FileUp },
124
+ url: { label: '链接', icon: Link },
124
125
  }
125
126
 
126
127
  function formatBytes(bytes: number | undefined): string {
@@ -148,7 +149,7 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
148
149
  const [error, setError] = useState<string | null>(null)
149
150
  const [notice, setNotice] = useState<string | null>(null)
150
151
  const [busy, setBusy] = useState(false)
151
- const [form, setForm] = useState({ kind: 'upload', path: '', text: '', title: '', sourceType: '', adapter: '' })
152
+ const [form, setForm] = useState({ kind: 'upload', path: '', url: '', text: '', title: '', sourceType: '', adapter: '' })
152
153
  const [uploadFile, setUploadFile] = useState<File | null>(null)
153
154
  const [ingestForm, setIngestForm] = useState({ sourceType: '', title: '', planOnly: false })
154
155
 
@@ -222,6 +223,13 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
222
223
  headers: { 'content-type': 'application/json' },
223
224
  body: JSON.stringify({ kind: 'file', local_path: form.path.trim(), ...common }),
224
225
  })
226
+ } else if (form.kind === 'url') {
227
+ if (!form.url.trim()) throw new Error('粘贴一条 http(s) 链接。')
228
+ payload = await requestJson(`${BASE}/api/tasks`, {
229
+ method: 'POST',
230
+ headers: { 'content-type': 'application/json' },
231
+ body: JSON.stringify({ kind: 'url', url: form.url.trim(), ...common }),
232
+ })
225
233
  } else {
226
234
  payload = await requestJson(`${BASE}/api/tasks`, {
227
235
  method: 'POST',
@@ -288,7 +296,7 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
288
296
  `- 标题:${hints.title}`,
289
297
  `- 正文必须使用下面这些图片相对路径(相对 02_wiki/<entity_type>/ 解析):`,
290
298
  ...hints.imageRefs.map(ref => ` - ${ref}`),
291
- '调用 write_knowledge_card 时把图片引用原样写进 content。',
299
+ '调用 propose_wiki_cards 时把图片引用原样写进 content(正文只读,之后改不了,所以一次写对)。',
292
300
  ].join('\n')
293
301
  }, [ingest])
294
302
 
@@ -361,7 +369,12 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
361
369
  <Button variant="ghost" size="sm" title="详情" onClick={() => void openTask(task)}><ScanEye size={13} /></Button>
362
370
  {task.state === 'running' || task.state === 'queued'
363
371
  ? <Button variant="ghost" size="sm" title="取消" onClick={() => void act(task.id, 'cancel')}><X size={13} /></Button>
364
- : <Button variant="ghost" size="sm" title="重试" onClick={() => void act(task.id, 'retry')}><RotateCcw size={13} /></Button>}
372
+ : task.state === 'failed' || task.state === 'cancelled'
373
+ // 只有这两个状态还能重试。已交付的任务**输入在交付后就清理了**,
374
+ // 给它一枚重试按钮,点了只会得到 TASK_ALREADY_DELIVERED,
375
+ // 而修复之前那一次点击还会顺手删掉刚交付的产物。
376
+ ? <Button variant="ghost" size="sm" title="重试" onClick={() => void act(task.id, 'retry')}><RotateCcw size={13} /></Button>
377
+ : null}
365
378
  {task.state === 'succeeded' && (
366
379
  <a className="tita-uiButton" href={`${BASE}/api/tasks/${encodeURIComponent(task.id)}/files/index.md`} target="_blank" rel="noreferrer" title="打开 index.md">
367
380
  <Download size={13} />
@@ -385,6 +398,7 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
385
398
  aria-label="输入类型"
386
399
  options={[
387
400
  { value: 'upload', label: '上传文件' },
401
+ { value: 'url', label: '网页链接' },
388
402
  { value: 'file', label: '文档(本机路径)' },
389
403
  { value: 'text', label: '内联文本' },
390
404
  ]}
@@ -424,6 +438,24 @@ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
424
438
  </label>
425
439
  )}
426
440
 
441
+ {form.kind === 'url' && (
442
+ <label className={styles.field}>
443
+ <span className={styles.fieldLabel}>网页链接</span>
444
+ <Input
445
+ type="url"
446
+ inputMode="url"
447
+ spellCheck={false}
448
+ value={form.url}
449
+ placeholder="https://mp.weixin.qq.com/s?__biz=…"
450
+ onChange={(event: ChangeEvent<HTMLInputElement>) => setForm({ ...form, url: event.target.value })}
451
+ />
452
+ <span className={styles.fieldHint}>
453
+ 微信公众号文章直接粘整条链接。只抓这一个地址(不跟进页面里的链接),正文里的图片会下载成本地
454
+ assets/,远程地址不会留在产物里。正文由 JS 渲染的站点抓不到,会明确报错并给出替代做法。
455
+ </span>
456
+ </label>
457
+ )}
458
+
427
459
  <div className={styles.grid2}>
428
460
  <label className={styles.field}>
429
461
  <span className={styles.fieldLabel}>标题(可选)</span>
@@ -0,0 +1,134 @@
1
+ /*
2
+ * 独立整页(SourceRaw 全文预览)的页面样式。
3
+ *
4
+ * 它是一份**单独的浏览器文档**:没有 DSH 外壳、没有侧栏的 380px 宽度限制,唯一的
5
+ * 目的是把一份很长的原文读下来。所以这里的排版决定都围绕阅读:固定一列约 72ch 的
6
+ * 字号宽度、行高 1.75、顶部一条常驻的窄条回答「我在读什么、读到哪儿了」。
7
+ *
8
+ * 颜色全部走共享令牌(`@tita-data-agent/ui/styles.css` 带进来的那一套),元素排版走
9
+ * `@tita-data-agent/ui/markdown.css`。这一页不引入任何 Tailwind 工具类:它要能在一个
10
+ * 没有 DSH 模块表的文档里自足,样式越少越不容易在别处漂移。
11
+ */
12
+
13
+ :root { color-scheme: light; }
14
+
15
+ html { -webkit-text-size-adjust: 100%; }
16
+
17
+ body {
18
+ margin: 0;
19
+ background: var(--tita-canvas);
20
+ color: var(--tita-ink);
21
+ font-family: var(--tita-font);
22
+ font-size: 15px;
23
+ line-height: 1.75;
24
+ }
25
+
26
+ /* 常驻窄条:长文档滚到一半时,「这是哪一份、还有多少」必须还在视野里。 */
27
+ .docPage-bar {
28
+ position: sticky;
29
+ top: 0;
30
+ z-index: 2;
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 10px;
34
+ padding: 9px max(24px, calc(50vw - 380px));
35
+ border-bottom: 1px solid var(--tita-line);
36
+ background: color-mix(in srgb, var(--tita-surface) 92%, transparent);
37
+ backdrop-filter: blur(6px);
38
+ }
39
+
40
+ .docPage-bar h1 {
41
+ min-width: 0;
42
+ flex: 1 1 auto;
43
+ margin: 0;
44
+ overflow: hidden;
45
+ font-size: 14px;
46
+ font-weight: 680;
47
+ letter-spacing: -0.01em;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ }
51
+
52
+ .docPage-barMeta { flex: 0 0 auto; color: var(--tita-ink-muted); font-size: 11.5px; }
53
+
54
+ .docPage-main { padding: 26px 24px 96px; }
55
+
56
+ /* 正文一列:65–75ch 是阅读行长的区间,中文按 15px 算约 40 个汉字换成 760px 上限。 */
57
+ .docPage-column { max-width: 760px; margin: 0 auto; }
58
+
59
+ .docPage-head { margin-bottom: 22px; }
60
+
61
+ .docPage-head h2 {
62
+ margin: 0 0 6px;
63
+ font-size: 22px;
64
+ font-weight: 700;
65
+ letter-spacing: -0.02em;
66
+ line-height: 1.3;
67
+ }
68
+
69
+ .docPage-headMeta { margin: 0; color: var(--tita-ink-muted); font-size: 12.5px; }
70
+ /* 路径与 id 是**逐字比对**的值:等宽,别和正文混成一个字体。 */
71
+ .docPage-headMeta code {
72
+ font-family: var(--tita-mono);
73
+ font-size: 0.92em;
74
+ }
75
+ .docPage-headMeta code:last-child { word-break: break-all; }
76
+
77
+ /* 分页读取:每一段自己是一个渲染单元(见 doc-page.tsx 的理由)。段与段之间靠
78
+ 首个子元素的上外边距接上 —— `markdown.css` 把每段第一个子元素的上边距清零了
79
+ (那是给抽屉顶部用的),所以第二段起要把它还回来,否则接缝处会贴在一起。 */
80
+ .docPage-segment + .docPage-segment .tita-markdownLocal > :first-child { margin-top: 1.2em; }
81
+
82
+ .docPage-foot {
83
+ display: flex;
84
+ flex-direction: column;
85
+ align-items: center;
86
+ gap: 8px;
87
+ margin-top: 34px;
88
+ padding-top: 18px;
89
+ border-top: 1px solid var(--tita-line);
90
+ }
91
+
92
+ .docPage-progress { color: var(--tita-ink-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
93
+
94
+ .docPage-more {
95
+ height: 32px;
96
+ padding: 0 16px;
97
+ border: 1px solid var(--tita-line-strong);
98
+ border-radius: 999px;
99
+ background: var(--tita-surface);
100
+ color: var(--tita-ink);
101
+ font: inherit;
102
+ font-size: 13px;
103
+ font-weight: 600;
104
+ cursor: pointer;
105
+ }
106
+
107
+ .docPage-more:hover { border-color: var(--tita-primary); color: var(--tita-primary); }
108
+ .docPage-more:disabled { opacity: 0.55; cursor: default; }
109
+ .docPage-more:focus-visible { outline: 3px solid var(--tita-focus-ring); outline-offset: 1px; }
110
+
111
+ /* 自动加载的哨兵:它没有内容,只是「滚到这里就取下一页」的位置标记。 */
112
+ .docPage-sentinel { height: 1px; }
113
+
114
+ .docPage-state {
115
+ display: flex;
116
+ flex-direction: column;
117
+ align-items: flex-start;
118
+ gap: 8px;
119
+ max-width: 760px;
120
+ margin: 0 auto;
121
+ color: var(--tita-ink-muted);
122
+ font-size: 14px;
123
+ }
124
+
125
+ .docPage-state h2 { margin: 0; color: var(--tita-ink); font-size: 17px; font-weight: 680; }
126
+ .docPage-state p { margin: 0; line-height: 1.7; }
127
+ .docPage-state code {
128
+ padding: 0.12em 0.36em;
129
+ border: 1px solid var(--tita-line);
130
+ border-radius: 4px;
131
+ background: var(--tita-surface-muted);
132
+ font-family: var(--tita-mono);
133
+ font-size: 0.88em;
134
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * SourceRaw 的**独立整页**:在一个新标签里读一份原文的全文。
3
+ *
4
+ * 为什么需要它(而不是把抽屉加宽):抽屉只有 380px,而原文可能是一份几十万字的转换
5
+ * 文档。抽屉因此只发前 `SOURCE_PREVIEW_CHARS`(4000)个字符 —— 那是预览,不是阅读。
6
+ * 要读全文就得有一个**为阅读建的**页面:整页宽度、常驻的标题条、一列 65–75ch 的行长。
7
+ *
8
+ * 三个决定:
9
+ *
10
+ * 1. **分页取,不一次取全文**。「打开要等半天」的成因就是把整份正文一次发过来;
11
+ * 这里一次只要 20000 字符,滚到底再取下一页(哨兵自动取,按钮是它的可见形态与
12
+ * 无障碍通路)。所以这份页面第一次可读的时间与文档大小**无关**。
13
+ * 2. **每页各自是一个渲染单元**,不把累计文本拼成一份再渲染。拼起来等于每翻一页
14
+ * 都把前面重渲染一遍(O(n²) 的渲染开销);分开渲染让每页的成本只和这一页有关,
15
+ * 代价是分页接缝可能落在一个块中间 —— 服务端已经把切点对齐到空行。
16
+ * 3. **它没有 DSH 模块表**:一个普通浏览器文档,所以正文走共享的本地子集渲染器
17
+ * (`@tita-data-agent/ui/markdown`),而不是面板里那个平台原子。这也是渲染器
18
+ * 必须有共享一份的原因。
19
+ *
20
+ * 入口地址:`/knowledge/documents/<docId>/view?kb=<知识库 id>`(外壳由宿主渲染,
21
+ * 见 `standaloneDocumentHtml()`)。
22
+ */
23
+
24
+ import { StrictMode, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
25
+ import { createRoot } from 'react-dom/client'
26
+ import { LocalMarkdown } from '@tita-data-agent/ui/markdown'
27
+ import '@tita-data-agent/ui/styles.css'
28
+ import '@tita-data-agent/ui/markdown.css'
29
+ import './doc-page.css'
30
+ import { formatBytes } from './format.ts'
31
+ import { imageResolverOf, type AssetRef } from './asset-images.ts'
32
+
33
+ /** 一次取多少字符。与抽屉的预览上限同量级:一页 ≈ 一次能读完的一屏多一点。 */
34
+ const PAGE_CHARS = 20000
35
+
36
+ interface DocumentMeta {
37
+ docId: string
38
+ title: string
39
+ sourceType: string
40
+ sourceTypeKnown?: boolean
41
+ sourceFormat: string
42
+ parser: string
43
+ bytes: number
44
+ contentChars: number
45
+ contentTruncated: boolean
46
+ relativePath: string
47
+ ext: string
48
+ updatedAt: string
49
+ assets: AssetRef[]
50
+ }
51
+
52
+ interface ContentPage {
53
+ text: string
54
+ offset: number
55
+ nextOffset: number
56
+ totalChars: number
57
+ eof: boolean
58
+ }
59
+
60
+ interface Envelope<T> { ok?: boolean; data?: T; error?: { message?: string; suggestion?: string } }
61
+
62
+ async function requestJson<T>(url: string): Promise<T> {
63
+ const response = await fetch(url)
64
+ const text = await response.text()
65
+ if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
66
+ let payload: Envelope<T>
67
+ try { payload = JSON.parse(text) as Envelope<T> } catch { throw new Error(`服务端返回了无效响应(HTTP ${response.status})`) }
68
+ if (!response.ok || payload.ok === false) throw new Error(payload.error?.message ?? `请求失败(${response.status})`)
69
+ if (payload.data === undefined) throw new Error('服务端没有返回内容')
70
+ return payload.data
71
+ }
72
+
73
+ /** 这个页面只服务一条地址形状;不匹配时如实说。 */
74
+ function readTarget(): { docId: string; kb: string } | undefined {
75
+ const match = /^\/knowledge\/documents\/([^/]+)\/view$/.exec(window.location.pathname)
76
+ if (match === null) return undefined
77
+ let docId = match[1] as string
78
+ try { docId = decodeURIComponent(docId) } catch { /* 不是合法的转义就按原样用 */ }
79
+ return { docId, kb: new URLSearchParams(window.location.search).get('kb') ?? '' }
80
+ }
81
+
82
+ /*
83
+ * 图片解析搬去了 `asset-images.ts`(0047)。
84
+ *
85
+ * 独立整页与面板抽屉问的是同一个问题:「正文里这个相对引用对应哪张真图」。两处各写
86
+ * 一遍,结果就是整页能看图、抽屉看不到图 —— 而抽屉才是默认入口。现在只有一份实现,
87
+ * 这里只管把它的返回值喂给 `LocalMarkdown`。
88
+ */
89
+
90
+ function App(): ReactNode {
91
+ const target = useMemo(readTarget, [])
92
+ const [phase, setPhase] = useState<'loading' | 'ready' | 'error'>('loading')
93
+ const [failure, setFailure] = useState<string>('')
94
+ const [meta, setMeta] = useState<DocumentMeta | null>(null)
95
+ const [segments, setSegments] = useState<string[]>([])
96
+ const [shown, setShown] = useState(0)
97
+ const [totalChars, setTotalChars] = useState(0)
98
+ const [eof, setEof] = useState(false)
99
+ const [busy, setBusy] = useState(false)
100
+ const [pageError, setPageError] = useState<string | null>(null)
101
+ const [reloadToken, setReloadToken] = useState(0)
102
+
103
+ /** 取到哪儿了 / 有没有在取 —— 观察器回调与按钮都可能触发,用 ref 挡住重复请求。 */
104
+ const offsetRef = useRef(0)
105
+ const busyRef = useRef(false)
106
+ const sentinelRef = useRef<HTMLDivElement | null>(null)
107
+
108
+ const loadPage = useCallback(async (from: number): Promise<void> => {
109
+ if (target === undefined || busyRef.current) return
110
+ busyRef.current = true
111
+ setBusy(true)
112
+ setPageError(null)
113
+ try {
114
+ const query = new URLSearchParams({ kb: target.kb, offset: String(from), limit: String(PAGE_CHARS) })
115
+ const page = await requestJson<ContentPage>(`/knowledge/documents/${encodeURIComponent(target.docId)}/content?${query.toString()}`)
116
+ if (page.text !== '') setSegments(previous => [...previous, page.text])
117
+ offsetRef.current = page.nextOffset
118
+ setShown(page.nextOffset)
119
+ setTotalChars(page.totalChars)
120
+ setEof(page.eof)
121
+ } catch (error) {
122
+ setPageError(error instanceof Error ? error.message : String(error))
123
+ } finally {
124
+ busyRef.current = false
125
+ setBusy(false)
126
+ }
127
+ }, [target])
128
+
129
+ /** 第一页:先把详情读来(标题、大小、资产),再取正文第一页。 */
130
+ useEffect(() => {
131
+ if (target === undefined) { setPhase('error'); return undefined }
132
+ let cancelled = false
133
+ offsetRef.current = 0
134
+ setPhase('loading'); setFailure('')
135
+ setSegments([]); setShown(0); setTotalChars(0); setEof(false); setPageError(null)
136
+ void (async () => {
137
+ try {
138
+ const query = new URLSearchParams(target.kb === '' ? {} : { kb: target.kb })
139
+ const payload = await requestJson<{ document: DocumentMeta }>(`/knowledge/documents/${encodeURIComponent(target.docId)}?${query.toString()}`)
140
+ if (cancelled) return
141
+ setMeta(payload.document)
142
+ setTotalChars(payload.document.contentChars)
143
+ // 浏览器标签页的标题:整页是拿来和别的东西并排看的,标签上得认得出是哪一份。
144
+ window.document.title = `${payload.document.title} · 原文`
145
+ setPhase('ready')
146
+ await loadPage(0)
147
+ } catch (error) {
148
+ if (cancelled) return
149
+ setFailure(error instanceof Error ? error.message : String(error))
150
+ setPhase('error')
151
+ }
152
+ })()
153
+ return () => { cancelled = true }
154
+ }, [target, loadPage, reloadToken])
155
+
156
+ /**
157
+ * 滚到底自动取下一页。
158
+ *
159
+ * `rootMargin` 提前 600px 触发,所以正常阅读不会看到「正在读」这一行 —— 按钮仍然
160
+ * 留在页脚,它是这条通路的**可见形态**:没有 JS 观察器的环境(或用户就是想手动翻)
161
+ * 也能读完,键盘用户 Tab 得到它。
162
+ */
163
+ useEffect(() => {
164
+ const node = sentinelRef.current
165
+ if (node === null || phase !== 'ready' || eof) return undefined
166
+ const observer = new IntersectionObserver(
167
+ entries => { if (entries.some(entry => entry.isIntersecting)) void loadPage(offsetRef.current) },
168
+ { rootMargin: '600px 0px' },
169
+ )
170
+ observer.observe(node)
171
+ return () => observer.disconnect()
172
+ }, [phase, eof, loadPage, segments.length])
173
+
174
+ const resolveImage = useMemo(() => imageResolverOf(meta?.assets ?? []), [meta])
175
+
176
+ if (target === undefined) {
177
+ return <main className="docPage-main"><div className="docPage-state">
178
+ <h2>这个地址不是一份文档的整页</h2>
179
+ <p>整页的地址形状是 <code>/knowledge/documents/&lt;docId&gt;/view?kb=&lt;知识库 id&gt;</code>。
180
+ 它由知识库面板里「原文 → 内容 → 新页面打开」打开,不需要手写。</p>
181
+ </div></main>
182
+ }
183
+
184
+ if (phase === 'loading') {
185
+ return <main className="docPage-main"><p className="docPage-state" role="status">正在读取这份原文…</p></main>
186
+ }
187
+
188
+ if (phase === 'error' || meta === null) {
189
+ return <main className="docPage-main"><div className="docPage-state">
190
+ <h2>没能读到这份原文</h2>
191
+ <p>文档 id:<code>{target.docId}</code></p>
192
+ {failure !== '' && <p role="alert">{failure}</p>}
193
+ <button type="button" className="docPage-more" onClick={() => setReloadToken(token => token + 1)}>重试</button>
194
+ </div></main>
195
+ }
196
+
197
+ const metaLine = [
198
+ meta.sourceTypeKnown === false ? `${meta.sourceType}(未知分类)` : meta.sourceType,
199
+ meta.ext,
200
+ meta.sourceFormat || 'unknown',
201
+ formatBytes(meta.bytes),
202
+ `${meta.contentChars.toLocaleString()} 字符`,
203
+ ].join(' · ')
204
+
205
+ return <>
206
+ <header className="docPage-bar">
207
+ <h1 title={meta.title}>{meta.title}</h1>
208
+ <span className="docPage-barMeta">
209
+ {eof ? '已到末尾' : `已显示 ${shown.toLocaleString()} / ${totalChars.toLocaleString()} 字符`}
210
+ </span>
211
+ </header>
212
+ <main className="docPage-main">
213
+ <div className="docPage-column">
214
+ <div className="docPage-head">
215
+ <h2>{meta.title}</h2>
216
+ <p className="docPage-headMeta">{metaLine} · <code>{meta.relativePath}</code></p>
217
+ </div>
218
+
219
+ {segments.map((text, index) => <div className="docPage-segment" key={`segment-${index}`}>
220
+ <LocalMarkdown text={text} {...(resolveImage === undefined ? {} : { resolveImage })} />
221
+ </div>)}
222
+
223
+ <div className="docPage-foot">
224
+ <p className="docPage-progress" role="status">
225
+ {pageError !== null
226
+ ? `这一页没取到:${pageError}`
227
+ : eof
228
+ ? `已到文档末尾 · 共 ${totalChars.toLocaleString()} 字符`
229
+ : `已显示 ${shown.toLocaleString()} / ${totalChars.toLocaleString()} 字符`}
230
+ </p>
231
+ {!eof && <button type="button" className="docPage-more" disabled={busy} onClick={() => void loadPage(offsetRef.current)}>
232
+ {pageError !== null ? '重试这一页' : busy ? '正在读…' : '加载更多'}
233
+ </button>}
234
+ <div className="docPage-sentinel" ref={sentinelRef} aria-hidden="true" />
235
+ </div>
236
+ </div>
237
+ </main>
238
+ </>
239
+ }
240
+
241
+ const container = document.getElementById('tita-document-root')
242
+ if (container !== null) createRoot(container).render(<StrictMode><App /></StrictMode>)
@@ -8,6 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'
8
8
  import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
9
9
  import { createElement, useEffect, useState, type ReactNode } from 'react'
10
10
  import { KnowledgePanel } from './KnowledgePanel.tsx'
11
+ import { KnowledgeToolRow, KNOWLEDGE_TOOL_KEYS } from './KnowledgeToolRow.tsx'
11
12
  import type { SettingsScopeLike } from './NodeSettings.tsx'
12
13
  import { groupScope } from './config-scope.ts'
13
14
  import { type AgentPresetRemote } from './AgentPresetCard.tsx'
@@ -95,4 +96,21 @@ export function apply(ctx: ClientContext): void {
95
96
  const injected: TabInjected = { ...settings, remote: (ctx as unknown as { remote?: AgentPresetRemote }).remote ?? {} }
96
97
  ctx.effect(() => ctx.sidebarRightTabs.register(definition()), 'tita knowledge: tab type')
97
98
  ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: ID }, props => Tab(props, injected))), 'tita knowledge: tab body')
99
+ /**
100
+ * 知识工具在**对话流**里的卡片(0045)。
101
+ *
102
+ * 按**工具名**命中(`key: '<tool name>'`):命中就替换平台那条通用行 ——
103
+ * 通用行只显示「工具名 + 一坨 JSON」,而人要看的是「导了几份、编出几张卡、
104
+ * 命中哪几条」。数据来自宿主侧的 `presentationMeta`(见 `src/present.ts`),
105
+ * 它随会话日志落盘,所以回放与当时一致。
106
+ *
107
+ * 五个工具名字来自 `KNOWLEDGE_TOOL_KEYS`:一处声明,注册与测试共用一份,
108
+ * 免得「加了工具忘了加卡片」只能靠翻代码发现。
109
+ */
110
+ for (const key of KNOWLEDGE_TOOL_KEYS) {
111
+ ctx.effect(
112
+ () => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key }, props => createElement(KnowledgeToolRow, props))),
113
+ `tita knowledge: tool row ${key}`,
114
+ )
115
+ }
98
116
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 「上次看到哪儿」:一级 tab、知识库分栏、选中的知识库。
3
+ *
4
+ * ## 为什么它们必须活在 React 之外
5
+ *
6
+ * DSH 会在若干动作之后重新挂载右侧面板 —— 刷新页面必然,换会话按宿主的保留策略也会,
7
+ * 而 `useState` 随手就没了。用户看到的是**每次切回来都跳回「Agent 概览」**
8
+ * (2026-09-26 GUI 反馈原话)。兄弟插件 `dsh-data-table` 的 `store.ts` 记着同一条结论,
9
+ * 它记的是上次打开的工作簿;这里记的是三个位置。
10
+ *
11
+ * ## 三条约定
12
+ *
13
+ * 1. **读出来的值必须校验。** 存的是上一版写下的字符串时(tab 改名、知识库被删、
14
+ * 手改过 localStorage),面板不能因此走进一个不存在的位置。所以带 `valid` 的那个读法
15
+ * 对着注册表核对,不认识就返回 `null`,由调用方用它自己的默认值 —— 校验放在调用方,
16
+ * 因为**注册表在那边**(`VIEW_ORDER` / `KNOWLEDGE_TABS`),这个模块不认识它们。
17
+ * 2. **存储是尽力而为。** 锁死 localStorage 的浏览器里退化成「这个页面生命周期内有效」:
18
+ * 每个值同时留一份内存副本,内存优先,全程不抛。
19
+ * 3. **key 只在这里拼。** 三处各写一次字符串,改名时就会漏掉一处,然后表现为
20
+ * 「有的记住了、有的没记住」。
21
+ *
22
+ * 这个模块**不 import 任何东西**(连 React 都不 import),所以它可以被单独打包进 node
23
+ * 里跑真实现 —— `tests/panel-state.test.mjs` 就是这么测的。
24
+ *
25
+ * @module
26
+ */
27
+
28
+ /**
29
+ * 被记住的位置 → localStorage key。
30
+ *
31
+ * `dsh.` 前缀与兄弟插件一致(`dsh.data-sheet.lastWorkbookId`),便于在浏览器里
32
+ * 「哪些是本产品写的」一眼可辨;中间那段是插件名,避免不同插件撞 key。
33
+ */
34
+ const KEYS = {
35
+ view: 'dsh.data-domain-knowledge.activeView',
36
+ knowledgeTab: 'dsh.data-domain-knowledge.knowledgeTab',
37
+ knowledgeBase: 'dsh.data-domain-knowledge.activeBase',
38
+ } as const
39
+
40
+ export type RememberedSlot = keyof typeof KEYS
41
+
42
+ /** 内存副本:localStorage 不可用时的兜底,同时也避免每次读都碰存储。 */
43
+ const cache = new Map<RememberedSlot, string>()
44
+
45
+ function storage(): Storage | null {
46
+ try {
47
+ return globalThis.localStorage ?? null
48
+ } catch {
49
+ // 某些浏览器在「禁止所有 Cookie」时连读 `localStorage` 这个属性都会抛。
50
+ return null
51
+ }
52
+ }
53
+
54
+ function readRaw(slot: RememberedSlot): string | null {
55
+ const cached = cache.get(slot)
56
+ if (cached !== undefined) return cached === '' ? null : cached
57
+ let raw: string | null = null
58
+ try {
59
+ raw = storage()?.getItem(KEYS[slot]) ?? null
60
+ } catch {
61
+ raw = null
62
+ }
63
+ cache.set(slot, raw ?? '')
64
+ return raw === null || raw === '' ? null : raw
65
+ }
66
+
67
+ /**
68
+ * 读一个记住的值,并且**只认 `valid` 里的成员**。
69
+ *
70
+ * @param valid - 允许的取值,来自那个东西自己的注册表(tab 顺序表、分栏表)。传空数组
71
+ * 等于「什么都不要」—— 那时返回 `null`,调用方用默认值。
72
+ */
73
+ export function readRemembered<T extends string>(slot: RememberedSlot, valid: readonly T[]): T | null {
74
+ const raw = readRaw(slot)
75
+ if (raw === null) return null
76
+ return (valid as readonly string[]).includes(raw) ? (raw as T) : null
77
+ }
78
+
79
+ /**
80
+ * 读一个**没有注册表可校验**的记住值(目前只有知识库 id —— 它来自服务端,是数据不是枚举)。
81
+ * 拿不到就返回空串,调用方按「没有偏好」处理。
82
+ */
83
+ export function readRememberedId(slot: RememberedSlot): string {
84
+ return readRaw(slot) ?? ''
85
+ }
86
+
87
+ /** 记住一个值。写失败只影响「换个页面还记得不记得」,不影响本次会话。 */
88
+ export function remember(slot: RememberedSlot, value: string): void {
89
+ if (cache.get(slot) === value) return
90
+ cache.set(slot, value)
91
+ try {
92
+ if (value === '') storage()?.removeItem(KEYS[slot])
93
+ else storage()?.setItem(KEYS[slot], value)
94
+ } catch {
95
+ // 写不进去就用内存那份;它已经更新过了。
96
+ }
97
+ }
98
+
99
+ /** 测试用:忘掉内存副本,让下一次读重新问存储。 */
100
+ export function forgetRememberedCache(): void {
101
+ cache.clear()
102
+ }
103
+
104
+ /** 测试用:这个模块会写哪些 key。把它暴露出来,测试才能断言不撞别的插件。 */
105
+ export const REMEMBERED_KEYS: Readonly<Record<RememberedSlot, string>> = KEYS
@@ -80,7 +80,7 @@ export const PROBLEM_DOMAINS: ProblemDomain[] = [
80
80
  industry: 'ThoughtSpot SpotIQ 的 Change Analysis 做跨周期、跨维度对比并回答「变了什么、为什么变」,配合 anomaly alert 告警;SmartBI 用指标树 + 维度贡献度分解;经典算法为 Adtributor / HotSpot / iDice。',
81
81
  status: { label: '设计中', tone: 'warning' },
82
82
  actions: [
83
- { label: '三路召回', node: 'recall' },
83
+ { label: '两路召回', node: 'recall' },
84
84
  { label: '上下文包', node: 'context' },
85
85
  { label: '排查图谱(0021)' },
86
86
  ],
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 插件面板与宿主路由之间的**那一层薄封装**:把信封拆成「成功的数据」或一个能读的错误。
3
+ *
4
+ * 它存在的理由是**错误信息必须带 suggestion**:服务端返回的 `suggestion` 是「下一步该
5
+ * 做什么」(去哪个设置、先调哪个工具),丢掉它之后界面上只剩一句错误码,而用户没有
6
+ * 别的办法知道该往哪走。
7
+ *
8
+ * 它原来是 `KnowledgeBaseTab.tsx` 里的一个局部函数。抽出来是因为第二个消费者出现了
9
+ * (`ProposalReview.tsx` 的待办队列),而抄第二份的下场是两处的错误处理迟早不一致 ——
10
+ * 而「同一个失败在两屏上读起来不一样」正是最让人怀疑系统状态的那种不一致。
11
+ */
12
+
13
+ export interface Envelope<T> {
14
+ ok?: boolean
15
+ data?: T
16
+ warnings?: string[]
17
+ error?: { code?: string; message?: string; suggestion?: string } | null
18
+ }
19
+
20
+ export async function requestJson<T>(url: string, init?: RequestInit): Promise<Envelope<T>> {
21
+ const response = await fetch(url, init)
22
+ const text = await response.text()
23
+ if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
24
+ let payload: Envelope<T>
25
+ try {
26
+ payload = JSON.parse(text) as Envelope<T>
27
+ } catch {
28
+ throw new Error(`服务端返回了无效响应(HTTP ${response.status})`)
29
+ }
30
+ const error = payload.error
31
+ if (!response.ok || payload.ok === false) {
32
+ const detail = [error?.code, error?.message].filter(Boolean).join(' · ') || `请求失败(${response.status})`
33
+ throw new Error(error?.suggestion ? `${detail} — ${error.suggestion}` : detail)
34
+ }
35
+ return payload
36
+ }