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
@@ -9,15 +9,26 @@
9
9
  * 组件自包含:列表与编辑弹窗在这里,数据由父级(知识库面板)以 props 传入,
10
10
  * 写入统一走 `onApply`(整表替换)。它另外自己拉一次 `/knowledge/wiki-types`
11
11
  * 取"待补"清单与内置只读判定规则——那是展示信息,不影响写入契约。
12
+ *
13
+ * 2026-09-26 的呈现修正(0040 §4.3 / §4.4)都收在同一个毛病上:**只读内容与可编辑
14
+ * 内容原来长得一样**。所以这一屏现在有三条硬规则:
15
+ *
16
+ * 1. 不可编辑的值不给控件形态:内置类型的 id 用 `readOnly`(沉底 + 浅描边 + 文本
17
+ * 光标,可选中复制),模板卡预览用只读排版块,都不再是"看起来能点"的输入框。
18
+ * 2. 模板卡预览**按层分两块**:必填字段落在 front matter、必填小节是正文标题,
19
+ * 三层校验本来就分开报错,这里就分开呈现。
20
+ * 3. `diagnosis/*` 这类带命名空间的 id 在列表里按前缀折成一组(0040 §4.3)。
12
21
  */
13
22
 
14
23
  import { useCallback, useEffect, useState, type ChangeEvent, type ReactNode } from 'react'
15
24
  import {
16
- AlertCircle, Check, ClipboardCheck, Eye, Pencil, Plus, RotateCcw, ShieldCheck, Trash2, X,
25
+ AlertCircle, Check, ChevronRight, Eye, Pencil, Plus, ShieldCheck, Trash2, X,
17
26
  } from '@tita-data-agent/ui/icons'
18
- import { Badge, Button, Input, Label, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
27
+ import { Badge, Button, Field, Input, SelectMenu, Switch, Textarea, cn } from '@tita-data-agent/ui/components'
19
28
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@tita-data-agent/ui/overlays'
20
29
  import { styles } from './styles.ts'
30
+ import { InfoHint } from './InfoHint.tsx'
31
+ import { wikiTypeRows } from './wiki-type-groups.ts'
21
32
 
22
33
  export interface WikiSectionSpec { label: string; required: boolean }
23
34
  export interface WikiFieldSpec { name: string; required: boolean; note: string }
@@ -29,7 +40,6 @@ export interface WikiTypeView {
29
40
  layer: 'type' | 'instance'
30
41
  sections: WikiSectionSpec[]
31
42
  fields: WikiFieldSpec[]
32
- sourceTypes: string[]
33
43
  examples: string[]
34
44
  template: string
35
45
  enabled: boolean
@@ -56,27 +66,20 @@ interface WikiTypeDraft {
56
66
  sections: string
57
67
  /** 每行一条,`name|说明`;`name|说明|可选` 表示非必填。 */
58
68
  fields: string
59
- /** 每行一个来源分类 id;留空表示不限制。 */
60
- sourceTypes: string
61
69
  examples: string
62
70
  }
63
71
 
64
- const EMPTY_DRAFT: WikiTypeDraft = { open: false, mode: 'create', id: '', label: '', description: '', layer: 'type', sections: '', fields: '', sourceTypes: '', examples: '' }
72
+ const EMPTY_DRAFT: WikiTypeDraft = { open: false, mode: 'create', id: '', label: '', description: '', layer: 'type', sections: '', fields: '', examples: '' }
65
73
 
66
74
  const TYPE_ID_PATTERN = /^[a-z][a-z0-9_/-]{1,60}$/
67
75
 
68
76
  interface Props {
69
77
  kbId: string
70
78
  types: WikiTypeView[]
71
- mode: 'strict' | 'report'
72
- /** 用户是否改过(决定「恢复默认」是否可用)。 */
73
- customized: boolean
74
79
  canEdit: boolean
75
80
  busy: boolean
76
81
  /** 整表替换 wikiTypes。 */
77
82
  onApply: (next: WikiTypeView[], describe: string) => Promise<void>
78
- onModeChange: (mode: 'strict' | 'report') => Promise<void>
79
- onReset: () => Promise<void>
80
83
  }
81
84
 
82
85
  /** 由小节派生模板卡 —— 与服务端 `templateCardFor` 同一规则,避免两处漂移。 */
@@ -103,7 +106,44 @@ function parseFields(text: string): WikiFieldSpec[] {
103
106
 
104
107
  const LAYER_LABEL: Record<WikiTypeView['layer'], string> = { type: '类型层', instance: '实例层' }
105
108
 
106
- export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy, onApply, onModeChange, onReset }: Props): ReactNode {
109
+ /**
110
+ * 只读字段的记号:一个 `data-` 属性,样式写在插件的 `styles.css` 里。
111
+ *
112
+ * 为什么不直接给工具类:**插件没有自己的 Tailwind 构建**(`packages/ui` 的
113
+ * `tailwind.css` 只在它自己构建时按源码扫一遍,插件改不了那份清单),所以
114
+ * `disabled:opacity-50` 这类只在恰好被别处用到时才存在。而 `Input` 的 `readonly`
115
+ * 样式只解决"沉底 + 浅描边",缺"这不能改"的那点灰(0040 §4.4 第 1 条)。
116
+ * 给一个属性而不是直接改 `input[readonly]`:这条规约只该作用于**声明了它**的字段,
117
+ * 不能顺手把别处的只读输入框也一起改掉。
118
+ */
119
+ const READ_ONLY_FIELD = { 'data-tita-readonly': 'true' } as const
120
+
121
+ /**
122
+ * 带命名空间的前缀中文别名(0040 §4.3)。
123
+ *
124
+ * 组名默认取前缀本身 —— 只有真正需要翻译的才写进这张表。写死五个 `diagnosis/*`
125
+ * 的 id 是不行的:自定义类型也允许带斜杠,`diagnosis` 前缀下的类型是可增删的。
126
+ */
127
+ /** 预览里没有任何可显示的必填字段 / 小节时,也要把这一块画出来:两块的分工本身是信息。 */
128
+ const PREVIEW_EMPTY = '(没有必填项 —— 这一类不受它约束)'
129
+
130
+ /**
131
+ * 一个类型的口径:定义 + 必填小节 + 必填字段。
132
+ *
133
+ * 这三样原来铺在每一行卡片上(十一个类型就是三十多行小字),把列表拉成一屏多 ——
134
+ * 而这一页要做的事是「看一眼有哪些类型、哪个没启用」。所以它们进行首那枚图标,
135
+ * 行本身只留身份、张数与操作。同样的处理也用在「来源类型」的每一行上。
136
+ */
137
+ function wikiTypeHint(type: WikiTypeView): string {
138
+ const required = type.sections.filter(section => section.required).map(section => section.label)
139
+ const fields = type.fields.filter(field => field.required).map(field => field.name)
140
+ const parts = [type.description || '(没有定义:模型只能靠猜)']
141
+ parts.push(`必填小节:${required.join(' / ') || '(无)'}`)
142
+ if (fields.length > 0) parts.push(`必填字段:${fields.join(' / ')}`)
143
+ return parts.join('。')
144
+ }
145
+
146
+ export function WikiTypeSection({ kbId, types, canEdit, busy, onApply }: Props): ReactNode {
107
147
  const [draft, setDraft] = useState<WikiTypeDraft>(EMPTY_DRAFT)
108
148
  const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
109
149
  const [rules, setRules] = useState('')
@@ -129,7 +169,6 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
129
169
  open: true, mode: 'edit', id: type.id, label: type.label, description: type.description, layer: type.layer,
130
170
  sections: type.sections.map(section => section.required ? section.label : `${section.label}?`).join('\n'),
131
171
  fields: type.fields.map(field => [field.name, field.note, field.required ? '' : '可选'].filter(Boolean).join('|')).join('\n'),
132
- sourceTypes: type.sourceTypes.join('\n'),
133
172
  examples: type.examples.join('\n'),
134
173
  }
135
174
  : { ...EMPTY_DRAFT, open: true })
@@ -154,7 +193,6 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
154
193
  layer: draft.layer,
155
194
  sections,
156
195
  fields,
157
- sourceTypes: draft.sourceTypes.split('\n').map(line => line.trim()).filter(Boolean),
158
196
  examples: draft.examples.split('\n').map(line => line.trim()).filter(Boolean),
159
197
  template: templatePreview(sections, fields),
160
198
  enabled: types.find(type => type.id === id)?.enabled ?? true,
@@ -172,35 +210,43 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
172
210
  const remove = (type: WikiTypeView): Promise<void> =>
173
211
  onApply(types.filter(item => item.id !== type.id), `已删除卡片类型「${type.label}」`)
174
212
 
175
- const enabledCount = types.filter(type => type.enabled).length
213
+ /**
214
+ * 一行类型。**一行就是一行**:身份(名字 / id / 层级 / 自定义)+ 说明图标,
215
+ * 右边是张数、开关、编辑、删除。
216
+ *
217
+ * 提出来是因为它现在有两条渲染路径 —— 平铺的行、分组里的行 —— 而两条路径必须是
218
+ * **同一个行**。复制一份出来,迟早会在其中一份上改样式或加操作,然后两处不一样。
219
+ */
220
+ const renderRow = (type: WikiTypeView): ReactNode => <li key={type.id} className={styles.wikiTypeRow}>
221
+ <span className={styles.wikiTypeName}>
222
+ <span className={styles.wikiTypeLabel}>{type.label}</span>
223
+ <code className={styles.kbSourceId}>{type.id}</code>
224
+ <Badge tone={type.layer === 'instance' ? 'warning' : 'neutral'}>{LAYER_LABEL[type.layer]}</Badge>
225
+ {type.builtin ? '' : <span className={styles.wikiTypeTag}>自定义</span>}
226
+ <InfoHint text={wikiTypeHint(type)} />
227
+ </span>
228
+ {/* 张数是**算出来的**,不是填进去的:它没有编辑入口,所以也不该长得像控件。 */}
229
+ <span className={styles.kbTypeCount} title="由该库已落库卡片实时算出,不可编辑">{type.cardCount} 张</span>
230
+ <Switch checked={type.enabled} disabled={!canEdit || busy} onCheckedChange={checked => void toggle(type, checked)} />
231
+ <Button variant="ghost" size="icon" title={`编辑 ${type.label}`} aria-label={`编辑类型 ${type.label}`} disabled={!canEdit || busy} onClick={() => openEditor(type)}><Pencil size={13} /></Button>
232
+ <Button
233
+ variant="ghost" size="icon"
234
+ disabled={!canEdit || busy || type.builtin || type.cardCount > 0}
235
+ title={type.builtin ? '内置类型不可删除,只能停用' : type.cardCount > 0 ? `该类型下还有 ${type.cardCount} 张卡片,先改类型再删除` : `删除类型 ${type.label}`}
236
+ aria-label={`删除类型 ${type.label}`}
237
+ onClick={() => void remove(type)}
238
+ ><Trash2 size={13} /></Button>
239
+ </li>
176
240
 
177
241
  return <>
178
- <div className={styles.kbSubHead}>
179
- <h4><ClipboardCheck size={13} />Wiki 类型(卡片 schema)</h4>
180
- <span>{enabledCount} / {types.length} 启用{customized ? ' · 已自定义' : ''}</span>
181
- </div>
182
- <p className={styles.kbHint}>
183
- 从原文件生成 Wiki 卡片时,<b>类型决定这张卡片该长什么样</b>:先按「定义 + 例如」判断属于哪一类,
184
- 再按<b>必填小节</b>写正文、按<b>必填字段</b>填 front matter。模板卡由小节派生,新建卡片时照抄即可。
185
- </p>
186
-
187
242
  <div className={styles.wikiTypeBar}>
188
- <label className={styles.wikiTypeMode}>
189
- <Switch
190
- checked={mode === 'strict'}
191
- disabled={!canEdit || busy}
192
- onCheckedChange={checked => void onModeChange(checked ? 'strict' : 'report')}
193
- />
194
- <span>
195
- <strong>严格模式</strong>
196
- <small>{mode === 'strict' ? '缺必填小节 / 字段直接拒绝写入' : '只把问题标在卡片上,不拦截写入'}</small>
197
- </span>
198
- </label>
243
+ {/* 0044:原来的「严格模式」开关(缺必填小节 / 字段直接拒绝写入)撤掉了。
244
+ 缺信息现在照常入库、只标成待补 —— 留一个什么都不拦的开关,比没有更糟:
245
+ 界面上摆着它,人就会以为它在起作用。校验的结果仍然报在右边那行。 */}
199
246
  <span className={styles.wikiTypeIssueCount} title="对该库已落库卡片跑三层校验的结果">
200
247
  {issueCount > 0 ? `${issueCount} 张卡片待补` : '卡片均符合 schema'}
201
248
  </span>
202
249
  <span className={styles.kbCardActions}>
203
- {canEdit && <Button variant="ghost" size="sm" disabled={busy || !customized} title={customized ? '清除自定义,回到出厂类型' : '当前就是出厂类型'} onClick={() => void onReset()}><RotateCcw size={13} />恢复默认</Button>}
204
250
  <Button variant="outline" size="sm" disabled={!canEdit || busy} onClick={() => openEditor()}><Plus size={13} />新增类型</Button>
205
251
  </span>
206
252
  </div>
@@ -210,31 +256,33 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
210
256
  <button type="button" className={styles.kbNoticeClose} aria-label="关闭提示" onClick={() => setNotice(null)}><X size={12} /></button>
211
257
  </p>}
212
258
 
259
+ {/*
260
+ 列表按 id 前缀分组(0040 §4.3):`diagnosis/*` 是**一套 schema**,五个类型
261
+ 铺在一级看起来就是五个互不相干的条目。分组是呈现层行为 —— 注册表结构、
262
+ `entity_type`、组内顺序都不动。
263
+
264
+ **「摆成几行」这件事不在 JSX 里算**,在 `wikiTypeRows()` 里算。上一版把判断写成
265
+ 一个三元,`prefix === ''` 那一支只渲染了 `members[0]` —— 于是六个没有斜杠的通用
266
+ 类型在界面上只剩第一个,另外五个不见了,而且没有任何报错、所有测试都是绿的。
267
+ 把计划抽成纯函数之后,这一条由 `tests/wiki-type-groups.test.mjs` 钉住:
268
+ **行的总数必须与注册表一一对应,一个都不能少**。
269
+ */}
213
270
  <ul className={styles.wikiTypeList}>
214
- {types.map(type => <li key={type.id} className={styles.wikiTypeRow}>
215
- <span className={styles.wikiTypeName}>
216
- <span className={styles.wikiTypeLabel}>{type.label}</span>
217
- <code className={styles.kbSourceId}>{type.id}</code>
218
- <Badge tone={type.layer === 'instance' ? 'warning' : 'neutral'}>{LAYER_LABEL[type.layer]}</Badge>
219
- {type.builtin ? '' : <span className={styles.wikiTypeTag}>自定义</span>}
220
- </span>
221
- <span className={styles.wikiTypeMeta}>
222
- {type.description || '(没有定义:模型只能靠猜)'}
223
- <br />
224
- 必填小节:{type.sections.filter(section => section.required).map(section => section.label).join(' / ') || '(无)'}
225
- {type.fields.filter(field => field.required).length > 0 ? `;必填字段:${type.fields.filter(field => field.required).map(field => field.name).join(' / ')}` : ''}
226
- </span>
227
- <span className={styles.kbTypeCount}>{type.cardCount} 张</span>
228
- <Switch checked={type.enabled} disabled={!canEdit || busy} onCheckedChange={checked => void toggle(type, checked)} />
229
- <Button variant="ghost" size="icon" title={`编辑 ${type.label}`} aria-label={`编辑类型 ${type.label}`} disabled={!canEdit || busy} onClick={() => openEditor(type)}><Pencil size={13} /></Button>
230
- <Button
231
- variant="ghost" size="icon"
232
- disabled={!canEdit || busy || type.builtin || type.cardCount > 0}
233
- title={type.builtin ? '内置类型不可删除,只能停用' : type.cardCount > 0 ? `该类型下还有 ${type.cardCount} 张卡片,先改类型再删除` : `删除类型 ${type.label}`}
234
- aria-label={`删除类型 ${type.label}`}
235
- onClick={() => void remove(type)}
236
- ><Trash2 size={13} /></Button>
237
- </li>)}
271
+ {wikiTypeRows(types).map(row => row.kind === 'row'
272
+ ? renderRow(row.type)
273
+ : <li key={`group:${row.prefix}`} className={styles.wikiTypeGroup}>
274
+ <details className={styles.wikiTypeGroupBox} open>
275
+ <summary className={styles.wikiTypeGroupHead}>
276
+ {/* 折叠箭头用 lucide 的实体图标,不用 `▾` 这种字符顶替:那个字符的
277
+ 描边粗细、基线、大小都不属于这套图标系统,缩放时先散架的就是它。 */}
278
+ <ChevronRight size={12} className={styles.wikiTypeGroupCaret} aria-hidden />
279
+ <span className={styles.wikiTypeGroupLabel}>{row.label}</span>
280
+ <code className={styles.kbSourceId}>{row.prefix}/*</code>
281
+ <span className={styles.wikiTypeGroupCount}>{row.members.length} 个类型</span>
282
+ </summary>
283
+ <ul className={styles.wikiTypeGroupList}>{row.members.map(renderRow)}</ul>
284
+ </details>
285
+ </li>)}
238
286
  </ul>
239
287
 
240
288
  {issueCount > 0 && <details className={styles.kbPrompt}>
@@ -258,22 +306,34 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
258
306
  {draft.open && <DialogContent>
259
307
  <DialogHeader><DialogTitle>{draft.mode === 'create' ? '新增 Wiki 卡片类型' : `编辑类型 · ${draft.label}`}</DialogTitle></DialogHeader>
260
308
  <div className={styles.kbForm}>
261
- <label className={styles.kbField}><Label>类型 id</Label>
262
- <Input value={draft.id} disabled={draft.mode === 'edit'} spellCheck={false} placeholder="metric"
263
- onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
264
- <span className={styles.kbHint}>小写字母开头,可含数字 / 下划线 / 连字符 / 斜杠(如 <code>diagnosis/symptom</code>);创建后不可修改——它是卡片的 <code>entity_type</code>。</span>
265
- </label>
266
- <label className={styles.kbField}><Label>显示名</Label>
309
+ {/*
310
+ id 两态两种控件形态(0040 §4.4 第 1 条):
311
+ - 新建:真的是一个要填的输入框;
312
+ - 编辑:**现在是只读值**,不是"看起来能点的灰框"。用 `readOnly` 而不是
313
+ `disabled`——只读的值还该能被选中复制(它是卡片的 `entity_type`,
314
+ 人常常要把它拷去写卡片),`disabled` 连选中都做不到。样式见
315
+ `READ_ONLY_FIELD`:沉底 + 浅描边 + 文本光标,由共享 `Input` 自带。
316
+ */}
317
+ {draft.mode === 'edit'
318
+ ? <Field as="div" label={<>类型 id<Badge tone="neutral">只读</Badge></>} hint={<>创建后不可修改——它是卡片的 <code>entity_type</code>,改了等于换一个类型。</>}>
319
+ <Input value={draft.id} readOnly spellCheck={false} {...READ_ONLY_FIELD} />
320
+ </Field>
321
+ : <Field label="类型 id" hint={<>小写字母开头,可含数字 / 下划线 / 连字符 / 斜杠(如 <code>diagnosis/symptom</code>);创建后不可修改——它是卡片的 <code>entity_type</code>。</>}>
322
+ <Input value={draft.id} spellCheck={false} placeholder="metric"
323
+ onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
324
+ </Field>}
325
+ <Field label="显示名">
267
326
  <Input value={draft.label} placeholder="指标"
268
327
  onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, label: event.target.value })} />
269
- </label>
270
- <label className={styles.kbField}><Label>定义(必填)</Label>
271
- <textarea className={styles.kbTextarea} value={draft.description} spellCheck={false}
328
+ </Field>
329
+ <Field label="定义(必填)" hint="写清这一类「是什么」——它是模型判断卡片属于哪一类的第一依据。">
330
+ <Textarea value={draft.description} spellCheck={false}
272
331
  placeholder="一个可计算、可归属的量化口径,回答「这个数是怎么算出来的」。"
273
332
  onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, description: event.target.value })} />
274
- <span className={styles.kbHint}>写清这一类「是什么」——它是模型判断卡片属于哪一类的第一依据。</span>
275
- </label>
276
- <div className={styles.kbField}><Label>层级</Label>
333
+ </Field>
334
+ {/* 选择控件不是 labelable 元素里可被包裹的那种:Radix 的 trigger 是 `<button>`,
335
+ 外层再套一个 `<label>` 会让点标题也把菜单打开。所以这一格用 `as="div"`。 */}
336
+ <Field as="div" label="层级">
277
337
  <SelectMenu
278
338
  value={draft.layer}
279
339
  onValueChange={next => setDraft({ ...draft, layer: next as 'type' | 'instance' })}
@@ -283,35 +343,23 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
283
343
  { value: 'instance', label: '实例层(只发生过一次,如案例)' },
284
344
  ]}
285
345
  />
286
- </div>
287
- <label className={styles.kbField}><Label>必填小节(每行一条,结尾加 ? 表示可选)</Label>
288
- <textarea className={styles.kbTextarea} value={draft.sections} spellCheck={false}
346
+ </Field>
347
+ <Field label="必填小节(每行一条,结尾加 ? 表示可选)" hint="小节就是正文的 Markdown 标题,也是模板卡与校验的依据。">
348
+ <Textarea value={draft.sections} spellCheck={false}
289
349
  placeholder={'口径定义\n计算逻辑\n负责人?'}
290
350
  onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, sections: event.target.value })} />
291
- <span className={styles.kbHint}>小节就是正文的 Markdown 标题,也是模板卡与校验的依据。</span>
292
- </label>
293
- <label className={styles.kbField}><Label>字段(每行一条:<code>名称|说明</code>,说明后加 <code>|可选</code> 表示非必填)</Label>
294
- <textarea className={styles.kbTextarea} value={draft.fields} spellCheck={false}
351
+ </Field>
352
+ <Field label={<>字段(每行一条:<code>名称|说明</code>,说明后加 <code>|可选</code> 表示非必填)</>} hint="字段落在 front matter / metadata 上,是机器读的部分。">
353
+ <Textarea value={draft.fields} spellCheck={false}
295
354
  placeholder={'formula|可执行的表达式或伪代码\nowner|谁对这个口径负责'}
296
355
  onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, fields: event.target.value })} />
297
- <span className={styles.kbHint}>字段落在 front matter / metadata 上,是机器读的部分。</span>
298
- </label>
299
- <label className={styles.kbField}><Label>期望来源分类(每行一个 id,留空表示不限制)</Label>
300
- <textarea className={styles.kbTextarea} value={draft.sourceTypes} spellCheck={false}
301
- placeholder={'document\nprd'}
302
- onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, sourceTypes: event.target.value })} />
303
- <span className={styles.kbHint}>来自「来源类型」的 id。来源不匹配时只提示,不阻断写作。</span>
304
- </label>
305
- <label className={styles.kbField}><Label>例如 · 用户会怎么提到它(每行一条)</Label>
306
- <textarea className={styles.kbTextarea} value={draft.examples} spellCheck={false}
356
+ </Field>
357
+ <Field label="例如 · 用户会怎么提到它(每行一条)" hint="按用户的真实说法写,模型靠它判断该生成哪一类卡片。">
358
+ <Textarea value={draft.examples} spellCheck={false}
307
359
  placeholder={'上个月的 GMV 怎么算\n转化率的口径是什么'}
308
360
  onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, examples: event.target.value })} />
309
- <span className={styles.kbHint}>按用户的真实说法写,模型靠它判断该生成哪一类卡片。</span>
310
- </label>
311
- {(draft.sections.trim() || draft.fields.trim()) && <div className={styles.kbField}>
312
- <Label>模板卡预览(由小节派生,只读)</Label>
313
- <pre className={styles.kbPre}>{templatePreview(parseSections(draft.sections), parseFields(draft.fields))}</pre>
314
- </div>}
361
+ </Field>
362
+ <TemplateCardPreview sections={parseSections(draft.sections)} fields={parseFields(draft.fields)} />
315
363
  </div>
316
364
  <DialogFooter>
317
365
  <Button variant="ghost" onClick={() => setDraft(EMPTY_DRAFT)}>取消</Button>
@@ -330,3 +378,57 @@ export function WikiTemplateHint({ template }: { template: string }): ReactNode
330
378
  <pre className={styles.kbPre}>{template}</pre>
331
379
  </details>
332
380
  }
381
+
382
+ /**
383
+ * 模板卡预览(0040 §4.4 第 3、4 条)。**它是只读的**:内容由 `fields` / `sections`
384
+ * 派生,人在这里什么都改不了 —— 真源是上面那两个输入框。
385
+ *
386
+ * 所以它不用输入框形态。原来是一个 `<textarea>`;人第一反应是"这里能编辑",
387
+ * 试两次才发现点不动,那是纯粹的误导。现在是一块沉底的只读排版块 + 一枚「只读」
388
+ * 徽标 + 文本光标可选中的等宽内容,形态与旁边那几个控件明显不同。
389
+ *
390
+ * 并且**按层分成两块**:
391
+ * - front matter:必填字段。落在卡片的 metadata 上,是机器读的部分;
392
+ * - 正文小节:必填小节。正文的 Markdown 标题。
393
+ *
394
+ * 这两件事原来混在一段文本里。而编译器的三层校验本来就分开报错
395
+ * (少字段报字段、少小节报小节),定义界面按同样的分法展示,人看到"格式错误"
396
+ * 才知道该往哪补。
397
+ */
398
+ function TemplateCardPreview({ sections, fields }: { sections: WikiSectionSpec[]; fields: WikiFieldSpec[] }): ReactNode {
399
+ const requiredFields = fields.filter(field => field.required)
400
+ const requiredSections = sections.filter(section => section.required)
401
+ return <section className={styles.wikiPreview} aria-labelledby="tita-wiki-preview-title">
402
+ <div className={styles.wikiPreviewHead}>
403
+ <h4 id="tita-wiki-preview-title">模板卡预览</h4>
404
+ <Badge tone="neutral">只读</Badge>
405
+ <span className={styles.wikiPreviewOrigin}>由上面的字段与小节派生,不单独保存</span>
406
+ </div>
407
+
408
+ <div className={styles.wikiPreviewBlock}>
409
+ <h5 className={styles.wikiPreviewBlockTitle}>front matter · 必填字段</h5>
410
+ <p className={styles.wikiPreviewBlockNote}>写在卡片正文之前,卡片的机器可读部分;顺序按你填的顺序。</p>
411
+ {requiredFields.length > 0
412
+ ? <dl className={styles.wikiPreviewFields}>
413
+ {requiredFields.map(field => <div key={field.name} className={styles.wikiPreviewField}>
414
+ <dt><code>{field.name}</code>:</dt>
415
+ <dd>{field.note || '(还没写说明)'}</dd>
416
+ </div>)}
417
+ </dl>
418
+ : <p className={styles.wikiPreviewEmpty}>{PREVIEW_EMPTY}</p>}
419
+ </div>
420
+
421
+ <div className={styles.wikiPreviewBlock}>
422
+ <h5 className={styles.wikiPreviewBlockTitle}>正文 · 必填小节</h5>
423
+ <p className={styles.wikiPreviewBlockNote}>写在正文里的 Markdown 标题,缺哪一个都会被点名。</p>
424
+ {requiredSections.length > 0
425
+ ? <ul className={styles.wikiPreviewSections}>
426
+ {requiredSections.map(section => <li key={section.label}>
427
+ <code>{`## ${section.label}`}</code>
428
+ <span className={styles.wikiPreviewPlaceholder}>(写什么)</span>
429
+ </li>)}
430
+ </ul>
431
+ : <p className={styles.wikiPreviewEmpty}>{PREVIEW_EMPTY}</p>}
432
+ </div>
433
+ </section>
434
+ }
@@ -14,8 +14,12 @@
14
14
  * 缩放**不用 G6 的 `fitView` / `zoomTo`**:这版 G6 对 HTML 节点算不出内容包围盒,
15
15
  * 两个 API 都实测无效(画布按 0.45 定了尺寸,内容仍按 1:1 画出去被裁掉)。
16
16
  * 改成画布永远按设计尺寸渲染、再缩整块板:比例是我们算的,不依赖 G6 的判断。
17
- * 4. **画布不接管视口。** 不注册 `drag-canvas` / `zoom-canvas`:滚轮该滚容器,
18
- * 而不是缩画布;平移交给滚动条。G6 在这一页只负责画和点选。
17
+ * 4. **画布不让人拖,整图居中。** 0050 撤掉了 0042 那套「空格 + 拖拽平移」:
18
+ * 那张图会被拖到一边、留出半屏空白,而用户要的是「一眼看全、永远居中」。
19
+ * 现在整图按**两个方向**适应容器(`fit: 'both'`)并用 `safe center` 居中 ——
20
+ * 图永远完整落在框里,所以既不需要平移也不需要滚动。1:1 档放不下时容器自己
21
+ * 出滚动条(那一档的用途就是读字,读数优先于「一屏看全」)。
22
+ * 仍然不注册 G6 的 `drag-canvas` / `zoom-canvas`:滚轮该滚容器而不是缩画布。
19
23
  * 5. **操作是浮层,画布上没有图例。**
20
24
  * 0026 把图例、提示、状态统计和全屏按钮放在画布上方的一条 flex 行里,栏宽一窄
21
25
  * `flex-wrap` 就把全屏按钮甩到第二行最左边 —— 用户看到的「全屏不在右上角」就是这么来的。
@@ -71,7 +75,11 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
71
75
  selectDomainRef.current = onSelectDomain
72
76
  const [fullscreen, setFullscreen] = useState(false)
73
77
  const [mode, setMode] = useState<ZoomMode>('fit')
74
- const { scale, boardH } = useFitScale(scroller, CANVAS_WIDTH, CANVAS_HEIGHT, visible, fullscreen, mode)
78
+ // **两个方向都适应**(0050):图永远完整落在框里,所以不需要平移,也就没有「被拖偏」
79
+ // 这回事。全屏与侧栏同一套规则 —— 两个档位的差别只剩「适应」与「1:1」。
80
+ const { scale, boardH } = useFitScale(
81
+ scroller, CANVAS_WIDTH, CANVAS_HEIGHT, visible, fullscreen, mode, 'both',
82
+ )
75
83
  /** 选中态要在「建图 / 换数据 / 换选中」三条路径上都对得上,所以集中存一份。 */
76
84
  const stateRef = useRef({ selected, selectedDomain, stats, version })
77
85
  stateRef.current = { selected, selectedDomain, stats, version }
@@ -89,12 +97,13 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
89
97
  container: el,
90
98
  width: el.clientWidth || CANVAS_WIDTH,
91
99
  height: el.clientHeight || CANVAS_HEIGHT,
92
- // 画布底色交给容器(`.tita-archScroll` 的 `--tita-surface`):canvas 不认 CSS 变量,
100
+ // 画布底色交给容器(`.tita-archScrollStage` 的 `--tita-canvas`):canvas 不认 CSS 变量,
93
101
  // 它会先 clearRect(清成全透明)再把 background 当 fillStyle 填 —— 传令牌字符串等于
94
102
  // 交给浏览器一个无效值,填色被忽略,默认的黑色 fillStyle 会直接盖上来。透明才是
95
103
  //「跟着主题走」在画布上的唯一正确写法,浅色下与原来的纯白一致。
96
104
  background: 'transparent',
97
- // 画布不承担缩放平移:视口交给外层滚动条与缩放档,G6 只管画。
105
+ // 画布不承担缩放与移动:缩放由外层那块板上的 `transform` 决定,位置由容器的居中
106
+ // 决定(0050 起没有平移手势),G6 只管画与点选。
98
107
  // 图级 `node.type` / `edge.type` **不要设**:G6 v5 用它覆盖每个元素自己声明的
99
108
  // `type`,于是装饰节点会被当成 HTML 节点画成空盒子,整张图只剩悬空的卡片
100
109
  // (0025 起就是这样,实测确认)。类型一律由元素自己声明。
@@ -173,9 +182,11 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
173
182
  return () => document.removeEventListener('fullscreenchange', onChange)
174
183
  }, [])
175
184
 
176
- // 画布永远是设计尺寸,缩放由这块板承担。`fit` 档下板高就是缩完的高度,
177
- // 容器因此没有可滚的余量;`1:1` 档下板高是设计高度,横向(窄栏)或双向(全屏)滚动。
178
- const scrollStyle = fullscreen ? undefined : { height: boardH }
185
+ // 画布永远是设计尺寸,缩放由板上的 transform 承担。**外面还要一层按比例定尺寸的框**:
186
+ // transform 不改变布局尺寸,只写 transform 的话容器内容永远按 1172×1132 参与布局,
187
+ // 居中就会偏(居中的是那块没缩的尺寸,不是看得见的图)。框的尺寸 = 缩完的尺寸,
188
+ // 于是「居中」居的正好是图的边界。
189
+ const frameStyle = { width: CANVAS_WIDTH * scale, height: CANVAS_HEIGHT * scale }
179
190
  const boardStyle = {
180
191
  width: CANVAS_WIDTH,
181
192
  height: CANVAS_HEIGHT,
@@ -189,21 +200,26 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
189
200
  浮层正文里,左上角那块说明板只是把图挡掉一角。 */}
190
201
  <div className={styles.archOverlay}>
191
202
  <div className={styles.archOverlayActions}>
192
- {/* 缩放档:两段的分段控件。两段都在,是因为一个来回翻转的按钮说不清「现在是哪一档」。 */}
203
+ {/* 缩放档:两段的分段控件。两段都在,是因为一个来回翻转的按钮说不清「现在是哪一档」。
204
+ 0050 撤掉了「空格 + 拖拽平移」的提示 —— 那个手势没有了(见文件头第 4 条)。 */}
193
205
  <div className={styles.archZoom} role="group" aria-label="缩放">
194
206
  <button type="button" className={cn(styles.archZoomOption, mode === 'fit' && styles.archZoomOn)}
195
- aria-pressed={mode === 'fit'} onClick={() => setMode('fit')} title="整图缩到栏宽,一眼看全三条链路">适应</button>
207
+ aria-pressed={mode === 'fit'} onClick={() => setMode('fit')} title="整图缩进框里:一眼看全,永远居中">适应</button>
196
208
  <button type="button" className={cn(styles.archZoomOption, mode === 'actual' && styles.archZoomOn)}
197
- aria-pressed={mode === 'actual'} onClick={() => setMode('actual')} title="按设计尺寸 1:1 显示,横向滚动查看">1:1</button>
209
+ aria-pressed={mode === 'actual'} onClick={() => setMode('actual')} title="按设计尺寸 1:1 显示,读字用;放不下时容器出滚动条">1:1</button>
198
210
  </div>
199
211
  <Button variant="outline" size="sm" onClick={toggleFullscreen} aria-pressed={fullscreen}>
200
212
  {fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}{fullscreen ? '退出全屏' : '全屏'}
201
213
  </Button>
202
214
  </div>
203
215
  </div>
204
- <div className={styles.archScroll} style={scrollStyle} ref={scroller}>
205
- <div className={styles.archBoard} style={boardStyle}>
206
- <div className={styles.archCanvas} ref={host} />
216
+ {/* 容器仍然是一个滚动容器(1:1 档放不下时要有出路),但**没有拖拽手势**:
217
+ 0050 起它只负责「把整图居中摆好」。 */}
218
+ <div className={cn(styles.archScroll, styles.archScrollStage)} ref={scroller}>
219
+ <div className={styles.archFrame} style={frameStyle}>
220
+ <div className={styles.archBoard} style={boardStyle}>
221
+ <div className={styles.archCanvas} ref={host} />
222
+ </div>
207
223
  </div>
208
224
  </div>
209
225
  {/* 详情浮层:画布内,不缩放,可关闭。关掉之后画布恢复成完整的一张图。 */}
@@ -21,7 +21,7 @@ import {
21
21
  import { Badge, Button, cn } from '@tita-data-agent/ui/components'
22
22
  import {
23
23
  NODE_META, NODE_STATUS_LABEL, jumpTargetOf, laneOf, nodeMetrics,
24
- type KnowledgeStats, type KnowledgeTab, type NodeId, type NodeStatus, type PanelTab,
24
+ type KnowledgeStats, type KnowledgeTab, type NodeId, type NodeStatus, type PanelTab, type WorkspaceTabKind,
25
25
  } from '../architecture.tsx'
26
26
  import { LANE_PALETTE, recordRows } from './model.ts'
27
27
  import { styles } from '../styles.ts'
@@ -38,7 +38,8 @@ export interface NodeLogicPanelProps {
38
38
  version: number
39
39
  /** 跳到节点所属的功能 tab;`knowledgeTab` 存在时还要落到知识库里那一栏。 */
40
40
  onJump: (tab: PanelTab, knowledgeTab?: KnowledgeTab) => void
41
- onOpenTab?: (kind: 'data-sheet' | 'data-notebook') => void
41
+ /** 打开一个工作区页面(Table / Notebook / 画布)。 */
42
+ onOpenTab?: (kind: WorkspaceTabKind) => void
42
43
  onClose: () => void
43
44
  }
44
45
 
@@ -123,9 +124,11 @@ export function NodeLogicPanel({ selected, stats, version, onJump, onOpenTab, on
123
124
  去配置<ArrowRight size={14} />
124
125
  </Button>
125
126
  : <p className={styles.archPanelHint}><CircleHelp size={13} />这个节点没有可配置页:{meta.status === 'host' ? '它由宿主提供' : meta.status === 'planned' ? '它还没实现,先在这里说明设计意图' : '它的行为由代码决定,没有可调参数'}。</p>}
126
- {(selected === 'answer' || selected === 'data-sheet' || selected === 'data-notebook') && onOpenTab && <div className={styles.archPanelActions}>
127
+ {(selected === 'answer' || selected === 'data-sheet' || selected === 'data-notebook' || selected === 'data-canvas') && onOpenTab && <div className={styles.archPanelActions}>
127
128
  <Button variant="outline" size="sm" onClick={() => onOpenTab('data-sheet')}><ChevronRight size={13} />打开 Table</Button>
128
129
  <Button variant="outline" size="sm" onClick={() => onOpenTab('data-notebook')}><ChevronRight size={13} />打开 Notebook</Button>
130
+ {/* 画布与 Table / Notebook 并列:三者都是这一格的输出产物,之前少列了一个。 */}
131
+ <Button variant="outline" size="sm" onClick={() => onOpenTab('data-excalidraw')}><ChevronRight size={13} />打开画布</Button>
129
132
  </div>}
130
133
  </PanelSection>
131
134
  </div>