// 图表 spec 与「从模型输出里抽取 ```chart 代码块」的解析。 // 约定:chart 工具返回图表 JSON,agent 在答复里用 ```chart 围栏原样包裹;前端据此渲染 SVG。 export interface ChartSeries { name?: string; data: number[]; } export interface ChartSpec { type: "bar" | "line" | "pie"; title?: string; labels: string[]; series: ChartSeries[]; } // Segment:把一段输出拆成「普通文本」与「图表」交错的有序片段,供输出区分别渲染。 export type Segment = { kind: "text"; text: string } | { kind: "chart"; spec: ChartSpec }; const chartFence = /```chart\s*\n([\s\S]*?)```/g; // isChartSpec 校验解析出的对象是否是合法图表 spec(防脏数据炸渲染)。 export function isChartSpec(v: unknown): v is ChartSpec { if (!v || typeof v !== "object") return false; const o = v as Record; if (o.type !== "bar" && o.type !== "line" && o.type !== "pie") return false; if (!Array.isArray(o.labels) || o.labels.length === 0) return false; if (!Array.isArray(o.series) || o.series.length === 0) return false; return (o.series as unknown[]).every( (s) => s && typeof s === "object" && Array.isArray((s as ChartSeries).data), ); } // extractChartBlocks 把输出文本拆为 text / chart 片段(保持原顺序)。 // ```chart 块解析失败或非法 → 回退为普通文本,绝不丢内容。 export function extractChartBlocks(input: string): Segment[] { if (!input) return []; const segs: Segment[] = []; let last = 0; for (let m = chartFence.exec(input); m; m = chartFence.exec(input)) { if (m.index > last) segs.push({ kind: "text", text: input.slice(last, m.index) }); let parsed: unknown; try { parsed = JSON.parse(m[1].trim()); } catch { parsed = null; } if (isChartSpec(parsed)) segs.push({ kind: "chart", spec: parsed }); else segs.push({ kind: "text", text: m[0] }); // 解析失败:原样当文本,不丢 last = m.index + m[0].length; } chartFence.lastIndex = 0; if (last < input.length) segs.push({ kind: "text", text: input.slice(last) }); return segs; } // hasChart 快速判断输出里是否含图表块(决定是否走分段渲染)。 export function hasChart(input: string): boolean { const has = /```chart\s*\n/.test(input); return has; }