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) => | {il(c, `th${k}-${i}`)} | )}
+
+
+ {rows.map((r, ri) => (
+ {r.map((c, ci) => | {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,