From 21b642d07de9ddd639dc5fa81f69ffd3e82cecf0 Mon Sep 17 00:00:00 2001 From: A-Dawn <67786671+A-Dawn@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:57:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=90=8C=E6=AD=A5=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E9=9D=9E=E7=AE=97=E6=B3=95=E6=94=B9=E5=8A=A8=E5=88=B0=E4=B8=8A?= =?UTF-8?q?=E6=B8=B8=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留反馈纠错、WebUI 与运行时增强。\n移除不应提交的 algorithm_redesign 设计目录及其专项测试。 --- dashboard/src/lib/memory-api.ts | 114 +++ .../__tests__/knowledge-base.test.tsx | 133 ++++ .../src/routes/resource/knowledge-base.tsx | 694 ++++++++++++++++ .../test_feedback_correction_chat_flow.py | 740 ++++++++++++++++++ .../test_feedback_correction_core.py | 396 ++++++++++ pytests/webui/test_memory_routes.py | 33 + src/A_memorix/core/embedding/api_adapter.py | 64 +- src/A_memorix/core/storage/metadata_store.py | 2 +- .../scripts/release_vnext_migrate.py | 24 + src/config/official_configs.py | 78 +- 10 files changed, 2244 insertions(+), 34 deletions(-) create mode 100644 pytests/A_memorix_test/test_feedback_correction_chat_flow.py create mode 100644 pytests/A_memorix_test/test_feedback_correction_core.py diff --git a/dashboard/src/lib/memory-api.ts b/dashboard/src/lib/memory-api.ts index 18a584b2..fd4a1b7c 100644 --- a/dashboard/src/lib/memory-api.ts +++ b/dashboard/src/lib/memory-api.ts @@ -496,6 +496,77 @@ export interface MemoryDeleteOperationDetailPayload { error?: string } +export interface MemoryFeedbackAffectedCountsPayload { + relations?: number + stale_paragraphs?: number + episode_sources?: number + profile_person_ids?: number + correction_paragraphs?: number + corrected_relations?: number +} + +export interface MemoryFeedbackActionLogPayload { + id: number + task_id: number + query_tool_id: string + action_type: string + target_hash: string + reason?: string + before_payload?: Record + after_payload?: Record + created_at?: number +} + +export interface MemoryFeedbackCorrectionSummaryPayload { + task_id: number + query_tool_id: string + session_id: string + query_text: string + query_timestamp?: number + task_status: string + decision: string + decision_confidence: number + feedback_message_count: number + rollback_status: string + affected_counts: MemoryFeedbackAffectedCountsPayload + created_at?: number + updated_at?: number +} + +export interface MemoryFeedbackCorrectionDetailTaskPayload extends MemoryFeedbackCorrectionSummaryPayload { + query_snapshot?: Record + decision_payload?: Record + rollback_plan_summary?: Record + rollback_result?: Record + rollback_error?: string + rollback_requested_by?: string + rollback_reason?: string + rollback_requested_at?: number + rolled_back_at?: number + action_logs?: MemoryFeedbackActionLogPayload[] +} + +export interface MemoryFeedbackCorrectionListPayload { + success: boolean + items: MemoryFeedbackCorrectionSummaryPayload[] + count?: number + error?: string +} + +export interface MemoryFeedbackCorrectionDetailPayload { + success: boolean + task?: MemoryFeedbackCorrectionDetailTaskPayload | null + error?: string +} + +export interface MemoryFeedbackCorrectionRollbackPayload { + success: boolean + already_rolled_back?: boolean + result?: Record + task?: MemoryFeedbackCorrectionDetailTaskPayload | null + error?: string +} + export interface MemorySourceItemPayload { source: string paragraph_count?: number @@ -610,6 +681,49 @@ export async function getMemoryDeleteOperation( return requestJson(`/delete/operations/${encodeURIComponent(operationId)}`) } +export async function getMemoryFeedbackCorrections( + options?: { + limit?: number + status?: string + rollbackStatus?: string + query?: string + }, +): Promise { + const params = new URLSearchParams({ + limit: String(options?.limit ?? 50), + }) + if (options?.status?.trim()) { + params.set('status', options.status.trim()) + } + if (options?.rollbackStatus?.trim()) { + params.set('rollback_status', options.rollbackStatus.trim()) + } + if (options?.query?.trim()) { + params.set('query', options.query.trim()) + } + return requestJson(`/feedback-corrections?${params.toString()}`) +} + +export async function getMemoryFeedbackCorrection( + taskId: number, +): Promise { + return requestJson(`/feedback-corrections/${taskId}`) +} + +export async function rollbackMemoryFeedbackCorrection( + taskId: number, + payload: { + requested_by?: string + reason?: string + }, +): Promise { + return requestJson(`/feedback-corrections/${taskId}/rollback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) +} + export async function getMemorySources(): Promise { return requestJson('/sources') } diff --git a/dashboard/src/routes/resource/__tests__/knowledge-base.test.tsx b/dashboard/src/routes/resource/__tests__/knowledge-base.test.tsx index a745abeb..a26fc395 100644 --- a/dashboard/src/routes/resource/__tests__/knowledge-base.test.tsx +++ b/dashboard/src/routes/resource/__tests__/knowledge-base.test.tsx @@ -81,9 +81,12 @@ vi.mock('@/lib/memory-api', () => ({ getMemorySources: vi.fn(), getMemoryDeleteOperations: vi.fn(), getMemoryDeleteOperation: vi.fn(), + getMemoryFeedbackCorrections: vi.fn(), + getMemoryFeedbackCorrection: vi.fn(), previewMemoryDelete: vi.fn(), executeMemoryDelete: vi.fn(), restoreMemoryDelete: vi.fn(), + rollbackMemoryFeedbackCorrection: vi.fn(), })) function mockImportTask(taskId: string, status: string = 'running'): memoryApi.MemoryImportTaskPayload { @@ -357,6 +360,82 @@ describe('KnowledgeBasePage import workflow', () => { items: [], }, }) + vi.mocked(memoryApi.getMemoryFeedbackCorrections).mockResolvedValue({ + success: true, + items: [ + { + task_id: 11, + query_tool_id: 'tool-query-11', + session_id: 'session-1', + query_text: '测试用户最喜欢的颜色是什么', + query_timestamp: 1_710_000_010, + task_status: 'applied', + decision: 'correct', + decision_confidence: 0.97, + feedback_message_count: 1, + rollback_status: 'none', + affected_counts: { + relations: 1, + stale_paragraphs: 1, + episode_sources: 2, + profile_person_ids: 1, + correction_paragraphs: 1, + corrected_relations: 1, + }, + created_at: 1_710_000_011, + updated_at: 1_710_000_012, + }, + ], + count: 1, + }) + vi.mocked(memoryApi.getMemoryFeedbackCorrection).mockResolvedValue({ + success: true, + task: { + task_id: 11, + query_tool_id: 'tool-query-11', + session_id: 'session-1', + query_text: '测试用户最喜欢的颜色是什么', + query_timestamp: 1_710_000_010, + task_status: 'applied', + decision: 'correct', + decision_confidence: 0.97, + feedback_message_count: 1, + rollback_status: 'none', + affected_counts: { + relations: 1, + stale_paragraphs: 1, + episode_sources: 2, + profile_person_ids: 1, + correction_paragraphs: 1, + corrected_relations: 1, + }, + query_snapshot: { query: '测试用户最喜欢的颜色是什么', hits: [{ hash: 'paragraph-1' }] }, + decision_payload: { decision: 'correct', confidence: 0.97 }, + rollback_plan_summary: { + forgotten_relations: [{ hash: 'rel-old', subject: '测试用户', predicate: '最喜欢的颜色是', object: '蓝色' }], + corrected_write: { + paragraph_hashes: ['paragraph-new'], + corrected_relations: [{ hash: 'rel-new', subject: '测试用户', predicate: '最喜欢的颜色是', object: '绿色' }], + }, + }, + rollback_result: {}, + action_logs: [ + { + id: 1, + task_id: 11, + query_tool_id: 'tool-query-11', + action_type: 'forget_relation', + target_hash: 'rel-old', + reason: '用户明确纠正为绿色', + before_payload: { hash: 'rel-old', subject: '测试用户', predicate: '最喜欢的颜色是', object: '蓝色' }, + after_payload: { is_inactive: true }, + created_at: 1_710_000_013, + }, + ], + created_at: 1_710_000_011, + updated_at: 1_710_000_012, + }, + }) vi.mocked(memoryApi.previewMemoryDelete).mockResolvedValue({ success: true, mode: 'source', @@ -380,6 +459,37 @@ describe('KnowledgeBasePage import workflow', () => { deleted_source_count: 1, } as never) vi.mocked(memoryApi.restoreMemoryDelete).mockResolvedValue({ success: true } as never) + vi.mocked(memoryApi.rollbackMemoryFeedbackCorrection).mockResolvedValue({ + success: true, + result: { restored_relation_hashes: ['rel-old'] }, + task: { + task_id: 11, + query_tool_id: 'tool-query-11', + session_id: 'session-1', + query_text: '测试用户最喜欢的颜色是什么', + query_timestamp: 1_710_000_010, + task_status: 'applied', + decision: 'correct', + decision_confidence: 0.97, + feedback_message_count: 1, + rollback_status: 'rolled_back', + affected_counts: { + relations: 1, + stale_paragraphs: 1, + episode_sources: 2, + profile_person_ids: 1, + correction_paragraphs: 1, + corrected_relations: 1, + }, + query_snapshot: { query: '测试用户最喜欢的颜色是什么', hits: [{ hash: 'paragraph-1' }] }, + decision_payload: { decision: 'correct', confidence: 0.97 }, + rollback_plan_summary: {}, + rollback_result: { restored_relation_hashes: ['rel-old'] }, + action_logs: [], + created_at: 1_710_000_011, + updated_at: 1_710_000_012, + }, + }) vi.mocked(memoryApi.refreshMemoryRuntimeSelfCheck).mockResolvedValue({ success: true, report: { ok: true }, @@ -619,4 +729,27 @@ describe('KnowledgeBasePage import workflow', () => { }), ) }, 20_000) + + it('shows feedback correction history and supports rollback', async () => { + const user = userEvent.setup() + render() + + await screen.findByText('长期记忆控制台', undefined, { timeout: 10_000 }) + await user.click(screen.getByRole('tab', { name: '纠错历史' })) + await screen.findByText('反馈纠错历史') + await screen.findByText('测试用户最喜欢的颜色是什么') + await waitFor(() => expect(memoryApi.getMemoryFeedbackCorrection).toHaveBeenCalledWith(11)) + + await user.click(screen.getByRole('button', { name: '回退本次纠错' })) + const rollbackReason = await screen.findByLabelText('回退原因') + await user.type(rollbackReason, '人工确认回退') + await user.click(screen.getByRole('button', { name: '确认回退' })) + + await waitFor(() => + expect(memoryApi.rollbackMemoryFeedbackCorrection).toHaveBeenCalledWith(11, { + requested_by: 'knowledge_base', + reason: '人工确认回退', + }), + ) + }, 20_000) }) diff --git a/dashboard/src/routes/resource/knowledge-base.tsx b/dashboard/src/routes/resource/knowledge-base.tsx index d50426c2..5cc3602f 100644 --- a/dashboard/src/routes/resource/knowledge-base.tsx +++ b/dashboard/src/routes/resource/knowledge-base.tsx @@ -24,6 +24,14 @@ 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 { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Progress } from '@/components/ui/progress' @@ -48,6 +56,8 @@ import { createMemoryRawScanImport, createMemoryTemporalBackfillImport, executeMemoryDelete, + getMemoryFeedbackCorrection, + getMemoryFeedbackCorrections, getMemoryImportPathAliases, getMemoryImportSettings, getMemoryImportTask, @@ -74,6 +84,7 @@ import { type MemoryImportTaskPayload, previewMemoryDelete, refreshMemoryRuntimeSelfCheck, + rollbackMemoryFeedbackCorrection, resolveMemoryImportPath, retryMemoryImportTask, restoreMemoryDelete, @@ -82,6 +93,9 @@ import { type MemoryConfigSchemaPayload, type MemoryDeleteExecutePayload, type MemoryDeleteOperationPayload, + type MemoryFeedbackActionLogPayload, + type MemoryFeedbackCorrectionDetailTaskPayload, + type MemoryFeedbackCorrectionSummaryPayload, type MemorySourceItemPayload, type MemoryRuntimeConfigPayload, type MemoryTaskPayload, @@ -90,6 +104,9 @@ import { const DELETE_OPERATION_FETCH_LIMIT = 100 const DELETE_OPERATION_PAGE_SIZE = 6 const DELETE_OPERATION_ITEM_PAGE_SIZE = 8 +const FEEDBACK_CORRECTION_FETCH_LIMIT = 100 +const FEEDBACK_CORRECTION_PAGE_SIZE = 6 +const FEEDBACK_ACTION_LOG_PAGE_SIZE = 8 const IMPORT_CHUNK_PAGE_SIZE = 50 const RUNNING_IMPORT_STATUS = new Set(['preparing', 'running', 'cancel_requested']) @@ -270,6 +287,90 @@ function formatDeleteOperationTime(timestamp?: number | null): string { }) } +function formatFeedbackDecision(decision: string): string { + switch (decision) { + case 'correct': + return '纠正' + case 'reject': + return '否定' + case 'confirm': + return '确认' + case 'supplement': + return '补充' + case 'none': + return '无动作' + default: + return decision || '未知' + } +} + +function formatFeedbackTaskStatus(status: string): string { + switch (status) { + case 'pending': + return '待处理' + case 'running': + return '处理中' + case 'applied': + return '已应用' + case 'skipped': + return '已跳过' + case 'error': + return '失败' + default: + return status || '未知' + } +} + +function formatFeedbackRollbackStatus(status: string): string { + switch (status) { + case 'none': + return '未回退' + case 'running': + return '回退中' + case 'rolled_back': + return '已回退' + case 'error': + return '回退失败' + default: + return status || '未知' + } +} + +function getFeedbackStatusVariant( + status: string, +): 'default' | 'secondary' | 'destructive' | 'outline' { + if (status === 'applied' || status === 'rolled_back') { + return 'default' + } + if (status === 'error') { + return 'destructive' + } + if (status === 'running' || status === 'pending') { + return 'outline' + } + return 'secondary' +} + +function summarizeFeedbackActionPayload(value: Record | undefined): string { + if (!value) { + return '' + } + const hash = String(value.hash ?? '').trim() + const subject = String(value.subject ?? '').trim() + const predicate = String(value.predicate ?? '').trim() + const object = String(value.object ?? '').trim() + if (subject && predicate && object) { + return formatDeleteRelationText(subject, predicate, object) + } + if (hash) { + return hash + } + if (Array.isArray(value.target_hashes) && value.target_hashes.length > 0) { + return `targets ${value.target_hashes.length}` + } + return trimDeleteItemText(JSON.stringify(value, null, 2), 120) +} + type DeleteOperationItem = NonNullable[number] function trimDeleteItemText(value: string, maxLength: number = 140): string { @@ -471,6 +572,20 @@ export function KnowledgeBasePage() { const [deleteRestoring, setDeleteRestoring] = useState(false) const [deleteResult, setDeleteResult] = useState(null) const [pendingDeleteRequest, setPendingDeleteRequest] = useState(null) + const [feedbackCorrections, setFeedbackCorrections] = useState([]) + const [feedbackSearch, setFeedbackSearch] = useState('') + const [feedbackStatusFilter, setFeedbackStatusFilter] = useState('all') + const [feedbackRollbackFilter, setFeedbackRollbackFilter] = useState('all') + const [feedbackPage, setFeedbackPage] = useState(1) + const [selectedFeedbackTaskId, setSelectedFeedbackTaskId] = useState(0) + const [selectedFeedbackTaskDetail, setSelectedFeedbackTaskDetail] = useState(null) + const [selectedFeedbackTaskLoading, setSelectedFeedbackTaskLoading] = useState(false) + const [selectedFeedbackTaskError, setSelectedFeedbackTaskError] = useState('') + const [feedbackActionLogSearch, setFeedbackActionLogSearch] = useState('') + const [feedbackActionLogPage, setFeedbackActionLogPage] = useState(1) + const [feedbackRollbackDialogOpen, setFeedbackRollbackDialogOpen] = useState(false) + const [feedbackRollbackReason, setFeedbackRollbackReason] = useState('') + const [feedbackRollingBack, setFeedbackRollingBack] = useState(false) const [tuningObjective, setTuningObjective] = useState('precision_priority') const [tuningIntensity, setTuningIntensity] = useState('standard') const [tuningSampleSize, setTuningSampleSize] = useState('24') @@ -491,6 +606,7 @@ export function KnowledgeBasePage() { tuningTaskPayload, sourcePayload, deleteOperationPayload, + feedbackCorrectionPayload, ] = await Promise.all([ getMemoryConfigSchema(), getMemoryConfig(), @@ -503,6 +619,7 @@ export function KnowledgeBasePage() { getMemoryTuningTasks(20), getMemorySources(), getMemoryDeleteOperations(DELETE_OPERATION_FETCH_LIMIT), + getMemoryFeedbackCorrections({ limit: FEEDBACK_CORRECTION_FETCH_LIMIT }), ]) setSchemaPayload(schema) @@ -519,6 +636,7 @@ export function KnowledgeBasePage() { setTuningTasks(tuningTaskPayload.items ?? []) setMemorySources(sourcePayload.items ?? []) setDeleteOperations(deleteOperationPayload.items ?? []) + setFeedbackCorrections(feedbackCorrectionPayload.items ?? []) if (!selectedImportTaskId && (importTaskPayload.items ?? []).length > 0) { const initialTaskId = String(importTaskPayload.items?.[0]?.task_id ?? '') if (initialTaskId) { @@ -1494,6 +1612,212 @@ export function KnowledgeBasePage() { setDeleteDialogOpen(true) }, []) + const filteredFeedbackCorrections = useMemo(() => { + const keyword = feedbackSearch.trim().toLowerCase() + return feedbackCorrections.filter((item) => { + const taskStatus = String(item.task_status ?? '').trim().toLowerCase() + const rollbackStatus = String(item.rollback_status ?? '').trim().toLowerCase() + if (feedbackStatusFilter !== 'all' && taskStatus !== feedbackStatusFilter) { + return false + } + if (feedbackRollbackFilter !== 'all' && rollbackStatus !== feedbackRollbackFilter) { + return false + } + if (!keyword) { + return true + } + return [ + item.query_tool_id, + item.session_id, + item.query_text, + item.decision, + item.task_status, + item.rollback_status, + ] + .map((value) => String(value ?? '').toLowerCase()) + .some((value) => value.includes(keyword)) + }) + }, [feedbackCorrections, feedbackRollbackFilter, feedbackSearch, feedbackStatusFilter]) + + const feedbackPageCount = Math.max(1, Math.ceil(filteredFeedbackCorrections.length / FEEDBACK_CORRECTION_PAGE_SIZE)) + const pagedFeedbackCorrections = useMemo(() => { + const start = (feedbackPage - 1) * FEEDBACK_CORRECTION_PAGE_SIZE + return filteredFeedbackCorrections.slice(start, start + FEEDBACK_CORRECTION_PAGE_SIZE) + }, [feedbackPage, filteredFeedbackCorrections]) + + const selectedFeedbackCorrection = useMemo( + () => + filteredFeedbackCorrections.find((item) => item.task_id === selectedFeedbackTaskId) + ?? pagedFeedbackCorrections[0] + ?? null, + [filteredFeedbackCorrections, pagedFeedbackCorrections, selectedFeedbackTaskId], + ) + + useEffect(() => { + setFeedbackPage(1) + }, [feedbackSearch, feedbackStatusFilter, feedbackRollbackFilter]) + + useEffect(() => { + if (feedbackPage > feedbackPageCount) { + setFeedbackPage(feedbackPageCount) + } + }, [feedbackPage, feedbackPageCount]) + + useEffect(() => { + if (!selectedFeedbackCorrection) { + if (selectedFeedbackTaskId) { + setSelectedFeedbackTaskId(0) + } + setSelectedFeedbackTaskDetail(null) + setSelectedFeedbackTaskError('') + return + } + if (selectedFeedbackCorrection.task_id !== selectedFeedbackTaskId) { + setSelectedFeedbackTaskId(selectedFeedbackCorrection.task_id) + } + }, [selectedFeedbackCorrection, selectedFeedbackTaskId]) + + useEffect(() => { + const taskId = selectedFeedbackCorrection?.task_id + if (!taskId) { + setSelectedFeedbackTaskDetail(null) + setSelectedFeedbackTaskError('') + return + } + + let cancelled = false + setSelectedFeedbackTaskLoading(true) + setSelectedFeedbackTaskError('') + + void getMemoryFeedbackCorrection(taskId) + .then((payload) => { + if (cancelled) { + return + } + if (!payload.success || !payload.task) { + setSelectedFeedbackTaskDetail(null) + setSelectedFeedbackTaskError(payload.error || '未能加载纠错任务详情') + return + } + setSelectedFeedbackTaskDetail(payload.task) + }) + .catch((error) => { + if (cancelled) { + return + } + setSelectedFeedbackTaskDetail(null) + setSelectedFeedbackTaskError(error instanceof Error ? error.message : '未能加载纠错任务详情') + }) + .finally(() => { + if (!cancelled) { + setSelectedFeedbackTaskLoading(false) + } + }) + + return () => { + cancelled = true + } + }, [selectedFeedbackCorrection?.task_id]) + + const selectedFeedbackResolved = useMemo(() => { + if (!selectedFeedbackCorrection) { + return null + } + if (selectedFeedbackTaskDetail?.task_id === selectedFeedbackCorrection.task_id) { + return { + ...selectedFeedbackCorrection, + ...selectedFeedbackTaskDetail, + } satisfies MemoryFeedbackCorrectionDetailTaskPayload + } + return selectedFeedbackTaskDetail ?? selectedFeedbackCorrection + }, [selectedFeedbackCorrection, selectedFeedbackTaskDetail]) + + const selectedFeedbackActionLogs = Array.isArray(selectedFeedbackResolved?.action_logs) + ? selectedFeedbackResolved.action_logs + : [] + const filteredFeedbackActionLogs = useMemo(() => { + const keyword = feedbackActionLogSearch.trim().toLowerCase() + if (!keyword) { + return selectedFeedbackActionLogs + } + return selectedFeedbackActionLogs.filter((item) => + [ + item.action_type, + item.target_hash, + item.reason, + summarizeFeedbackActionPayload(item.before_payload), + summarizeFeedbackActionPayload(item.after_payload), + ] + .map((value) => String(value ?? '').toLowerCase()) + .some((value) => value.includes(keyword)), + ) + }, [feedbackActionLogSearch, selectedFeedbackActionLogs]) + const feedbackActionLogPageCount = Math.max( + 1, + Math.ceil(filteredFeedbackActionLogs.length / FEEDBACK_ACTION_LOG_PAGE_SIZE), + ) + const pagedFeedbackActionLogs = useMemo(() => { + const start = (feedbackActionLogPage - 1) * FEEDBACK_ACTION_LOG_PAGE_SIZE + return filteredFeedbackActionLogs.slice(start, start + FEEDBACK_ACTION_LOG_PAGE_SIZE) + }, [feedbackActionLogPage, filteredFeedbackActionLogs]) + + useEffect(() => { + setFeedbackActionLogPage(1) + }, [selectedFeedbackTaskId, feedbackActionLogSearch]) + + useEffect(() => { + if (feedbackActionLogPage > feedbackActionLogPageCount) { + setFeedbackActionLogPage(feedbackActionLogPageCount) + } + }, [feedbackActionLogPage, feedbackActionLogPageCount]) + + const openFeedbackRollbackDialog = useCallback(() => { + setFeedbackRollbackReason('') + setFeedbackRollbackDialogOpen(true) + }, []) + + const executeFeedbackRollback = useCallback(async () => { + const taskId = selectedFeedbackResolved?.task_id + if (!taskId) { + return + } + try { + setFeedbackRollingBack(true) + const payload = await rollbackMemoryFeedbackCorrection(taskId, { + requested_by: 'knowledge_base', + reason: feedbackRollbackReason.trim(), + }) + if (!payload.success) { + throw new Error(payload.error || '回退失败') + } + toast({ + title: payload.already_rolled_back ? '该纠错已回退' : '纠错回退成功', + description: `任务 ${taskId} 的回退结果已写入日志`, + }) + setFeedbackRollbackDialogOpen(false) + const [listPayload, detailPayload] = await Promise.all([ + getMemoryFeedbackCorrections({ limit: FEEDBACK_CORRECTION_FETCH_LIMIT }), + getMemoryFeedbackCorrection(taskId), + ]) + setFeedbackCorrections(listPayload.items ?? []) + setSelectedFeedbackTaskDetail(detailPayload.task ?? null) + const [sourcePayload, runtimePayload] = await Promise.all([ + getMemorySources(), + getMemoryRuntimeConfig(), + ]) + setMemorySources(sourcePayload.items ?? []) + setRuntimeConfig(runtimePayload) + } catch (error) { + toast({ + title: '纠错回退失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive', + }) + } finally { + setFeedbackRollingBack(false) + } + }, [feedbackRollbackReason, selectedFeedbackResolved?.task_id, toast]) + const selectedOperationResolved = useMemo(() => { if (!selectedDeleteOperation) { return null @@ -1776,6 +2100,9 @@ export function KnowledgeBasePage() { 删除 + + 纠错历史 + @@ -3314,6 +3641,327 @@ export function KnowledgeBasePage() { + + +
+ + + + + 反馈纠错历史 + + + 查看 feedback correction 的判定、修改轨迹与回退结果;本期仅覆盖自动纠错任务 + + + +
+ setFeedbackSearch(event.target.value)} + placeholder="搜索 query_tool_id / session / query / reason" + /> + + +
+ +
+ 当前命中 {filteredFeedbackCorrections.length} 条记录,已加载最近 {feedbackCorrections.length} 条 + 第 {feedbackPage} / {feedbackPageCount} 页,每页显示 {FEEDBACK_CORRECTION_PAGE_SIZE} 条 +
+ +
+ +
+ {pagedFeedbackCorrections.length > 0 ? pagedFeedbackCorrections.map((item) => { + const isSelected = selectedFeedbackCorrection?.task_id === item.task_id + return ( + + ) + }) : ( +
+ 当前筛选条件下没有纠错历史 +
+ )} +
+
+ +
+ {selectedFeedbackCorrection ? ( +
+
+
+
+ + {formatFeedbackTaskStatus(String(selectedFeedbackResolved?.task_status ?? ''))} + + + {formatFeedbackRollbackStatus(String(selectedFeedbackResolved?.rollback_status ?? 'none'))} + + + {formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))} + +
+
+ {selectedFeedbackResolved?.query_text || '无查询文本'} +
+
+ {selectedFeedbackResolved?.query_tool_id} +
+
+ +
+ +
+
+
会话
+
{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} + +
+
+
查询快照
+
+                                  {JSON.stringify(selectedFeedbackResolved?.query_snapshot ?? {}, null, 2)}
+                                
+
+
+
判定结果
+
+                                  {JSON.stringify(selectedFeedbackResolved?.decision_payload ?? {}, null, 2)}
+                                
+
+
+ +
+
+
回退计划摘要
+
+                                  {JSON.stringify(selectedFeedbackResolved?.rollback_plan_summary ?? {}, null, 2)}
+                                
+
+
+
回退结果
+
+                                  {JSON.stringify(selectedFeedbackResolved?.rollback_result ?? {}, null, 2)}
+                                
+
+
+ +
+
+
动作时间线
+
+ setFeedbackActionLogSearch(event.target.value)} + placeholder="搜索动作 / hash / 预览内容" + className="lg:w-80" + /> +
+ 第 {feedbackActionLogPage} / {feedbackActionLogPageCount} 页,每页 {FEEDBACK_ACTION_LOG_PAGE_SIZE} 项 +
+
+
+ +
+ {pagedFeedbackActionLogs.length > 0 ? pagedFeedbackActionLogs.map((item: MemoryFeedbackActionLogPayload) => ( +
+
+ {item.action_type} + {item.target_hash ? ( + {item.target_hash} + ) : null} +
+ {item.reason ? ( +
+ {item.reason} +
+ ) : null} + {item.before_payload && Object.keys(item.before_payload).length > 0 ? ( +
+ Before: + {summarizeFeedbackActionPayload(item.before_payload)} +
+ ) : null} + {item.after_payload && Object.keys(item.after_payload).length > 0 ? ( +
+ After: + {summarizeFeedbackActionPayload(item.after_payload)} +
+ ) : null} +
+ {formatDeleteOperationTime(item.created_at)} +
+
+ )) : ( +
+ {selectedFeedbackActionLogs.length > 0 ? '当前筛选条件下没有动作日志' : '当前任务没有动作日志'} +
+ )} +
+
+
+ +
支持按动作类型、hash 和摘要检索
+ +
+
+
+ ) : ( +
+ 当前没有可查看的纠错详情 +
+ )} +
+
+ +
+ +
+ 支持按 query、任务状态和回退状态检索 +
+ +
+
+
+
+
@@ -3332,6 +3980,52 @@ export function KnowledgeBasePage() { onExecute={() => void executePendingDelete()} onRestore={() => void (deleteResult?.operation_id ? restoreDeleteOperation(deleteResult.operation_id) : Promise.resolve())} /> + + + + + 回退本次纠错 + + 这会恢复旧 relation 状态、隐藏纠错写入段落,并重新触发 episode/profile 的异步修复。 + + +
+
+
{selectedFeedbackResolved?.query_text || '无查询文本'}
+
+ {selectedFeedbackResolved?.query_tool_id} +
+
+
+ +