diff --git a/dashboard/src/routes/resource/knowledge-base.tsx b/dashboard/src/routes/resource/knowledge-base.tsx index b539aff2..a4accd75 100644 --- a/dashboard/src/routes/resource/knowledge-base.tsx +++ b/dashboard/src/routes/resource/knowledge-base.tsx @@ -14,6 +14,10 @@ import { Sparkles, Trash2, Upload, + CheckCircle2, + CircleAlert, + FolderOpen, + HardDrive, } from 'lucide-react' import { CodeEditor } from '@/components/CodeEditor' @@ -143,13 +147,13 @@ const IMPORT_STEP_TEXT: Record = { } const IMPORT_KIND_OPTIONS: Array<{ value: MemoryImportTaskKind; label: string; description: string }> = [ - { value: 'upload', label: '上传文件', description: '从本地批量上传文本文件' }, + { value: 'upload', label: '上传文件', description: '从本地批量上传资料文件' }, { value: 'paste', label: '粘贴导入', description: '直接粘贴文本或 JSON 内容创建任务' }, { value: 'raw_scan', label: '本地扫描', description: '按路径别名和匹配规则批量扫描导入' }, { value: 'lpmm_openie', label: 'LPMM OpenIE', description: '读取 LPMM 数据并抽取关系' }, { value: 'lpmm_convert', label: 'LPMM 转换', description: '将 LPMM 数据转换到目标目录' }, - { value: 'temporal_backfill', label: '时序回填', description: '对既有数据执行时间字段回填' }, - { value: 'maibot_migration', label: 'MaiBot 迁移', description: '从 MaiBot 历史库迁移长期记忆数据' }, + { value: 'temporal_backfill', label: '时序回填', description: '为已有数据补充时间字段' }, + { value: 'maibot_migration', label: 'MaiBot 迁移', description: '从 MaiBot 历史数据迁移长期记忆' }, ] function normalizeProgress(value: number | string | null | undefined): number { @@ -371,6 +375,185 @@ function summarizeFeedbackActionPayload(value: Record | undefin return trimDeleteItemText(JSON.stringify(value, null, 2), 120) } +function pickFeedbackRelationTriplet(value: unknown): Record | null { + if (!value || typeof value !== 'object') { + return null + } + const record = value as Record + const subject = String(record.subject ?? '').trim() + const predicate = String(record.predicate ?? '').trim() + const object = String(record.object ?? '').trim() + if (!subject || !predicate || !object) { + return null + } + return record +} + +function formatFeedbackRelationTriplet(value: unknown): string { + const triplet = pickFeedbackRelationTriplet(value) + if (!triplet) { + return '' + } + return formatDeleteRelationText( + String(triplet.subject ?? ''), + String(triplet.predicate ?? ''), + String(triplet.object ?? ''), + ) +} + +function getFeedbackCorrectionPreview(task: MemoryFeedbackCorrectionDetailTaskPayload | MemoryFeedbackCorrectionSummaryPayload | null): { + headline: string + oldRelation: string + newRelation: string +} { + if (!task) { + return { + headline: '当前没有纠错摘要', + oldRelation: '', + newRelation: '', + } + } + + const detailTask = task as MemoryFeedbackCorrectionDetailTaskPayload + const rollbackPlanSummary = detailTask.rollback_plan_summary ?? {} + const forgottenRelations = Array.isArray(rollbackPlanSummary.forgotten_relations) + ? rollbackPlanSummary.forgotten_relations + : [] + const correctedWrite = rollbackPlanSummary.corrected_write && typeof rollbackPlanSummary.corrected_write === 'object' + ? rollbackPlanSummary.corrected_write + : {} + const correctedRelations = Array.isArray((correctedWrite as Record).corrected_relations) + ? ((correctedWrite as Record).corrected_relations as unknown[]) + : [] + + const oldRelation = formatFeedbackRelationTriplet(forgottenRelations[0]) + const newRelation = formatFeedbackRelationTriplet(correctedRelations[0]) + + if (oldRelation && newRelation) { + return { + headline: `将“${oldRelation}”纠正为“${newRelation}”`, + oldRelation, + newRelation, + } + } + if (newRelation) { + return { + headline: `补充了新的纠错结论:“${newRelation}”`, + oldRelation: '', + newRelation, + } + } + if (oldRelation) { + return { + headline: `撤销了旧记忆关系:“${oldRelation}”`, + oldRelation, + newRelation: '', + } + } + return { + headline: task.query_text || '当前纠错没有可读摘要', + oldRelation: '', + newRelation: '', + } +} + +function buildFeedbackImpactSummary(task: MemoryFeedbackCorrectionDetailTaskPayload | MemoryFeedbackCorrectionSummaryPayload | null): string[] { + if (!task) { + return [] + } + + const counts = task.affected_counts ?? {} + const items: string[] = [] + if (Number(counts.relations ?? 0) > 0) { + items.push(`影响关系 ${Number(counts.relations ?? 0)} 条`) + } + if (Number(counts.corrected_relations ?? 0) > 0) { + items.push(`新增纠正关系 ${Number(counts.corrected_relations ?? 0)} 条`) + } + if (Number(counts.correction_paragraphs ?? 0) > 0) { + items.push(`写入纠错段落 ${Number(counts.correction_paragraphs ?? 0)} 条`) + } + if (Number(counts.stale_paragraphs ?? 0) > 0) { + items.push(`标记旧段落 ${Number(counts.stale_paragraphs ?? 0)} 条`) + } + if (Number(counts.episode_sources ?? 0) > 0) { + items.push(`触发 Episode 修复 ${Number(counts.episode_sources ?? 0)} 个来源`) + } + if (Number(counts.profile_person_ids ?? 0) > 0) { + items.push(`触发 Profile 刷新 ${Number(counts.profile_person_ids ?? 0)} 个对象`) + } + return items +} + +function formatFeedbackActionType(actionType: string): string { + switch (actionType) { + case 'classification': + return '判定纠错' + case 'forget_relation': + return '撤销旧关系' + case 'mark_stale_paragraph': + return '标记旧段落' + case 'write_correction': + return '写入纠错' + case 'rollback_restore_relation': + return '恢复旧关系' + case 'rollback_delete_correction_paragraph': + return '隐藏纠错段落' + case 'rollback_revert_corrected_relation': + return '撤销纠正关系' + case 'rollback_clear_stale_mark': + return '清除脏段落标记' + case 'rollback_enqueue_episode_rebuild': + return '加入 Episode 修复队列' + case 'rollback_enqueue_profile_refresh': + return '加入 Profile 刷新队列' + case 'rollback_error': + return '回退失败' + case 'error': + return '处理失败' + case 'skip': + return '跳过处理' + default: + return actionType || '未知动作' + } +} + +function describeFeedbackActionLog(item: MemoryFeedbackActionLogPayload): string { + const beforeSummary = summarizeFeedbackActionPayload(item.before_payload) + const afterSummary = summarizeFeedbackActionPayload(item.after_payload) + + switch (item.action_type) { + case 'classification': + return afterSummary ? `系统完成判定:${afterSummary}` : '系统完成纠错判定' + case 'forget_relation': + return beforeSummary ? `旧关系已失效:${beforeSummary}` : '旧关系已被标记为失效' + case 'mark_stale_paragraph': + return '旧段落已标记为待复核,后续检索会更谨慎地使用它' + case 'write_correction': + return afterSummary ? `已写入新的纠错结果:${afterSummary}` : '已写入新的纠错段落和关系' + case 'rollback_restore_relation': + return afterSummary ? `已恢复旧关系状态:${afterSummary}` : '已恢复旧关系状态' + case 'rollback_delete_correction_paragraph': + return '已隐藏这次纠错写入的段落' + case 'rollback_revert_corrected_relation': + return '已撤销纠错阶段新增的关系' + case 'rollback_clear_stale_mark': + return '已清除旧段落的待复核标记' + case 'rollback_enqueue_episode_rebuild': + return '已重新加入 Episode 修复队列' + case 'rollback_enqueue_profile_refresh': + return '已重新加入 Profile 刷新队列' + case 'rollback_error': + return item.reason || '这次回退执行失败' + case 'error': + return item.reason || '这次纠错处理失败' + case 'skip': + return item.reason || '这次纠错被跳过' + default: + return afterSummary || beforeSummary || item.reason || '记录了一条动作日志' + } +} + type DeleteOperationItem = NonNullable[number] function trimDeleteItemText(value: string, maxLength: number = 140): string { @@ -675,10 +858,38 @@ export function KnowledgeBasePage() { return [] } return [ - { label: '运行状态', value: runtimeConfig.runtime_ready ? '就绪' : '未就绪' }, - { label: 'Embedding 维度', value: String(runtimeConfig.embedding_dimension) }, - { label: '自动保存', value: runtimeConfig.auto_save ? '开启' : '关闭' }, - { label: '数据目录', value: runtimeConfig.data_dir }, + { + label: '运行状态', + value: runtimeConfig.runtime_ready ? '就绪' : '未就绪', + description: runtimeConfig.embedding_degraded ? 'Embedding 降级运行' : '运行时检查通过', + icon: runtimeConfig.runtime_ready ? CheckCircle2 : CircleAlert, + className: runtimeConfig.runtime_ready ? 'border-emerald-500/20 bg-emerald-500/5' : 'border-amber-500/20 bg-amber-500/5', + iconClassName: runtimeConfig.runtime_ready ? 'text-emerald-500' : 'text-amber-500', + }, + { + label: 'Embedding 维度', + value: String(runtimeConfig.embedding_dimension), + description: runtimeConfig.relation_vectors_enabled ? '关系向量已启用' : '关系向量未启用', + icon: HardDrive, + className: 'border-sky-500/20 bg-sky-500/5', + iconClassName: 'text-sky-500', + }, + { + label: '自动保存', + value: runtimeConfig.auto_save ? '开启' : '关闭', + description: runtimeConfig.auto_save ? '运行数据会自动落盘' : '请留意手动保存', + icon: runtimeConfig.auto_save ? CheckCircle2 : CircleAlert, + className: runtimeConfig.auto_save ? 'border-primary/20 bg-primary/5' : 'border-muted-foreground/20 bg-muted/30', + iconClassName: runtimeConfig.auto_save ? 'text-primary' : 'text-muted-foreground', + }, + { + label: '数据目录', + value: runtimeConfig.data_dir, + description: '长期记忆存储位置', + icon: FolderOpen, + className: 'border-violet-500/20 bg-violet-500/5', + iconClassName: 'text-violet-500', + }, ] }, [runtimeConfig]) @@ -1731,6 +1942,14 @@ export function KnowledgeBasePage() { } return selectedFeedbackTaskDetail ?? selectedFeedbackCorrection }, [selectedFeedbackCorrection, selectedFeedbackTaskDetail]) + const selectedFeedbackPreview = useMemo( + () => getFeedbackCorrectionPreview(selectedFeedbackResolved), + [selectedFeedbackResolved], + ) + const selectedFeedbackImpactSummary = useMemo( + () => buildFeedbackImpactSummary(selectedFeedbackResolved), + [selectedFeedbackResolved], + ) const selectedFeedbackActionLogs: MemoryFeedbackActionLogPayload[] = Array.isArray(selectedFeedbackResolved?.action_logs) ? selectedFeedbackResolved.action_logs @@ -2074,10 +2293,18 @@ export function KnowledgeBasePage() {
{runtimeBadges.map((item) => ( - - - {item.label} - {item.value} + + +
+
+ {item.label} + {item.value} +
+
+ +
+
+
{item.description}
))} @@ -2183,7 +2410,7 @@ export function KnowledgeBasePage() { 长期记忆配置 - 常用字段可视化编辑,长尾高级项继续通过原始 TOML 维护 + 常用字段可在这里可视化编辑;高级配置仍可通过原始 TOML 维护。
@@ -2209,7 +2436,7 @@ export function KnowledgeBasePage() { {!rawConfigExists || rawConfigUsingDefault ? ( - 检测到配置文件尚未保存,当前展示的是默认模板内容点击“保存”后相关配置文件会自动创建 + 检测到配置文件尚未保存。当前展示的是默认模板内容,点击“保存”后会自动创建配置文件: {' '} {configPath} @@ -2252,7 +2479,7 @@ export function KnowledgeBasePage() { 创建导入任务 - 同页完成模式选择、参数设置与任务创建,不再切换到旧导入页 + 按“选择导入方式 → 检查公共参数 → 创建任务”的顺序完成导入。
- + {IMPORT_KIND_OPTIONS.map((item) => (
-
+
公共参数
-
所有导入模式共用,创建任务时会自动并入请求参数
+
这些设置会应用到当前导入任务。一般保持默认即可,只在批量导入或排查问题时调整。
-
+
-
同时处理的文件数量
+
同时处理多少个文件;文件很多时再适当调高。
-
单文件内并行分块数量
+
单个文件内并行处理多少个分块;过高会增加资源占用。
setImportCommonChunkConcurrency(event.target.value)} />
-
- setImportCommonLlmEnabled(Boolean(value))} - /> - 启用 LLM 抽取 +
+
+ setImportCommonLlmEnabled(Boolean(value))} + /> + 启用 LLM 抽取 +
+
需要模型参与抽取,质量更高但耗时更长。
-
- setImportCommonChatLog(Boolean(value))} - /> - 按聊天日志解析 +
+
+ setImportCommonChatLog(Boolean(value))} + /> + 按聊天日志解析 +
+
适合导入聊天记录,会尽量保留时间和对话上下文。
- 高级参数 + 高级参数(通常不用修改)
- + setImportCommonStrategyOverride(event.target.value)} @@ -2357,7 +2590,7 @@ export function KnowledgeBasePage() { checked={importCommonClearManifest} onCheckedChange={(value) => setImportCommonClearManifest(Boolean(value))} /> - 清空任务清单 + 清空导入清单
@@ -2365,7 +2598,7 @@ export function KnowledgeBasePage() {
-
上传本地文件,适合批量导入
+
选择一个或多个本地文件创建导入任务,适合批量导入资料或聊天记录。
@@ -2398,7 +2631,7 @@ export function KnowledgeBasePage() {
-
粘贴少量文本,适合临时导入
+
直接粘贴少量文本或 JSON,适合临时补充一段资料。
@@ -2558,7 +2791,7 @@ export function KnowledgeBasePage() {
setBackfillDryRun(Boolean(value))} /> - 仅演练(不落盘) + 只预演,不写入数据
setMaibotNoResume(Boolean(value))} /> - 不续跑 + 从头开始,不继续上次进度
setMaibotResetState(Boolean(value))} /> @@ -2646,7 +2879,7 @@ export function KnowledgeBasePage() {
setMaibotDryRun(Boolean(value))} /> - 仅演练(不落盘) + 只预演,不写入数据
setMaibotVerifyOnly(Boolean(value))} /> @@ -2668,13 +2901,13 @@ export function KnowledgeBasePage() { 路径预检 - 基于路径别名解析目标路径,提前发现路径越界或不存在问题 + 在创建本地扫描、转换或迁移任务前,先确认路径会被解析到哪里。
-
选择预设的数据根目录
+
选择后端允许访问的数据根目录。
setPathResolveRelativePath(event.target.value)} @@ -2727,8 +2960,13 @@ export function KnowledgeBasePage() {
- 运行中 {runningImportTasks.length},排队中 {queuedImportTasks.length},最近完成 {recentImportTasks.length} + 查看任务是否正在运行、排队等待或已经结束。点击任务卡片可查看详情。 +
+ 运行中 {runningImportTasks.length} + 排队中 {queuedImportTasks.length} + 最近完成 {recentImportTasks.length} +