diff --git a/sundynix-desktop/frontend/src/components/Markdown.tsx b/sundynix-desktop/frontend/src/components/Markdown.tsx index 51d0833..4ac14bd 100644 --- a/sundynix-desktop/frontend/src/components/Markdown.tsx +++ b/sundynix-desktop/frontend/src/components/Markdown.tsx @@ -37,8 +37,37 @@ export function Markdown({ text, className, onLink }: { text: string; className? const blocks: ReactNode[] = []; let para: string[] = []; let list: { ordered: boolean; items: string[] } | null = null; + let tableBuf: string[] = []; // 连续的 | … | 行,flush 时判定是否真表格 + let code: { lang: string; lines: string[] } | null = null; // ``` 围栏代码块 let k = 0; + const cells = (r: string) => r.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim()); + const isSep = (s: string) => s.includes("-") && /^\s*\|?[\s:|-]+\|?\s*$/.test(s); + const flushTable = () => { + if (!tableBuf.length) return; + if (tableBuf.length >= 2 && isSep(tableBuf[1])) { + const head = cells(tableBuf[0]); + const rows = tableBuf.slice(2).map(cells); + blocks.push( +
+ + + {head.map((c, i) => )} + + + {rows.map((r, ri) => ( + {r.map((c, ci) => )} + ))} + +
{il(c, `th${k}-${i}`)}
{il(c, `td${k}-${ri}-${ci}`)}
+
, + ); + } else { + para.push(...tableBuf); // 不是合法表格 → 当普通段落 + } + tableBuf = []; + }; + const flushPara = () => { if (para.length) { blocks.push( @@ -67,20 +96,43 @@ export function Markdown({ text, className, onLink }: { text: string; className? } }; + const flushAll = () => { flushTable(); flushPara(); flushList(); }; + for (const raw of lines) { const line = raw.trimEnd(); + + // 围栏代码块 ```lang … ```(含未被图表分流的普通代码)。 + if (code) { + if (line.trim().startsWith("```")) { + blocks.push(
{code.lines.join("\n")}
); + code = null; + } else code.lines.push(raw); + continue; + } + if (line.trim().startsWith("```")) { + flushAll(); + code = { lang: line.trim().slice(3), lines: [] }; + continue; + } + // 表格行:连续的 | … | 缓冲,flush 时判定真伪。 + if (line.trim().startsWith("|")) { + flushPara(); + flushList(); + tableBuf.push(line); + continue; + } + const h = /^(#{1,3})\s+(.*)$/.exec(line); const ol = /^\d+\.\s+(.*)$/.exec(line); const ul = /^[-*]\s+(.*)$/.exec(line); if (line.trim() === "") { - flushPara(); - flushList(); + flushAll(); } else if (/^(---|\*\*\*|___)\s*$/.test(line)) { - flushPara(); - flushList(); + flushAll(); blocks.push(
); } else if (h) { + flushTable(); flushPara(); flushList(); const lvl = h[1].length; @@ -88,14 +140,14 @@ export function Markdown({ text, className, onLink }: { text: string; className? const content = il(h[2], `h${k}`); blocks.push(lvl === 1 ?

{content}

: lvl === 2 ?

{content}

:

{content}

); } else if (line.startsWith(">")) { - flushPara(); - flushList(); + flushAll(); blocks.push(
{il(line.replace(/^>\s?/, ""), `q${k}`)}
, ); } else if (ol) { + flushTable(); flushPara(); if (!list || !list.ordered) { flushList(); @@ -103,6 +155,7 @@ export function Markdown({ text, className, onLink }: { text: string; className? } list.items.push(ol[1]); } else if (ul) { + flushTable(); flushPara(); if (!list || list.ordered) { flushList(); @@ -110,12 +163,13 @@ export function Markdown({ text, className, onLink }: { text: string; className? } list.items.push(ul[1]); } else { + flushTable(); flushList(); para.push(line); } } - flushPara(); - flushList(); + if (code) blocks.push(
{code.lines.join("\n")}
); + flushAll(); return
{blocks}
; } diff --git a/sundynix-desktop/frontend/src/views/RunsView.tsx b/sundynix-desktop/frontend/src/views/RunsView.tsx index 5a3c7e6..6a45b8c 100644 --- a/sundynix-desktop/frontend/src/views/RunsView.tsx +++ b/sundynix-desktop/frontend/src/views/RunsView.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Activity, FileText, History, Wrench } from "lucide-react"; import { ExecTrace } from "../components/ExecTrace"; import { Markdown } from "../components/Markdown"; +import { ChartView } from "../components/ChartView"; +import { extractChartBlocks, hasChart } from "../lib/chartspec"; import { deriveNodes, emptyRun, type RunState } from "../lib/run"; import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api"; import { Tabs, Panel, Dot, Badge, EmptyState, cn, type TabDef } from "../ui"; @@ -141,7 +143,7 @@ export function RunsView({ run }: { run: RunState }) { {/* 右:模型输出(Markdown,固定面板,与之前一致) */} {cur.output ? ( - + ) : ( )} @@ -151,6 +153,22 @@ export function RunsView({ run }: { run: RunState }) { ); } +// OutputView:模型输出 = Markdown 渲染;含 ```chart 块时分段(文本走 Markdown,图表走 ChartView)。 +function OutputView({ output }: { output: string }) { + if (!hasChart(output)) return ; + return ( +
+ {extractChartBlocks(output).map((seg, i) => + seg.kind === "chart" ? ( + + ) : ( + seg.text.trim() && + ), + )} +
+ ); +} + // ToolCalls:从执行事件筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。 function ToolCalls({ run }: { run: RunState }) { const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool"); diff --git a/sundynix-dispatcher/internal/eino/react_agent.go b/sundynix-dispatcher/internal/eino/react_agent.go index bdca86e..6a950c3 100644 --- a/sundynix-dispatcher/internal/eino/react_agent.go +++ b/sundynix-dispatcher/internal/eino/react_agent.go @@ -20,7 +20,8 @@ import ( ) // reactMaxStep 限制 ReAct 推理-工具循环的步数上限(控成本/时延)。 -const reactMaxStep = 8 +// 研究型任务(搜索→抓取→推理 多轮)8 步偏紧易触顶报错;默认 12,可经 REACT_MAX_STEP 调。 +func reactMaxStep() int { return envInt("REACT_MAX_STEP", 12) } // streamHasToolCall 扫描模型整段流输出,任一片段含 tool call 即判定为工具调用。 // 比默认"只看首片段"鲁棒(兼容先吐文本/思考再给 tool call 的模型,如 deepseek)。 @@ -180,7 +181,7 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar ag, err := react.NewAgent(ctx, &react.AgentConfig{ ToolCallingModel: tcm, ToolsConfig: compose.ToolsNodeConfig{Tools: tools}, - MaxStep: reactMaxStep, + MaxStep: reactMaxStep(), // 默认检查器只看首个流片段;deepseek 等常先吐文本再给 tool call → 漏判。 // 改成扫描整段流,任一片段含 tool call 即判定为工具调用。 StreamToolCallChecker: streamHasToolCall,