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.
- package/README.md +147 -19
- package/cordis.patch.yml +25 -10
- package/lib/client.js +6400 -3326
- package/lib/doc-page.js +7761 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +63 -11
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +7 -4
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +52 -14
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/convert/types.d.ts +2 -2
- package/lib/types/convert/types.d.ts.map +1 -1
- package/lib/types/convert/types.js +2 -2
- package/lib/types/convert/types.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +57 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +245 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +19 -2
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +95 -25
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +297 -166
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/extract-model.d.ts +67 -27
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js +225 -97
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract.d.ts +16 -0
- package/lib/types/extract.d.ts.map +1 -1
- package/lib/types/extract.js +19 -2
- package/lib/types/extract.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +11 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +41 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +466 -63
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/types.js +1 -1
- package/lib/types/intent/types.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +23 -12
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +37 -44
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/layout.d.ts +3 -3
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +4 -4
- package/lib/types/layout.js.map +1 -1
- package/lib/types/migrate.d.ts +1 -1
- package/lib/types/migrate.js +2 -2
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +99 -0
- package/lib/types/model-text.d.ts.map +1 -0
- package/lib/types/model-text.js +103 -0
- package/lib/types/model-text.js.map +1 -0
- package/lib/types/present.d.ts +239 -0
- package/lib/types/present.d.ts.map +1 -0
- package/lib/types/present.js +255 -0
- package/lib/types/present.js.map +1 -0
- package/lib/types/recall-rank.d.ts +37 -0
- package/lib/types/recall-rank.d.ts.map +1 -1
- package/lib/types/recall-rank.js +41 -1
- package/lib/types/recall-rank.js.map +1 -1
- package/lib/types/runtime.d.ts +564 -31
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +2645 -292
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +4 -2
- package/lib/types/settings-schema.d.ts.map +1 -1
- package/lib/types/settings-schema.js +2 -0
- package/lib/types/settings-schema.js.map +1 -1
- package/lib/types/source-images.d.ts +118 -0
- package/lib/types/source-images.d.ts.map +1 -0
- package/lib/types/source-images.js +288 -0
- package/lib/types/source-images.js.map +1 -0
- package/lib/types/types.d.ts +356 -28
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +112 -18
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +140 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -0
- package/lib/types/wiki-finalize.js +269 -0
- package/lib/types/wiki-finalize.js.map +1 -0
- package/lib/types/wiki-types.d.ts +10 -2
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +27 -30
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +25 -10
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/DrawerSection.tsx +18 -0
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +458 -158
- package/src/client/KnowledgePanel.tsx +44 -10
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/KnowledgeToolRow.tsx +331 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +178 -43
- package/src/client/RecallPanel.tsx +418 -127
- package/src/client/TypeDefinitionDrawer.tsx +154 -54
- package/src/client/WikiCardReview.tsx +266 -0
- package/src/client/WikiFlowCard.tsx +57 -95
- package/src/client/WikiTypeEditor.tsx +197 -95
- package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +25 -10
- package/src/client/architecture.tsx +66 -41
- package/src/client/asset-images.ts +125 -0
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +501 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +280 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +36 -4
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +242 -0
- package/src/client/index.ts +18 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/problem-domains.ts +1 -1
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +397 -54
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +68 -9
- package/src/convert/ingest.ts +61 -18
- package/src/convert/types.ts +2 -2
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +251 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +29 -4
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +310 -173
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/extract-model.ts +254 -99
- package/src/extract.ts +35 -3
- package/src/fs-atomic.ts +41 -0
- package/src/index.ts +499 -62
- package/src/intent/types.ts +1 -1
- package/src/knowledge-bases.ts +50 -49
- package/src/layout.ts +4 -4
- package/src/migrate.ts +2 -2
- package/src/model-text.ts +126 -0
- package/src/present.ts +367 -0
- package/src/recall-rank.ts +57 -2
- package/src/runtime.ts +2802 -293
- package/src/settings-schema.ts +2 -0
- package/src/source-images.ts +326 -0
- package/src/types.ts +446 -45
- package/src/wiki-finalize.ts +310 -0
- package/src/wiki-types.ts +28 -32
|
@@ -35,21 +35,26 @@
|
|
|
35
35
|
|
|
36
36
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type ReactNode } from 'react'
|
|
37
37
|
import {
|
|
38
|
-
AlertCircle, Check, Eye, FileText, FileUp, FolderOpen,
|
|
39
|
-
Layers3, RefreshCw, Search,
|
|
38
|
+
AlertCircle, Check, ChevronDown, Eye, ExternalLink, FileText, FileUp, FolderOpen,
|
|
39
|
+
Layers3, RefreshCw, Search, Tags, Trash2, Upload, X,
|
|
40
40
|
} from '@tita-data-agent/ui/icons'
|
|
41
41
|
import type { LucideIcon } from '@tita-data-agent/ui/icons'
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
Badge, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
|
|
44
|
+
Field, Input, SelectMenu, Tabs, TabsContent, TabsList, TabsTrigger, cn,
|
|
45
|
+
} from '@tita-data-agent/ui/components'
|
|
43
46
|
import {
|
|
44
47
|
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
|
|
45
48
|
Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle,
|
|
46
49
|
} from '@tita-data-agent/ui/overlays'
|
|
47
50
|
import { ObjectPicker } from '@tita-data-agent/ui/workspace'
|
|
48
51
|
import { RestoreDialog, type VersionManagerEntry } from '@tita-data-agent/ui/versions'
|
|
52
|
+
import { readRemembered, readRememberedId, remember } from './panel-state.ts'
|
|
49
53
|
import { styles } from './styles.ts'
|
|
50
54
|
import { formatBytes, formatDateTime, formatTime } from './format.ts'
|
|
51
|
-
import { PreviewContent, PreviewMeta, PreviewVersions } from './PreviewSections.tsx'
|
|
52
|
-
import type {
|
|
55
|
+
import { PreviewContent, PreviewMeta, PreviewRelated, PreviewVersions, type PreviewRow } from './PreviewSections.tsx'
|
|
56
|
+
import type { AssetRef } from './asset-images.ts'
|
|
57
|
+
import { KNOWLEDGE_TABS, type KnowledgeTab } from './architecture.tsx'
|
|
53
58
|
import { KnowledgeStepRail } from './KnowledgeStepRail.tsx'
|
|
54
59
|
import { NodeSettingsPanel, SettingsChip, useScope, type SettingsNode, type SettingsScopeLike } from './NodeSettings.tsx'
|
|
55
60
|
import { RecallPanel } from './RecallPanel.tsx'
|
|
@@ -57,7 +62,8 @@ import { KnowledgeGraphView } from './KnowledgeGraphView.tsx'
|
|
|
57
62
|
import type { GraphEdgeView, GraphNodeView } from './graph/layout.ts'
|
|
58
63
|
import type { WikiTypeView } from './WikiTypeEditor.tsx'
|
|
59
64
|
import { WikiFlowCard } from './WikiFlowCard.tsx'
|
|
60
|
-
import { TypeDefinitionDrawer } from './TypeDefinitionDrawer.tsx'
|
|
65
|
+
import { TypeDefinitionDrawer, type TypeDefinitionSection } from './TypeDefinitionDrawer.tsx'
|
|
66
|
+
import { WikiCardReview } from './WikiCardReview.tsx'
|
|
61
67
|
|
|
62
68
|
// ------------------------------------------------------------------ contract
|
|
63
69
|
// Mirrors the plugin HTTP contract (see plugins/dsh-data-domain-knowledge/README.md).
|
|
@@ -77,9 +83,19 @@ export interface SourceTypeDef {
|
|
|
77
83
|
enabled: boolean
|
|
78
84
|
builtin: boolean
|
|
79
85
|
fallback: boolean
|
|
86
|
+
/** 这一类来源该带哪些元数据字段(0048)。缺省 = 没有声明。 */
|
|
87
|
+
metadataFields?: SourceMetadataField[]
|
|
80
88
|
sourceCount: number
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** 一个元数据字段声明。`key` 是 front matter 顶层键,或 `sources[0].author` 这种路径。 */
|
|
92
|
+
export interface SourceMetadataField {
|
|
93
|
+
key: string
|
|
94
|
+
label: string
|
|
95
|
+
description?: string
|
|
96
|
+
required?: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
export interface SourceClassifierStep {
|
|
84
100
|
type: string
|
|
85
101
|
rule: string
|
|
@@ -121,6 +137,8 @@ export interface SourceTypeDraft {
|
|
|
121
137
|
description: string
|
|
122
138
|
signals: string
|
|
123
139
|
excludes: string
|
|
140
|
+
/** 这一类来源该带哪些元数据字段(0048)。空行在保存前会被丢掉。 */
|
|
141
|
+
metadataFields?: SourceMetadataField[]
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
export interface KnowledgeBase {
|
|
@@ -133,7 +151,6 @@ export interface KnowledgeBase {
|
|
|
133
151
|
/** Wiki 卡片类型注册表(0024)。 */
|
|
134
152
|
wikiTypes: WikiTypeView[]
|
|
135
153
|
/** `report` 只标记问题,`strict` 拒绝不合规的写入。 */
|
|
136
|
-
wikiSchemaMode: 'strict' | 'report'
|
|
137
154
|
/** 计算字段:这套 schema 是否已偏离出厂定义。 */
|
|
138
155
|
wikiTypesCustomized: boolean
|
|
139
156
|
createdAt: string
|
|
@@ -165,8 +182,15 @@ export interface KnowledgeDocument {
|
|
|
165
182
|
|
|
166
183
|
export interface KnowledgeDocumentDetail extends KnowledgeDocument {
|
|
167
184
|
content: string
|
|
185
|
+
/**
|
|
186
|
+
* 正文总字符数。`content` 可能只是前一段 —— 明细接口按 `SOURCE_PREVIEW_CHARS`
|
|
187
|
+
* 封顶(4000),所以**不能**再用 `content.length` 当「全文有多少」。
|
|
188
|
+
*/
|
|
189
|
+
contentChars: number
|
|
190
|
+
/** `content` 是不是被截断过。为真时给「新页面打开」。 */
|
|
191
|
+
contentTruncated: boolean
|
|
168
192
|
frontmatter: Record<string, unknown>
|
|
169
|
-
assets:
|
|
193
|
+
assets: AssetRefView[]
|
|
170
194
|
lineage?: Record<string, string>
|
|
171
195
|
}
|
|
172
196
|
|
|
@@ -181,6 +205,98 @@ export interface DocumentVersion {
|
|
|
181
205
|
}
|
|
182
206
|
|
|
183
207
|
/** Wiki 卡片(0025:带上 metadata,界面要能显示 schema 待补项)。 */
|
|
208
|
+
/** 图片素材的客户端形状。解析规则住在 `asset-images.ts`,两个预览抽屉共用它。 */
|
|
209
|
+
export type AssetRefView = AssetRef
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 格式类 → 人话(0049)。
|
|
213
|
+
*
|
|
214
|
+
* `source_format` 是转换内核的词汇(`web_page` / `office` / `anydoc@0.2.4/pptx` 这一类),
|
|
215
|
+
* 直接摆在界面上等于让人先学一遍实现。表里没有的格式**原样显示**:一个看得出是原始值
|
|
216
|
+
* 的字符串,好过猜一个像人话的错标签。
|
|
217
|
+
*/
|
|
218
|
+
const SOURCE_FORMAT_LABEL: Record<string, string> = {
|
|
219
|
+
web_page: '网页',
|
|
220
|
+
html: '网页',
|
|
221
|
+
markdown: 'Markdown',
|
|
222
|
+
text: '纯文本',
|
|
223
|
+
csv: '表格',
|
|
224
|
+
json: 'JSON',
|
|
225
|
+
office: 'Office 文档',
|
|
226
|
+
pdf: 'PDF',
|
|
227
|
+
image: '图片',
|
|
228
|
+
audio: '音频',
|
|
229
|
+
video: '视频',
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sourceFormatLabel(format: string | undefined): string {
|
|
233
|
+
if (!format) return 'unknown'
|
|
234
|
+
return SOURCE_FORMAT_LABEL[format] ?? format
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `generated.by`(谁把它转出来的)。它只在悬停里出现,所以不给它一行。 */
|
|
238
|
+
function generatorOf(frontmatter: Record<string, unknown>): string {
|
|
239
|
+
const generated = (frontmatter.generated ?? {}) as Record<string, unknown>
|
|
240
|
+
return typeof generated.by === 'string' && generated.by.trim() ? `生成者 ${generated.by}` : ''
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* OKF 的那几个顶层键 → 元信息行(0048)。
|
|
245
|
+
*
|
|
246
|
+
* 只显示**文件里真的有**的键:没有就不占一行。`sources[]` 取第一条(这份文件的来源),
|
|
247
|
+
* 它里面最要紧的两项是作者与发布时间 —— 0048 之前它们被抽出来就扔了,知识库里
|
|
248
|
+
* 没有任何一份来源记得住「谁写的、什么时候发的」。
|
|
249
|
+
*/
|
|
250
|
+
function sourceProvenanceRows(frontmatter: Record<string, unknown>): PreviewRow[] {
|
|
251
|
+
const rows: PreviewRow[] = []
|
|
252
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')
|
|
253
|
+
const resource = text(frontmatter.resource)
|
|
254
|
+
if (resource) rows.push({ label: '来源链接', value: /^https?:/i.test(resource) ? <a href={resource} target="_blank" rel="noreferrer">{resource}</a> : resource, code: true })
|
|
255
|
+
const generated = (frontmatter.generated ?? {}) as Record<string, unknown>
|
|
256
|
+
const at = text(generated.at)
|
|
257
|
+
if (at) rows.push({ label: '采集时间', value: formatDateTime(at) })
|
|
258
|
+
const first = Array.isArray(frontmatter.sources) ? (frontmatter.sources[0] as Record<string, unknown> | undefined) : undefined
|
|
259
|
+
const author = text(first?.author)
|
|
260
|
+
if (author) rows.push({ label: '作者 / 账号', value: author })
|
|
261
|
+
const lastModified = text(first?.last_modified)
|
|
262
|
+
if (lastModified) rows.push({ label: '来源更新时间', value: formatDateTime(lastModified) })
|
|
263
|
+
const description = text(frontmatter.description)
|
|
264
|
+
if (description) rows.push({ label: '摘要', value: description })
|
|
265
|
+
return rows
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 分类声明的元数据字段 → 元信息行(0048)。
|
|
270
|
+
*
|
|
271
|
+
* 缺的字段**照样列出来**并写「缺」:那一行本身就是提示(「这类来源通常要带作者」),
|
|
272
|
+
* 藏起来等于让人自己去猜分类声明了什么。
|
|
273
|
+
*/
|
|
274
|
+
function declaredMetadataRows(fields: SourceMetadataField[] | undefined, frontmatter: Record<string, unknown>): PreviewRow[] {
|
|
275
|
+
if (!fields || fields.length === 0) return []
|
|
276
|
+
const valueAt = (key: string): string => {
|
|
277
|
+
let current: unknown = frontmatter
|
|
278
|
+
for (const part of key.split('.')) {
|
|
279
|
+
const name = part.replace(/\[\d+\]/g, '')
|
|
280
|
+
if (name) {
|
|
281
|
+
if (current === null || typeof current !== 'object') return ''
|
|
282
|
+
current = (current as Record<string, unknown>)[name]
|
|
283
|
+
}
|
|
284
|
+
for (const index of part.matchAll(/\[(\d+)\]/g)) {
|
|
285
|
+
current = Array.isArray(current) ? current[Number(index[1])] : undefined
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return typeof current === 'string' ? current.trim() : current === undefined || current === null ? '' : String(current)
|
|
289
|
+
}
|
|
290
|
+
return fields.map(field => {
|
|
291
|
+
const value = valueAt(field.key)
|
|
292
|
+
return {
|
|
293
|
+
label: field.label,
|
|
294
|
+
value: value !== '' ? value : <span className={styles.kbHint}>缺{field.required === true ? '(这一类通常要带)' : ''}</span>,
|
|
295
|
+
...(field.description ? { title: field.description } : {}),
|
|
296
|
+
} as PreviewRow
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
184
300
|
export interface KnowledgeCardView {
|
|
185
301
|
entityId: string
|
|
186
302
|
entityType: string
|
|
@@ -191,6 +307,26 @@ export interface KnowledgeCardView {
|
|
|
191
307
|
sourceRefs: string[]
|
|
192
308
|
metadata?: Record<string, unknown>
|
|
193
309
|
updatedAt: string
|
|
310
|
+
/** 人工备注(0044)。正文之外的东西 —— 编译不碰它,也不进召回正文。 */
|
|
311
|
+
annotations?: Array<{ id: string; kind: 'note' | 'anomaly'; note: string; author?: string; createdAt?: string; sourceRef?: string }>
|
|
312
|
+
/** review 结论与评分(0044)。缺省 = 还没人看过。 */
|
|
313
|
+
review?: { state: 'unreviewed' | 'reviewed'; score: number; reviewedAt?: string; reviewedBy?: string }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 一张卡片的相关卡片(0049):出链与入链。服务端按图上的关系边与「同一份来源」算好
|
|
318
|
+
* 之后给过来,界面只负责显示成可点的行 —— 客户端不解析正文里那块 `## 相关`。
|
|
319
|
+
*/
|
|
320
|
+
export interface CardRelatedView {
|
|
321
|
+
outgoing: CardRelatedEntry[]
|
|
322
|
+
incoming: CardRelatedEntry[]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface CardRelatedEntry {
|
|
326
|
+
entityId: string
|
|
327
|
+
entityType: string
|
|
328
|
+
title: string
|
|
329
|
+
reason: 'relation' | 'source'
|
|
194
330
|
}
|
|
195
331
|
|
|
196
332
|
/** 一张 wiki 卡片的历史版本快照。 */
|
|
@@ -205,25 +341,7 @@ export interface WikiCardVersionView {
|
|
|
205
341
|
isCurrent: boolean
|
|
206
342
|
}
|
|
207
343
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
async function requestJson<T>(url: string, init?: RequestInit): Promise<Envelope<T>> {
|
|
211
|
-
const response = await fetch(url, init)
|
|
212
|
-
const text = await response.text()
|
|
213
|
-
if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
|
|
214
|
-
let payload: Envelope<T>
|
|
215
|
-
try {
|
|
216
|
-
payload = JSON.parse(text) as Envelope<T>
|
|
217
|
-
} catch {
|
|
218
|
-
throw new Error(`服务端返回了无效响应(HTTP ${response.status})`)
|
|
219
|
-
}
|
|
220
|
-
const error = payload.error
|
|
221
|
-
if (!response.ok || payload.ok === false) {
|
|
222
|
-
const detail = [error?.code, error?.message].filter(Boolean).join(' · ') || `请求失败(${response.status})`
|
|
223
|
-
throw new Error(error?.suggestion ? `${detail} — ${error.suggestion}` : detail)
|
|
224
|
-
}
|
|
225
|
-
return payload
|
|
226
|
-
}
|
|
344
|
+
import { requestJson, type Envelope } from './request.ts'
|
|
227
345
|
|
|
228
346
|
/** Text-ish formats the browser can read and POST as SourceRaw directly. */
|
|
229
347
|
const DIRECT_READ_EXT = new Set(['.md', '.markdown', '.txt', '.json', '.tsv', '.csv', '.html'])
|
|
@@ -275,7 +393,22 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
275
393
|
const [loading, setLoading] = useState(false)
|
|
276
394
|
const [busy, setBusy] = useState<string | null>(null)
|
|
277
395
|
const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
278
|
-
|
|
396
|
+
/**
|
|
397
|
+
* 分栏与选中的知识库。**起始值来自记住的位置**(`panel-state.ts`):面板会被 DSH
|
|
398
|
+
* 重新挂载(刷新页面必然),只放在 `useState` 里的话,用户每次回来都落回
|
|
399
|
+
* 「SourceRaw + 默认知识库」。分栏存的是上一版的 id 时先对着 `KNOWLEDGE_TABS` 校验;
|
|
400
|
+
* 知识库 id 是服务端数据、没有注册表可校验,所以由 `loadBases` 用它自己的
|
|
401
|
+
* 「不存在就退回默认库」那条规则兜底。
|
|
402
|
+
*/
|
|
403
|
+
const [tab, setTab] = useState<KnowledgeTab>(() => readRemembered('knowledgeTab', KNOWLEDGE_TABS.map(item => item.id)) ?? 'source-raw')
|
|
404
|
+
/**
|
|
405
|
+
* wiki 栏内部的二级 tab(0042):卡片 / 抽取流程。
|
|
406
|
+
*
|
|
407
|
+
* **不记进 `panel-state.ts`**:那里记的是「上次看到哪儿」的三个大位置(一级 tab、
|
|
408
|
+
* 分栏、知识库),而这是某一栏内部的两种读法 —— 面板重新挂载时落回「卡片」
|
|
409
|
+
* (这一栏的第一件事)是正确的默认,而且不必为一个二级 tab 去改那个模块的 key 表。
|
|
410
|
+
*/
|
|
411
|
+
const [wikiTab, setWikiTab] = useState<'cards' | 'flow'>('cards')
|
|
279
412
|
const [editOpen, setEditOpen] = useState(false)
|
|
280
413
|
const [draft, setDraft] = useState({ id: '', name: '', description: '' })
|
|
281
414
|
/**
|
|
@@ -284,18 +417,39 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
284
417
|
* 拉取,因为停用的分类要在提示词里标记出来。
|
|
285
418
|
*/
|
|
286
419
|
const [catalog, setCatalog] = useState<SourceTypeCatalog | null>(null)
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 换分栏的**唯一入口**:顺手记一笔,下次挂载落回同一栏。两个调用点(步骤轨点击、
|
|
423
|
+
* 从架构图节点跳过来)都走它,所以不会出现「这个入口记得、那个入口不记得」。
|
|
424
|
+
*/
|
|
425
|
+
const changeTab = useCallback((next: KnowledgeTab): void => {
|
|
426
|
+
setTab(next)
|
|
427
|
+
remember('knowledgeTab', next)
|
|
428
|
+
}, [])
|
|
287
429
|
/**
|
|
288
430
|
* 「类型定义」抽屉(0034):本库的词汇表 —— 来源分类 + Wiki 卡片 schema。
|
|
289
431
|
* 弹窗草稿在抽屉内部,这里只留开关与写路径。
|
|
290
432
|
*/
|
|
291
|
-
|
|
433
|
+
/**
|
|
434
|
+
* 类型定义抽屉的打开状态 + **该落在哪一节**。
|
|
435
|
+
*
|
|
436
|
+
* 两件事合成一个 state 是有意的:分开写时,「打开」与「看哪一节」会在两次渲染里生效,
|
|
437
|
+
* 抽屉先按默认节滚一次、再滚一次到目标节 —— 人看到的是闪一下。
|
|
438
|
+
*/
|
|
439
|
+
const [typeDef, setTypeDef] = useState<{ open: boolean; section: TypeDefinitionSection }>({ open: false, section: 'source' })
|
|
292
440
|
/** 上传 / 导入时用的来源分类;注册表变了要跟着回退,不能指向一个已停用的 id。 */
|
|
293
441
|
const [uploadType, setUploadType] = useState('document')
|
|
294
442
|
const [detail, setDetail] = useState<{ document: KnowledgeDocumentDetail; versions: DocumentVersion[] } | null>(null)
|
|
295
443
|
const [detailLoading, setDetailLoading] = useState(false)
|
|
296
444
|
const [restoreTarget, setRestoreTarget] = useState<VersionManagerEntry | null>(null)
|
|
297
445
|
/** Wiki 卡片详情与版本(0025)。 */
|
|
298
|
-
|
|
446
|
+
/**
|
|
447
|
+
* 卡片详情的三个部分:卡片本体、历史版本、图片素材。
|
|
448
|
+
*
|
|
449
|
+
* `assets` 是服务端把卡片正文里的图解析成的可加载 URL(0047)。少了它,预览抽屉里
|
|
450
|
+
* 卡片的图只能按 GUI 的地址去解析 `_assets/...`,全是 404。
|
|
451
|
+
*/
|
|
452
|
+
const [cardDetail, setCardDetail] = useState<{ card: KnowledgeCardView; versions: WikiCardVersionView[]; assets: AssetRefView[]; related?: CardRelatedView } | null>(null)
|
|
299
453
|
const [cardLoading, setCardLoading] = useState(false)
|
|
300
454
|
const [cardRestore, setCardRestore] = useState<VersionManagerEntry | null>(null)
|
|
301
455
|
const [graph, setGraph] = useState<{ nodes: GraphNodeView[]; edges: GraphEdgeView[] }>({ nodes: [], edges: [] })
|
|
@@ -349,7 +503,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
349
503
|
|
|
350
504
|
/** 架构图节点跳转:落到对应分栏。 */
|
|
351
505
|
useEffect(() => {
|
|
352
|
-
if (visible && focus)
|
|
506
|
+
if (visible && focus) changeTab(focus.tab)
|
|
353
507
|
}, [visible, focus?.tab, focus?.nonce]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
354
508
|
|
|
355
509
|
/** `sourceId → wiki cards`, so a file row can say how many slices it produced. */
|
|
@@ -369,13 +523,16 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
369
523
|
const loadBases = useCallback(async (preferred?: string): Promise<{ next: KnowledgeBase[]; resolved: string }> => {
|
|
370
524
|
const payload = await requestJson<{ bases: KnowledgeBase[]; defaultBaseId: string }>('/knowledge/bases')
|
|
371
525
|
const next = payload.data?.bases ?? []
|
|
372
|
-
|
|
526
|
+
// 优先级:显式指定(新建 / 编辑后回到那一个)→ 本次挂载里已经选中的 → **记住的那个**。
|
|
527
|
+
// 最后一档就是「刷新之后还在原来那个库」,而它不合法时下面那句会退回默认库。
|
|
528
|
+
const requested = preferred ?? (activeIdRef.current !== '' ? activeIdRef.current : readRememberedId('knowledgeBase'))
|
|
373
529
|
const resolved = next.some(base => base.id === requested)
|
|
374
530
|
? requested
|
|
375
531
|
: payload.data?.defaultBaseId ?? next[0]?.id ?? ''
|
|
376
532
|
setBases(next)
|
|
377
533
|
setActiveId(resolved)
|
|
378
534
|
activeIdRef.current = resolved
|
|
535
|
+
remember('knowledgeBase', resolved)
|
|
379
536
|
return { next, resolved }
|
|
380
537
|
}, [])
|
|
381
538
|
|
|
@@ -531,6 +688,9 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
531
688
|
description: draft.description.trim(),
|
|
532
689
|
signals: draft.signals.split('\n').map(line => line.trim()).filter(Boolean),
|
|
533
690
|
excludes: draft.excludes.split('\n').map(line => line.trim()).filter(Boolean),
|
|
691
|
+
// 元数据字段声明(0048):与 signals / excludes 同一条约定 —— 「没传」不改动磁盘上
|
|
692
|
+
// 已有的声明,传了空数组才是清空。
|
|
693
|
+
...(draft.metadataFields === undefined ? {} : { metadataFields: draft.metadataFields }),
|
|
534
694
|
}
|
|
535
695
|
const isNew = draft.mode === 'create'
|
|
536
696
|
setBusy('source-type')
|
|
@@ -610,44 +770,6 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
610
770
|
}
|
|
611
771
|
}
|
|
612
772
|
|
|
613
|
-
const patchWikiSchemaMode = async (mode: 'strict' | 'report'): Promise<void> => {
|
|
614
|
-
const base = active
|
|
615
|
-
if (!base) return
|
|
616
|
-
setBusy('wiki-mode')
|
|
617
|
-
try {
|
|
618
|
-
await requestJson(`/knowledge/bases/${encodeURIComponent(base.id)}`, {
|
|
619
|
-
method: 'PATCH',
|
|
620
|
-
headers: { 'content-type': 'application/json' },
|
|
621
|
-
body: JSON.stringify({ wikiSchemaMode: mode }),
|
|
622
|
-
})
|
|
623
|
-
await loadBases(base.id)
|
|
624
|
-
say('success', mode === 'strict' ? '已开启严格模式:缺必填小节 / 字段将拒绝写入' : '已切回 report 模式:只标记问题,不拦截写入')
|
|
625
|
-
} catch (error) {
|
|
626
|
-
say('error', error instanceof Error ? error.message : String(error))
|
|
627
|
-
} finally {
|
|
628
|
-
setBusy(null)
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/** 清除 wiki 类型与模式上的用户层覆盖,回到出厂 11 个类型。 */
|
|
633
|
-
const resetWikiTypes = async (): Promise<void> => {
|
|
634
|
-
const base = active
|
|
635
|
-
if (!base) return
|
|
636
|
-
setBusy('wiki-types')
|
|
637
|
-
try {
|
|
638
|
-
await requestJson(`/knowledge/bases/${encodeURIComponent(base.id)}`, {
|
|
639
|
-
method: 'PATCH',
|
|
640
|
-
headers: { 'content-type': 'application/json' },
|
|
641
|
-
body: JSON.stringify({ resetWikiTypes: true }),
|
|
642
|
-
})
|
|
643
|
-
await loadBases(base.id)
|
|
644
|
-
say('success', '已恢复出厂 Wiki 类型')
|
|
645
|
-
} catch (error) {
|
|
646
|
-
say('error', error instanceof Error ? error.message : String(error))
|
|
647
|
-
} finally {
|
|
648
|
-
setBusy(null)
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
773
|
|
|
652
774
|
const writeSource = async (title: string, content: string, externalId: string): Promise<void> => {
|
|
653
775
|
const base = active
|
|
@@ -738,11 +860,18 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
738
860
|
// 的**原文状态重新满足 `open`,于是面板「自己又弹回来」——互斥必须两边都成立。
|
|
739
861
|
setCardDetail(null)
|
|
740
862
|
setDetailLoading(true)
|
|
741
|
-
setDetail({ document: { ...document, content: '', frontmatter: {}, assets: [] }, versions: [] })
|
|
863
|
+
setDetail({ document: { ...document, content: '', contentChars: 0, contentTruncated: false, frontmatter: {}, assets: [] }, versions: [] })
|
|
742
864
|
try {
|
|
743
865
|
const q = `?kb=${encodeURIComponent(baseId)}`
|
|
744
|
-
|
|
745
|
-
|
|
866
|
+
/**
|
|
867
|
+
* 两条请求**并行**。原来是串行的(先详情、再版本),于是一次打开弹层的等待
|
|
868
|
+
* 是两段的和 —— 而它们之间没有任何依赖。详情返回之后弹层就能读,
|
|
869
|
+
* 版本时间线自己在 `detailLoading` 里补。
|
|
870
|
+
*/
|
|
871
|
+
const [docPayload, versionPayload] = await Promise.all([
|
|
872
|
+
requestJson<{ document: KnowledgeDocumentDetail }>(`/knowledge/documents/${encodeURIComponent(document.docId)}${q}`),
|
|
873
|
+
requestJson<{ versions: DocumentVersion[] }>(`/knowledge/documents/${encodeURIComponent(document.docId)}/versions${q}`),
|
|
874
|
+
])
|
|
746
875
|
if (docPayload.data?.document) {
|
|
747
876
|
setDetail({ document: docPayload.data.document, versions: versionPayload.data?.versions ?? [] })
|
|
748
877
|
}
|
|
@@ -796,20 +925,41 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
796
925
|
|
|
797
926
|
// ------------------------------------------------------------ Wiki 详情
|
|
798
927
|
|
|
799
|
-
/**
|
|
800
|
-
|
|
928
|
+
/**
|
|
929
|
+
* 点开一张卡片:内容 + 版本历史(版本由服务端在每次正文变化前快照)。
|
|
930
|
+
*
|
|
931
|
+
*
|
|
932
|
+
* 入参只要求 `entityId` + `entityType`:正文列表里有完整卡片对象,而「相关卡片」
|
|
933
|
+
* 那一列(0049)只知道对方的身份与标题 —— 详情随后由接口取回,占位对象只在取数
|
|
934
|
+
* 失败时用得上,所以它由这里就地补全,不要求每个调用方都造一份假的 KnowledgeCardView。
|
|
935
|
+
*/
|
|
936
|
+
const openCard = async (card: Pick<KnowledgeCardView, 'entityId' | 'entityType'> & Partial<KnowledgeCardView>): Promise<void> => {
|
|
801
937
|
const baseId = activeIdRef.current
|
|
802
938
|
if (!baseId) return
|
|
803
939
|
setDetail(null)
|
|
804
940
|
setCardLoading(true)
|
|
805
|
-
setCardDetail({
|
|
941
|
+
setCardDetail({
|
|
942
|
+
card: {
|
|
943
|
+
entityId: card.entityId, entityType: card.entityType, title: card.title ?? card.entityId,
|
|
944
|
+
status: card.status ?? 'unreviewed', content: card.content ?? '', sourceRefs: card.sourceRefs ?? [], updatedAt: card.updatedAt ?? '',
|
|
945
|
+
},
|
|
946
|
+
versions: [], assets: [],
|
|
947
|
+
})
|
|
806
948
|
try {
|
|
807
949
|
const q = `?kb=${encodeURIComponent(baseId)}&entity_type=${encodeURIComponent(card.entityType)}`
|
|
808
950
|
const [detailPayload, versionPayload] = await Promise.all([
|
|
809
|
-
requestJson<{ card: KnowledgeCardView }>(`/knowledge/cards/${encodeURIComponent(card.entityId)}${q}`),
|
|
951
|
+
requestJson<{ card: KnowledgeCardView; assets?: AssetRefView[]; related?: CardRelatedView }>(`/knowledge/cards/${encodeURIComponent(card.entityId)}${q}`),
|
|
810
952
|
requestJson<{ versions: WikiCardVersionView[] }>(`/knowledge/cards/${encodeURIComponent(card.entityId)}/versions${q}`),
|
|
811
953
|
])
|
|
812
|
-
setCardDetail(
|
|
954
|
+
setCardDetail(current => ({
|
|
955
|
+
card: detailPayload.data?.card ?? current?.card ?? {
|
|
956
|
+
entityId: card.entityId, entityType: card.entityType, title: card.title ?? card.entityId,
|
|
957
|
+
status: card.status ?? 'unreviewed', content: card.content ?? '', sourceRefs: card.sourceRefs ?? [], updatedAt: card.updatedAt ?? '',
|
|
958
|
+
},
|
|
959
|
+
versions: versionPayload.data?.versions ?? [],
|
|
960
|
+
assets: detailPayload.data?.assets ?? [],
|
|
961
|
+
...(detailPayload.data?.related ? { related: detailPayload.data.related } : {}),
|
|
962
|
+
}))
|
|
813
963
|
} catch (error) {
|
|
814
964
|
setNotice({ tone: 'error', text: error instanceof Error ? error.message : String(error) })
|
|
815
965
|
setCardDetail(null)
|
|
@@ -818,6 +968,33 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
818
968
|
}
|
|
819
969
|
}
|
|
820
970
|
|
|
971
|
+
/**
|
|
972
|
+
* 抽屉里做完一次写(review / 备注)之后,把**这张卡片**和**卡片列表**都重新读一遍。
|
|
973
|
+
*
|
|
974
|
+
* 为什么不复用 `openCard`:它先把 `cardDetail` 清成 `{versions: []}` 再拉全量,
|
|
975
|
+
* 那会在人刚点完「review 通过」时把卡片正文闪成空的。这里只覆盖数据、保住抽屉
|
|
976
|
+
* 已经渲染出来的东西。
|
|
977
|
+
*
|
|
978
|
+
* 列表也必须刷:行上的「已 review / 异常点 N」徽标就是列表回答的那个问题。
|
|
979
|
+
*/
|
|
980
|
+
const refreshCardAfterReview = async (): Promise<void> => {
|
|
981
|
+
const baseId = activeIdRef.current
|
|
982
|
+
const current = cardDetail
|
|
983
|
+
if (!baseId || !current) return
|
|
984
|
+
try {
|
|
985
|
+
const q = `?kb=${encodeURIComponent(baseId)}&entity_type=${encodeURIComponent(current.card.entityType)}`
|
|
986
|
+
const payload = await requestJson<{ card: KnowledgeCardView }>(`/knowledge/cards/${encodeURIComponent(current.card.entityId)}${q}`)
|
|
987
|
+
// 用函数式更新:人可能在这两次往返之间点了别的卡片,闭包里的 `current` 已经不是
|
|
988
|
+
// 抽屉里现在那一张了。
|
|
989
|
+
setCardDetail(previous => previous && previous.card.entityId === current.card.entityId
|
|
990
|
+
? { ...previous, card: payload.data?.card ?? previous.card }
|
|
991
|
+
: previous)
|
|
992
|
+
} catch (error) {
|
|
993
|
+
setNotice({ tone: 'error', text: error instanceof Error ? error.message : String(error) })
|
|
994
|
+
}
|
|
995
|
+
void loadCards(baseId)
|
|
996
|
+
}
|
|
997
|
+
|
|
821
998
|
const revertCard = async (): Promise<void> => {
|
|
822
999
|
const target = cardRestore
|
|
823
1000
|
const current = cardDetail
|
|
@@ -917,14 +1094,38 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
917
1094
|
.map(([ext, count]) => ({ value: ext, label: `${ext}(${count})` }))
|
|
918
1095
|
}, [documents])
|
|
919
1096
|
/**
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1097
|
+
* 一次性迁移里没有静默处理掉的事(来源分类改名冲突、卡片改名迁移失败)。
|
|
1098
|
+
*
|
|
1099
|
+
* 它必须显示出来:**迁移失败而没人知道,等价于数据已经丢了**。放在卡片列表上方,
|
|
1100
|
+
* 因为它比「库里有什么」更紧急。
|
|
922
1101
|
*/
|
|
923
|
-
const
|
|
1102
|
+
const migrationProblems = Array.isArray(stats.migrationProblems) ? stats.migrationProblems as string[] : []
|
|
924
1103
|
const currentVersionId = detail?.versions.find(version => version.isCurrent)?.versionId
|
|
925
1104
|
const currentCardVersionId = cardDetail?.versions.find(version => version.isCurrent)?.versionId
|
|
926
1105
|
const detailCards = detail ? cardsBySource.get(detail.document.docId) ?? [] : []
|
|
927
1106
|
const pendingCards = cards.filter(card => schemaIssuesOf(card).length > 0)
|
|
1107
|
+
/**
|
|
1108
|
+
* 需要人看一眼的卡片数:**没人看过**,或者**被标了异常点**(0044)。
|
|
1109
|
+
*
|
|
1110
|
+
* 0044 之前这里是一整块待办区,逐条列出正文与动作。现在 review 长在卡片预览里,
|
|
1111
|
+
* 列表只需要回答「有几张要我动手」—— 具体是哪几张、缺什么、谁说过什么,点开就见。
|
|
1112
|
+
*/
|
|
1113
|
+
const unreviewedCards = cards.filter(card =>
|
|
1114
|
+
card.review?.state !== 'reviewed' || (card.annotations ?? []).some(item => item.kind === 'anomaly')).length
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* 在独立整页里读这份原文。
|
|
1118
|
+
*
|
|
1119
|
+
* 抽屉宽 380px,而原文可能是一份几十万字的转换文档 —— 那里只显示前 4000 字符
|
|
1120
|
+
* (`SOURCE_PREVIEW_CHARS`),剩下的出口是这个整页:新标签、可整页搜索、自带分页,
|
|
1121
|
+
* 一次只取一段。
|
|
1122
|
+
*/
|
|
1123
|
+
const openFullDocument = useCallback((document: KnowledgeDocument): void => {
|
|
1124
|
+
const kb = activeIdRef.current
|
|
1125
|
+
if (!kb) return
|
|
1126
|
+
const url = `/knowledge/documents/${encodeURIComponent(document.docId)}/view?kb=${encodeURIComponent(kb)}`
|
|
1127
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
1128
|
+
}, [])
|
|
928
1129
|
|
|
929
1130
|
/**
|
|
930
1131
|
* 文件类型筛在客户端做(接口没有这个过滤口),其余两个筛选在服务端。
|
|
@@ -949,14 +1150,21 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
949
1150
|
/**
|
|
950
1151
|
* 步骤轨上每一步的真实事实。计数来自落盘与设置:`active` 的计数是服务端算的,
|
|
951
1152
|
* 图谱计数优先读 `/knowledge/stats`(它统计的是全量),拿不到才退回这一页拉到的前 50 条。
|
|
1153
|
+
*
|
|
1154
|
+
* 0042:wiki 这一步的 note 补上 wikiNotes 数、索引版本、增量指纹与卡片快照 ——
|
|
1155
|
+
* 它们原来在「编译 wiki 与 wikiNotes」那张卡里,卡删掉之后这几条数得有地方去。
|
|
1156
|
+
* 顺序按重要性排:窄栏下 note 会省略号截断,先看到的必须是更该看到的。
|
|
952
1157
|
*/
|
|
953
1158
|
const stepFacts = useMemo(() => ({
|
|
954
1159
|
'source-raw': { value: active ? `${active.sourceCount} 个文件` : '—', note: '按格式路由解析,同 external_id 幂等' },
|
|
955
|
-
wiki: {
|
|
1160
|
+
wiki: {
|
|
1161
|
+
value: active ? `${active.cardCount} 张卡片` : '—',
|
|
1162
|
+
note: `${active?.noteCount ?? 0} 条 wikiNotes · 索引 v${active?.buildVersion ?? 0} · 指纹 ${String(stats.trackedCards ?? '—')} · 快照 ${String(stats.cardVersions ?? 0)}`,
|
|
1163
|
+
},
|
|
956
1164
|
graph: { value: `${stats.graphNodes ?? graph.nodes.length} 节点 / ${stats.graphEdges ?? graph.edges.length} 边`, note: 'supported_by 溯源边 + relates_to 关系边' },
|
|
957
|
-
recall: { value: `topK ${String(knowledgeValue?.recallTopK ?? 10)}`, note: `${String(knowledgeValue?.recallMode ?? 'hybrid')}
|
|
1165
|
+
recall: { value: `topK ${String(knowledgeValue?.recallTopK ?? 10)}`, note: `${String(knowledgeValue?.recallMode ?? 'hybrid')} 关键词 + 向量融合` },
|
|
958
1166
|
rerank: { value: `topN ${String(knowledgeValue?.contextTopN ?? 5)}`, note: 'BERT cross-encoder 精排' },
|
|
959
|
-
}), [active, stats.graphNodes, stats.graphEdges, graph.nodes.length, graph.edges.length, knowledgeValue?.recallTopK, knowledgeValue?.recallMode, knowledgeValue?.contextTopN])
|
|
1167
|
+
}), [active, stats.graphNodes, stats.graphEdges, stats.trackedCards, stats.cardVersions, graph.nodes.length, graph.edges.length, knowledgeValue?.recallTopK, knowledgeValue?.recallMode, knowledgeValue?.contextTopN])
|
|
960
1168
|
|
|
961
1169
|
// --------------------------------------------------------------- 分栏内容
|
|
962
1170
|
|
|
@@ -1056,9 +1264,40 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1056
1264
|
这里不再留任何指路文字:工具栏与卡片头各自带着入口,路牌只是搬迁留下的债。 */}
|
|
1057
1265
|
</div>
|
|
1058
1266
|
|
|
1059
|
-
/**
|
|
1267
|
+
/**
|
|
1268
|
+
* Wiki:**两种内容,两枚 tab**(0042)。
|
|
1269
|
+
*
|
|
1270
|
+
* 0042 之前这里是三块竖排:卡片列表 → 「编译 wiki 与 wikiNotes」→ 抽取流程。
|
|
1271
|
+
* 中间那一块同时讲动作(增量 / 全量按钮)与结果(8 行计数)—— 动作是「对这个库
|
|
1272
|
+
* 做什么」,归工具栏;结果是「这一段现在有多少东西」,归步骤轨。把它留在这里,
|
|
1273
|
+
* 等于让一栏内容替工具栏收着一枚按钮,还占掉一屏。
|
|
1274
|
+
*
|
|
1275
|
+
* **review 不进这一栏**(0044 第二次改):它第一版在这里插了一块「入库后的 review」
|
|
1276
|
+
* 待办区,按卡片逐条列出正文、校验与动作。那等于把同一张卡片摊在两个地方,而人点开
|
|
1277
|
+
* 卡片看内容的那一刻,正是他要判断这张卡对不对的那一刻。现在它长在卡片预览抽屉里
|
|
1278
|
+
* (`WikiCardReview`),列表只用行上的徽标回答「哪几张需要我看」。
|
|
1279
|
+
*/
|
|
1060
1280
|
const renderWiki = (): ReactNode => <div className={styles.viewStack}>
|
|
1061
|
-
{/*
|
|
1281
|
+
{/* 迁移里没静默处理掉的事排在最前面:**迁移失败而没人知道,等价于数据已经丢了**。
|
|
1282
|
+
它比「库里有什么」更紧急,所以不藏在统计里、也不只进日志。 */}
|
|
1283
|
+
{migrationProblems.length > 0 && <p className={cn(styles.kbNotice, styles.kbNoticeError)} role="alert">
|
|
1284
|
+
<AlertCircle size={13} />
|
|
1285
|
+
<span>
|
|
1286
|
+
<b>有 {migrationProblems.length} 处改名迁移没有生效</b>,旧文件都留在原地(没有覆盖、没有删除):
|
|
1287
|
+
{migrationProblems.join(';')}
|
|
1288
|
+
</span>
|
|
1289
|
+
</p>}
|
|
1290
|
+
{/* Pass 0 的候选 / 建卡计数(`candidatesExtracted` / `cardsCreated` / `cardsUpdated`)
|
|
1291
|
+
不在这一屏上显示。0042 撤掉的理由仍然成立:这三个数**没有指向任何可动手的东西** ——
|
|
1292
|
+
「上一轮抽出 0 个候选」既不是待办,也不是这一栏的内容。数字本身还在
|
|
1293
|
+
build-manifest 里,排查编译问题时从产物读,不占这一屏。 */}
|
|
1294
|
+
<Tabs className={styles.kbSubTabs} value={wikiTab} onValueChange={next => setWikiTab(next as 'cards' | 'flow')}>
|
|
1295
|
+
<TabsList className={cn(styles.tabsList, styles.kbSubTabsList)}>
|
|
1296
|
+
<TabsTrigger value="cards"><FileText size={12} />卡片</TabsTrigger>
|
|
1297
|
+
<TabsTrigger value="flow"><Layers3 size={12} />抽取流程</TabsTrigger>
|
|
1298
|
+
</TabsList>
|
|
1299
|
+
<TabsContent value="cards">
|
|
1300
|
+
{/* 卡片列表和 SourceRaw 的文件列表同一个位置、同一套筛选行 ——
|
|
1062
1301
|
两栏的「这一层有什么」都应该是打开就能看到的第一件东西。0027 之前这里
|
|
1063
1302
|
先是一张编译统计卡,卡片列表在第三位,得先读过参数才看得到内容。 */}
|
|
1064
1303
|
<Card className={styles.kbCard}>
|
|
@@ -1092,62 +1331,50 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1092
1331
|
/>
|
|
1093
1332
|
<Button variant="ghost" size="icon" title="刷新" aria-label="刷新卡片列表" onClick={() => void loadCards(activeIdRef.current)}><RefreshCw size={14} /></Button>
|
|
1094
1333
|
</div>
|
|
1095
|
-
{
|
|
1334
|
+
{/* 两件事分开说:**「有没有人看过」和「卡片填完没有」不是一回事**。
|
|
1335
|
+
合成一句「有 N 张有问题」,人既不知道要不要动手,也不知道动手做什么。 */}
|
|
1336
|
+
{(unreviewedCards > 0 || pendingCards.length > 0) && <p className={styles.kbFilterHint}>
|
|
1337
|
+
{unreviewedCards > 0 && <>有 <b>{unreviewedCards}</b> 张还没人看过或带着异常点 —— 点开任意一张卡片,review 与备注在它的预览侧栏里。</>}
|
|
1338
|
+
{unreviewedCards > 0 && pendingCards.length > 0 && ' '}
|
|
1339
|
+
{pendingCards.length > 0 && <>另有 <b>{pendingCards.length}</b> 张缺必填小节 / 字段。这不是错误:缺信息也入库,后面有涉及到的文档会补上。</>}
|
|
1340
|
+
</p>}
|
|
1096
1341
|
{visibleCards.length === 0 && <p className={styles.kbEmpty}>
|
|
1097
1342
|
{cards.length > 0
|
|
1098
1343
|
? '当前筛选没有命中的卡片。清掉筛选条件就能看到这个知识库的全部卡片。'
|
|
1099
|
-
:
|
|
1344
|
+
: <>还没有卡片。先录入一份来源(或上传文件),再点「编译」—— 编译器会按 wiki 类型的 schema 抽出实体并直接入库;缺信息也照进,后面有涉及到的文档会慢慢补上。</>}
|
|
1100
1345
|
</p>}
|
|
1101
1346
|
<ul className={styles.cardList}>
|
|
1102
1347
|
{visibleCards.map(card => {
|
|
1103
1348
|
const issues = schemaIssuesOf(card)
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
const extracted = card.metadata?.extraction === 'pass0'
|
|
1349
|
+
// 编译期抽出来的卡片是**机器写的**,与 Agent 直接写入的必须能一眼分开:
|
|
1350
|
+
// 两者的状态都可能是 unreviewed,但「没人看过」和「是抽出来的」是两件事。
|
|
1351
|
+
const extracted = card.metadata?.extraction === 'pass0' || card.metadata?.extraction === 'model'
|
|
1352
|
+
// 异常点是这张卡片**已经被指出有问题**:它照样召回得到(评分被压低),
|
|
1353
|
+
// 但列表里必须显眼 —— 看不见的异常点等于没有异常点。
|
|
1354
|
+
const anomalies = (card.annotations ?? []).filter(item => item.kind === 'anomaly').length
|
|
1107
1355
|
return <li key={card.entityId} className={styles.cardRow}>
|
|
1108
1356
|
<button type="button" className={styles.cardRowMain} onClick={() => void openCard(card)}>
|
|
1109
|
-
|
|
1357
|
+
{/* 一行一条:状态 + 标题 + 两枚状态徽标 +(右侧)类型与时间。
|
|
1358
|
+
0042 撤掉了摘要行与「类型 · id · 出处」行 —— 三行内容让每一张卡片都
|
|
1359
|
+
长成一个框,一屏三张卡就是三个框。扫一眼只需要认出「是哪一张」,
|
|
1360
|
+
摘要、entity_id、出处数都进抽屉(title 里挂 id 与出处数)。 */}
|
|
1361
|
+
<span className={styles.cardRowHead}>
|
|
1110
1362
|
<Badge tone={card.status === 'active' || card.status === 'reviewed' ? 'success' : card.status === 'problem' ? 'warning' : 'neutral'}>{card.status}</Badge>
|
|
1111
|
-
<span className={styles.cardRowTitle} title={card.entityId}>{card.title}</span>
|
|
1112
|
-
{extracted && <Badge tone="blue"
|
|
1363
|
+
<span className={styles.cardRowTitle} title={`${card.entityId} · ${card.sourceRefs.length} 个出处`}>{card.title}</span>
|
|
1364
|
+
{extracted && <Badge tone="blue">编译产出</Badge>}
|
|
1365
|
+
{card.review?.state === 'reviewed' && <Badge tone="success">已 review</Badge>}
|
|
1366
|
+
{anomalies > 0 && <Badge tone="warning">异常点 {anomalies}</Badge>}
|
|
1113
1367
|
{issues.length > 0 && <Badge tone="warning">待补 {issues.length}</Badge>}
|
|
1114
|
-
<span className={styles.kbTypeCount}>{formatTime(card.updatedAt)}</span>
|
|
1115
|
-
</
|
|
1116
|
-
<p className={styles.cardRowText}>{card.summary || card.content.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ').replace(/\s+/g, ' ').slice(0, 160) || '(空卡片)'}</p>
|
|
1117
|
-
<span className={styles.kbCode}>{card.entityType} · {card.entityId} · {card.sourceRefs.length} 个出处</span>
|
|
1368
|
+
<span className={styles.kbTypeCount}>{card.entityType} · {formatTime(card.updatedAt)}</span>
|
|
1369
|
+
</span>
|
|
1118
1370
|
</button>
|
|
1119
1371
|
</li>
|
|
1120
1372
|
})}
|
|
1121
1373
|
</ul>
|
|
1122
1374
|
</Card>
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
<div><h3><Layers3 size={13} />编译 wiki 与 wikiNotes</h3></div>
|
|
1127
|
-
<div className={styles.kbCardActions}>
|
|
1128
|
-
<Button variant="outline" size="sm" onClick={() => void rebuild('wiki')} disabled={busy !== null}><RefreshCw size={13} />{busy === 'compile' ? '编译中…' : '增量编译'}</Button>
|
|
1129
|
-
<Button variant="ghost" size="sm" onClick={() => void rebuild('all')} disabled={busy !== null}>全量重编</Button>
|
|
1130
|
-
</div>
|
|
1131
|
-
</div>
|
|
1132
|
-
<p className={styles.kbHint}>
|
|
1133
|
-
<b>增量</b>:只有正文 / 标题 / 状态 / 出处变过的卡片会重新切分成 wikiNotes,其余卡片的召回单元**逐字复用**(unitId 不变,向量也不重编码)。
|
|
1134
|
-
语料完全没变时这一次编译是**空操作**:不写产物、不升版本,因为升版本会让整份向量缓存作废。
|
|
1135
|
-
</p>
|
|
1136
|
-
<div className={styles.infoList}>
|
|
1137
|
-
<div><span>wiki 卡片</span><strong>{active?.cardCount ?? 0}</strong></div>
|
|
1138
|
-
<div><span>wikiNotes</span><strong>{active?.noteCount ?? 0}</strong></div>
|
|
1139
|
-
<div><span>已纳入增量指纹</span><strong>{String(stats.trackedCards ?? '—')}</strong></div>
|
|
1140
|
-
<div><span>卡片版本快照</span><strong>{String(stats.cardVersions ?? 0)}</strong></div>
|
|
1141
|
-
<div><span>索引版本</span><strong>v{active?.buildVersion ?? 0}</strong></div>
|
|
1142
|
-
<div><span>待补 schema 的卡片</span><strong>{pendingCards.length}</strong></div>
|
|
1143
|
-
{/* Pass 0 的四个诚实计数:抽了几个候选、拒了几个、建了几张、并进几张。
|
|
1144
|
-
它们来自上一轮编译的 build-manifest,不是估算。 */}
|
|
1145
|
-
<div><span>上一轮抽出候选</span><strong>{String(lastBuild.candidatesExtracted ?? '—')}</strong></div>
|
|
1146
|
-
<div><span>新建 / 合并卡片</span><strong>{stats.lastBuild ? `${lastBuild.cardsCreated ?? 0} / ${lastBuild.cardsMerged ?? 0}` : '—'}</strong></div>
|
|
1147
|
-
</div>
|
|
1148
|
-
</Card>
|
|
1149
|
-
|
|
1150
|
-
<WikiFlowCard />
|
|
1375
|
+
</TabsContent>
|
|
1376
|
+
<TabsContent value="flow"><WikiFlowCard /></TabsContent>
|
|
1377
|
+
</Tabs>
|
|
1151
1378
|
|
|
1152
1379
|
{/* **Wiki 类型**(卡片 schema)也不在这一栏了(0034):它和来源类型是同一类东西
|
|
1153
1380
|
—— 按库持久化的词汇表,一个决定「一份文件以什么业务身份进来」,一个决定「一张
|
|
@@ -1208,6 +1435,8 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1208
1435
|
<SettingsChip target="context" onOpen={onOpenSettings} />
|
|
1209
1436
|
</> } : {})}
|
|
1210
1437
|
resetKey={`${active?.id ?? ''}:${String(knowledgeValue?.recallTopK ?? '')}:${String(knowledgeValue?.recallMode ?? '')}:${String(knowledgeValue?.contextTopN ?? '')}`}
|
|
1438
|
+
/* 召回结果里的卡片点得开:抽屉是**宿主的**,面板只负责喊一声(它不认识抽屉)。 */
|
|
1439
|
+
onOpenWiki={card => { void openCard(card) }}
|
|
1211
1440
|
defaults={{
|
|
1212
1441
|
...(typeof knowledgeValue?.recallMode === 'string' ? { mode: knowledgeValue.recallMode } : {}),
|
|
1213
1442
|
...(typeof knowledgeValue?.recallTopK === 'number' ? { topK: knowledgeValue.recallTopK } : {}),
|
|
@@ -1247,16 +1476,42 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1247
1476
|
/>
|
|
1248
1477
|
{/* 三个动作同形:图标 + 文字的 outline 按钮,同一个 30px 高。内核控制台是一个
|
|
1249
1478
|
**链接**,所以它走 `Button asChild` 拿到同一套外观 —— 早先它是一枚裸的
|
|
1250
|
-
`.tita-uiButton` 锚点,在这两枚按钮旁边读起来像第三种控件。
|
|
1479
|
+
`.tita-uiButton` 锚点,在这两枚按钮旁边读起来像第三种控件。
|
|
1480
|
+
0042:「编译」原本在 wiki 栏的统计卡里(增量 + 全量两枚),现在它是**工具栏的
|
|
1481
|
+
一个动作**,两档收进同一枚按钮的下拉里 —— 编译是「对这个库做什么」,
|
|
1482
|
+
它和「选哪个库」「类型定义」同层,不属于任何一栏的内容。 */}
|
|
1251
1483
|
<span className={styles.kbToolbarActions}>
|
|
1252
1484
|
<Button
|
|
1253
1485
|
variant="outline"
|
|
1254
1486
|
size="sm"
|
|
1255
1487
|
disabled={!active}
|
|
1256
1488
|
title={active ? `编辑 ${active.name} 的来源类型与 Wiki 类型` : '先选择一个知识库'}
|
|
1257
|
-
onClick={() =>
|
|
1489
|
+
onClick={() => setTypeDef({ open: true, section: 'source' })}
|
|
1258
1490
|
><Tags size={13} />类型定义</Button>
|
|
1259
|
-
<
|
|
1491
|
+
<DropdownMenu>
|
|
1492
|
+
<DropdownMenuTrigger asChild>
|
|
1493
|
+
<Button variant="outline" size="sm" disabled={busy !== null || !active}>
|
|
1494
|
+
<RefreshCw size={14} />{busy === 'compile' ? '编译中…' : '编译'}<ChevronDown size={12} />
|
|
1495
|
+
</Button>
|
|
1496
|
+
</DropdownMenuTrigger>
|
|
1497
|
+
<DropdownMenuContent align="end" className={styles.kbCompileMenu}>
|
|
1498
|
+
{/* 每一项是「名字 + 一行说明」两层,不是一句长句子。
|
|
1499
|
+
共享组件的菜单默认把宽度锁成触发器的宽度(`编译 ⌄` 只有 90px 上下),
|
|
1500
|
+
长句子进去就是一根 8 个字一行的竖条 —— 宽度由 CSS 纠正,句子由这里拆短。 */}
|
|
1501
|
+
<DropdownMenuItem onSelect={() => void rebuild('wiki')}>
|
|
1502
|
+
<span className={styles.kbCompileItem}>
|
|
1503
|
+
<b>增量编译</b>
|
|
1504
|
+
<small>只重切变过的卡片,其余 wikiNotes 逐字复用</small>
|
|
1505
|
+
</span>
|
|
1506
|
+
</DropdownMenuItem>
|
|
1507
|
+
<DropdownMenuItem onSelect={() => void rebuild('all')}>
|
|
1508
|
+
<span className={styles.kbCompileItem}>
|
|
1509
|
+
<b>全量重编</b>
|
|
1510
|
+
<small>全部重切,并把索引版本加一</small>
|
|
1511
|
+
</span>
|
|
1512
|
+
</DropdownMenuItem>
|
|
1513
|
+
</DropdownMenuContent>
|
|
1514
|
+
</DropdownMenu>
|
|
1260
1515
|
<Button variant="outline" size="sm" asChild>
|
|
1261
1516
|
<a href="/convert/" target="_blank" rel="noreferrer" title="打开解析内核控制台"><FileUp size={14} />解析内核</a>
|
|
1262
1517
|
</Button>
|
|
@@ -1265,7 +1520,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1265
1520
|
|
|
1266
1521
|
{/* 分栏是**一条流水线**,所以切换器是步骤轨而不是一排等宽 tab:收录 → 编译 →
|
|
1267
1522
|
关联 → 检索 → 精排。方向本身就是信息(写入侧在左、读取侧在右)。 */}
|
|
1268
|
-
<KnowledgeStepRail tab={tab} onChange={
|
|
1523
|
+
<KnowledgeStepRail tab={tab} onChange={changeTab} facts={stepFacts} />
|
|
1269
1524
|
|
|
1270
1525
|
{notice && <p className={cn(styles.kbNotice, notice.tone === 'error' && styles.kbNoticeError)} role="status">
|
|
1271
1526
|
<AlertCircle size={13} /><span>{notice.text}</span>
|
|
@@ -1291,13 +1546,12 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1291
1546
|
{editOpen && <DialogContent>
|
|
1292
1547
|
<DialogHeader><DialogTitle>{draft.id ? '编辑知识库' : '新建知识库'}</DialogTitle></DialogHeader>
|
|
1293
1548
|
<div className={styles.kbForm}>
|
|
1294
|
-
<label
|
|
1549
|
+
<Field label="名称">
|
|
1295
1550
|
<Input value={draft.name} onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, name: event.target.value })} placeholder="例如:产品文档" />
|
|
1296
|
-
</
|
|
1297
|
-
<label
|
|
1551
|
+
</Field>
|
|
1552
|
+
<Field label="描述" hint="每个知识库有自己的根目录、来源分类与 Wiki 类型注册表;默认知识库沿用现有的知识库根目录。">
|
|
1298
1553
|
<Input value={draft.description} onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, description: event.target.value })} placeholder="这个知识库放什么" />
|
|
1299
|
-
</
|
|
1300
|
-
<p className={styles.kbHint}>每个知识库有自己的根目录、来源分类与 Wiki 类型注册表;默认知识库沿用现有的知识库根目录。</p>
|
|
1554
|
+
</Field>
|
|
1301
1555
|
</div>
|
|
1302
1556
|
<DialogFooter>
|
|
1303
1557
|
<Button variant="ghost" onClick={() => setEditOpen(false)}>取消</Button>
|
|
@@ -1310,13 +1564,13 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1310
1564
|
它**不是**设置抽屉里的第七块 —— 那两个注册表按库持久化,而设置是整个方案一份。
|
|
1311
1565
|
弹窗状态在这个组件内部;写路径仍走上面那几个 `patch*` / `saveSourceType`。 */}
|
|
1312
1566
|
{active && <TypeDefinitionDrawer
|
|
1313
|
-
open={
|
|
1314
|
-
onOpenChange={
|
|
1567
|
+
open={typeDef.open}
|
|
1568
|
+
onOpenChange={open => setTypeDef(current => ({ ...current, open }))}
|
|
1569
|
+
section={typeDef.section}
|
|
1315
1570
|
kbId={active.id}
|
|
1316
1571
|
kbName={active.name}
|
|
1317
1572
|
sourceTypes={active.sourceTypes ?? []}
|
|
1318
1573
|
wikiTypes={active.wikiTypes ?? []}
|
|
1319
|
-
schemaMode={active.wikiSchemaMode ?? 'report'}
|
|
1320
1574
|
customized={active.wikiTypesCustomized === true}
|
|
1321
1575
|
catalog={catalog}
|
|
1322
1576
|
busy={busy}
|
|
@@ -1324,8 +1578,6 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1324
1578
|
onToggleSourceType={toggleSourceType}
|
|
1325
1579
|
onRemoveSourceType={removeSourceType}
|
|
1326
1580
|
onApplyWikiTypes={patchWikiTypes}
|
|
1327
|
-
onModeChange={patchWikiSchemaMode}
|
|
1328
|
-
onResetWikiTypes={resetWikiTypes}
|
|
1329
1581
|
/>}
|
|
1330
1582
|
|
|
1331
1583
|
<RestoreDialog
|
|
@@ -1355,34 +1607,70 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1355
1607
|
一个 state —— 「同时开两个」在这里变成不可表达的状态。 */}
|
|
1356
1608
|
<Drawer open={detail !== null && cardDetail === null} onOpenChange={next => { if (!next) setDetail(null) }}>
|
|
1357
1609
|
{detail && <DrawerContent className={styles.drawer} resizable resizeKey="tita-source-preview">
|
|
1610
|
+
{/* 抽屉头:标题 + 一行「它是什么」。
|
|
1611
|
+
0042 把「新页面打开」搬进头里,是为了让它位置固定、不随正文是否被截断而出现;
|
|
1612
|
+
0049 又把它挪到「内容」小节头那一行 —— 用户的原话是「放到内容旁边」。
|
|
1613
|
+
它确实是「读这份内容的另一个去处」,与「内容」同处一行比挂在抽屉头更贴近它
|
|
1614
|
+
服务的那件事;而显隐仍然固定(不依赖正文长度),0042 那条约束没有被破坏。 */}
|
|
1358
1615
|
<DrawerHeader>
|
|
1359
|
-
<
|
|
1360
|
-
|
|
1616
|
+
<div className={styles.drawerHead}>
|
|
1617
|
+
<div className={styles.drawerHeadCopy}>
|
|
1618
|
+
<DrawerTitle>{detail.document.title}</DrawerTitle>
|
|
1619
|
+
<DrawerDescription>{detail.document.relativePath}</DrawerDescription>
|
|
1620
|
+
</div>
|
|
1621
|
+
</div>
|
|
1361
1622
|
</DrawerHeader>
|
|
1362
1623
|
<DrawerBody className={styles.drawerStack}>
|
|
1363
1624
|
{/* 0035:两种预览是**同一套分块**(元信息 → 内容 → 版本),SourceRaw 只多一块
|
|
1364
|
-
|
|
1625
|
+
「编译产物」。顺序不是随手排的:先确认「这是什么」(元信息,含分类与来源),
|
|
1365
1626
|
再读「它写了什么」(内容),再看「它编译出了什么」(派生结论),最后才是
|
|
1366
1627
|
「改过几次」(版本)。0030 那条「原文在编译产物之前」在这里仍然成立。
|
|
1367
|
-
时间一律用标准格式(`formatDateTime`)—— 抽屉里是逐字核对事实的地方。
|
|
1628
|
+
时间一律用标准格式(`formatDateTime`)—— 抽屉里是逐字核对事实的地方。
|
|
1629
|
+
|
|
1630
|
+
0049 按 **front matter 的键**重排了这一块,并撤掉纯技术项:
|
|
1631
|
+
- 行与键对得上(分类 = `type`,来源链接 = `resource`,作者 = `sources[0].author`,
|
|
1632
|
+
采集时间 = `generated.at`…),人读界面与读文件是同一件事;
|
|
1633
|
+
- `external_id`(业务系统里的 id)、`content_hash`(内容指纹)、`解析器`
|
|
1634
|
+
(`anydoc@0.2.4/pptx` 这种版本串)都不占一行 —— 它们是排查用的,
|
|
1635
|
+
不是「这份东西是什么」。前两项在文件里、在版本列表里都查得到;
|
|
1636
|
+
解析器与生成者收进「文件格式」那一行的悬停说明。 */}
|
|
1368
1637
|
<PreviewMeta rows={[
|
|
1369
|
-
{ label: '类型', value: `${detail.document.ext} · ${detail.document.sourceFormat || 'unknown'}` },
|
|
1370
|
-
{ label: '解析器', value: detail.document.parser || '—' },
|
|
1371
1638
|
{ label: '来源分类', value: `${sourceTypeLabel(detail.document.sourceType)} · ${detail.document.sourceType}` },
|
|
1372
|
-
{
|
|
1373
|
-
|
|
1639
|
+
{
|
|
1640
|
+
label: '文件格式',
|
|
1641
|
+
value: `${sourceFormatLabel(detail.document.sourceFormat)} · ${detail.document.ext}`,
|
|
1642
|
+
title: [
|
|
1643
|
+
detail.document.parser ? `解析器 ${detail.document.parser}` : '',
|
|
1644
|
+
generatorOf(detail.document.frontmatter),
|
|
1645
|
+
].filter(Boolean).join(' · '),
|
|
1646
|
+
},
|
|
1647
|
+
// 0048:OKF 的那几个顶层键(链接、作者、发布时间、采集时间)就排在分类下面
|
|
1648
|
+
// —— 它们是「这份东西是什么」的一部分,不该埋进 metadata 里让人去翻 JSON。
|
|
1649
|
+
...sourceProvenanceRows(detail.document.frontmatter),
|
|
1374
1650
|
{ label: '大小', value: formatBytes(detail.document.bytes) },
|
|
1375
1651
|
{ label: '创建', value: formatDateTime(detail.document.createdAt) },
|
|
1376
1652
|
{ label: '更新', value: formatDateTime(detail.document.updatedAt) },
|
|
1653
|
+
// 分类声明的元数据字段:有的显示值,缺的显示「缺」——「缺」本身就是信息,
|
|
1654
|
+
// 而且它指得出该去补什么。
|
|
1655
|
+
...declaredMetadataRows(active?.sourceTypes.find(def => def.id === detail.document.sourceType)?.metadataFields, detail.document.frontmatter),
|
|
1377
1656
|
]} />
|
|
1378
1657
|
|
|
1379
|
-
<PreviewContent
|
|
1658
|
+
<PreviewContent
|
|
1659
|
+
content={detail.document.content}
|
|
1660
|
+
assets={detail.document.assets}
|
|
1661
|
+
totalChars={detail.document.contentChars}
|
|
1662
|
+
action={<Button
|
|
1663
|
+
variant="outline" size="sm" className={styles.drawerHeadAction}
|
|
1664
|
+
title="在一个独立的整页里读完整原文(新标签页,可分页搜索)"
|
|
1665
|
+
onClick={() => openFullDocument(detail.document)}
|
|
1666
|
+
><ExternalLink size={12} />新页面打开</Button>}
|
|
1667
|
+
/>
|
|
1380
1668
|
|
|
1381
1669
|
<div className={styles.drawerSection}>
|
|
1382
1670
|
{/* WeKnora 的详情页在文件下面列分块;这里列的是这个 SourceRaw 编译出的 wiki 卡片。 */}
|
|
1383
1671
|
<div className={styles.kbSubHead}><h4><Layers3 size={13} />编译产物</h4><span>{detailCards.length} 张卡片</span></div>
|
|
1384
1672
|
{detailCards.length === 0
|
|
1385
|
-
? <p className={styles.kbHint}>还没有从这个文件编译出 wiki
|
|
1673
|
+
? <p className={styles.kbHint}>还没有从这个文件编译出 wiki 卡片。先「编译」—— 编译器会按 wiki 类型的 schema 抽出实体并直接入库。</p>
|
|
1386
1674
|
: <ul className={styles.cardList}>
|
|
1387
1675
|
{detailCards.map(card => <li key={card.entityId} className={styles.cardRow}>
|
|
1388
1676
|
<button type="button" className={styles.cardRowMain} onClick={() => void openCard(card)}>
|
|
@@ -1427,15 +1715,27 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1427
1715
|
{ label: '出处', value: cardDetail.card.sourceRefs.join(', ') || '—', code: true },
|
|
1428
1716
|
]} />
|
|
1429
1717
|
|
|
1430
|
-
{/*
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1718
|
+
{/* review 与备注排在元信息之后、正文之前(0044)。
|
|
1719
|
+
位置的理由:它是**人对这张卡做的判断**,与元信息同属「这张卡是什么」,
|
|
1720
|
+
而正文是要读的东西,可能很长。放在正文下面,人得先滚过全文才够得着按钮;
|
|
1721
|
+
放在正文上面,点开就能看见它现在是什么状态、别人说过什么。 */}
|
|
1722
|
+
<WikiCardReview
|
|
1723
|
+
kbId={activeId}
|
|
1724
|
+
card={cardDetail.card}
|
|
1725
|
+
onChanged={() => void refreshCardAfterReview()}
|
|
1726
|
+
/* 从卡片进来是奔着 Wiki 类型去的:让抽屉直接落在那一节,不用人再滚一遍。 */
|
|
1727
|
+
onOpenWikiTypes={() => { setCardDetail(null); setTypeDef({ open: true, section: 'wiki' }) }}
|
|
1728
|
+
/>
|
|
1729
|
+
|
|
1730
|
+
<PreviewContent content={cardDetail.card.content} assets={cardDetail.assets} />
|
|
1437
1731
|
|
|
1438
|
-
|
|
1732
|
+
{/* 相关卡片排在正文之后(0049):读完这张卡片,接着看它跟谁有关。放在正文
|
|
1733
|
+
之前会挡住「这张卡片到底说了什么」,而相关是读完之后才想做的事。 */}
|
|
1734
|
+
<PreviewRelated
|
|
1735
|
+
outgoing={cardDetail.related?.outgoing ?? []}
|
|
1736
|
+
incoming={cardDetail.related?.incoming ?? []}
|
|
1737
|
+
onOpen={link => void openCard(link)}
|
|
1738
|
+
/>
|
|
1439
1739
|
|
|
1440
1740
|
<PreviewVersions
|
|
1441
1741
|
hint="卡片正文每次变化前都会留一份快照;回滚是往前的写入,所以回滚前的那一版也会被快照 —— 回滚本身也可以回滚。"
|