parseMessageContent.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * 消息内容解析工具
  3. */
  4. import type { ChatMessageVO, ChatSession, SqlResultPage, ToolStep } from '../api/types';
  5. import { incrementalScan, isTablePayload, parseContentBlocks, type ContentBlock } from './scanner';
  6. /** 会话展示标题(空标题兜底为"新对话") */
  7. export function sessionTitle(session?: ChatSession | null): string {
  8. if (!session) return '新对话';
  9. return session.title || '新对话';
  10. }
  11. /** 安全解析工具事件 JSON(后端 tool_events 列为 JSON 字符串数组) */
  12. export function parseToolEvents(raw?: string | null): ToolStep[] {
  13. if (!raw) return [];
  14. try {
  15. const parsed = JSON.parse(raw);
  16. if (!Array.isArray(parsed)) return [];
  17. return parsed
  18. .filter((it) => it && typeof it === 'object')
  19. .map((it) => ({
  20. id: String(it.id ?? ''),
  21. toolName: String(it.toolName ?? ''),
  22. input: typeof it.input === 'string' ? it.input : it.input ? JSON.stringify(it.input) : '',
  23. result: typeof it.result === 'string' ? it.result : it.result ? JSON.stringify(it.result) : '',
  24. status: it.status || 'done',
  25. }));
  26. } catch {
  27. return [];
  28. }
  29. }
  30. /**
  31. * 从消息 metadata(JSON 字符串)里取出思考过程。
  32. *
  33. * 后端把模型 reasoning_content 累积后写入 `agent_message.metadata` 的 `thinking` 字段,
  34. * 刷新会话时用它还原「思考过程」面板;无该字段返回空串。
  35. */
  36. export function parseThinkingFromMetadata(raw?: string | null): string {
  37. if (!raw) return '';
  38. try {
  39. const parsed = JSON.parse(raw);
  40. return typeof parsed?.thinking === 'string' ? parsed.thinking : '';
  41. } catch {
  42. return '';
  43. }
  44. }
  45. /** 从工具结果文本里抽取表格载荷(execute_sql 的返回就是表格 JSON) */
  46. export function parseTableFromToolResult(raw?: string): SqlResultPage | null {
  47. if (!raw) return null;
  48. try {
  49. const parsed = JSON.parse(raw);
  50. if (isTablePayload(parsed)) return parsed as SqlResultPage;
  51. // 兼容 { code, data: {...} } 包装
  52. if (parsed && typeof parsed === 'object' && isTablePayload(parsed.data)) {
  53. return parsed.data as SqlResultPage;
  54. }
  55. } catch {
  56. /* 非 JSON 结果(如 list_tables 的文本) */
  57. }
  58. return null;
  59. }
  60. /** 解析消息正文为内容块 */
  61. export function parseMessageBlocks(content?: string): ContentBlock[] {
  62. return parseContentBlocks(content || '');
  63. }
  64. /** 流式增量解析 */
  65. export function parseMessageBlocksIncremental(prev: ContentBlock[] | undefined, content: string) {
  66. return incrementalScan(prev, content);
  67. }
  68. /** 取出正文中所有表格块 */
  69. export function collectTableBlocks(blocks: ContentBlock[]): SqlResultPage[] {
  70. return blocks
  71. .filter((b) => b.kind === 'table' && b.data && isTablePayload(b.data))
  72. .map((b) => b.data as SqlResultPage);
  73. }
  74. /** 取出正文中所有图表块 */
  75. export function collectChartBlocks(blocks: ContentBlock[]) {
  76. return blocks.filter((b) => b.kind === 'echarts' && b.data);
  77. }
  78. /**
  79. * 内容块 → 纯 markdown 文本(用于导出/复制)
  80. * 表格块还原为 JSON(保持可回溯),其余保持原文。
  81. */
  82. export function blocksToMarkdown(blocks: ContentBlock[]): string {
  83. return blocks
  84. .map((b) => {
  85. switch (b.kind) {
  86. case 'table':
  87. return b.pending ? b.text : `\`\`\`json\n${b.text}\n\`\`\``;
  88. case 'echarts':
  89. case 'graph':
  90. return `\`\`\`${b.lang || b.kind}\n${b.text}\n\`\`\``;
  91. case 'html':
  92. return b.text;
  93. case 'code':
  94. return `\`\`\`${b.lang || ''}\n${b.text}\n\`\`\``;
  95. default:
  96. return b.text;
  97. }
  98. })
  99. .join('\n\n')
  100. .replace(/\n{3,}/g, '\n\n')
  101. .trim();
  102. }
  103. /** 消息 → 可导出的 markdown 全文 */
  104. export function messageToMarkdown(message: ChatMessageVO): string {
  105. const blocks = parseMessageBlocks(message.content);
  106. return blocksToMarkdown(blocks);
  107. }
  108. /** 会话消息列表 → 完整 markdown 文档 */
  109. export function conversationToMarkdown(messages: ChatMessageVO[], title: string): string {
  110. const lines: string[] = [`# ${title || 'AI 分析报告'}`, '', `> 导出时间:${formatNow()}`, ''];
  111. for (const m of messages) {
  112. const role = m.role === 'user' ? '🧑 提问' : '🤖 回答';
  113. lines.push(`## ${role}`, '');
  114. lines.push(messageToMarkdown(m));
  115. lines.push('', '---', '');
  116. }
  117. return lines.join('\n');
  118. }
  119. /** 生成安全的文件名片段 */
  120. export function safeFileName(name: string, fallback = 'ai-report'): string {
  121. const cleaned = (name || '')
  122. .replace(/[\\/:*?"<>|\r\n\t]/g, '_')
  123. .replace(/\s+/g, ' ')
  124. .trim()
  125. .slice(0, 60);
  126. return cleaned || fallback;
  127. }
  128. /** yyyyMMdd-HHmm */
  129. export function timestampTag(date = new Date()): string {
  130. const p = (n: number) => String(n).padStart(2, '0');
  131. return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}`;
  132. }
  133. /** yyyy-MM-dd HH:mm:ss */
  134. export function formatNow(date = new Date()): string {
  135. const p = (n: number) => String(n).padStart(2, '0');
  136. return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
  137. }
  138. /** 相对时间展示 */
  139. export function fromNow(value?: string): string {
  140. if (!value) return '';
  141. const t = new Date(value.replace(/-/g, '/')).getTime();
  142. if (Number.isNaN(t)) return value;
  143. const diff = Date.now() - t;
  144. if (diff < 60_000) return '刚刚';
  145. if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`;
  146. if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`;
  147. if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`;
  148. return value.slice(0, 16);
  149. }