mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
三个用户可见问题,逐个说明根因与改法。 1) 首页「一闪一闪」——运行态每 3s 轮询一次,renderRuntime 无条件重建 #rt-panel 的 innerHTML:数据没变也把整块 DOM(含各级条的 transition) 推倒重来。改法:缓存数据签名(**不含 uptime**——它每秒都变,带上等于没缓存), 签名相同直接 return,一个字节都不动。另:renderOverview 会整块重建 #rt-panel(面板本身是空的),所以那里必须让签名失效,否则空面板填不上。 2) 通道分配只显示内核/根 agent,看不见驻留子——根因是状态面只暴露了设备能力 (KernelStatus.Channels,来自 iom.ListChannels),而「这条输入归谁」是 ChannelRegistry 的属性(InputChannel.Owner/Capacity/Output),从未出过内核。 而登记表本来就是根 agent 与驻留子**共用同一份**,所以数据一直都在,只是没画。 改法:KernelStatus 新增 InputChannels(+ sdk.InputChannelInfo), /api/v1/runtime 带出 input_channels;前端把它按 owner 分进「归属容器」, 驻留子即使一条 inputch 都没划到也照样出现在图里(否则"子存在但看不见" 与"子不存在"无法区分),并显示其 allowed_outputs / 轮次 / 上下文满标记。 3) 「这些信息明明可以图形化」——四级中断的登记/抢占由纯文本改成并排迷你条; 通道分配用归属容器 + 容量滑块(轨道/填充/把手/读数),并把回程通道、 注册插件做成胶囊标签。设备能力拓扑(原有)保留。 验证:go build/vet 干净;go test ./internal/agent/... ./internal/plugin/... ./internal/sdk/... ./cmd/... 全绿;node --check dashboard.js 语法通过。 TestRuntimeEndpoint 扩展为同时钉住 input_channels 的归属与「驻留子划走的那条」。
323 lines
9.0 KiB
Go
323 lines
9.0 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
|
||
|
||
// InputChannelInfo 是 inputch 的登记与归属视图(见 internal/sdk/status.go)。
|
||
type InputChannelInfo = sdk.InputChannelInfo
|
||
|
||
// 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))
|
||
}
|
||
// inputch 登记与归属:与 Channels(设备能力)互补——那条回答「能做什么」,
|
||
// 这条回答「这条输入归谁」。登记表是根 agent 与驻留子共用的,所以驻留子
|
||
// 划走的 inputch(owner=子 id)也会出现在这里。
|
||
for _, ic := range iom.InputChannels() {
|
||
status.InputChannels = append(status.InputChannels, InputChannelInfo{
|
||
Name: ic.Name,
|
||
Plugin: ic.Plugin,
|
||
Owner: ic.Owner,
|
||
Capacity: ic.Capacity,
|
||
Output: ic.Output,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|