mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-24 10:58:13 +00:00
feat(status+webui): 运行态图形化 —— 排队/四级中断队列/中断栈/驻留子/通道拓扑
需求:首页不该只有文字,要能一眼看出内核在忙什么——排队消息数、各级中断
排队与中断栈、驻留子 agent 数量;这些要向**内部 SDK 暴露接口**,供 WebUI 等应用
展示;通道划分也要能画出来。
## 一、内核状态面(internal/sdk,内部 SDK,不受公开 SDK 冻结约束)
* SchedulerStatus 补:
- interrupt_queues[5]:**四级中断队列各自的深度**(下标即级别 1..4,下标 0 恒 0,
这样 level 能直接当数组下标用)。此前只有 pending_interrupts 总数,
看不出"堵在 L1 还是 L4"——四级是抢占优先级,堵在哪级是完全不同的运行状态。
- immediate:刚抢占成功、下一个安全点立即运行的那个中断(此前完全不可见)。
- suspend_frames:中断栈的帧(栈底→栈顶,只给任务标识),depth 之外还能看出
"谁被谁打断"。
- interrupts_by_level / preempts_by_level:各级累计登记数与抢占成功数。
* KernelStatus 补 residents(驻留子运行时视图:状态/轮次/上下文已满/输入通道/允许输出)。
刻意**不带**每个驻留子的 inputch 登记明细——状态面会被反复轮询,明细会让
每次 /status 背上几十 KB;只给表大小,要明细走专门接口。
* ChannelInfo 补 direction(in/out/io)、description、tools、output_caps、caps_text。
此前 collectKernelStatus 只透传 Name/Type,把描述/工具/能力**全丢了**,
前端只能画出一排光秃秃的名字。
## 二、WebUI
* 新增只读 `/api/v1/runtime`:只回运行态三件事(scheduler/residents/channels),
实测 **1.0KB**(/kernel 是 30KB 级)——所以能 3 秒轮询做"实时"感,
而不必反复拉全量状态。
* 首页新增「运行态」面板(纯 CSS + 内联 SVG,前端仍无构建链):
- 四个数字块:排队任务 / 待处理中断 / 中断栈(深度/上限) / 驻留子 Agent,带占比条;
- 四级中断队列条形图:每级"深度 · 登记/抢占",四级语义**照抄内核**
(L4 内核独占 / L3 交互 / L2 消息 / L1 后台),不自己起名字;
- 中断栈层叠图(栈顶在上)+ ⚡立即运行项;
- 通道拓扑:输入通道 → 内核 → 输出通道,双向通道两侧都出现,能力以胶囊标签显示。
* 3 秒轮询只在总览页可见时才发请求;切回总览时 renderAll 会立刻补一次。
## 验证
* 新增 TestSchedulerStatusExposesLevelsAndStack(四级队列/立即项/栈帧/各级计数映射,
并断言"未使用的级别必须为 0"与"下标 0 恒 0")、TestChannelInfoCarriesTopology、
TestRuntimeEndpoint(形状 + 不携带 tools/plugins + 无状态源时 503)。
* go build / vet / agent+core+sdk+plugin+webui 全量测试绿。
* 真实浏览器实测(CDP 驱动,注入运行态样本走真实渲染路径):
数字块 [3, 5, 2/4, 2];四级条 L4=1/L3=2/L2=1/L1=1 与数据一致;
栈帧按"栈底→栈顶"渲染且标出栈顶;通道左右分列、io 通道两侧都出现。
This commit is contained in:
@ -1387,3 +1387,75 @@ func TestChatHistoryDefaultIsPaged(t *testing.T) {
|
||||
t.Fatalf("limit=5 应回 5 条,实际 %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeStatus 是给 /runtime 用的最小内核状态桩。
|
||||
type fakeStatus struct{ ks *sdk.KernelStatus }
|
||||
|
||||
func (f fakeStatus) GetKernelStatus() *sdk.KernelStatus { return f.ks }
|
||||
|
||||
// TestRuntimeEndpoint 钉住运行态小接口的形状:
|
||||
// 只回运行态三件事(调度器/驻留子/通道),且体积远小于 /kernel ——
|
||||
// 前端靠它做秒级刷新,字段一丢图就画不出来。
|
||||
func TestRuntimeEndpoint(t *testing.T) {
|
||||
ks := &sdk.KernelStatus{
|
||||
Uptime: "1m",
|
||||
Scheduler: sdk.SchedulerStatus{
|
||||
ReadyQueueDepth: 2,
|
||||
PendingInterrupts: 1,
|
||||
SuspendStack: 1,
|
||||
MaxSuspendDepth: 4,
|
||||
InterruptQueues: [5]int{0, 0, 0, 0, 1},
|
||||
SuspendFrames: []sdk.SchedulerFrame{{Task: sdk.SchedulerTask{ID: 9, Level: 3, Kind: "input"}}},
|
||||
},
|
||||
Residents: []sdk.ResidentStatus{{ID: "r1", State: "running", Rounds: 3}},
|
||||
Channels: []sdk.ChannelInfo{{Name: "webui", Type: "1", Direction: "out", Ready: true, OutputCaps: 7, CapsText: "[text file image]"}},
|
||||
}
|
||||
s := testSDK(sdk.SDKConfig{
|
||||
Settings: sdk.NewSettings("webui", internalConfig.NewConfigRegistry("")),
|
||||
Config: sdk.NewConfig(&types.Config{}),
|
||||
Status: fakeStatus{ks: ks},
|
||||
})
|
||||
h := NewHandler(s)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/runtime", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleRuntime(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var out struct {
|
||||
Scheduler sdk.SchedulerStatus `json:"scheduler"`
|
||||
Residents []sdk.ResidentStatus `json:"residents"`
|
||||
Channels []sdk.ChannelInfo `json:"channels"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if out.Scheduler.ReadyQueueDepth != 2 || out.Scheduler.PendingInterrupts != 1 {
|
||||
t.Fatalf("调度器字段丢失:%+v", out.Scheduler)
|
||||
}
|
||||
if out.Scheduler.InterruptQueues[4] != 1 {
|
||||
t.Fatalf("四级队列深度丢失:%v", out.Scheduler.InterruptQueues)
|
||||
}
|
||||
if len(out.Scheduler.SuspendFrames) != 1 || out.Scheduler.SuspendFrames[0].Task.ID != 9 {
|
||||
t.Fatalf("中断栈帧丢失:%+v", out.Scheduler.SuspendFrames)
|
||||
}
|
||||
if len(out.Residents) != 1 || out.Residents[0].ID != "r1" {
|
||||
t.Fatalf("驻留子丢失:%+v", out.Residents)
|
||||
}
|
||||
if len(out.Channels) != 1 || out.Channels[0].Direction != "out" {
|
||||
t.Fatalf("通道方向丢失:%+v", out.Channels)
|
||||
}
|
||||
// 只回运行态:不该把 tools/plugins 这类大块带上
|
||||
if strings.Contains(w.Body.String(), "\"tools\"") || strings.Contains(w.Body.String(), "\"plugins\"") {
|
||||
t.Fatal("/runtime 不应携带 tools/plugins(那是 /kernel 的内容)")
|
||||
}
|
||||
// 没有内核状态时给 503,而不是空对象
|
||||
s2 := testSDK(sdk.SDKConfig{Settings: sdk.NewSettings("webui", internalConfig.NewConfigRegistry(""))})
|
||||
h2 := NewHandler(s2)
|
||||
w2 := httptest.NewRecorder()
|
||||
h2.handleRuntime(w2, httptest.NewRequest(http.MethodGet, "/api/v1/runtime", nil))
|
||||
if w2.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("无状态源应回 503,实际 %d", w2.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user