- {tab === "output" && (
-
- {run.output || "在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。"}
-
- )}
+ {tab === "output" &&
}
{tab === "trace" &&
}
{tab === "tools" &&
}
{tab === "cite" &&
引用列表:RAG 答案的来源块(源文档 + 分数 + 来源徽标)。
}
@@ -122,6 +120,35 @@ function ApprovalBar({ taskId, node, title, summary }: { taskId: string; node: s
);
}
+// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。
+function OutputView({ output }: { output: string }) {
+ if (!output) {
+ return (
+
+ 在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。
+
+ );
+ }
+ if (!hasChart(output)) {
+ return
{output};
+ }
+ return (
+
+ {extractChartBlocks(output).map((seg, i) =>
+ seg.kind === "chart" ? (
+
+ ) : (
+ seg.text.trim() && (
+
+ {seg.text}
+
+ )
+ ),
+ )}
+
+ );
+}
+
// ToolCalls:从执行事件里筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。
function ToolCalls({ run }: { run: RunState }) {
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
diff --git a/sundynix-mcp-go/internal/mcp/chart.go b/sundynix-mcp-go/internal/mcp/chart.go
new file mode 100644
index 0000000..a0aae2d
--- /dev/null
+++ b/sundynix-mcp-go/internal/mcp/chart.go
@@ -0,0 +1,60 @@
+package mcp
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "context"
+
+ "github.com/sundynix/sundynix-shared/contract"
+)
+
+// chartSeries 是一条数据系列。
+type chartSeries struct {
+ Name string `json:"name,omitempty"`
+ Data []float64 `json:"data"`
+}
+
+// chartSpec 是图表的结构化规范(工具产出,前端据此渲染 SVG,不在后端出图)。
+type chartSpec struct {
+ Type string `json:"type"` // bar / line / pie
+ Title string `json:"title,omitempty"` //
+ Labels []string `json:"labels"` // x 轴/扇区标签
+ Series []chartSeries `json:"series"` // 一条或多条数据系列(pie 取第一条)
+}
+
+// chart 工具:只校验并返回规范化图表 JSON(渲染交前端)。职责单一、零图片传输。
+// 返回内容即一段 chart JSON;工具说明会指示 agent 在最终答复里用 ```chart 围栏原样包裹它。
+func (g *Gateway) chart(_ context.Context, call *contract.ToolCall) *contract.ToolResult {
+ // 用 JSON round-trip 把 args 收进类型化结构(args 里 labels/series 是 []any,手解繁琐)。
+ raw, _ := json.Marshal(call.Args)
+ var in chartSpec
+ if err := json.Unmarshal(raw, &in); err != nil {
+ return &contract.ToolResult{OK: false, Error: "chart: 参数解析失败 —— " + err.Error()}
+ }
+ switch in.Type {
+ case "bar", "line", "pie":
+ case "":
+ in.Type = "bar"
+ default:
+ return &contract.ToolResult{OK: false, Error: "chart: type 仅支持 bar / line / pie"}
+ }
+ if len(in.Labels) == 0 {
+ return &contract.ToolResult{OK: false, Error: "chart: labels 必填"}
+ }
+ if len(in.Series) == 0 || len(in.Series[0].Data) == 0 {
+ return &contract.ToolResult{OK: false, Error: "chart: series 至少一条且 data 非空"}
+ }
+ for i, s := range in.Series {
+ if len(s.Data) != len(in.Labels) {
+ return &contract.ToolResult{OK: false,
+ Error: fmt.Sprintf("chart: 第 %d 条系列 data 长度(%d) 与 labels 长度(%d) 不一致", i+1, len(s.Data), len(in.Labels))}
+ }
+ }
+ if in.Type == "pie" {
+ in.Series = in.Series[:1] // pie 只用第一条系列
+ }
+ out, _ := json.Marshal(in)
+ // 提示 agent:把这段 JSON 用 ```chart 围栏原样放进最终答复,前端会渲染成图。
+ return &contract.ToolResult{OK: true, Content: string(out)}
+}
diff --git a/sundynix-mcp-go/internal/mcp/gateway.go b/sundynix-mcp-go/internal/mcp/gateway.go
index eaa259c..78ea6bb 100644
--- a/sundynix-mcp-go/internal/mcp/gateway.go
+++ b/sundynix-mcp-go/internal/mcp/gateway.go
@@ -142,6 +142,17 @@ func (g *Gateway) buildRegistry() map[string]toolDef {
params: []paramSpec{{Name: "sql", Type: "string", Desc: "只读 SQL,如 SELECT count(*) FROM sundynix_task", Required: true}},
handler: g.sqlQuery,
},
+ "chart": {
+ cn: "图表", desc: "把数据生成图表。返回图表 JSON——请在最终答复中用 ```chart 代码块原样包裹该 JSON,前端会渲染成图。需要可视化数据分布/趋势时调用。",
+ agent: true,
+ params: []paramSpec{
+ {Name: "type", Type: "string", Desc: "图表类型:bar / line / pie", Required: true},
+ {Name: "title", Type: "string", Desc: "图表标题"},
+ {Name: "labels", Type: "array", Desc: "x 轴/扇区标签数组,如 [\"Q1\",\"Q2\"]", Required: true},
+ {Name: "series", Type: "array", Desc: "数据系列数组,如 [{\"name\":\"销量\",\"data\":[120,180]}]", Required: true},
+ },
+ handler: g.chart,
+ },
// —— 仅内部/流水线/管理用,不暴露给自主 agent ——
"kb_ingest": {cn: "知识入库", desc: "文本切块 → 向量化 → 写入 Milvus / Bleve", handler: g.kbIngest},