dsh-data-domain-knowledge 0.1.0-alpha.3 → 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 (148) hide show
  1. package/README.md +126 -22
  2. package/cordis.patch.yml +25 -16
  3. package/lib/client.js +2144 -1571
  4. package/lib/doc-page.js +40 -19
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +27 -2
  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.js +2 -2
  17. package/lib/types/diagnosis/run.d.ts +7 -1
  18. package/lib/types/diagnosis/run.d.ts.map +1 -1
  19. package/lib/types/diagnosis/run.js +8 -2
  20. package/lib/types/diagnosis/run.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.js +1 -1
  22. package/lib/types/diagnosis/seed.d.ts +1 -1
  23. package/lib/types/diagnosis/seed.js +1 -1
  24. package/lib/types/diagnosis/store.d.ts +13 -31
  25. package/lib/types/diagnosis/store.d.ts.map +1 -1
  26. package/lib/types/diagnosis/store.js +33 -71
  27. package/lib/types/diagnosis/store.js.map +1 -1
  28. package/lib/types/diagnosis/types.d.ts +1 -1
  29. package/lib/types/diagnosis/types.js +1 -1
  30. package/lib/types/extract-model.d.ts +67 -27
  31. package/lib/types/extract-model.d.ts.map +1 -1
  32. package/lib/types/extract-model.js +225 -97
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/extract.d.ts +16 -0
  35. package/lib/types/extract.d.ts.map +1 -1
  36. package/lib/types/extract.js +19 -2
  37. package/lib/types/extract.js.map +1 -1
  38. package/lib/types/fs-atomic.d.ts +3 -4
  39. package/lib/types/fs-atomic.d.ts.map +1 -1
  40. package/lib/types/fs-atomic.js +3 -4
  41. package/lib/types/fs-atomic.js.map +1 -1
  42. package/lib/types/index.d.ts.map +1 -1
  43. package/lib/types/index.js +302 -89
  44. package/lib/types/index.js.map +1 -1
  45. package/lib/types/intent/types.js +1 -1
  46. package/lib/types/intent/types.js.map +1 -1
  47. package/lib/types/knowledge-bases.d.ts +13 -10
  48. package/lib/types/knowledge-bases.d.ts.map +1 -1
  49. package/lib/types/knowledge-bases.js +18 -27
  50. package/lib/types/knowledge-bases.js.map +1 -1
  51. package/lib/types/layout.d.ts +3 -3
  52. package/lib/types/layout.d.ts.map +1 -1
  53. package/lib/types/layout.js +4 -4
  54. package/lib/types/layout.js.map +1 -1
  55. package/lib/types/migrate.d.ts +1 -1
  56. package/lib/types/migrate.js +2 -2
  57. package/lib/types/migrate.js.map +1 -1
  58. package/lib/types/model-text.d.ts +99 -0
  59. package/lib/types/model-text.d.ts.map +1 -0
  60. package/lib/types/model-text.js +103 -0
  61. package/lib/types/model-text.js.map +1 -0
  62. package/lib/types/present.d.ts +239 -0
  63. package/lib/types/present.d.ts.map +1 -0
  64. package/lib/types/present.js +255 -0
  65. package/lib/types/present.js.map +1 -0
  66. package/lib/types/recall-rank.d.ts +37 -0
  67. package/lib/types/recall-rank.d.ts.map +1 -1
  68. package/lib/types/recall-rank.js +41 -1
  69. package/lib/types/recall-rank.js.map +1 -1
  70. package/lib/types/runtime.d.ts +419 -78
  71. package/lib/types/runtime.d.ts.map +1 -1
  72. package/lib/types/runtime.js +2104 -334
  73. package/lib/types/runtime.js.map +1 -1
  74. package/lib/types/settings-schema.d.ts +2 -0
  75. package/lib/types/settings-schema.d.ts.map +1 -1
  76. package/lib/types/settings-schema.js +2 -0
  77. package/lib/types/settings-schema.js.map +1 -1
  78. package/lib/types/source-images.d.ts +118 -0
  79. package/lib/types/source-images.d.ts.map +1 -0
  80. package/lib/types/source-images.js +288 -0
  81. package/lib/types/source-images.js.map +1 -0
  82. package/lib/types/types.d.ts +316 -39
  83. package/lib/types/types.d.ts.map +1 -1
  84. package/lib/types/types.js +57 -11
  85. package/lib/types/types.js.map +1 -1
  86. package/lib/types/wiki-finalize.d.ts +140 -0
  87. package/lib/types/wiki-finalize.d.ts.map +1 -0
  88. package/lib/types/wiki-finalize.js +269 -0
  89. package/lib/types/wiki-finalize.js.map +1 -0
  90. package/lib/types/wiki-types.d.ts +2 -1
  91. package/lib/types/wiki-types.d.ts.map +1 -1
  92. package/lib/types/wiki-types.js +13 -7
  93. package/lib/types/wiki-types.js.map +1 -1
  94. package/package.json +3 -3
  95. package/preset/tita-data-agent/agent.cordis.yml +25 -16
  96. package/src/client/DrawerSection.tsx +18 -0
  97. package/src/client/KnowledgeBaseTab.tsx +294 -104
  98. package/src/client/KnowledgePanel.tsx +4 -3
  99. package/src/client/KnowledgeToolRow.tsx +331 -0
  100. package/src/client/PreviewSections.tsx +115 -34
  101. package/src/client/RecallPanel.tsx +418 -127
  102. package/src/client/TypeDefinitionDrawer.tsx +82 -16
  103. package/src/client/WikiCardReview.tsx +266 -0
  104. package/src/client/WikiFlowCard.tsx +57 -95
  105. package/src/client/WikiTypeEditor.tsx +6 -20
  106. package/src/client/arch/AgentArchitectureGraph.tsx +22 -135
  107. package/src/client/architecture.css +22 -27
  108. package/src/client/architecture.tsx +12 -12
  109. package/src/client/asset-images.ts +125 -0
  110. package/src/client/attribution/AttributionTab.tsx +4 -10
  111. package/src/client/attribution/StageChrome.tsx +8 -24
  112. package/src/client/convert/ConvertPanel.tsx +30 -3
  113. package/src/client/doc-page.tsx +6 -27
  114. package/src/client/index.ts +18 -0
  115. package/src/client/problem-domains.ts +1 -1
  116. package/src/client/styles.css +80 -11
  117. package/src/convert/host.ts +27 -2
  118. package/src/convert/ingest.ts +61 -18
  119. package/src/convert/types.ts +2 -2
  120. package/src/diagnosis/cards.ts +2 -2
  121. package/src/diagnosis/run.ts +8 -2
  122. package/src/diagnosis/seed-source.ts +1 -1
  123. package/src/diagnosis/seed.ts +1 -1
  124. package/src/diagnosis/store.ts +32 -79
  125. package/src/diagnosis/types.ts +2 -2
  126. package/src/extract-model.ts +254 -99
  127. package/src/extract.ts +35 -3
  128. package/src/fs-atomic.ts +3 -4
  129. package/src/index.ts +338 -88
  130. package/src/intent/types.ts +1 -1
  131. package/src/knowledge-bases.ts +31 -32
  132. package/src/layout.ts +4 -4
  133. package/src/migrate.ts +2 -2
  134. package/src/model-text.ts +126 -0
  135. package/src/present.ts +367 -0
  136. package/src/recall-rank.ts +57 -2
  137. package/src/runtime.ts +2282 -374
  138. package/src/settings-schema.ts +2 -0
  139. package/src/source-images.ts +326 -0
  140. package/src/types.ts +354 -47
  141. package/src/wiki-finalize.ts +310 -0
  142. package/src/wiki-types.ts +13 -7
  143. package/lib/types/proposals.d.ts +0 -166
  144. package/lib/types/proposals.d.ts.map +0 -1
  145. package/lib/types/proposals.js +0 -238
  146. package/lib/types/proposals.js.map +0 -1
  147. package/src/client/ProposalReview.tsx +0 -313
  148. package/src/proposals.ts +0 -337
@@ -156,7 +156,7 @@ function EvalTab({ visible, settings, onSelect }: { visible: boolean; settings?:
156
156
  <p className={styles.cardDescription}>评测集接入前,这一栏只声明要评什么;左边的单问题检查已经是真实链路。</p>
157
157
  <div className={styles.evalRows}>
158
158
  <div><span>编译正确性</span><strong>待运行</strong></div>
159
- <div><span>三路 Hybrid Recall@10</span><strong>待运行</strong></div>
159
+ <div><span>Hybrid Recall@10(关键词 + 向量)</span><strong>待运行</strong></div>
160
160
  <div><span>引用可追溯性</span><strong>待运行</strong></div>
161
161
  <div><span>多模态 Markdown</span><strong>待运行</strong></div>
162
162
  <div><span>增量编译复用率</span><strong>待运行</strong></div>
@@ -311,8 +311,9 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
311
311
  折叠区里 —— 三块内容、两条边框,读起来是三个东西。现在画布是四条带
312
312
  (A/B/C 三条链路 + 一条问题域带),点节点或问题域牌子都在**画布内**的
313
313
  浮层里展开详情:不占侧栏、不缩字、可关闭。
314
- 0042:这一栏改走「舞台」版式,画布不再是一张会自动收缩的卡片 ——
315
- 它占满可视高度、只按宽度适应,纵向溢出靠空格 + 拖拽平移。
314
+ 0050:这一栏是**一块带发丝线的画布,整图居中、不可拖拽** ——
315
+ 两个方向都适应容器,所以「图被拖到一边」这种状态不再存在;
316
+ 1:1 档放不下时由容器自己出滚动条。
316
317
  设置不在这里:面板上唯一的设置入口在顶部 tab 栏最右边的齿轮里。 */}
317
318
  <AgentArchitectureGraph
318
319
  stats={stats} version={version} selected={selected}
@@ -0,0 +1,331 @@
1
+ /**
2
+ * 知识工具在**对话流**里的卡片(0045)。
3
+ *
4
+ * ## 为什么要有它
5
+ *
6
+ * 0045 之前,`import_sources` / `recall_knowledge` 这类调用在对话里只有两样东西:工具名
7
+ * 和一坨 JSON。人看到的是「import_sources」加一屏花括号,于是「Agent 到底导了几份、
8
+ * 编出了几张卡、召回命中哪几条」只能自己去右边面板里找 —— 而右侧面板压根不知道
9
+ * 刚才那一次调用做了什么。
10
+ *
11
+ * 现在这一层注册的是平台的 `tool.call.toolview` 槽位,按**工具名**命中
12
+ * (`key: 'recall_knowledge'`),命中即**替换**默认的通用行。数据有两个来源:
13
+ *
14
+ * 1. **参数**(`block.argsRaw` / `phase === 'start'`):跑起来的那一刻就能显示
15
+ * 「导入 12 份来源并编译」,不必等结果;
16
+ * 2. **`block.meta`**:宿主侧 `presentationMeta` 产出的结构化账本(见 `src/present.ts`),
17
+ * 随会话日志落盘,所以**回放时与当时看到的一模一样**。
18
+ *
19
+ * ## 三条实现约束
20
+ *
21
+ * - 卡片长在**对话区**里,不是右侧面板里:排版密度、字号跟着对话走(13px / 24px 行高),
22
+ * 不套面板那套 11px 的小字。颜色仍然读同一份共享令牌(`--tita-*` 挂在 `:root, body`
23
+ * 上),所以深浅色主题自动一致。
24
+ * - **不猜**。meta 缺字段就少显示一行,不编造 0 或「未知」。宿主没给 meta(旧会话、或
25
+ * 调用在复合传输里跑的)时退回默认通用行 —— 由 `undefined` 返回触发。
26
+ * - 参数解析失败**不抛**:一条工具记录因为渲染报错整块消失,比显示得不好看严重得多。
27
+ */
28
+
29
+ import type { ReactNode } from 'react'
30
+ import {
31
+ ChevronRight, Database, FileText, Layers3, RefreshCw, Search, TriangleAlert, X,
32
+ } from '@tita-data-agent/ui/icons'
33
+ import { Badge, cn } from '@tita-data-agent/ui/components'
34
+
35
+ /** 本插件注册了专用卡片的工具名 —— 与 `src/index.ts` 里的注册一一对应。 */
36
+ export const KNOWLEDGE_TOOL_KEYS = ['import_sources', 'recall_knowledge', 'rebuild_knowledge', 'add_source', 'propose_wiki_cards'] as const
37
+
38
+ /**
39
+ * 槽位 props 的**结构性子集**。
40
+ *
41
+ * 平台把 `tool.call.toolview` 的完整类型声明在 `@deepseek-ai/dsh-client-ui-tool/client`,
42
+ * 而那个包不在本插件的依赖里(客户端只 inject 了 sidebar-right / slots / renderer /
43
+ * settings 四个)。为一个**纯类型**去加一条 peer dependency,代价是发布面与安装面都变大,
44
+ * 而这里真正用到的只有五个字段。
45
+ *
46
+ * 所以:自己声明这五个字段 + 把槽位登记进 `SlotMap`(下面那段 `declare module`),
47
+ * 形状以平台那份声明为准(`ToolCallCommonProps` + 三个阶段 props)。
48
+ * **平台那份一旦改名,这里会编译不过** —— 那正是我们想要的失败方式。
49
+ */
50
+ export interface KnowledgeToolViewProps {
51
+ /** 三个阶段二选一:`preparing` 连参数都没有。 */
52
+ phase: 'preparing' | 'start' | 'result'
53
+ /** 平台派发用的工具名(本卡片的键)。 */
54
+ toolName: string
55
+ callId: string
56
+ block: {
57
+ /** 只有 `start` 阶段有完整参数。 */
58
+ argsRaw?: string
59
+ /** 结果阶段:模型可见的内容块。 */
60
+ content?: readonly unknown[]
61
+ isError?: boolean
62
+ /** 宿主 `presentationMeta` 落下来的账本。 */
63
+ meta?: unknown
64
+ }
65
+ /** 平台提供的展开状态钩子:**每个调用各自持有**,切轮次会被重置。 */
66
+ useDisclosure: () => { readonly expanded: boolean; readonly setExpanded: (open: boolean) => void; readonly toggle: () => void }
67
+ /** 在轨迹视图里定位这次调用(可能没有)。 */
68
+ inspect?: () => void
69
+ }
70
+
71
+ /**
72
+ * 把槽位登记进平台的 `SlotMap`。
73
+ *
74
+ * 平台自己的声明在 `dsh-client-ui-tool/client` 里;这里补一份**同样的形状**,因为那个包
75
+ * 不在本插件的类型可见范围内。运行时不受影响 —— 派发按名字字符串走,这段只在编译期
76
+ * 让 `slots.register({ name: 'tool.call.toolview' })` 通过检查。
77
+ */
78
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
79
+ interface SlotMap {
80
+ 'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: KnowledgeToolViewProps }
81
+ }
82
+ }
83
+
84
+ /** meta 里那本账的最小形状。所有字段都可缺:旧会话、失败信封都可能是残的。 */
85
+ interface ToolMeta {
86
+ kind?: string
87
+ // 导入
88
+ attempted?: number
89
+ written?: Array<{ sourceId?: string; title?: string; bytes?: number }>
90
+ failed?: Array<{ index?: number; title?: string; code?: string; message?: string }>
91
+ compile?: { cardsCreated?: number; cardsUpdated?: number; cardsReused?: number; notesReused?: number; notesRecompiled?: number; sourcesExtracted?: number; sourcesSkipped?: number; embedding?: string; version?: number } | null
92
+ // 召回
93
+ query?: string
94
+ hits?: Array<{ entityId?: string; entityType?: string; title?: string; score?: number; rerankScore?: number; reviewScore?: number }>
95
+ contextTopN?: number
96
+ backend?: string
97
+ routes?: string[]
98
+ rerankStatus?: string
99
+ insufficientEvidence?: boolean
100
+ // 编译
101
+ only?: string
102
+ version?: number
103
+ cardsCreated?: number
104
+ cardsUpdated?: number
105
+ cardsReused?: number
106
+ notesReused?: number
107
+ notesRecompiled?: number
108
+ sourcesExtracted?: number
109
+ sourcesSkipped?: number
110
+ embedding?: string
111
+ skipped?: boolean
112
+ // 写卡
113
+ ids?: string[]
114
+ // 单份来源
115
+ sourceId?: string
116
+ path?: string
117
+ title?: string
118
+ sourceType?: string
119
+ bytes?: number
120
+ rejected?: Array<{ title?: string; reason?: string; code?: string }>
121
+ }
122
+
123
+ /** 参数:`phase === 'preparing'` 时还没有,别去猜。 */
124
+ function argsOf(block: unknown): Record<string, any> {
125
+ const raw = (block as { argsRaw?: unknown } | undefined)?.argsRaw
126
+ if (typeof raw !== 'string' || raw.trim() === '') return {}
127
+ try {
128
+ const parsed = JSON.parse(raw)
129
+ return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : {}
130
+ } catch {
131
+ return {}
132
+ }
133
+ }
134
+
135
+ /** 模型可见的文本结果 —— 结果阶段之外用它兜底(也用于错误态)。 */
136
+ function textOf(block: unknown): string {
137
+ const content = (block as { content?: unknown } | undefined)?.content
138
+ if (!Array.isArray(content)) return ''
139
+ return content
140
+ .map(part => (part && typeof part === 'object' && (part as { type?: string }).type === 'text' ? String((part as { text?: unknown }).text ?? '') : ''))
141
+ .filter(Boolean)
142
+ .join('\n')
143
+ }
144
+
145
+ function short(text: string, max = 60): string {
146
+ const flat = text.replace(/\s+/g, ' ').trim()
147
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat
148
+ }
149
+
150
+ /** 标题与摘要:**先写这一下要动什么**,结果回来之后再补上产物。 */
151
+ function headOf(toolName: string, args: Record<string, any>, meta: ToolMeta | null): { icon: ReactNode; title: string; summary: string } {
152
+ const icon = (size = 14): ReactNode => {
153
+ switch (toolName) {
154
+ case 'recall_knowledge': return <Search size={size} />
155
+ case 'import_sources': return <Database size={size} />
156
+ case 'rebuild_knowledge': return <RefreshCw size={size} />
157
+ case 'propose_wiki_cards': return <Layers3 size={size} />
158
+ default: return <FileText size={size} />
159
+ }
160
+ }
161
+ if (toolName === 'recall_knowledge') {
162
+ const query = short(String(meta?.query ?? args?.query ?? ''), 36)
163
+ const hits = meta?.hits?.length
164
+ const summary = meta?.insufficientEvidence
165
+ ? '没有内容越过相关性阈值'
166
+ : meta
167
+ ? [hits !== undefined ? `命中 ${hits} 条` : '', meta.backend ? `${meta.backend}` : '', meta.contextTopN !== undefined ? `进上下文 ${meta.contextTopN} 条` : ''].filter(Boolean).join(' · ')
168
+ : '正在检索…'
169
+ return { icon: icon(), title: `召回「${query}」`, summary }
170
+ }
171
+ if (toolName === 'import_sources') {
172
+ const count = Array.isArray(args?.sources) ? args.sources.length : 0
173
+ const summary = meta
174
+ ? [
175
+ `写入 ${meta.written?.length ?? 0} 份`,
176
+ meta.failed && meta.failed.length > 0 ? `失败 ${meta.failed.length} 份` : '',
177
+ meta.compile ? `新建 ${meta.compile.cardsCreated ?? 0} 张卡 · 并入 ${meta.compile.cardsUpdated ?? 0} 张` : '未编译',
178
+ ].filter(Boolean).join(' · ')
179
+ : '正在写入并编译…'
180
+ return { icon: icon(), title: `导入 ${count} 份来源${args?.compile === false ? '' : '并编译'}`, summary }
181
+ }
182
+ if (toolName === 'rebuild_knowledge') {
183
+ const only = String(meta?.only ?? args?.only ?? 'all')
184
+ const what = only === 'all' ? '全链路' : only === 'wiki' ? 'wiki 与 wikiNotes' : only === 'embeddings' ? '向量索引' : '注册表'
185
+ const summary = meta
186
+ ? meta.skipped
187
+ ? '没有变化,全部复用'
188
+ : `新建 ${meta.cardsCreated ?? 0} · 并入 ${meta.cardsUpdated ?? 0} · 复用 ${meta.cardsReused ?? 0}${meta.version !== undefined ? ` · v${meta.version}` : ''}`
189
+ : '正在编译…'
190
+ return { icon: icon(), title: `编译知识库 · ${what}`, summary }
191
+ }
192
+ if (toolName === 'propose_wiki_cards') {
193
+ const count = Array.isArray(args?.proposals) ? args.proposals.length : 0
194
+ const summary = meta
195
+ ? `入库 ${meta.ids?.length ?? 0} 张${meta.rejected && meta.rejected.length > 0 ? ` · 被拒 ${meta.rejected.length} 张` : ''}`
196
+ : '正在写入…'
197
+ return { icon: icon(), title: `写入 ${count} 张 wiki 卡片`, summary }
198
+ }
199
+ const title = args?.title ? short(String(args.title), 30) : '(未命名)'
200
+ const summary = meta
201
+ ? `${meta.sourceType ?? 'document'} · ${meta.bytes ?? 0} 字符`
202
+ : '正在写入…'
203
+ return { icon: icon(), title: `写入来源「${title}」`, summary }
204
+ }
205
+
206
+ /** 展开之后的正文:**这一屏要回答「刚才到底发生了什么」**。 */
207
+ function DetailsOf({ toolName, meta, fallback }: { toolName: string; meta: ToolMeta | null; fallback: string }): ReactNode {
208
+ if (!meta) {
209
+ return fallback
210
+ ? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
211
+ : null
212
+ }
213
+ if (meta.kind === 'recall') {
214
+ if (meta.insufficientEvidence) {
215
+ return <p className="text-[12px] leading-relaxed text-tita-ink-muted">库里没有一条越过相关性阈值 —— 这是结论,不是错误。把来源导进来再问一次。</p>
216
+ }
217
+ return <div className="flex flex-col gap-1">
218
+ {(meta.hits ?? []).map((hit, index) => {
219
+ const score = typeof hit.rerankScore === 'number' ? hit.rerankScore : hit.score
220
+ return <div key={`${hit.entityType}:${hit.entityId}:${index}`} className="flex items-baseline gap-2 text-[12px]">
221
+ <span className="w-4 shrink-0 text-right tabular-nums text-tita-ink-faint">{index + 1}</span>
222
+ <span className="min-w-0 flex-1 truncate text-tita-ink" title={hit.title}>{hit.title || hit.entityId}</span>
223
+ <code className="shrink-0 font-mono text-[10.5px] text-tita-ink-faint">{hit.entityType}:{hit.entityId}</code>
224
+ <span className="w-12 shrink-0 text-right tabular-nums text-tita-ink-muted">{typeof score === 'number' ? score.toFixed(3) : '—'}</span>
225
+ {typeof hit.reviewScore === 'number' && <span className="shrink-0 text-[10.5px] text-tita-ink-faint">评分 {hit.reviewScore.toFixed(2)}</span>}
226
+ </div>
227
+ })}
228
+ {/* 路由与重排状态:它们解释「为什么是这个顺序」,所以和命中列在同一块里。 */}
229
+ <p className="text-[11px] text-tita-ink-faint">
230
+ {[meta.backend ? `检索 ${meta.backend}` : '', meta.routes && meta.routes.length > 0 ? `路由 ${meta.routes.join('+')}` : '', meta.rerankStatus ? `重排 ${meta.rerankStatus}` : ''].filter(Boolean).join(' · ')}
231
+ </p>
232
+ </div>
233
+ }
234
+ if (meta.kind === 'import') {
235
+ return <div className="flex flex-col gap-1.5">
236
+ <ul className="flex flex-col gap-0.5">
237
+ {(meta.written ?? []).map(item => <li key={item.sourceId} className="flex items-baseline gap-2 text-[12px]">
238
+ <FileText size={11} className="mt-1 shrink-0 text-tita-ink-faint" />
239
+ <span className="min-w-0 flex-1 truncate text-tita-ink" title={item.title}>{item.title || item.sourceId}</span>
240
+ <span className="shrink-0 tabular-nums text-tita-ink-faint">{item.bytes ?? 0} 字符</span>
241
+ </li>)}
242
+ </ul>
243
+ {(meta.failed ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
244
+ {(meta.failed ?? []).map(item => <li key={`${item.index}-${item.code}`} className="flex items-baseline gap-2 text-[12px] text-tita-danger">
245
+ <X size={11} className="mt-1 shrink-0" />
246
+ <span className="min-w-0 flex-1">第 {(item.index ?? 0) + 1} 份{item.title ? `「${short(item.title, 24)}」` : ''}:{item.message}</span>
247
+ <code className="shrink-0 font-mono text-[10.5px]">{item.code}</code>
248
+ </li>)}
249
+ </ul>}
250
+ {meta.compile
251
+ ? <p className="text-[11.5px] text-tita-ink-muted">
252
+ 编译:新建 {meta.compile.cardsCreated ?? 0} 张 · 并入 {meta.compile.cardsUpdated ?? 0} 张 · 复用 {meta.compile.cardsReused ?? 0} 张 · wikiNotes 复用 {meta.compile.notesReused ?? 0} / 重编 {meta.compile.notesRecompiled ?? 0}
253
+ {meta.compile.version !== undefined ? ` · 索引 v${meta.compile.version}` : ''}
254
+ </p>
255
+ : <p className="text-[11.5px] text-tita-ink-muted">这次没有编译(<code className="font-mono">compile: false</code>)—— 卡片还没生成,需要时再编译一次。</p>}
256
+ </div>
257
+ }
258
+ if (meta.kind === 'rebuild') {
259
+ return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
260
+ {meta.skipped ? '知识库没有变化:产物逐字复用,没有生成新的索引版本。' : <>新建 {meta.cardsCreated ?? 0} 张 · 并入 {meta.cardsUpdated ?? 0} 张 · 复用 {meta.cardsReused ?? 0} 张 · 重抽来源 {meta.sourcesExtracted ?? 0} 份 / 跳过 {meta.sourcesSkipped ?? 0} 份</>}
261
+ {meta.embedding ? ` · 向量 ${meta.embedding}` : ''}{meta.version !== undefined ? ` · 索引 v${meta.version}` : ''}
262
+ </p>
263
+ }
264
+ if (meta.kind === 'writeCards') {
265
+ return <div className="flex flex-col gap-1">
266
+ <ul className="flex flex-col gap-0.5">
267
+ {(meta.ids ?? []).map(id => <li key={id} className="text-[12px]"><code className="font-mono text-[11px] text-tita-ink-muted">{id}</code></li>)}
268
+ </ul>
269
+ {(meta.rejected ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
270
+ {(meta.rejected ?? []).map((item, index) => <li key={index} className="text-[12px] text-tita-danger">{item.title ? `「${short(item.title, 24)}」` : '一条'}被拒:{item.reason ?? item.code ?? '原因未知'}</li>)}
271
+ </ul>}
272
+ </div>
273
+ }
274
+ if (meta.kind === 'addSource') {
275
+ return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
276
+ 已写入 <code className="font-mono text-[11px]">{meta.sourceId}</code>({meta.sourceType},{meta.bytes ?? 0} 字符)。下一步:编译它,才会生成 wiki 卡片。
277
+ </p>
278
+ }
279
+ return fallback
280
+ ? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
281
+ : null
282
+ }
283
+
284
+ /**
285
+ * 一条知识工具调用的卡片。
286
+ *
287
+ * 返回 `null` 会让槽位退回默认通用行 —— 这是**刻意的出口**:拿到不认识的形状时,
288
+ * 宁可显示平台默认的样子,也不要显示一张自己编的卡。
289
+ */
290
+ export function KnowledgeToolRow(props: KnowledgeToolViewProps): ReactNode {
291
+ const { phase, toolName, block, useDisclosure } = props
292
+ const { expanded, toggle } = useDisclosure()
293
+
294
+ const args = phase === 'preparing' ? {} : argsOf(block)
295
+ const meta = phase === 'result' ? (((block as { meta?: unknown }).meta ?? null) as ToolMeta | null) : null
296
+ const isError = phase === 'result' && (block as { isError?: boolean }).isError === true
297
+ const head = headOf(toolName, args, meta)
298
+ const fallback = phase === 'result' ? textOf(block) : ''
299
+ const details = phase === 'result' || Object.keys(args).length > 0
300
+ const summary = isError ? '失败' : head.summary
301
+
302
+ return <div className="flex flex-col" data-tool={toolName} data-phase={phase}>
303
+ <button
304
+ type="button"
305
+ className={cn(
306
+ 'group flex w-full min-w-0 items-center gap-1.5 py-0.5 text-left',
307
+ details ? 'cursor-pointer' : 'cursor-default',
308
+ )}
309
+ aria-expanded={details ? expanded : undefined}
310
+ onClick={() => { if (details) toggle() }}
311
+ >
312
+ <span className={cn('flex size-4 shrink-0 items-center justify-center', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>
313
+ {isError ? <TriangleAlert size={14} /> : head.icon}
314
+ </span>
315
+ <span className="shrink-0 text-[13px] text-tita-ink" title={head.title}>{head.title}</span>
316
+ <span className="size-[2px] shrink-0 rounded-full bg-tita-ink-faint" aria-hidden />
317
+ <span className={cn('min-w-0 flex-1 truncate text-[13px]', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>{summary}</span>
318
+ {meta?.kind === 'import' && (meta.failed?.length ?? 0) > 0 && <Badge tone="warning" className="shrink-0">失败 {meta.failed?.length}</Badge>}
319
+ {meta?.insufficientEvidence && <Badge tone="warning" className="shrink-0">无命中</Badge>}
320
+ {details && <ChevronRight size={12} className={cn('shrink-0 text-tita-ink-faint transition-transform', expanded && 'rotate-90')} />}
321
+ </button>
322
+ {expanded && details && <div className="mt-1 mb-1 ml-[22px]">
323
+ {isError
324
+ ? <p className="flex items-start gap-1.5 text-[12px] leading-relaxed text-tita-danger">
325
+ <TriangleAlert size={12} className="mt-0.5 shrink-0" />
326
+ <span className="min-w-0 flex-1">{short(fallback || '调用失败', 400)}</span>
327
+ </p>
328
+ : <DetailsOf toolName={toolName} meta={meta} fallback={fallback} />}
329
+ </div>}
330
+ </div>
331
+ }
@@ -21,10 +21,12 @@
21
21
  * 下面的分块位置不再随正文长度漂移。
22
22
  */
23
23
 
24
- import { useState, type ReactNode } from 'react'
25
- import { ChevronDown, ChevronUp, FileCode2, FileText, History, Image, ListTree } from '@tita-data-agent/ui/icons'
24
+ import { useMemo, useState, type ReactNode } from 'react'
25
+ import { ChevronDown, ChevronUp, FileCode2, FileText, GitBranch, History, ListTree } from '@tita-data-agent/ui/icons'
26
26
  import { Button, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
27
27
  import { VersionManager, type VersionManagerProps } from '@tita-data-agent/ui/versions'
28
+ import { DrawerSectionHead } from './DrawerSection.tsx'
29
+ import { pathImagesOf, referencedRelsOf } from './asset-images.ts'
28
30
  // 平台提供的 Markdown 原子(DSH 的模块表里有它,本插件的 client bundle 也已把它列为
29
31
  // external)。原文与卡片正文本身就是 Markdown,用 `<pre>` 显示等于把标题、表格、链接
30
32
  // 全都拍平成源码 —— 所以默认阅读态走它,源码视图是**另一个**选项,不是替代品。
@@ -63,27 +65,18 @@ export interface PreviewRow {
63
65
  value: ReactNode
64
66
  /** 等宽显示:hash / id / 路径这类要逐字比对的值。 */
65
67
  code?: boolean
66
- }
67
-
68
- /**
69
- * 抽屉里的小节头 —— 三种层级里的中间那层(标题 15px / 小节头 13px / 正文 12px)。
70
- * 分隔线在 `.tita-drawerSection` 上(小节与上一块之间),所以这里只管内容。
71
- */
72
- function SectionHead({ icon, title, children }: { icon: ReactNode; title: string; children?: ReactNode }): ReactNode {
73
- return <div className={styles.drawerSubHead}>
74
- <h4>{icon}{title}</h4>
75
- {children}
76
- </div>
68
+ /** 悬停说明。**必须渲染出来** —— 否则调用方写了它、界面上什么也没有。 */
69
+ title?: string
77
70
  }
78
71
 
79
72
  /** 元信息:一张两列的事实表。标签列宽固定,所以标签要短且彼此对齐。 */
80
73
  export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
81
74
  return <section className={styles.drawerSection}>
82
- <SectionHead icon={<ListTree size={13} />} title="元信息" />
75
+ <DrawerSectionHead icon={<ListTree size={13} />} title="元信息" />
83
76
  <dl className={styles.kbKv}>
84
77
  {rows.map(row => <div key={row.label}>
85
78
  <dt>{row.label}</dt>
86
- <dd className={row.code ? styles.kbCode : undefined}>{row.value}</dd>
79
+ <dd className={row.code ? styles.kbCode : undefined} {...(row.title ? { title: row.title } : {})}>{row.value}</dd>
87
80
  </div>)}
88
81
  </dl>
89
82
  </section>
@@ -92,21 +85,31 @@ export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
92
85
  export interface PreviewAsset { rel: string; url: string; bytes: number }
93
86
 
94
87
  /**
95
- * 内容:小节头 + 读法工具栏 + 正文(锁高内滚)。
88
+ * 内容:小节头 + 读法工具栏 + 正文(锁高内滚)+ 正文没提到的图。
96
89
  *
97
90
  * 两个视图是**同一份内容**的两种读法:Markdown 是阅读态(默认),文本是源码态,
98
91
  * 用于逐字核对 —— 与版本 diff 是同一种读法。切换不重新取数,也不改内容。
99
- * 图片素材排在这一块的末尾:它也是内容,只是渲染器没能就地展开的那部分。
92
+ *
93
+ * **图片与正文是一份内容,不是一个附件区**(0049)。此前末尾另起一块「图片素材」
94
+ * 网格:正文里的图已经就地渲染过一遍,末尾再排一遍,同一张图一屏里出现两次。
95
+ * 现在只有一份读法 —— 正文里提到过的图就地渲染,**没提到的排在正文末尾**,
96
+ * 都在同一个滚动容器里。独立整页也是这么读的(它靠 `resolveImage` 就地渲染)。
97
+ *
98
+ * **正文里的图要就地显示**(0047):`MarkdownText` 的 `pathImages` 拿 `assets` 把
99
+ * 相对引用换成 `/knowledge/assets/...`。不接它的话,浏览器会按当前页面地址去解析
100
+ * `_assets/<bucket>/img-x.png`(结果是 `/_assets/...` 404),于是「预览里看不到图」。
100
101
  *
101
102
  * `totalChars` 说全文有多少字符(服务端按 `SOURCE_PREVIEW_CHARS` 封顶,
102
- * `content.length` 已经不是全文长度了)。读全文的出口在**抽屉头**上,不在这一块里:
103
- * 它是「这份东西还能去哪看」,位置要固定,不该随正文是否被截断而出现或消失。
103
+ * `content.length` 已经不是全文长度了)。读全文的出口由调用方放在**小节头上**
104
+ * (`action`):它是「这份东西还能去哪看」,位置要固定,不该随正文是否被截断而出现或消失。
104
105
  */
105
- export function PreviewContent({ content, assets = [], totalChars }: {
106
+ export function PreviewContent({ content, assets = [], totalChars, action }: {
106
107
  content: string
107
108
  assets?: PreviewAsset[]
108
109
  /** 全文总字符数。省略时按 `content.length` 算(它本来就是全文)。 */
109
110
  totalChars?: number
111
+ /** 小节头右侧的动作(例如「新页面打开」)。与「内容」同一行,不占正文宽度。 */
112
+ action?: ReactNode
110
113
  }): ReactNode {
111
114
  const [view, setView] = useState<'md' | 'text'>('md')
112
115
  /** 卡片正文超过上限时的本地展开:卡片没有整页出口,只能在这里给出路。 */
@@ -115,10 +118,27 @@ export function PreviewContent({ content, assets = [], totalChars }: {
115
118
  const total = totalChars ?? content.length
116
119
  const clipped = shown.length < total
117
120
  const canExpand = content.length > PREVIEW_CHARS
121
+ /**
122
+ * 解析器按 `assets` 的记忆化:`MarkdownText` 是 memo 组件,每次渲染换一个新对象
123
+ * 会让它的流式渲染缓存整体作废(平台对这个 prop 的身份有明确要求)。
124
+ */
125
+ const pathImages = useMemo(() => pathImagesOf(assets), [assets])
126
+ /** 正文没提到的图:排在正文末尾,与正文同一个滚动容器。 */
127
+ const extra = useMemo(() => {
128
+ const referenced = referencedRelsOf(content, assets)
129
+ return assets.filter(asset => !referenced.has(asset.rel))
130
+ }, [content, assets])
118
131
  return <section className={styles.drawerSection}>
119
- <SectionHead icon={<FileText size={13} />} title="内容">
120
- <span className={styles.drawerHeadFact}>{total} 字符{clipped ? ` · 这里显示前 ${shown.length}` : ''}</span>
121
- </SectionHead>
132
+ <DrawerSectionHead icon={<FileText size={13} />} title="内容">
133
+ {/* 事实与动作**成组**靠右:`drawerSubHead` 是 `space-between`,三个平级子节点会
134
+ 把「20 字符」摆到行的正中间,读起来像另一栏。 */}
135
+ <span className={styles.drawerHeadAside}>
136
+ <span className={styles.drawerHeadFact}>
137
+ {total} 字符{clipped ? ` · 这里显示前 ${shown.length}` : ''}{assets.length > 0 ? ` · ${assets.length} 张图` : ''}
138
+ </span>
139
+ {action}
140
+ </span>
141
+ </DrawerSectionHead>
122
142
  {/* 读法工具栏:这个分块的**两种读法**是它自己的事,不再挤进小节标题那一行。 */}
123
143
  <Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
124
144
  <div className={styles.drawerToolbar}>
@@ -133,21 +153,82 @@ export function PreviewContent({ content, assets = [], totalChars }: {
133
153
  {expanded ? '收起' : `展开全部(还有 ${content.length - PREVIEW_CHARS} 字符)`}</Button>}
134
154
  </div>
135
155
  <TabsContent value="md">
136
- <div className={styles.drawerBody}><div className={styles.kbMarkdown}><MarkdownText text={shown} labels={MARKDOWN_LABELS} /></div></div>
156
+ <div className={styles.drawerBody}>
157
+ <div className={styles.kbMarkdown}>
158
+ <MarkdownText text={shown} labels={MARKDOWN_LABELS} {...(pathImages === undefined ? {} : { pathImages })} />
159
+ </div>
160
+ {extra.length > 0 && <>
161
+ {/* 这块不是「另一些内容」,是**同一份内容的余下部分**:正文里没出现(或落在
162
+ 预览长度之外)的图。写清楚它是什么,人才不会以为图丢了。 */}
163
+ <p className={styles.kbAssetsNote}>
164
+ 正文里没出现的图 · {extra.length} 张{clipped ? `(正文只显示前 ${shown.length} 字符)` : ''}
165
+ </p>
166
+ <div className={styles.kbAssets}>
167
+ {extra.map(asset => <figure key={asset.rel} className={styles.kbAsset} title={asset.rel}>
168
+ {/* alt 用文件名而不是整条相对路径:图挂了的时候,`01_source_raw/_assets/…`
169
+ 会在 84px 的格子里折成三行,把这一块撑得比正文还高。 */}
170
+ <img src={asset.url} alt={asset.rel.split('/').pop() ?? asset.rel} loading="lazy" />
171
+ <figcaption>{formatBytes(asset.bytes)}</figcaption>
172
+ </figure>)}
173
+ </div>
174
+ </>}
175
+ </div>
137
176
  </TabsContent>
138
177
  <TabsContent value="text">
139
178
  <div className={styles.drawerBody}><pre className={styles.kbPre}>{shown}</pre></div>
140
179
  </TabsContent>
141
180
  </Tabs>
142
- {assets.length > 0 && <>
143
- <SectionHead icon={<Image size={13} />} title="图片素材"><span className={styles.drawerHeadFact}>{assets.length} 张</span></SectionHead>
144
- <div className={styles.kbAssets}>
145
- {assets.map(asset => <figure key={asset.rel} className={styles.kbAsset}>
146
- <img src={asset.url} alt={asset.rel} loading="lazy" />
147
- <figcaption>{formatBytes(asset.bytes)}</figcaption>
148
- </figure>)}
149
- </div>
150
- </>}
181
+ </section>
182
+ }
183
+
184
+ /**
185
+ * 相关卡片(0049):出链与入链,两处都是**可点的入口**。
186
+ *
187
+ * 为什么正文里已经有 `## 相关` 一块、这里还要再列一遍:正文里那一块写的是
188
+ * `[[类型/标题]]`,是给 `grep` 和编辑器用的**文件内引用**(面板渲染不了它,卡片也没有
189
+ * 自己的页面地址);这里这几行是面板里真的能点开的东西 —— 点一下换到那张卡片。两处
190
+ * 的清单来自同一个计划(服务端的 `planRelated`),所以不会一处写着 A、一处列着 B。
191
+ *
192
+ * 「它引用 / 引用它」分开列,是因为方向不同:前者是这张卡片自己的判断,后者是别人
193
+ * 把它拉进来的。合成一列就分不清「我提到它」和「它提到我」。
194
+ */
195
+ export interface RelatedLink {
196
+ entityId: string
197
+ entityType: string
198
+ title: string
199
+ reason: 'relation' | 'source'
200
+ }
201
+
202
+ const RELATED_REASON: Record<RelatedLink['reason'], string> = {
203
+ relation: '来源里写明的关系',
204
+ source: '同一份来源',
205
+ }
206
+
207
+ export function PreviewRelated({ outgoing, incoming, onOpen }: {
208
+ outgoing: RelatedLink[]
209
+ incoming: RelatedLink[]
210
+ onOpen: (link: RelatedLink) => void
211
+ }): ReactNode {
212
+ if (outgoing.length === 0 && incoming.length === 0) return null
213
+ const group = (label: string, links: RelatedLink[], prefix: string): ReactNode => links.length === 0 ? null : <>
214
+ <p className={styles.kbRelatedGroup}>{label}</p>
215
+ <ul className={styles.kbRelatedList}>
216
+ {links.map(link => <li key={`${prefix}-${link.entityType}:${link.entityId}`}>
217
+ <button type="button" className={styles.kbRelatedRow} title={`${link.entityType}/${link.entityId}`} onClick={() => onOpen(link)}>
218
+ <span className={styles.kbRelatedTitle}>{link.title}</span>
219
+ <span className={styles.kbRelatedMeta}>{RELATED_REASON[link.reason]}</span>
220
+ </button>
221
+ </li>)}
222
+ </ul>
223
+ </>
224
+ return <section className={styles.drawerSection}>
225
+ <DrawerSectionHead icon={<GitBranch size={13} />} title="相关卡片">
226
+ <span className={styles.drawerHeadFact}>{outgoing.length + incoming.length} 条</span>
227
+ </DrawerSectionHead>
228
+ <div className={styles.kbRelated}>
229
+ {group('它引用', outgoing, 'out')}
230
+ {group('引用它', incoming, 'in')}
231
+ </div>
151
232
  </section>
152
233
  }
153
234
 
@@ -163,7 +244,7 @@ export function PreviewContent({ content, assets = [], totalChars }: {
163
244
  */
164
245
  export function PreviewVersions({ hint, ...manager }: { hint?: string } & VersionManagerProps): ReactNode {
165
246
  return <section className={styles.drawerSection}>
166
- <SectionHead icon={<History size={13} />} title="版本"><span className={styles.drawerHeadFact}>{manager.entries?.length ?? 0} 个</span></SectionHead>
247
+ <DrawerSectionHead icon={<History size={13} />} title="版本"><span className={styles.drawerHeadFact}>{manager.entries?.length ?? 0} 个</span></DrawerSectionHead>
167
248
  {hint && <p className={styles.kbHint}>{hint}</p>}
168
249
  <VersionManager {...manager} />
169
250
  </section>