/** * 消息内容解析工具 */ import type { ChatMessageVO, ChatSession, SqlResultPage, ToolStep } from '../api/types'; import { incrementalScan, isTablePayload, parseContentBlocks, type ContentBlock } from './scanner'; /** 会话展示标题(空标题兜底为"新对话") */ export function sessionTitle(session?: ChatSession | null): string { if (!session) return '新对话'; return session.title || '新对话'; } /** 安全解析工具事件 JSON(后端 tool_events 列为 JSON 字符串数组) */ export function parseToolEvents(raw?: string | null): ToolStep[] { if (!raw) return []; try { const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed .filter((it) => it && typeof it === 'object') .map((it) => ({ id: String(it.id ?? ''), toolName: String(it.toolName ?? ''), input: typeof it.input === 'string' ? it.input : it.input ? JSON.stringify(it.input) : '', result: typeof it.result === 'string' ? it.result : it.result ? JSON.stringify(it.result) : '', status: it.status || 'done', })); } catch { return []; } } /** * 从消息 metadata(JSON 字符串)里取出思考过程。 * * 后端把模型 reasoning_content 累积后写入 `agent_message.metadata` 的 `thinking` 字段, * 刷新会话时用它还原「思考过程」面板;无该字段返回空串。 */ export function parseThinkingFromMetadata(raw?: string | null): string { if (!raw) return ''; try { const parsed = JSON.parse(raw); return typeof parsed?.thinking === 'string' ? parsed.thinking : ''; } catch { return ''; } } /** 从工具结果文本里抽取表格载荷(execute_sql 的返回就是表格 JSON) */ export function parseTableFromToolResult(raw?: string): SqlResultPage | null { if (!raw) return null; try { const parsed = JSON.parse(raw); if (isTablePayload(parsed)) return parsed as SqlResultPage; // 兼容 { code, data: {...} } 包装 if (parsed && typeof parsed === 'object' && isTablePayload(parsed.data)) { return parsed.data as SqlResultPage; } } catch { /* 非 JSON 结果(如 list_tables 的文本) */ } return null; } /** 解析消息正文为内容块 */ export function parseMessageBlocks(content?: string): ContentBlock[] { return parseContentBlocks(content || ''); } /** 流式增量解析 */ export function parseMessageBlocksIncremental(prev: ContentBlock[] | undefined, content: string) { return incrementalScan(prev, content); } /** 取出正文中所有表格块 */ export function collectTableBlocks(blocks: ContentBlock[]): SqlResultPage[] { return blocks .filter((b) => b.kind === 'table' && b.data && isTablePayload(b.data)) .map((b) => b.data as SqlResultPage); } /** 取出正文中所有图表块 */ export function collectChartBlocks(blocks: ContentBlock[]) { return blocks.filter((b) => b.kind === 'echarts' && b.data); } /** * 内容块 → 纯 markdown 文本(用于导出/复制) * 表格块还原为 JSON(保持可回溯),其余保持原文。 */ export function blocksToMarkdown(blocks: ContentBlock[]): string { return blocks .map((b) => { switch (b.kind) { case 'table': return b.pending ? b.text : `\`\`\`json\n${b.text}\n\`\`\``; case 'echarts': case 'graph': return `\`\`\`${b.lang || b.kind}\n${b.text}\n\`\`\``; case 'html': return b.text; case 'code': return `\`\`\`${b.lang || ''}\n${b.text}\n\`\`\``; default: return b.text; } }) .join('\n\n') .replace(/\n{3,}/g, '\n\n') .trim(); } /** 消息 → 可导出的 markdown 全文 */ export function messageToMarkdown(message: ChatMessageVO): string { const blocks = parseMessageBlocks(message.content); return blocksToMarkdown(blocks); } /** 会话消息列表 → 完整 markdown 文档 */ export function conversationToMarkdown(messages: ChatMessageVO[], title: string): string { const lines: string[] = [`# ${title || 'AI 分析报告'}`, '', `> 导出时间:${formatNow()}`, '']; for (const m of messages) { const role = m.role === 'user' ? '🧑 提问' : '🤖 回答'; lines.push(`## ${role}`, ''); lines.push(messageToMarkdown(m)); lines.push('', '---', ''); } return lines.join('\n'); } /** 生成安全的文件名片段 */ export function safeFileName(name: string, fallback = 'ai-report'): string { const cleaned = (name || '') .replace(/[\\/:*?"<>|\r\n\t]/g, '_') .replace(/\s+/g, ' ') .trim() .slice(0, 60); return cleaned || fallback; } /** yyyyMMdd-HHmm */ export function timestampTag(date = new Date()): string { const p = (n: number) => String(n).padStart(2, '0'); return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}`; } /** yyyy-MM-dd HH:mm:ss */ export function formatNow(date = new Date()): string { const p = (n: number) => String(n).padStart(2, '0'); return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`; } /** 相对时间展示 */ export function fromNow(value?: string): string { if (!value) return ''; const t = new Date(value.replace(/-/g, '/')).getTime(); if (Number.isNaN(t)) return value; const diff = Date.now() - t; if (diff < 60_000) return '刚刚'; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`; if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`; return value.slice(0, 16); }