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:
Blizzard
2026-06-12 14:29:28 +08:00
parent ba8c6b3c43
commit cdc5b3a847
19 changed files with 587 additions and 63 deletions
+31
View File
@@ -134,6 +134,37 @@ func (b *Bus) SubscribeTokens(taskID string, onToken func([]byte), onDone func()
return sub.Unsubscribe, nil
}
// ---- 执行可视化事件(core NATS,与 Token 流分流)----
// PublishExec 把一条执行事件(JSON)发到 sundynix.exec.<taskID>。
func (b *Bus) PublishExec(taskID string, data []byte) error {
return b.nc.Publish(contract.ExecSubject(taskID), data)
}
// CompleteExec 发送执行事件流结束信号(空体 + 结束头)。
func (b *Bus) CompleteExec(taskID string) error {
msg := nats.NewMsg(contract.ExecSubject(taskID))
msg.Header.Set(contract.HeaderStreamEnd, "1")
return b.nc.PublishMsg(msg)
}
// SubscribeExec 订阅某 task 的执行事件流。每条事件触发 onEvent;结束触发 onDone。
func (b *Bus) SubscribeExec(taskID string, onEvent func([]byte), onDone func()) (unsub func() error, err error) {
sub, err := b.nc.Subscribe(contract.ExecSubject(taskID), func(m *nats.Msg) {
if m.Header.Get(contract.HeaderStreamEnd) == "1" {
onDone()
return
}
data := make([]byte, len(m.Data))
copy(data, m.Data)
onEvent(data)
})
if err != nil {
return nil, fmt.Errorf("subscribe exec: %w", err)
}
return sub.Unsubscribe, nil
}
// ---- MCP 工具调用(core NATS request-reply----
// CallTool 同步调用一个 MCP 工具:发到 subject,阻塞等待应答。
+26 -6
View File
@@ -6,11 +6,11 @@ import "encoding/json"
// NATS subject / stream 约定(与 README、各服务 config 保持一致)。
const (
StreamTasks = "SUNDYNIX_TASKS" // JetStream stream 名
SubjectTasks = "sundynix.tasks" // 任务发布主题前缀;实际为 sundynix.tasks.<id>
SubjectTasksAll = "sundynix.tasks.>" // stream 捕获的通配
SubjectStream = "sundynix.streams" // Token 回流前缀;实际 sundynix.streams.<id>
ConsumerDurable = "dispatchers" // Dispatcher 持久消费者(队列组负载均衡)
StreamTasks = "SUNDYNIX_TASKS" // JetStream stream 名
SubjectTasks = "sundynix.tasks" // 任务发布主题前缀;实际为 sundynix.tasks.<id>
SubjectTasksAll = "sundynix.tasks.>" // stream 捕获的通配
SubjectStream = "sundynix.streams" // Token 回流前缀;实际 sundynix.streams.<id>
ConsumerDurable = "dispatchers" // Dispatcher 持久消费者(队列组负载均衡)
// HeaderStreamEnd 是 Token 流的结束信号(core NATS 消息头)。
// 置为 "1" 的消息体为空,表示该 task 的 Token 流结束。
@@ -47,6 +47,26 @@ const (
func ConfigGetSubject(kind string) string { return "sundynix.config." + kind + ".get" }
func ConfigUpdatedSubject(kind string) string { return "sundynix.config." + kind + ".updated" }
// SubjectExec 是执行可视化事件的回流前缀;实际 sundynix.exec.<task_id>。
// 与 Token 流(sundynix.streams.<id>)分流:Token 是零拷贝字节,Exec 是结构化节点事件。
const SubjectExec = "sundynix.exec"
// ExecSubject 返回某任务的执行事件回流主题。
func ExecSubject(id string) string { return SubjectExec + "." + id }
// ExecEvent 是一次任务执行中某节点/阶段的生命周期事件(经 sundynix.exec.<id> 回流给 UI
// 用于"运行·观测"的实时轨迹:节点点亮、工具调用入参/产出、各阶段耗时)。
type ExecEvent struct {
Seq int `json:"seq"` // 任务内自增序号(保序)
TS int64 `json:"ts"` // unix 毫秒
Node string `json:"node"` // 稳定节点 idinit / tool:wiki_search / prompt / model / plan / section:0 / render
Kind string `json:"kind"` // 归类着色:memory|tool|prompt|model|plan|section|render|system
Phase string `json:"phase"` // start|end|error|info
Label string `json:"label"` // 人读标题
Detail string `json:"detail,omitempty"` // 入参/产出/计数预览
MS int64 `json:"ms,omitempty"` // end 事件的耗时(毫秒)
}
// IngestEvent 是入库流水线的实时进度事件(经 sundynix.streams.<job_id> 回流给 UI)。
type IngestEvent struct {
Stage string `json:"stage"` // 解析/切块/向量化/写Milvus/写Bleve/完成/失败
@@ -103,7 +123,7 @@ type ToolResult struct {
}
// Marshal / Unmarshal 便捷方法。
func (t *Task) Marshal() ([]byte, error) { return json.Marshal(t) }
func (t *Task) Marshal() ([]byte, error) { return json.Marshal(t) }
func Unmarshal(b []byte) (*Task, error) {
var t Task
if err := json.Unmarshal(b, &t); err != nil {