mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
需求:首页不该只有文字,要能一眼看出内核在忙什么——排队消息数、各级中断
排队与中断栈、驻留子 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 通道两侧都出现。
308 lines
8.4 KiB
Go
308 lines
8.4 KiB
Go
package core
|
||
|
||
import (
|
||
"fmt"
|
||
"runtime"
|
||
"time"
|
||
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||
)
|
||
|
||
// StatusProvider 内核状态查询接口。插件通过此接口查看内核运行动态。
|
||
type StatusProvider interface {
|
||
GetKernelStatus() *KernelStatus
|
||
}
|
||
|
||
// 状态 DTO 使用内置 SDK 的中立类型,保证与插件层解耦。
|
||
type KernelStatus = sdk.KernelStatus
|
||
type PluginInfo = sdk.PluginInfo
|
||
type ChannelInfo = sdk.ChannelInfo
|
||
|
||
// ResidentStatus 是驻留式子 agent 的运行时视图(见 internal/sdk/status.go)。
|
||
type ResidentStatus = sdk.ResidentStatus
|
||
type MemoryStatus = sdk.MemoryStatus
|
||
type KnowledgeStatus = sdk.KnowledgeStatus
|
||
type DocumentStatus = sdk.DocumentStatus
|
||
type TextMemoryStatus = sdk.TextMemoryStatus
|
||
type SocialStatus = sdk.SocialStatus
|
||
type LLMStatus = sdk.LLMStatus
|
||
type ContextStatus = sdk.ContextStatus
|
||
type RuntimeStatus = sdk.RuntimeStatus
|
||
type TrackerStatus = sdk.TrackerStatus
|
||
type BuildStatus = sdk.BuildStatus
|
||
|
||
func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
|
||
tools := make([]string, 0, len(ch.Tools))
|
||
for _, t := range ch.Tools {
|
||
tools = append(tools, t.Name)
|
||
}
|
||
return ChannelInfo{
|
||
Name: ch.Name,
|
||
Type: fmt.Sprintf("%d", ch.Type),
|
||
Direction: channelDirection(ch.Type),
|
||
Ready: true,
|
||
Description: ch.Description,
|
||
Tools: tools,
|
||
OutputCaps: int(ch.OutputCaps),
|
||
CapsText: ch.OutputCaps.String(),
|
||
}
|
||
}
|
||
|
||
// channelDirection 把 DeviceType 翻成可读方向(前端画拓扑用)。
|
||
// 未知取值落到 "io":宁可当作双向,也不要把它画成只进或只出。
|
||
func channelDirection(t agentIO.DeviceType) string {
|
||
switch t {
|
||
case agentIO.DeviceInput:
|
||
return "in"
|
||
case agentIO.DeviceOutput:
|
||
return "out"
|
||
default:
|
||
return "io"
|
||
}
|
||
}
|
||
|
||
// collectKernelStatus 聚合内核各子系统状态快照。
|
||
// 接收所有子系统引用(均为可选——nil 表示不可用),返回统一的状态报告。
|
||
func collectKernelStatus(
|
||
startTime time.Time,
|
||
agentID string,
|
||
providerName string,
|
||
sourceCount int,
|
||
stageHost *StageHost,
|
||
iom *agentIO.IOManager,
|
||
pluginReg *plugin.Registry,
|
||
memDB *memory.GraphDB,
|
||
ks interface{ List() []string },
|
||
docStore *document.Store,
|
||
textMem *text.Memory,
|
||
socialStore *social.SocialStore,
|
||
trk *tracker.Tracker,
|
||
) *KernelStatus {
|
||
status := &KernelStatus{
|
||
Uptime: time.Since(startTime).Round(time.Second).String(),
|
||
StartTime: startTime.Format(time.RFC3339),
|
||
AgentID: agentID,
|
||
// 构建身份取自内核自己的 meta(-ldflags 注入点),
|
||
// 而非 SDK 仓的硬编码版本。
|
||
Build: BuildStatus{
|
||
Version: meta.Version,
|
||
Commit: meta.Commit,
|
||
BuildTime: meta.BuildTime,
|
||
SDKCompatible: meta.SDKCompatibleVersion,
|
||
KernelName: meta.KernelName,
|
||
// AGPL-3.0 §13:状态页向网络使用者展示取得源码的入口。
|
||
SourceURL: meta.SourceURL,
|
||
},
|
||
Runtime: RuntimeStatus{
|
||
Goroutines: runtime.NumGoroutine(),
|
||
GoVersion: runtime.Version(),
|
||
},
|
||
LLM: LLMStatus{
|
||
Available: providerName != "",
|
||
Provider: providerName,
|
||
Sources: sourceCount,
|
||
},
|
||
}
|
||
|
||
// Plugins
|
||
if pluginReg != nil {
|
||
names := pluginReg.List()
|
||
for _, n := range names {
|
||
status.Plugins = append(status.Plugins, PluginInfo{Name: n, Loaded: true})
|
||
}
|
||
}
|
||
|
||
// Tools
|
||
if stageHost != nil {
|
||
status.Tools = stageHost.GetToolDefs()
|
||
}
|
||
|
||
// Channels
|
||
if iom != nil {
|
||
for _, ch := range iom.ListChannels() {
|
||
status.Channels = append(status.Channels, channelInfoFromIO(ch))
|
||
}
|
||
}
|
||
|
||
// Graph memory
|
||
if memDB != nil {
|
||
status.Memory.Available = true
|
||
if info, err := memDB.Introspect(); err == nil {
|
||
if ec, ok := info["entity_count"].(int); ok {
|
||
status.Memory.EntityCount = ec
|
||
}
|
||
if rc, ok := info["relation_count"].(int); ok {
|
||
status.Memory.RelationCount = rc
|
||
}
|
||
if et, ok := info["entity_type_count"].(int); ok {
|
||
status.Memory.EntityTypes = et
|
||
}
|
||
}
|
||
}
|
||
|
||
// Knowledge
|
||
if ks != nil {
|
||
status.Knowledge.Available = true
|
||
status.Knowledge.Items = ks.List()
|
||
status.Knowledge.ItemCount = len(status.Knowledge.Items)
|
||
}
|
||
|
||
// Documents
|
||
if docStore != nil {
|
||
status.Documents.Available = true
|
||
stats := docStore.Stats()
|
||
if dc, ok := stats["doc_count"].(int); ok {
|
||
status.Documents.DocCount = dc
|
||
}
|
||
if vc, ok := stats["vector_count"].(int); ok {
|
||
status.Documents.VectorCount = vc
|
||
}
|
||
}
|
||
|
||
// Text memory
|
||
if textMem != nil {
|
||
status.TextMemory.Available = true
|
||
status.TextMemory.FileCount = textMem.FileCount()
|
||
}
|
||
|
||
// Social
|
||
if socialStore != nil {
|
||
status.Social.Available = true
|
||
if persons, err := socialStore.ListPersons(); err == nil {
|
||
status.Social.PersonCount = len(persons)
|
||
}
|
||
}
|
||
|
||
// Tracker
|
||
if trk != nil {
|
||
status.Tracker.Available = true
|
||
status.Tracker.Dir = trk.MergeDir()
|
||
}
|
||
|
||
// Memory
|
||
var m runtime.MemStats
|
||
runtime.ReadMemStats(&m)
|
||
status.Runtime.MemoryMB = int64(m.Alloc / 1024 / 1024)
|
||
|
||
return status
|
||
}
|
||
|
||
// GetKernelStatus 返回 Agent 驱动的内核状态快照。
|
||
func (a *Agent) GetKernelStatus() *KernelStatus {
|
||
providerName := ""
|
||
sourceCount := 0
|
||
if a.providerManager != nil {
|
||
sourceCount = len(a.providerManager.List())
|
||
}
|
||
if a.provider != nil {
|
||
providerName = a.provider.Name()
|
||
}
|
||
|
||
var textMem *text.Memory
|
||
if a.textMem != nil {
|
||
textMem = a.textMem
|
||
}
|
||
|
||
var socialStore *social.SocialStore
|
||
if a.social != nil {
|
||
socialStore = a.social
|
||
}
|
||
|
||
var trk *tracker.Tracker
|
||
if a.tracker != nil {
|
||
trk = a.tracker
|
||
}
|
||
|
||
// 注意:knowledge 在 collectKernelStatus 里是**接口**参数,
|
||
// 而 (*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic。
|
||
// 所以这里必须先判具体指针再进行接口赋值(healthcheck_kernel 会走到这条路径)。
|
||
var knowledgeLister interface{ List() []string }
|
||
if a.knowledge != nil {
|
||
knowledgeLister = a.knowledge
|
||
}
|
||
|
||
ks := collectKernelStatus(
|
||
a.startTime,
|
||
string(a.id),
|
||
providerName,
|
||
sourceCount,
|
||
a.stageHost,
|
||
a.io,
|
||
a.pluginReg,
|
||
a.memory,
|
||
knowledgeLister,
|
||
a.docStore,
|
||
textMem,
|
||
socialStore,
|
||
trk,
|
||
)
|
||
ks.ONNX = a.onnxStatus()
|
||
ks.Scheduler = a.schedulerStatus()
|
||
ks.Residents = residentStatuses(a.Residents())
|
||
|
||
return ks
|
||
}
|
||
|
||
// onnxStatus 汇总统一多模态向量空间(ONNX 模型)的启用状态。
|
||
//
|
||
// 判据是 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」——
|
||
// 后者在模型缺失 / 运行时缺失时也为真,拿它当判据就是假绿。
|
||
func (a *Agent) onnxStatus() sdk.ONNXStatus {
|
||
st := sdk.ONNXStatus{Provider: a.embeddingProvider}
|
||
if a.multimodalSpace != nil && a.multimodalSpace.Loaded() {
|
||
st.Enabled = true
|
||
st.Dim = a.multimodalSpace.Dim()
|
||
st.Fingerprint = a.multimodalSpace.Fingerprint()
|
||
// 模态是可选能力:只有底层 provider 报出来时才带出。
|
||
if mr, ok := a.multimodalSpace.(interface{ Modalities() []string }); ok {
|
||
st.Modalities = mr.Modalities()
|
||
}
|
||
return st
|
||
}
|
||
switch {
|
||
case a.embeddingError != "":
|
||
st.Reason = "打开失败: " + a.embeddingError
|
||
case a.embeddingProvider == "":
|
||
st.Reason = "未配置统一向量空间 provider(走词嵌入/TF-IDF 回退路径)"
|
||
default:
|
||
st.Reason = "provider 未加载"
|
||
}
|
||
return st
|
||
}
|
||
|
||
var _ StatusProvider = (*Agent)(nil)
|
||
var _ sdk.StatusAPI = (*Agent)(nil)
|
||
|
||
// residentStatuses 把内核的驻留子视图映射成 SDK DTO。
|
||
//
|
||
// 刻意**不带** ResidentInfo.Table(那是每个驻留子的 inputch 登记明细):状态面
|
||
// 是给所有应用轮询的,把它塞进来会让每次 /status 都背上几十 KB。
|
||
// 只给表的大小(InputChTable),要看明细走专门的接口。
|
||
func residentStatuses(list []ResidentInfo) []ResidentStatus {
|
||
out := make([]ResidentStatus, 0, len(list))
|
||
for _, r := range list {
|
||
st := ResidentStatus{
|
||
ID: r.ID,
|
||
State: r.State,
|
||
Rounds: r.Rounds,
|
||
ContextFull: r.ContextFull,
|
||
InputChs: r.InputChs,
|
||
AllowedOutputs: r.AllowedOutputs,
|
||
InputChTable: r.TableSize,
|
||
}
|
||
if !r.CreatedAt.IsZero() {
|
||
st.CreatedAt = r.CreatedAt.Format(time.RFC3339)
|
||
}
|
||
out = append(out, st)
|
||
}
|
||
return out
|
||
}
|