| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- /**
- * 消息内容解析工具
- */
- 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);
- }
|