diff --git a/dashboard/src/components/memory/MemoryMiniTabs.tsx b/dashboard/src/components/memory/MemoryMiniTabs.tsx new file mode 100644 index 00000000..ebf253cc --- /dev/null +++ b/dashboard/src/components/memory/MemoryMiniTabs.tsx @@ -0,0 +1,54 @@ +import { TabsList, TabsTrigger } from '@/components/ui/tabs' +import { cn } from '@/lib/utils' + +export interface MemoryMiniTabItem { + value: TValue + label: string + description?: string +} + +export interface MemoryMiniTabsProps { + items: ReadonlyArray> + className?: string + /** 触发器额外样式 */ + triggerClassName?: string +} + +/** + * 长期记忆控制台统一的迷你标签页样式。 + * + * - 复用 shadcn `Tabs` 原语,仅替换样式以保留无障碍能力(`role="tab"` 与文案不变)。 + * - 胶囊形外观,激活态使用主色渐变,便于在密集表单上快速定位当前页签。 + */ +export function MemoryMiniTabs({ + items, + className, + triggerClassName, +}: MemoryMiniTabsProps) { + return ( + + {items.map((item) => ( + + {item.label} + + ))} + + ) +} diff --git a/dashboard/src/components/memory/MemoryProgressIndicator.tsx b/dashboard/src/components/memory/MemoryProgressIndicator.tsx new file mode 100644 index 00000000..c1ab53aa --- /dev/null +++ b/dashboard/src/components/memory/MemoryProgressIndicator.tsx @@ -0,0 +1,130 @@ +import { Loader2 } from 'lucide-react' + +import { Badge } from '@/components/ui/badge' +import { Progress } from '@/components/ui/progress' +import { cn } from '@/lib/utils' + +export interface MemoryProgressIndicatorProps { + /** 0-100 之间的进度百分比 */ + value: number + /** 任务状态文本(如 “运行中”、“已完成”) */ + statusLabel?: string + /** 当前步骤文本(如 “分块中”) */ + stepLabel?: string + /** 状态对应的语义色(用于左侧圆环和徽标) */ + tone?: 'default' | 'success' | 'warning' | 'destructive' | 'muted' + /** 是否显示加载动画(运行中/取消中场景) */ + busy?: boolean + /** 紧凑模式:用于队列列表项 */ + compact?: boolean + /** 额外说明(如 “已完成 36 / 120 分块”) */ + detail?: string + className?: string +} + +const TONE_RING_CLASS: Record, string> = { + default: 'text-primary', + success: 'text-emerald-500', + warning: 'text-amber-500', + destructive: 'text-rose-500', + muted: 'text-muted-foreground', +} + +const TONE_BADGE_VARIANT: Record< + NonNullable, + 'default' | 'secondary' | 'destructive' | 'outline' +> = { + default: 'default', + success: 'secondary', + warning: 'outline', + destructive: 'destructive', + muted: 'outline', +} + +/** + * 长期记忆控制台统一的任务进度展示组件。 + * + * 设计目标: + * - 让用户一眼看清「整体百分比 + 语义状态 + 当前步骤」。 + * - 复用 shadcn `Progress` 与 `Badge`,避免引入额外样式来源。 + * - 在紧凑模式下保留可读性,可放进队列卡片;非紧凑模式带圆环用于详情区。 + */ +export function MemoryProgressIndicator({ + value, + statusLabel, + stepLabel, + tone = 'default', + busy = false, + compact = false, + detail, + className, +}: MemoryProgressIndicatorProps) { + const safeValue = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0 + const ringSize = compact ? 36 : 56 + const ringStroke = compact ? 4 : 5 + const radius = (ringSize - ringStroke) / 2 + const circumference = 2 * Math.PI * radius + const dashOffset = circumference * (1 - safeValue / 100) + + return ( +
+ + +
+
+ {statusLabel ? ( + + {statusLabel} + + ) : null} + {stepLabel ? ( + {stepLabel} + ) : null} + {!compact ? ( + + {safeValue.toFixed(1)}% + + ) : null} +
+ + {detail ?
{detail}
: null} +
+
+ ) +} diff --git a/dashboard/src/lib/memory-progress-client.ts b/dashboard/src/lib/memory-progress-client.ts new file mode 100644 index 00000000..a1c62a80 --- /dev/null +++ b/dashboard/src/lib/memory-progress-client.ts @@ -0,0 +1,99 @@ +import { unifiedWsClient, type WsEventEnvelope } from './unified-ws' + +export type MemoryProgressTopic = 'import_progress' | 'delete_progress' | 'feedback_progress' + +export interface MemoryProgressEvent { + topic: MemoryProgressTopic + event: string + data: Record +} + +type ProgressListener = (event: MemoryProgressEvent) => void + +const DOMAIN = 'memory' +const KNOWN_TOPICS: MemoryProgressTopic[] = ['import_progress', 'delete_progress', 'feedback_progress'] + +/** + * 长期记忆控制台的统一 WebSocket 桥接客户端。 + * + * 负责: + * 1. 订阅 `memory` 域下的若干 topic(导入/删除/反馈进度)。 + * 2. 把后端推送的事件分发给所有已注册的监听器。 + * 3. 即使后端尚未广播也保持安全:监听器为空时不抛错,订阅幂等。 + * + * 与 `pluginProgressClient` 保持一致的形状,便于复用。 + */ +class MemoryProgressClient { + private initialized = false + private listeners: Set = new Set() + private activeTopics: Set = new Set() + + private initialize(): void { + if (this.initialized) { + return + } + + unifiedWsClient.addEventListener((message: WsEventEnvelope) => { + if (message.domain !== DOMAIN) { + return + } + const topic = (message.topic ?? '') as MemoryProgressTopic + if (!KNOWN_TOPICS.includes(topic)) { + return + } + const payload: MemoryProgressEvent = { + topic, + event: message.event, + data: message.data ?? {}, + } + this.listeners.forEach((listener) => { + try { + listener(payload) + } catch (error) { + console.error('长期记忆进度监听器执行失败:', error) + } + }) + }) + + this.initialized = true + } + + async subscribe( + listener: ProgressListener, + topics: MemoryProgressTopic[] = KNOWN_TOPICS, + ): Promise<() => Promise> { + this.initialize() + this.listeners.add(listener) + + // 仅订阅尚未激活的 topic,避免重复 subscribe + for (const topic of topics) { + if (this.activeTopics.has(topic)) { + continue + } + try { + await unifiedWsClient.subscribe(DOMAIN, topic) + this.activeTopics.add(topic) + } catch (error) { + // 后端可能尚未实现该 topic,订阅失败时只记录,不抛出,确保 polling 仍可作为兜底 + console.warn(`订阅长期记忆 topic 失败(将退化到轮询兜底): ${topic}`, error) + } + } + + return async () => { + this.listeners.delete(listener) + if (this.listeners.size === 0) { + const topicsToRelease = Array.from(this.activeTopics) + this.activeTopics.clear() + for (const topic of topicsToRelease) { + try { + await unifiedWsClient.unsubscribe(DOMAIN, topic) + } catch (error) { + console.warn(`取消订阅长期记忆 topic 失败: ${topic}`, error) + } + } + } + } + } +} + +export const memoryProgressClient = new MemoryProgressClient() diff --git a/dashboard/src/routes/auth.tsx b/dashboard/src/routes/auth.tsx index 8c6addea..4b56d821 100644 --- a/dashboard/src/routes/auth.tsx +++ b/dashboard/src/routes/auth.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' @@ -63,23 +63,58 @@ export function AuthPage() { const { t } = useTranslation() const { enableWavesBackground, setEnableWavesBackground } = useAnimation() const { theme, setTheme } = useTheme() + // 避免 React StrictMode 下重复触发 URL token 自动登录。 + const urlTokenHandledRef = useRef(false) - // 如果已经认证,直接跳转到首页 - useEffect(() => { - const verifyAuth = async () => { - try { - const isAuth = await checkAuthStatus() - if (isAuth) { - navigate({ to: '/' }) - } - } catch { - // 忽略错误,保持在登录页 - } finally { - setCheckingAuth(false) + // 从 URL 中提取 token(支持 query 与 hash 两种位置)。 + // 允许 ?token=xxx 、&token=xxx(query)以及 #/foo?token=xxx、#token=xxx(hash)。 + const extractUrlToken = useCallback((): string => { + if (typeof window === 'undefined') return '' + const fromQuery = new URLSearchParams(window.location.search).get('token') + if (fromQuery && fromQuery.trim()) return fromQuery.trim() + // hash 中可能形如 "#token=xxx" 或 "#/path?token=xxx" + const rawHash = window.location.hash.replace(/^#/, '') + if (!rawHash) return '' + const queryIdx = rawHash.indexOf('?') + const hashQuery = queryIdx >= 0 ? rawHash.slice(queryIdx + 1) : rawHash + const fromHash = new URLSearchParams(hashQuery).get('token') + return fromHash && fromHash.trim() ? fromHash.trim() : '' + }, []) + + // 从当前 URL 中移除 token 参数,避免令牌被书签/Referer/浏览器历史泄露。 + const stripTokenFromUrl = useCallback(() => { + if (typeof window === 'undefined') return + try { + const url = new URL(window.location.href) + let changed = false + if (url.searchParams.has('token')) { + url.searchParams.delete('token') + changed = true } + const rawHash = url.hash.replace(/^#/, '') + if (rawHash) { + const queryIdx = rawHash.indexOf('?') + if (queryIdx >= 0) { + const path = rawHash.slice(0, queryIdx) + const hashParams = new URLSearchParams(rawHash.slice(queryIdx + 1)) + if (hashParams.has('token')) { + hashParams.delete('token') + const next = hashParams.toString() + url.hash = next ? `#${path}?${next}` : `#${path}` + changed = true + } + } else if (/^token=/.test(rawHash)) { + url.hash = '' + changed = true + } + } + if (changed) { + window.history.replaceState(null, '', url.toString()) + } + } catch { + // 忽略 URL 解析异常 } - verifyAuth() - }, [navigate]) + }, []) // 获取实际应用的主题(处理 system 情况) const getActualTheme = () => { @@ -97,83 +132,120 @@ export function AuthPage() { setTheme(newTheme) } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') + const verifyToken = useCallback( + async (rawToken: string): Promise => { + const trimmed = rawToken.trim() + setError('') - if (!token.trim()) { - setError(t('auth.tokenRequired')) - return - } - - setIsValidating(true) - - console.log('开始验证 token...') - - try { - // 向后端发送请求验证 token(后端会设置 HttpOnly Cookie) - const response = await fetch('/api/webui/auth/verify', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', // 确保接收并存储 Cookie - body: JSON.stringify({ token: token.trim() }), - }) - - console.log('Token 验证响应状态:', response.status) - - const result = await parseResponse<{ - valid: boolean - is_first_setup?: boolean - message?: string - }>(response) - - if (!result.success) { - console.error('Token 验证失败:', result.error) - setError(result.error) - return + if (!trimmed) { + setError(t('auth.tokenRequired')) + return false } - const data = result.data - console.log('Token 验证响应数据:', data) + setIsValidating(true) + console.log('开始验证 token...') - if (data.valid) { - console.log('Token 验证成功,准备跳转...') - console.log('is_first_setup:', data.is_first_setup) + try { + // 向后端发送请求验证 token(后端会设置 HttpOnly Cookie) + const response = await fetch('/api/webui/auth/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // 确保接收并存储 Cookie + body: JSON.stringify({ token: trimmed }), + }) - // Token 验证成功,Cookie 已由后端设置 - // 等待一小段时间确保 Cookie 已设置 - await new Promise((resolve) => setTimeout(resolve, 100)) + console.log('Token 验证响应状态:', response.status) - // 再次检查认证状态 - const authCheck = await checkAuthStatus() - console.log('跳转前认证状态检查:', authCheck) + const result = await parseResponse<{ + valid: boolean + is_first_setup?: boolean + message?: string + }>(response) - // 直接使用验证响应中的 is_first_setup 字段,避免额外请求 - if (data.is_first_setup) { - console.log('跳转到首次配置页面') - // 需要首次配置,跳转到配置向导 - navigate({ to: '/setup' }) - } else { - console.log('跳转到首页') - // 不需要配置或配置已完成,跳转到首页 - navigate({ to: '/' }) + if (!result.success) { + console.error('Token 验证失败:', result.error) + setError(result.error) + return false } - } else { + + const data = result.data + console.log('Token 验证响应数据:', data) + + if (data.valid) { + console.log('Token 验证成功,准备跳转...') + console.log('is_first_setup:', data.is_first_setup) + + // Token 验证成功,Cookie 已由后端设置 + // 等待一小段时间确保 Cookie 已设置 + await new Promise((resolve) => setTimeout(resolve, 100)) + + // 再次检查认证状态 + const authCheck = await checkAuthStatus() + console.log('跳转前认证状态检查:', authCheck) + + // 直接使用验证响应中的 is_first_setup 字段,避免额外请求 + if (data.is_first_setup) { + console.log('跳转到首次配置页面') + navigate({ to: '/setup' }) + } else { + console.log('跳转到首页') + navigate({ to: '/' }) + } + return true + } + console.error('Token 验证失败:', data.message) setError(data.message || t('auth.verifyFailed')) + return false + } catch (err) { + console.error('Token 验证错误:', err) + setError(err instanceof Error ? err.message : t('auth.connFailed')) + return false + } finally { + setIsValidating(false) } - } catch (err) { - console.error('Token 验证错误:', err) - setError( - err instanceof Error ? err.message : t('auth.connFailed') - ) - } finally { - setIsValidating(false) - } + }, + [navigate, t] + ) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + await verifyToken(token) } + // 如果已经认证,直接跳转到首页;否则尝试用 URL 中的 token 自动登录。 + useEffect(() => { + const verifyAuth = async () => { + try { + const isAuth = await checkAuthStatus() + if (isAuth) { + // 已登录场景下,URL 中残留的 token 也清掉,避免外泄。 + stripTokenFromUrl() + navigate({ to: '/' }) + return + } + + // 未登录:检查 URL 是否带 token,带了就自动尝试登录。 + const urlToken = extractUrlToken() + if (urlToken && !urlTokenHandledRef.current) { + urlTokenHandledRef.current = true + // 立即从 URL 中剥离 token,防止刷新/复制链接时再次暴露。 + stripTokenFromUrl() + setToken(urlToken) + // 异步触发验证;失败时错误信息会显示在表单上,token 也会保留在输入框中以便用户修正。 + void verifyToken(urlToken) + } + } catch { + // 忽略错误,保持在登录页 + } finally { + setCheckingAuth(false) + } + } + verifyAuth() + }, [navigate, extractUrlToken, stripTokenFromUrl, verifyToken]) + // 正在检查认证状态时显示加载 if (checkingAuth) { return ( diff --git a/dashboard/src/routes/resource/knowledge-base/constants.ts b/dashboard/src/routes/resource/knowledge-base/constants.ts new file mode 100644 index 00000000..39d2543b --- /dev/null +++ b/dashboard/src/routes/resource/knowledge-base/constants.ts @@ -0,0 +1,52 @@ +import type { MemoryImportTaskKind } from '@/lib/memory-api' + +export const DELETE_OPERATION_FETCH_LIMIT = 100 +export const DELETE_OPERATION_PAGE_SIZE = 6 +export const DELETE_OPERATION_ITEM_PAGE_SIZE = 8 +export const FEEDBACK_CORRECTION_FETCH_LIMIT = 100 +export const FEEDBACK_CORRECTION_PAGE_SIZE = 6 +export const FEEDBACK_ACTION_LOG_PAGE_SIZE = 8 +export const IMPORT_CHUNK_PAGE_SIZE = 50 + +export const RUNNING_IMPORT_STATUS = new Set(['preparing', 'running', 'cancel_requested']) +export const QUEUED_IMPORT_STATUS = new Set(['queued']) + +export const IMPORT_STATUS_TEXT: Record = { + queued: '排队中', + preparing: '准备中', + running: '运行中', + cancel_requested: '取消中', + cancelled: '已取消', + completed: '已完成', + completed_with_errors: '完成(有错误)', + failed: '失败', +} + +export const IMPORT_STEP_TEXT: Record = { + queued: '排队中', + preparing: '准备中', + running: '运行中', + splitting: '分块中', + extracting: '抽取中', + writing: '写入中', + saving: '保存中', + backfilling: '回填中', + converting: '转换中', + verifying: '校验中', + switching: '切换中', + cancel_requested: '取消中', + cancelled: '已取消', + completed: '已完成', + completed_with_errors: '完成(有错误)', + failed: '失败', +} + +export const IMPORT_KIND_OPTIONS: Array<{ value: MemoryImportTaskKind; label: string; description: string }> = [ + { 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 历史数据迁移长期记忆' }, +] diff --git a/dashboard/src/routes/resource/knowledge-base/tabs/DeleteTab.tsx b/dashboard/src/routes/resource/knowledge-base/tabs/DeleteTab.tsx new file mode 100644 index 00000000..ba67a891 --- /dev/null +++ b/dashboard/src/routes/resource/knowledge-base/tabs/DeleteTab.tsx @@ -0,0 +1,492 @@ +import type { Dispatch, SetStateAction } from 'react' + +import { CircleAlert, RotateCcw, Trash2 } from 'lucide-react' + +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { TabsContent } from '@/components/ui/tabs' +import { cn } from '@/lib/utils' +import type { MemoryDeleteOperationPayload, MemorySourceItemPayload } from '@/lib/memory-api' + +import { DELETE_OPERATION_ITEM_PAGE_SIZE, DELETE_OPERATION_PAGE_SIZE } from '../constants' +import { + formatDeleteOperationMode, + formatDeleteOperationStatus, + formatDeleteOperationTime, + getDeleteOperationItemLabel, + getDeleteOperationItemPreview, + getDeleteOperationItemSource, + type DeleteOperationItem, +} from '../utils' + +export interface DeleteTabProps { + sourceSearch: string + setSourceSearch: Dispatch> + selectedSources: string[] + setSelectedSources: Dispatch> + filteredSources: MemorySourceItemPayload[] + openSourceDeletePreview: () => Promise + toggleSourceSelection: (source: string, checked: boolean) => void + + operationSearch: string + setOperationSearch: Dispatch> + operationModeFilter: string + setOperationModeFilter: Dispatch> + operationStatusFilter: string + setOperationStatusFilter: Dispatch> + filteredDeleteOperations: MemoryDeleteOperationPayload[] + deleteOperations: MemoryDeleteOperationPayload[] + operationPage: number + setOperationPage: Dispatch> + deleteOperationPageCount: number + pagedDeleteOperations: MemoryDeleteOperationPayload[] + selectedDeleteOperation: MemoryDeleteOperationPayload | null + setSelectedOperationId: Dispatch> + restoreDeleteOperation: (operationId: string) => Promise + deleteRestoring: boolean + selectedOperationCounts: Record + selectedOperationDetailLoading: boolean + selectedOperationDetailError: string + selectedOperationSources: string[] + selectedOperationItems: DeleteOperationItem[] + filteredSelectedOperationItems: DeleteOperationItem[] + selectedOperationItemSearch: string + setSelectedOperationItemSearch: Dispatch> + selectedOperationItemPage: number + setSelectedOperationItemPage: Dispatch> + selectedOperationItemPageCount: number + pagedSelectedOperationItems: DeleteOperationItem[] +} + +export function DeleteTab(props: DeleteTabProps) { + const { + sourceSearch, + setSourceSearch, + selectedSources, + setSelectedSources, + filteredSources, + openSourceDeletePreview, + toggleSourceSelection, + operationSearch, + setOperationSearch, + operationModeFilter, + setOperationModeFilter, + operationStatusFilter, + setOperationStatusFilter, + filteredDeleteOperations, + deleteOperations, + operationPage, + setOperationPage, + deleteOperationPageCount, + pagedDeleteOperations, + selectedDeleteOperation, + setSelectedOperationId, + restoreDeleteOperation, + deleteRestoring, + selectedOperationCounts, + selectedOperationDetailLoading, + selectedOperationDetailError, + selectedOperationSources, + selectedOperationItems, + filteredSelectedOperationItems, + selectedOperationItemSearch, + setSelectedOperationItemSearch, + selectedOperationItemPage, + setSelectedOperationItemPage, + selectedOperationItemPageCount, + pagedSelectedOperationItems, + } = props + + return ( + +
+ + +
+ + + 来源批量删除 + + + 用于按来源清理测试数据或指定导入批次。该操作不会直接删除实体,只会删除来源段落和失去全部证据的关系。 + +
+ + + + 建议先在图谱里确认影响范围,再在这里执行批量来源删除。所有删除都会先经过预览,并支持按删除记录恢复。 + + +
+ +
+
+ + setSourceSearch(event.target.value)} + placeholder="搜索 source 名称" + /> +
+
+ + +
+
+ +
+ 当前命中 {filteredSources.length} 个来源 + 0 ? 'secondary' : 'outline'} className="bg-background/70"> + 已选择 {selectedSources.length} 个来源 + +
+ + + + + + 选中 + 来源 + 段落数 + 关系数 + + + + {filteredSources.length > 0 ? filteredSources.map((item) => { + const source = String(item.source ?? '') + const checked = selectedSources.includes(source) + return ( + + + toggleSourceSelection(source, Boolean(value))} /> + + {source} + {Number(item.paragraph_count ?? 0)} + {Number(item.relation_count ?? 0)} + + ) + }) : ( + + + 当前没有可删除的来源 + + + )} + +
+
+
+
+ + + + + + 删除操作恢复 + + 按列表浏览最近的删除操作,先选中记录,再在下方确认影响范围并执行恢复 + + +
+ setOperationSearch(event.target.value)} + placeholder="搜索 operation / reason / requested_by / source" + /> + + +
+ +
+ 当前命中 {filteredDeleteOperations.length} 条记录,已加载最近 {deleteOperations.length} 条 + 第 {operationPage} / {deleteOperationPageCount} 页,每页显示 {DELETE_OPERATION_PAGE_SIZE} 条 +
+ + +
+ {pagedDeleteOperations.length > 0 ? pagedDeleteOperations.map((operation) => { + const summary = (operation.summary ?? {}) as Record + const counts = ((summary.counts as Record | undefined) ?? {}) + const isSelected = selectedDeleteOperation?.operation_id === operation.operation_id + return ( + + ) + }) : ( +
+ 当前筛选条件下没有删除操作 +
+ )} +
+
+ +
+ +
+ 支持按删除记录、模式、状态、发起人和来源检索 +
+ +
+ +
+ {selectedDeleteOperation ? ( +
+
+
+
+ + {formatDeleteOperationStatus(String(selectedDeleteOperation.status ?? ''))} + + + {formatDeleteOperationMode(String(selectedDeleteOperation.mode ?? ''))} + +
+
{selectedDeleteOperation.operation_id}
+
+ {selectedDeleteOperation.reason || '未填写删除原因'} +
+
+ +
+ +
+
+
发起人
+
{selectedDeleteOperation.requested_by || '-'}
+
+
+
创建时间
+
{formatDeleteOperationTime(selectedDeleteOperation.created_at)}
+
+
+
恢复时间
+
{formatDeleteOperationTime(selectedDeleteOperation.restored_at)}
+
+
+
删除摘要
+
+ 实体 {Number(selectedOperationCounts.entities ?? 0)} + 关系 {Number(selectedOperationCounts.relations ?? 0)} + 段落 {Number(selectedOperationCounts.paragraphs ?? 0)} + 来源 {Number(selectedOperationCounts.sources ?? 0)} +
+
+
+ + {selectedOperationDetailLoading ? ( +
+ 正在加载影响对象详情... +
+ ) : null} + + {selectedOperationDetailError ? ( + + {selectedOperationDetailError} + + ) : null} + + {selectedOperationSources.length > 0 ? ( +
+
关联来源
+
+ {selectedOperationSources.map((source) => ( + + {source} + + ))} +
+
+ ) : null} + +
+
+
选择器
+
+                        {JSON.stringify(selectedDeleteOperation.selector ?? {}, null, 2)}
+                      
+
+ +
+
+
影响对象
+
+ 命中 {filteredSelectedOperationItems.length} / {selectedOperationItems.length} 项 +
+
+
+ setSelectedOperationItemSearch(event.target.value)} + placeholder="搜索对象类型 / 哈希 / 对象键 / 来源" + className="lg:max-w-sm" + /> +
+ 第 {selectedOperationItemPage} / {selectedOperationItemPageCount} 页 + 每页 {DELETE_OPERATION_ITEM_PAGE_SIZE} 项 +
+
+ +
+ {pagedSelectedOperationItems.length > 0 ? pagedSelectedOperationItems.map((item) => { + const source = getDeleteOperationItemSource(item) + const label = getDeleteOperationItemLabel(item) + const preview = getDeleteOperationItemPreview(item) + return ( +
+
+ {item.item_type} + {source ? {source} : null} + {item.item_key && item.item_key !== item.item_hash ? ( + {item.item_key} + ) : null} +
+
+ {label} +
+ {preview ? ( +
+ {preview} +
+ ) : null} +
+ {item.item_hash} +
+
+ ) + }) : ( +
+ {selectedOperationItems.length > 0 ? '当前筛选条件下没有明细项' : '当前操作没有记录明细项'} +
+ )} +
+
+
+ +
+ 支持按对象类型、哈希、对象键和来源检索 +
+ +
+
+
+
+ ) : ( +
+ 当前没有可查看的删除操作详情 +
+ )} +
+
+
+
+
+ ) +} diff --git a/dashboard/src/routes/resource/knowledge-base/tabs/FeedbackTab.tsx b/dashboard/src/routes/resource/knowledge-base/tabs/FeedbackTab.tsx new file mode 100644 index 00000000..f63c7978 --- /dev/null +++ b/dashboard/src/routes/resource/knowledge-base/tabs/FeedbackTab.tsx @@ -0,0 +1,512 @@ +import type { Dispatch, SetStateAction } from 'react' + +import { RotateCcw } from 'lucide-react' + +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { TabsContent } from '@/components/ui/tabs' +import { cn } from '@/lib/utils' +import type { + MemoryFeedbackActionLogPayload, + MemoryFeedbackCorrectionDetailTaskPayload, + MemoryFeedbackCorrectionSummaryPayload, +} from '@/lib/memory-api' + +import { FEEDBACK_ACTION_LOG_PAGE_SIZE, FEEDBACK_CORRECTION_PAGE_SIZE } from '../constants' +import { + buildFeedbackImpactSummary, + describeFeedbackActionLog, + formatDeleteOperationTime, + formatFeedbackActionType, + formatFeedbackDecision, + formatFeedbackRollbackStatus, + formatFeedbackTaskStatus, + getFeedbackCorrectionPreview, + getFeedbackStatusVariant, + summarizeFeedbackActionPayload, +} from '../utils' + +export interface FeedbackTabProps { + feedbackSearch: string + setFeedbackSearch: Dispatch> + feedbackStatusFilter: string + setFeedbackStatusFilter: Dispatch> + feedbackRollbackFilter: string + setFeedbackRollbackFilter: Dispatch> + filteredFeedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[] + feedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[] + pagedFeedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[] + feedbackPage: number + setFeedbackPage: Dispatch> + feedbackPageCount: number + selectedFeedbackCorrection: MemoryFeedbackCorrectionSummaryPayload | null + setSelectedFeedbackTaskId: Dispatch> + selectedFeedbackResolved: MemoryFeedbackCorrectionDetailTaskPayload | null + selectedFeedbackPreview: ReturnType + selectedFeedbackImpactSummary: string[] + openFeedbackRollbackDialog: () => void + feedbackRollingBack: boolean + selectedFeedbackTaskLoading: boolean + selectedFeedbackTaskError: string | null + feedbackActionLogPage: number + setFeedbackActionLogPage: Dispatch> + feedbackActionLogPageCount: number + feedbackActionLogSearch: string + setFeedbackActionLogSearch: Dispatch> + pagedFeedbackActionLogs: MemoryFeedbackActionLogPayload[] + selectedFeedbackActionLogs: MemoryFeedbackActionLogPayload[] +} + +export function FeedbackTab(props: FeedbackTabProps) { + const { + feedbackSearch, + setFeedbackSearch, + feedbackStatusFilter, + setFeedbackStatusFilter, + feedbackRollbackFilter, + setFeedbackRollbackFilter, + filteredFeedbackCorrections, + feedbackCorrections, + pagedFeedbackCorrections, + feedbackPage, + setFeedbackPage, + feedbackPageCount, + selectedFeedbackCorrection, + setSelectedFeedbackTaskId, + selectedFeedbackResolved, + selectedFeedbackPreview, + selectedFeedbackImpactSummary, + openFeedbackRollbackDialog, + feedbackRollingBack, + selectedFeedbackTaskLoading, + selectedFeedbackTaskError, + feedbackActionLogPage, + setFeedbackActionLogPage, + feedbackActionLogPageCount, + feedbackActionLogSearch, + setFeedbackActionLogSearch, + pagedFeedbackActionLogs, + selectedFeedbackActionLogs, + } = props + + return ( + +
+ + + + + 反馈纠错历史 + + + 查看 feedback correction 的判定、修改轨迹与回退结果;本期仅覆盖自动纠错任务 + + + +
+ setFeedbackSearch(event.target.value)} + placeholder="搜索查询编号 / 会话 / 查询内容 / 原因" + /> + + +
+ +
+ 当前命中 {filteredFeedbackCorrections.length} 条记录,已加载最近 {feedbackCorrections.length} 条 + 第 {feedbackPage} / {feedbackPageCount} 页,每页显示 {FEEDBACK_CORRECTION_PAGE_SIZE} 条 +
+ +
+ +
+ {pagedFeedbackCorrections.length > 0 ? pagedFeedbackCorrections.map((item) => { + const isSelected = selectedFeedbackCorrection?.task_id === item.task_id + const preview = getFeedbackCorrectionPreview(item) + const impactSummary = buildFeedbackImpactSummary(item) + return ( + + ) + }) : ( +
+ 当前筛选条件下没有纠错历史 +
+ )} +
+
+ +
+ {selectedFeedbackCorrection ? ( +
+
+
+
+ + {formatFeedbackTaskStatus(String(selectedFeedbackResolved?.task_status ?? ''))} + + + {formatFeedbackRollbackStatus(String(selectedFeedbackResolved?.rollback_status ?? 'none'))} + + + {formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))} + +
+
+ {selectedFeedbackPreview.headline} +
+
+ 查询:{selectedFeedbackResolved?.query_text || '无查询文本'} +
+
+ {selectedFeedbackResolved?.query_tool_id} +
+
+ +
+ +
+
+
本次纠错结论
+
+
+
纠错前
+
+ {selectedFeedbackPreview.oldRelation || '当前详情没有记录旧结论'} +
+
+
+
+
纠错后
+
+ {selectedFeedbackPreview.newRelation || '当前详情没有记录新结论'} +
+
+
+
+ +
+
影响范围摘要
+
+ {selectedFeedbackImpactSummary.length > 0 ? selectedFeedbackImpactSummary.map((summary) => ( + + {summary} + + )) : ( +
当前没有可展示的影响范围摘要
+ )} +
+
+
+ +
+
+
会话
+
{selectedFeedbackResolved?.session_id || '-'}
+
+
+
反馈消息数
+
{Number(selectedFeedbackResolved?.feedback_message_count ?? 0)}
+
+
+
判定置信度
+
{Number(selectedFeedbackResolved?.decision_confidence ?? 0).toFixed(2)}
+
+
+
回退时间
+
{formatDeleteOperationTime(selectedFeedbackResolved?.rolled_back_at)}
+
+
+ + {selectedFeedbackTaskLoading ? ( +
+ 正在加载纠错详情... +
+ ) : null} + + {selectedFeedbackTaskError ? ( + + {selectedFeedbackTaskError} + + ) : null} + + {selectedFeedbackResolved?.rollback_error ? ( + + {selectedFeedbackResolved.rollback_error} + + ) : null} + +
+
+
回退后会发生什么
+
+
会恢复旧关系状态,并撤销本次纠错写入的段落与关系。
+
会清理旧段落的待复核标记,并重新触发相关 Episode / Profile 修复。
+
如果你当前只是核对结果,可以先查看下面的详细数据,不必立刻执行回退。
+
+
+
+
处理摘要
+
+
判定:{formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))}
+
任务状态:{formatFeedbackTaskStatus(String(selectedFeedbackResolved?.task_status ?? ''))}
+
回退状态:{formatFeedbackRollbackStatus(String(selectedFeedbackResolved?.rollback_status ?? 'none'))}
+
反馈消息数:{Number(selectedFeedbackResolved?.feedback_message_count ?? 0)}
+
+
+
+ +
+
详细数据
+
+
+ 查询快照 JSON +
+                            {JSON.stringify(selectedFeedbackResolved?.query_snapshot ?? {}, null, 2)}
+                          
+
+
+ 判定结果 JSON +
+                            {JSON.stringify(selectedFeedbackResolved?.decision_payload ?? {}, null, 2)}
+                          
+
+
+ 回退计划摘要 JSON +
+                            {JSON.stringify(selectedFeedbackResolved?.rollback_plan_summary ?? {}, null, 2)}
+                          
+
+
+ 回退结果 JSON +
+                            {JSON.stringify(selectedFeedbackResolved?.rollback_result ?? {}, null, 2)}
+                          
+
+
+
+ +
+ + 动作时间线 + +
+
+
+ 第 {feedbackActionLogPage} / {feedbackActionLogPageCount} 页,每页 {FEEDBACK_ACTION_LOG_PAGE_SIZE} 项 +
+ setFeedbackActionLogSearch(event.target.value)} + placeholder="搜索动作 / 目标哈希 / 预览内容" + className="lg:w-80" + /> +
+ +
+ {pagedFeedbackActionLogs.length > 0 ? pagedFeedbackActionLogs.map((item: MemoryFeedbackActionLogPayload) => ( +
+
+
+ {formatFeedbackActionType(item.action_type)} + {item.target_hash ? ( + {item.target_hash} + ) : null} +
+
+ {formatDeleteOperationTime(item.created_at)} +
+
+
+ {describeFeedbackActionLog(item)} +
+ {item.reason ? ( +
+ 原因:{item.reason} +
+ ) : null} + {item.before_payload && Object.keys(item.before_payload).length > 0 ? ( +
+ 处理前: + {summarizeFeedbackActionPayload(item.before_payload)} +
+ ) : null} + {item.after_payload && Object.keys(item.after_payload).length > 0 ? ( +
+ 处理后: + {summarizeFeedbackActionPayload(item.after_payload)} +
+ ) : null} +
+ )) : ( +
+ {selectedFeedbackActionLogs.length > 0 ? '当前筛选条件下没有动作日志' : '当前任务没有动作日志'} +
+ )} +
+
+
+ +
支持按动作类型、目标哈希和摘要检索
+ +
+
+
+
+ ) : ( +
+ 当前没有可查看的纠错详情 +
+ )} +
+
+ +
+ +
+ 支持按查询内容、任务状态和回退状态检索 +
+ +
+
+
+
+
+ ) +} diff --git a/dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx b/dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx new file mode 100644 index 00000000..c284c8f8 --- /dev/null +++ b/dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx @@ -0,0 +1,1270 @@ +import type { Dispatch, SetStateAction } from 'react' + +import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Upload } from 'lucide-react' + +import { MemoryMiniTabs } from '@/components/memory/MemoryMiniTabs' +import { MemoryProgressIndicator } from '@/components/memory/MemoryProgressIndicator' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Progress } from '@/components/ui/progress' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Tabs, TabsContent } from '@/components/ui/tabs' +import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/lib/utils' +import type { + MemoryImportChunkPayload, + MemoryImportFilePayload, + MemoryImportInputMode, + MemoryImportRetrySummary, + MemoryImportSettings, + MemoryImportTaskKind, + MemoryImportTaskPayload, +} from '@/lib/memory-api' + +import { IMPORT_CHUNK_PAGE_SIZE, IMPORT_KIND_OPTIONS, RUNNING_IMPORT_STATUS } from '../constants' +import { + formatImportTime, + getImportStatusLabel, + getImportStatusVariant, + getImportStepLabel, + normalizeImportInputMode, + normalizeProgress, +} from '../utils' + +export interface ImportTabProps { + importCreateMode: MemoryImportTaskKind + setImportCreateMode: Dispatch> + importSettings: MemoryImportSettings + importCommonFileConcurrency: string + setImportCommonFileConcurrency: Dispatch> + importCommonChunkConcurrency: string + setImportCommonChunkConcurrency: Dispatch> + importCommonLlmEnabled: boolean + setImportCommonLlmEnabled: Dispatch> + importCommonChatLog: boolean + setImportCommonChatLog: Dispatch> + importCommonStrategyOverride: string + setImportCommonStrategyOverride: Dispatch> + importCommonDedupePolicy: string + setImportCommonDedupePolicy: Dispatch> + importCommonChatReferenceTime: string + setImportCommonChatReferenceTime: Dispatch> + importCommonForce: boolean + setImportCommonForce: Dispatch> + importCommonClearManifest: boolean + setImportCommonClearManifest: Dispatch> + + uploadInputMode: MemoryImportInputMode + setUploadInputMode: Dispatch> + uploadFiles: File[] + setUploadFiles: Dispatch> + + pasteName: string + setPasteName: Dispatch> + pasteMode: MemoryImportInputMode + setPasteMode: Dispatch> + pasteContent: string + setPasteContent: Dispatch> + + rawAlias: string + setRawAlias: Dispatch> + rawInputMode: MemoryImportInputMode + setRawInputMode: Dispatch> + rawRelativePath: string + setRawRelativePath: Dispatch> + rawGlob: string + setRawGlob: Dispatch> + rawRecursive: boolean + setRawRecursive: Dispatch> + + openieAlias: string + setOpenieAlias: Dispatch> + openieRelativePath: string + setOpenieRelativePath: Dispatch> + openieIncludeAllJson: boolean + setOpenieIncludeAllJson: Dispatch> + + convertAlias: string + setConvertAlias: Dispatch> + convertTargetAlias: string + setConvertTargetAlias: Dispatch> + convertRelativePath: string + setConvertRelativePath: Dispatch> + convertTargetRelativePath: string + setConvertTargetRelativePath: Dispatch> + convertDimension: string + setConvertDimension: Dispatch> + convertBatchSize: string + setConvertBatchSize: Dispatch> + + backfillAlias: string + setBackfillAlias: Dispatch> + backfillLimit: string + setBackfillLimit: Dispatch> + backfillRelativePath: string + setBackfillRelativePath: Dispatch> + backfillDryRun: boolean + setBackfillDryRun: Dispatch> + backfillNoCreatedFallback: boolean + setBackfillNoCreatedFallback: Dispatch> + + maibotSourceDb: string + setMaibotSourceDb: Dispatch> + maibotTimeFrom: string + setMaibotTimeFrom: Dispatch> + maibotTimeTo: string + setMaibotTimeTo: Dispatch> + maibotStartId: string + setMaibotStartId: Dispatch> + maibotEndId: string + setMaibotEndId: Dispatch> + maibotStreamIds: string + setMaibotStreamIds: Dispatch> + maibotGroupIds: string + setMaibotGroupIds: Dispatch> + maibotUserIds: string + setMaibotUserIds: Dispatch> + maibotReadBatchSize: string + setMaibotReadBatchSize: Dispatch> + maibotCommitWindowRows: string + setMaibotCommitWindowRows: Dispatch> + maibotEmbedWorkers: string + setMaibotEmbedWorkers: Dispatch> + maibotNoResume: boolean + setMaibotNoResume: Dispatch> + maibotResetState: boolean + setMaibotResetState: Dispatch> + maibotDryRun: boolean + setMaibotDryRun: Dispatch> + maibotVerifyOnly: boolean + setMaibotVerifyOnly: Dispatch> + + submitImportByMode: () => Promise + creatingImport: boolean + + pathResolveAlias: string + setPathResolveAlias: Dispatch> + importAliasKeys: string[] + pathResolveRelativePath: string + setPathResolveRelativePath: Dispatch> + pathResolveMustExist: boolean + setPathResolveMustExist: Dispatch> + resolveImportPath: () => Promise + resolvingPath: boolean + pathResolveOutput: string + + refreshImportQueue: () => Promise + runningImportTasks: MemoryImportTaskPayload[] + queuedImportTasks: MemoryImportTaskPayload[] + recentImportTasks: MemoryImportTaskPayload[] + selectedImportTaskId: string + selectImportTask: (taskId: string) => Promise + importAutoPolling: boolean + setImportAutoPolling: Dispatch> + importPollInterval: number + importErrorText: string + + cancelSelectedImportTask: () => Promise + retrySelectedImportTask: () => Promise + selectedImportTaskLoading: boolean + selectedImportTaskResolved: MemoryImportTaskPayload | null | undefined + selectedImportRetrySummary: MemoryImportRetrySummary | null | undefined + selectedImportTaskErrorText: string + + selectedImportFiles: MemoryImportFilePayload[] + selectedImportFileId: string + selectImportFile: (fileId: string) => Promise + + importChunkTotal: number + importChunkOffset: number + moveImportChunkPage: (direction: -1 | 1) => Promise + canImportChunkPrev: boolean + canImportChunkNext: boolean + importChunksLoading: boolean + selectedImportChunks: MemoryImportChunkPayload[] +} + +export function ImportTab(props: ImportTabProps) { + const { + importCreateMode, + setImportCreateMode, + importSettings, + importCommonFileConcurrency, + setImportCommonFileConcurrency, + importCommonChunkConcurrency, + setImportCommonChunkConcurrency, + importCommonLlmEnabled, + setImportCommonLlmEnabled, + importCommonChatLog, + setImportCommonChatLog, + importCommonStrategyOverride, + setImportCommonStrategyOverride, + importCommonDedupePolicy, + setImportCommonDedupePolicy, + importCommonChatReferenceTime, + setImportCommonChatReferenceTime, + importCommonForce, + setImportCommonForce, + importCommonClearManifest, + setImportCommonClearManifest, + uploadInputMode, + setUploadInputMode, + uploadFiles, + setUploadFiles, + pasteName, + setPasteName, + pasteMode, + setPasteMode, + pasteContent, + setPasteContent, + rawAlias, + setRawAlias, + rawInputMode, + setRawInputMode, + rawRelativePath, + setRawRelativePath, + rawGlob, + setRawGlob, + rawRecursive, + setRawRecursive, + openieAlias, + setOpenieAlias, + openieRelativePath, + setOpenieRelativePath, + openieIncludeAllJson, + setOpenieIncludeAllJson, + convertAlias, + setConvertAlias, + convertTargetAlias, + setConvertTargetAlias, + convertRelativePath, + setConvertRelativePath, + convertTargetRelativePath, + setConvertTargetRelativePath, + convertDimension, + setConvertDimension, + convertBatchSize, + setConvertBatchSize, + backfillAlias, + setBackfillAlias, + backfillLimit, + setBackfillLimit, + backfillRelativePath, + setBackfillRelativePath, + backfillDryRun, + setBackfillDryRun, + backfillNoCreatedFallback, + setBackfillNoCreatedFallback, + maibotSourceDb, + setMaibotSourceDb, + maibotTimeFrom, + setMaibotTimeFrom, + maibotTimeTo, + setMaibotTimeTo, + maibotStartId, + setMaibotStartId, + maibotEndId, + setMaibotEndId, + maibotStreamIds, + setMaibotStreamIds, + maibotGroupIds, + setMaibotGroupIds, + maibotUserIds, + setMaibotUserIds, + maibotReadBatchSize, + setMaibotReadBatchSize, + maibotCommitWindowRows, + setMaibotCommitWindowRows, + maibotEmbedWorkers, + setMaibotEmbedWorkers, + maibotNoResume, + setMaibotNoResume, + maibotResetState, + setMaibotResetState, + maibotDryRun, + setMaibotDryRun, + maibotVerifyOnly, + setMaibotVerifyOnly, + submitImportByMode, + creatingImport, + pathResolveAlias, + setPathResolveAlias, + importAliasKeys, + pathResolveRelativePath, + setPathResolveRelativePath, + pathResolveMustExist, + setPathResolveMustExist, + resolveImportPath, + resolvingPath, + pathResolveOutput, + refreshImportQueue, + runningImportTasks, + queuedImportTasks, + recentImportTasks, + selectedImportTaskId, + selectImportTask, + importAutoPolling, + setImportAutoPolling, + importPollInterval, + importErrorText, + cancelSelectedImportTask, + retrySelectedImportTask, + selectedImportTaskLoading, + selectedImportTaskResolved, + selectedImportRetrySummary, + selectedImportTaskErrorText, + selectedImportFiles, + selectedImportFileId, + selectImportFile, + importChunkTotal, + importChunkOffset, + moveImportChunkPage, + canImportChunkPrev, + canImportChunkNext, + importChunksLoading, + selectedImportChunks, + } = props + + return ( + +
+
+ + + + + 创建导入任务 + + 按“选择导入方式 → 检查公共参数 → 创建任务”的顺序完成导入。 + + + setImportCreateMode(value as MemoryImportTaskKind)} + className="space-y-4" + > +
+ + +
+ +
+
+
公共参数
+
这些设置会应用到当前导入任务。一般保持默认即可,只在批量导入或排查问题时调整。
+
+
+
+ +
同时处理多少个文件;文件很多时再适当调高。
+ setImportCommonFileConcurrency(event.target.value)} + /> +
+
+ +
单个文件内并行处理多少个分块;过高会增加资源占用。
+ setImportCommonChunkConcurrency(event.target.value)} + /> +
+
+
+ setImportCommonLlmEnabled(Boolean(value))} + /> + 启用 LLM 抽取 +
+
需要模型参与抽取,质量更高但耗时更长。
+
+
+
+ setImportCommonChatLog(Boolean(value))} + /> + 按聊天日志解析 +
+
适合导入聊天记录,会尽量保留时间和对话上下文。
+
+
+ +
+ + 高级参数(通常不用修改) + +
+
+ + setImportCommonStrategyOverride(event.target.value)} + /> +
+
+ + setImportCommonDedupePolicy(event.target.value)} + /> +
+
+ + setImportCommonChatReferenceTime(event.target.value)} + /> +
+
+ setImportCommonForce(Boolean(value))} + /> + 强制导入 +
+
+ setImportCommonClearManifest(Boolean(value))} + /> + 清空导入清单 +
+
+
+
+ + +
+
选择一个或多个本地文件创建导入任务,适合批量导入资料或聊天记录。
+
+
+ + +
+
+ + setUploadFiles(Array.from(event.target.files ?? []))} + /> +
+
+
已选择 {uploadFiles.length} 个文件
+
+
+ + +
+
直接粘贴少量文本或 JSON,适合临时补充一段资料。
+
+
+ + setPasteName(event.target.value)} /> +
+
+ + +
+
+ +