feat(observability): 执行可视化 — 节点级实时轨迹(运行·观测)
把任务执行做成可观测:Dispatcher 在每个节点/阶段发结构化 ExecEvent, 经独立 NATS 通道回流,前端逐节点点亮(状态/耗时/工具入参产出)。 - shared: contract.ExecEvent + ExecSubject(sundynix.exec.<id>,与 Token 流分流); bus.PublishExec/CompleteExec/SubscribeExec(core NATS,复用结束头) - dispatcher: execTracer(自增 Seq 保序 + span 自动计耗时); Orchestrator 加 ExecSink;通用图(init 召回 / 各 tool 入参→产出 / prompt / model 首token+token数)与报告编排(规划大纲 / 各章并行 start-end / 渲染)全程埋点 - gateway: SubscribeExec + GET /tasks/:id/exec SSE(与 token 流并行) - desktop: streamExec + deriveNodes(按 node 归并 start/end/error/info); 复用组件 ExecTrace(竖向轨道,按 kind 着色,运行中脉冲灯); 新 RunsView(运行·观测:轨迹+输出双栏);BottomDrawer 轨迹/工具调用 tab 接真实数据; ReportView 加执行轨迹栏;左导航「运行」置就绪 实测: - 报告任务 /exec:规划(2680ms,4章) → 4 章并行(seq 交错,各~7-8s 重叠=真并行, 每章带 docs 知识库检索预览+成稿字数) → 渲染(docx 落盘) - 通用图 /exec:tool:kb_search(678ms,入参→Milvus 产出) → prompt(2消息) → model(首token 860ms / 4 tokens) - 浏览器(Preview):报告页执行轨迹逐节点点亮、章节带耗时/字数/检索片段,完成后下载 Word Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,45 @@ func (h *Handler) StreamTask(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// StreamExec: 订阅 sundynix.exec.<task_id>,以 SSE 把执行轨迹事件推给客户端(运行·观测)。
|
||||
// 与 StreamTask(token 流)并行:前端同时连两路,token 走输出、exec 走轨迹/工具面板。
|
||||
func (h *Handler) StreamExec(c *gin.Context) {
|
||||
taskID := c.Param("id")
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
|
||||
events := make(chan []byte, 256)
|
||||
done := make(chan struct{})
|
||||
unsub, err := h.bus.SubscribeExec(taskID,
|
||||
func(ev []byte) {
|
||||
select {
|
||||
case events <- ev:
|
||||
default: // 背压保护:客户端过慢则丢弃,避免阻塞 NATS 回调
|
||||
}
|
||||
},
|
||||
func() { close(done) },
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer func() { _ = unsub() }()
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case ev := <-events:
|
||||
c.SSEvent("exec", string(ev))
|
||||
return true
|
||||
case <-done:
|
||||
c.SSEvent("done", taskID)
|
||||
return false
|
||||
case <-c.Request.Context().Done():
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SetMemory: 写入/更新一条用户偏好记忆,经 NATS 调 mcp-go 的 memory_upsert 工具。
|
||||
// 桌面端"偏好记忆面板"可用它让用户显式登记/纠正模型对自己的记忆。
|
||||
func (h *Handler) SetMemory(c *gin.Context) {
|
||||
|
||||
@@ -44,6 +44,11 @@ func (b *Bus) SubscribeTokens(taskID string, onToken func([]byte), onDone func()
|
||||
return b.inner.SubscribeTokens(taskID, onToken, onDone)
|
||||
}
|
||||
|
||||
// SubscribeExec 订阅 sundynix.exec.<taskID> 的执行轨迹事件(用于"运行·观测"SSE)。
|
||||
func (b *Bus) SubscribeExec(taskID string, onEvent func([]byte), onDone func()) (func() error, error) {
|
||||
return b.inner.SubscribeExec(taskID, onEvent, onDone)
|
||||
}
|
||||
|
||||
// CallTool 经 NATS 同步调用一个 MCP 工具(用于网关侧写偏好记忆等)。
|
||||
func (b *Bus) CallTool(ctx context.Context, subject string, call *contract.ToolCall) (*contract.ToolResult, error) {
|
||||
return b.inner.CallTool(ctx, subject, call)
|
||||
|
||||
@@ -13,17 +13,18 @@ import (
|
||||
// New 构建带有 Guardrail / 限流中间件的 Gin 引擎。
|
||||
func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus) *gin.Engine {
|
||||
r := gin.Default()
|
||||
r.Use(cors()) // 桌面端/浏览器跨源访问(开发期放开)
|
||||
r.Use(cors()) // 桌面端/浏览器跨源访问(开发期放开)
|
||||
r.Use(middleware.RateLimit(cache))
|
||||
r.Use(middleware.Guardrail()) // Harness: Input/Output Guardrail
|
||||
|
||||
h := handler.New(db, cache, bus)
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.POST("/tasks", h.SubmitTask) // 1. 解析 DSL 并 Publish 到 NATS
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // 4. SSE/WS 回流 Token Stream
|
||||
api.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
api.POST("/kb/ingest", h.KbIngest) // 知识库入库(文本,→ mcp-go kb_ingest)
|
||||
api.POST("/tasks", h.SubmitTask) // 1. 解析 DSL 并 Publish 到 NATS
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // 4. SSE/WS 回流 Token Stream
|
||||
api.GET("/tasks/:id/exec", h.StreamExec) // 4b. SSE 回流执行轨迹事件(运行·观测)
|
||||
api.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
api.POST("/kb/ingest", h.KbIngest) // 知识库入库(文本,→ mcp-go kb_ingest)
|
||||
api.POST("/kb/ingest_file", h.KbIngestFile) // 文件入库(docx/xlsx/pdf… 异步)
|
||||
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(实时监控)
|
||||
api.POST("/kb/search", h.KbSearch) // 知识库检索台(→ mcp-go kb_search)
|
||||
|
||||
Reference in New Issue
Block a user