From 594be6a317d467a8a9fd0938b205ec864c99a429 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 26 Jun 2026 17:33:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20=E6=A8=A1=E5=9E=8B=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=B8=B2=E6=9F=93=E5=9B=BE=E8=A1=A8=20+=20Markdown=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=A1=A8=E6=A0=BC/=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9B=B4=E6=A0=8F=EF=BC=9BReAct=20=E6=AD=A5=E6=95=B0=E6=8F=90?= =?UTF-8?q?=E8=87=B3=E5=8F=AF=E9=85=8D=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对运行页输出的两个渲染缺口 + 一个节点报错: - 图表:模型输出走 chart 块分段渲染(文本 Markdown,```chart 块用 ChartView 出图), 不再把图表 spec 显示成原始代码。 - 表格:轻量 Markdown 组件原本不支持 GFM 表格(| … | 显示成原始竖线)与 ``` 围栏代码, 现补上:表格解析成 、围栏渲染成
。
- ReAct 步数:reactMaxStep 由常量 8 改 env 可配(REACT_MAX_STEP,默认 12)。研究型任务
  (搜索→抓取→推理多轮)8 步偏紧易触顶报 exceeds max steps;提到 12 覆盖多数。

tsc+vite 通过,dispatcher build/vet 绿;wails 重启加载。

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .../frontend/src/components/Markdown.tsx      | 70 ++++++++++++++++---
 .../frontend/src/views/RunsView.tsx           | 20 +++++-
 .../internal/eino/react_agent.go              |  5 +-
 3 files changed, 84 insertions(+), 11 deletions(-)

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,