import type { MemoryDeleteOperationPayload, MemoryFeedbackActionLogPayload, MemoryFeedbackCorrectionDetailTaskPayload, MemoryFeedbackCorrectionSummaryPayload, MemoryImportInputMode, } from '@/lib/memory-api' import { IMPORT_STATUS_TEXT, IMPORT_STEP_TEXT, QUEUED_IMPORT_STATUS, RUNNING_IMPORT_STATUS, } from './constants' export type DeleteOperationItem = NonNullable[number] export function normalizeProgress(value: number | string | null | undefined): number { const numeric = Number(value ?? 0) if (!Number.isFinite(numeric)) { return 0 } if (numeric < 0) { return 0 } if (numeric > 100) { return 100 } return numeric } export function parseOptionalPositiveInt(input: string): number | undefined { const value = input.trim() if (!value) { return undefined } const parsed = Number(value) if (!Number.isInteger(parsed) || parsed <= 0) { return undefined } return parsed } export function parseCommaSeparatedList(input: string): string[] { return input .split(',') .map((item) => item.trim()) .filter(Boolean) } export function normalizeImportInputMode(value: string): MemoryImportInputMode { return value === 'json' ? 'json' : 'text' } export function getImportStatusLabel(status: string): string { const normalized = String(status ?? '').trim() if (!normalized) { return '-' } return IMPORT_STATUS_TEXT[normalized] ?? normalized } export function getImportStepLabel(step: string): string { const normalized = String(step ?? '').trim() if (!normalized) { return '-' } return IMPORT_STEP_TEXT[normalized] ?? normalized } export function getImportStatusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' { if (status === 'failed') { return 'destructive' } if (status === 'completed') { return 'default' } if (status === 'completed_with_errors' || status === 'cancelled') { return 'secondary' } if (RUNNING_IMPORT_STATUS.has(status) || QUEUED_IMPORT_STATUS.has(status)) { return 'outline' } return 'secondary' } export function formatImportTime(timestamp?: number | null): string { if (!timestamp) { return '-' } const normalized = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000 const value = new Date(normalized) if (Number.isNaN(value.getTime())) { return '-' } return value.toLocaleString('zh-CN', { hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } export function formatDeleteOperationMode(mode: string): string { switch (mode) { case 'entity': return '实体' case 'relation': return '关系' case 'paragraph': return '段落' case 'source': return '来源' case 'mixed': return '混合' default: return mode || '未知' } } export function formatDeleteOperationStatus(status: string): string { switch (status) { case 'executed': return '已执行' case 'restored': return '已恢复' default: return status || '未知' } } export function formatDeleteOperationTime(timestamp?: number | null): string { if (!timestamp) { return '未知时间' } const normalized = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000 const value = new Date(normalized) if (Number.isNaN(value.getTime())) { return '未知时间' } return value.toLocaleString('zh-CN', { hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } export function trimDeleteItemText(value: string, maxLength: number = 140): string { const normalized = String(value ?? '').trim().replace(/\s+/g, ' ') if (!normalized) { return '' } if (normalized.length <= maxLength) { return normalized } return `${normalized.slice(0, maxLength)}...` } export function formatDeleteRelationText(subject: string, predicate: string, object: string): string { const left = String(subject ?? '').trim() const middle = String(predicate ?? '').trim() const right = String(object ?? '').trim() return [left, middle, right].filter(Boolean).join(' -> ') } export function getDeleteOperationItemLabel(item: DeleteOperationItem): string { const payload = item.payload ?? {} if (item.item_type === 'entity') { const entity = (payload.entity ?? {}) as Record return String(entity.name ?? item.item_key ?? item.item_hash ?? '未命名实体') } if (item.item_type === 'relation') { const relation = (payload.relation ?? {}) as Record return ( formatDeleteRelationText( String(relation.subject ?? ''), String(relation.predicate ?? ''), String(relation.object ?? ''), ) || String(item.item_key ?? item.item_hash ?? '未命名关系') ) } if (item.item_type === 'paragraph') { const paragraph = (payload.paragraph ?? {}) as Record const source = String(paragraph.source ?? '').trim() return source || String(item.item_key ?? item.item_hash ?? '未命名段落') } return String(item.item_key ?? item.item_hash ?? '未命名对象') } export function getDeleteOperationItemPreview(item: DeleteOperationItem): string { const payload = item.payload ?? {} if (item.item_type === 'entity') { const paragraphLinks = Array.isArray(payload.paragraph_links) ? payload.paragraph_links : [] if (paragraphLinks.length > 0) { return `关联段落 ${paragraphLinks.length} 个` } return '实体快照' } if (item.item_type === 'relation') { const relation = (payload.relation ?? {}) as Record const paragraphHashes = Array.isArray(payload.paragraph_hashes) ? payload.paragraph_hashes : [] const { confidence } = relation const parts = [] if (paragraphHashes.length > 0) { parts.push(`证据段落 ${paragraphHashes.length} 个`) } if (typeof confidence === 'number') { parts.push(`置信度 ${confidence.toFixed(2)}`) } return parts.join(',') || '关系快照' } if (item.item_type === 'paragraph') { const paragraph = (payload.paragraph ?? {}) as Record return trimDeleteItemText(String(paragraph.content ?? '')) } return '' } export function getDeleteOperationItemSource(item: DeleteOperationItem): string { const payload = item.payload ?? {} if (item.item_type === 'paragraph') { const paragraph = (payload.paragraph ?? {}) as Record return String(paragraph.source ?? '').trim() } return String(payload.source ?? '').trim() } export 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 || '未知' } } export 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 || '未知' } } export function formatFeedbackRollbackStatus(status: string): string { switch (status) { case 'none': return '未回退' case 'running': return '回退中' case 'rolled_back': return '已回退' case 'error': return '回退失败' default: return status || '未知' } } export 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' } export 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) } export 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 } export function formatFeedbackRelationTriplet(value: unknown): string { const triplet = pickFeedbackRelationTriplet(value) if (!triplet) { return '' } return formatDeleteRelationText( String(triplet.subject ?? ''), String(triplet.predicate ?? ''), String(triplet.object ?? ''), ) } export 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: '', } } export 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 } export 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 || '未知动作' } } export 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 || '记录了一条动作日志' } }