refactor: migrate built-in plugins to SDK-only interface

- Six-phase plan complete: webui/cli/healthcheck/pluginmgr/clawhubadapter
  now interact with the kernel exclusively via internal/sdk interfaces;
  all Configure() calls and package-level global injection removed
- buildSDK in internal/plugin/registry.go is the single assembly point
- Add internal/sdk/events.go exporting event types/constants
- Fix ProviderManager cooldown sharing: LuaAdaptedProvider.Name() now
  returns the source name instead of lua_<adapter>, so multiple sources
  sharing an adapter (single script load via shared VM AdapterCache) no
  longer share failure-cooldown state
- Verified: build/vet/tests green, deployed to homeagent.service with
  full plugin capability testing via local OpenAI-compatible mock
This commit is contained in:
root
2026-08-01 12:17:17 +08:00
parent 512777e27d
commit 57712f220b
47 changed files with 1515 additions and 1137 deletions

View File

@ -211,7 +211,7 @@ type LuaAdaptedProvider struct {
client *http.Client
}
func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAdaptedProvider {
func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, name, adapter string) *LuaAdaptedProvider {
if cfg.Temperature == 0 {
cfg.Temperature = 0.7
}
@ -219,7 +219,7 @@ func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAda
cfg.MaxTokens = 4096
}
return &LuaAdaptedProvider{
name: fmt.Sprintf("lua_%s", adapter),
name: name,
cfg: cfg,
vm: vm,
adapter: adapter,

View File

@ -28,7 +28,7 @@ func TestQuickChatWithRealKey(t *testing.T) {
Model: "deepseek-v4-flash",
BaseURL: "https://api.deepseek.com",
APIKey: apiKey,
}, vm, "deepseek"))
}, vm, "deepseek", "deepseek"))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

View File

@ -21,43 +21,20 @@ type StatusProvider interface {
GetKernelStatus() *KernelStatus
}
// KernelStatus 内核各子系统运行状态的聚合快照。
type KernelStatus struct {
Uptime string `json:"uptime"`
StartTime string `json:"start_time"`
AgentID string `json:"agent_id"`
Plugins []PluginInfo `json:"plugins"`
Tools []sdk.ToolDef `json:"tools"`
Channels []ChannelInfo `json:"channels"`
Memory MemoryStatus `json:"memory"`
Knowledge KnowledgeStatus `json:"knowledge"`
Documents DocumentStatus `json:"documents"`
TextMemory TextMemoryStatus `json:"text_memory"`
Social SocialStatus `json:"social"`
Skills SkillsStatus `json:"skills"`
LLM LLMStatus `json:"llm"`
Context ContextStatus `json:"context"`
Runtime RuntimeStatus `json:"runtime"`
Tracker TrackerStatus `json:"tracker"`
}
type PluginInfo struct {
Name string `json:"name"`
Loaded bool `json:"loaded"`
}
type ChannelInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Ready bool `json:"ready"`
}
// 状态 DTO 使用内置 SDK 的中立类型,保证与插件层解耦。
type KernelStatus = sdk.KernelStatus
type PluginInfo = sdk.PluginInfo
type ChannelInfo = sdk.ChannelInfo
type MemoryStatus = sdk.MemoryStatus
type KnowledgeStatus = sdk.KnowledgeStatus
type DocumentStatus = sdk.DocumentStatus
type TextMemoryStatus = sdk.TextMemoryStatus
type SocialStatus = sdk.SocialStatus
type SkillsStatus = sdk.SkillsStatus
type LLMStatus = sdk.LLMStatus
type ContextStatus = sdk.ContextStatus
type RuntimeStatus = sdk.RuntimeStatus
type TrackerStatus = sdk.TrackerStatus
func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
return ChannelInfo{
@ -67,61 +44,6 @@ func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
}
}
type MemoryStatus struct {
Available bool `json:"available"`
EntityCount int `json:"entity_count"`
RelationCount int `json:"relation_count"`
EntityTypes int `json:"entity_types"`
}
type KnowledgeStatus struct {
Available bool `json:"available"`
ItemCount int `json:"item_count"`
Items []string `json:"items,omitempty"`
}
type DocumentStatus struct {
Available bool `json:"available"`
DocCount int `json:"doc_count"`
VectorCount int `json:"vector_count"`
}
type TextMemoryStatus struct {
Available bool `json:"available"`
FileCount int `json:"file_count"`
}
type SocialStatus struct {
Available bool `json:"available"`
PersonCount int `json:"person_count"`
}
type SkillsStatus struct {
Available bool `json:"available"`
SkillList []string `json:"skill_list,omitempty"`
}
type LLMStatus struct {
Available bool `json:"available"`
Provider string `json:"provider,omitempty"`
Sources int `json:"sources,omitempty"`
}
type ContextStatus struct {
EventCount int `json:"event_count,omitempty"`
}
type RuntimeStatus struct {
Goroutines int `json:"goroutines"`
MemoryMB int64 `json:"memory_mb"`
GoVersion string `json:"go_version"`
}
type TrackerStatus struct {
Available bool `json:"available"`
Dir string `json:"dir,omitempty"`
}
// collectKernelStatus 聚合内核各子系统状态快照。
// 接收所有子系统引用(均为可选——nil 表示不可用),返回统一的状态报告。
func collectKernelStatus(
@ -300,3 +222,4 @@ func (a *Agent) GetKernelStatus() *KernelStatus {
}
var _ StatusProvider = (*Agent)(nil)
var _ sdk.StatusAPI = (*Agent)(nil)

View File

@ -18,21 +18,23 @@ import (
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
// PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta。
type PluginMeta struct {
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
}
// 与内置 SDK 中的 PluginMeta 保持一致,供 PluginManager 接口返回。
type PluginMeta = sdk.PluginMeta
var globalPluginMeta sync.Map // name -> PluginMeta
// RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。
func RegisterPluginMeta(name, nameZh, nameEn string) {
globalPluginMeta.Store(name, PluginMeta{NameZh: nameZh, NameEn: nameEn})
globalPluginMeta.Store(name, sdk.PluginMeta{NameZh: nameZh, NameEn: nameEn})
}
// GetPluginMeta 查询插件的显示名称。
@ -76,6 +78,8 @@ type Registry struct {
mgr *agentAPI.ProviderManager
cfgReg *internalConfig.ConfigRegistry
plgDir string
lua *luaVM.VM
baseKey string
regTool sdk.ToolRegistrar
regStage sdk.StageRegistrar
@ -83,6 +87,14 @@ type Registry struct {
toolCleaner PluginToolCleaner
status sdk.StatusAPI
sup sdk.SupervisorAPI
skMgr *skill.Manager
trk *tracker.Tracker
cfg *types.Config
stageHost sdk.ToolSource
idx *memory.Indexer
knownDisabled map[string]bool
}
@ -104,10 +116,19 @@ func (r *Registry) SetKnowledge(ks *knowledge.Store) { r.ks = ks }
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm }
func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key }
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
func (r *Registry) SetStageRegistrar(fn sdk.StageRegistrar) { r.regStage = fn }
func (r *Registry) SetAPIRegistrar(fn sdk.APIRegistrar) { r.regAPI = fn }
func (r *Registry) SetToolCleaner(tc PluginToolCleaner) { r.toolCleaner = tc }
func (r *Registry) SetStatusProvider(sp sdk.StatusAPI) { r.status = sp }
func (r *Registry) SetSupervisor(sup sdk.SupervisorAPI) { r.sup = sup }
func (r *Registry) SetSkillManager(skMgr *skill.Manager) { r.skMgr = skMgr }
func (r *Registry) SetTracker(trk *tracker.Tracker) { r.trk = trk }
func (r *Registry) SetConfig(cfg *types.Config) { r.cfg = cfg }
func (r *Registry) SetStageHost(sh sdk.ToolSource) { r.stageHost = sh }
func (r *Registry) SetIndexer(idx *memory.Indexer) { r.idx = idx }
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
r.mu.Lock()
@ -182,7 +203,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
TextMemory: sdk.NewTextMemory(r.textMem),
DocMemory: sdk.NewDocMemory(r.docStore),
Knowledge: sdk.NewKnowledge(r.ks),
LLM: sdk.NewLLM(r.mgr),
LLM: sdk.NewLLM(r.mgr, r.cfgReg, r.lua, r.baseKey),
Settings: sett,
RegTool: regTool,
RegStage: regStage,
@ -190,6 +211,15 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
RegOutput: regOutput,
RegInput: regInput,
PluginMgr: r,
Status: r.status,
Supervisor: r.sup,
Skill: r.skMgr,
Adapter: sdk.NewAdapter(r.lua),
Tracker: r.trk,
Config: sdk.NewConfig(r.cfg),
Tool: sdk.NewTool(r.stageHost, r.iom),
Indexer: sdk.NewIndexer(r.idx),
})
}
@ -578,15 +608,19 @@ func (r *Registry) ListKnown() []string {
return list
}
func (r *Registry) PluginMetas() map[string]PluginMeta {
metas := make(map[string]PluginMeta)
func (r *Registry) PluginMetas() map[string]sdk.PluginMeta {
metas := make(map[string]sdk.PluginMeta)
globalPluginMeta.Range(func(key, val interface{}) bool {
metas[key.(string)] = val.(PluginMeta)
metas[key.(string)] = val.(sdk.PluginMeta)
return true
})
return metas
}
func (r *Registry) PluginDir() string {
return r.plgDir
}
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
// 尝试顺序:.so (Go plugin on Linux) → .dll (Windows) → .lua (跨平台)
for _, try := range []struct {

View File

@ -31,7 +31,12 @@ func (tc *toolCapture) RegisterAPI(name string) error {
func setupPlugin() (*Plugin, *toolCapture, error) {
p := New("agentcli")
tc := newToolCapture()
sdk := sdk.New("agentcli", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
sdk := sdk.New("agentcli", sdk.SDKConfig{
RegTool: tc.RegisterTool,
RegStage: tc.RegisterStage,
RegAPI: tc.RegisterAPI,
Settings: sdk.NewSettings("agentcli", nil),
})
if err := p.Start(sdk); err != nil {
return nil, nil, err
}

View File

@ -31,18 +31,11 @@ var pySimulatorSrc string
//go:embed simulator/openclaw_cli.js
var openclawCliSrc string
var SkillsDir string
var SimulatorDir string
func init() {
plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter")
plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
dir := SkillsDir
if dir == "" {
dataDir, ok := config["data_dir"].(string)
if !ok {
return nil, fmt.Errorf("clawhubadapter plugin: config missing 'data_dir' or not a string")
}
dir := ""
if dataDir, ok := config["data_dir"].(string); ok && dataDir != "" {
dir = filepath.Join(dataDir, "skills")
}
return New(name, dir), nil
@ -63,8 +56,8 @@ type Plugin struct {
}
func New(name, skillsDir string) *Plugin {
sd := SimulatorDir
if sd == "" {
sd := ""
if skillsDir != "" {
sd = filepath.Join(skillsDir, ".simulator")
}
return &Plugin{
@ -100,6 +93,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.simulatorDir = s
}
}
// 默认目录:内核 data_dir 下 skills 目录(与配置 core.daemon.data_dir 对齐)
if p.skillsDir == "" {
if v, _ := s.Settings().GetCore("daemon.data_dir"); v != nil {
if dir, ok := v.(string); ok && dir != "" {
p.skillsDir = filepath.Join(dir, "skills")
}
}
}
if p.skillsDir == "" {
p.skillsDir = filepath.Join("data", "skills")
}
if p.simulatorDir == "" {
p.simulatorDir = filepath.Join(p.skillsDir, ".simulator")
}
// Launch OC plugin manager first (handles OC-format plugin installation and lifecycle)
os.MkdirAll(p.skillsDir, 0755)
@ -283,7 +290,7 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error {
// Ensure skills dir exists for the manager to scan
os.MkdirAll(p.skillsDir, 0755)
sp, err := launchProcess("node", managerPath, p.skillsDir, "manager")
sp, err := launchProcess("node", managerPath, p.skillsDir, "manager", p.simulatorDir)
if err != nil {
return fmt.Errorf("launch manager: %w", err)
}
@ -775,7 +782,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
}
}
sp, err := launchProcess(pythonBin, simPath, dir, name)
sp, err := launchProcess(pythonBin, simPath, dir, name, p.simulatorDir)
if err != nil {
return fmt.Errorf("launch pysimulator: %w", err)
}
@ -808,7 +815,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
}
func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error {
sp, err := launchSidecar(dir, name)
sp, err := launchSidecar(dir, name, p.simulatorDir)
if err != nil {
return fmt.Errorf("launch: %w", err)
}

View File

@ -143,15 +143,15 @@ func (s *sidecarProcess) NotifyChan() <-chan OCNotification {
return s.notifyCh
}
func launchSidecar(dir, name string) (*sidecarProcess, error) {
func launchSidecar(dir, name, simDir string) (*sidecarProcess, error) {
mainJS := filepath.Join(dir, "main.js")
if _, err := os.Stat(mainJS); os.IsNotExist(err) {
return nil, nil
}
return launchProcess("node", mainJS, dir, name)
return launchProcess("node", mainJS, dir, name, simDir)
}
func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
func launchProcess(bin, arg, dir, name, simDir string) (*sidecarProcess, error) {
nodePath := bin
if bin == "node" {
if p := os.Getenv("NODE_PATH"); p != "" {
@ -164,8 +164,8 @@ func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
cmd.Stderr = os.Stderr
// Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command
if SimulatorDir != "" {
binDir := filepath.Join(SimulatorDir, "bin")
if simDir != "" {
binDir := filepath.Join(simDir, "bin")
if info, err := os.Stat(binDir); err == nil && info.IsDir() {
env := os.Environ()
binDirPath := binDir + string(os.PathListSeparator)

View File

@ -26,10 +26,12 @@ func (m *mockSettings) RegisterDef(def pubsdk.ConfigDef) {}
func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil }
func (m *mockSettings) Dump() map[string]interface{} { return nil }
func (m *mockSettings) Plugins() []string { return nil }
func (m *mockSettings) DefsCore(prefix string) []*sdk.ConfigDef { return nil }
func (m *mockSettings) DefsPlugin(plugin, prefix string) []*sdk.ConfigDef { return nil }
func TestLaunchSidecarNoMainJS(t *testing.T) {
tmpDir := t.TempDir()
sp, err := launchSidecar(tmpDir, "nonexistent")
sp, err := launchSidecar(tmpDir, "nonexistent", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -50,7 +52,7 @@ func TestLaunchSidecarAndListTools(t *testing.T) {
t.Fatalf("write test plugin: %v", err)
}
sp, err := launchSidecar(tmpDir, "echoplugin")
sp, err := launchSidecar(tmpDir, "echoplugin", "")
if err != nil {
t.Fatalf("launch sidecar: %v", err)
}
@ -92,7 +94,7 @@ func TestCallEchoTool(t *testing.T) {
t.Fatalf("write test plugin: %v", err)
}
sp, err := launchSidecar(tmpDir, "echoplugin")
sp, err := launchSidecar(tmpDir, "echoplugin", "")
if err != nil {
t.Fatalf("launch sidecar: %v", err)
}
@ -124,7 +126,7 @@ func TestCallAddTool(t *testing.T) {
t.Fatalf("write test plugin: %v", err)
}
sp, err := launchSidecar(tmpDir, "echoplugin")
sp, err := launchSidecar(tmpDir, "echoplugin", "")
if err != nil {
t.Fatalf("launch sidecar: %v", err)
}
@ -157,7 +159,7 @@ func TestCallNonexistentTool(t *testing.T) {
t.Fatalf("write test plugin: %v", err)
}
sp, err := launchSidecar(tmpDir, "echoplugin")
sp, err := launchSidecar(tmpDir, "echoplugin", "")
if err != nil {
t.Fatalf("launch sidecar: %v", err)
}
@ -182,7 +184,7 @@ func TestConcurrentCalls(t *testing.T) {
t.Fatalf("write test plugin: %v", err)
}
sp, err := launchSidecar(tmpDir, "echoplugin")
sp, err := launchSidecar(tmpDir, "echoplugin", "")
if err != nil {
t.Fatalf("launch sidecar: %v", err)
}
@ -217,7 +219,7 @@ func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess {
if err != nil {
t.Fatalf("abs simulator path: %v", err)
}
sp, err := launchProcess("node", simPath, pluginDir, name)
sp, err := launchProcess("node", simPath, pluginDir, name, "")
if err != nil {
t.Fatalf("launch simulator for %s: %v", name, err)
}
@ -338,8 +340,6 @@ func TestLoadOCPluginViaPluginStart(t *testing.T) {
}
}
SimulatorDir = filepath.Join(t.TempDir(), ".simulator")
p := New("openclaw", skillsDir)
var registeredTools []string

View File

@ -12,8 +12,6 @@ import (
"strings"
"sync"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
@ -21,22 +19,6 @@ import (
// DefaultSocket 由 main.go 在 Load() 前设置,覆盖默认 socket 路径。
var DefaultSocket string
// 以下通过 Configure() 注入内核依赖
var (
pluginReg *plugin.Registry
cfgReg *internalConfig.ConfigRegistry
statusProv agentCore.StatusProvider
pluginDir string
)
// Configure 由 main.go 在 Load() 前调用,注入内核依赖供结构化命令使用。
func Configure(pr *plugin.Registry, cr *internalConfig.ConfigRegistry, sp agentCore.StatusProvider, pDir string) {
pluginReg = pr
cfgReg = cr
statusProv = sp
pluginDir = pDir
}
func init() {
plugin.RegisterPluginMeta("cli", "CLI", "CLI")
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
@ -189,18 +171,16 @@ func (p *Plugin) cliAPIKey(s *sdk.PluginSDK) string {
}
}
}
return p.webuiAPIKey()
return p.webuiAPIKey(s)
}
func (p *Plugin) webuiAPIKey() string {
if cfgReg == nil {
func (p *Plugin) webuiAPIKey(s *sdk.PluginSDK) string {
if s == nil {
return ""
}
ps := cfgReg.PluginConfig("webui")
if v, _ := ps.Get("api_key"); v != nil {
if s, ok := v.(string); ok {
return s
}
v, _ := s.Settings().GetPlugin("webui", "api_key")
if k, ok := v.(string); ok {
return k
}
return ""
}
@ -215,11 +195,11 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
case "/help":
p.cmdHelp(conn)
case "/status":
p.cmdStatus(conn)
p.cmdStatus(conn, s)
case "/kernel":
p.cmdKernel(conn)
p.cmdKernel(conn, s)
case "/settings":
p.cmdSettings(conn, parts)
p.cmdSettings(conn, parts, s)
case "/plugin":
p.cmdPlugin(conn, parts, s)
case "/memory":
@ -227,7 +207,7 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
case "/knowledge":
p.cmdKnowledge(conn, s)
case "/agents":
p.cmdAgents(conn)
p.cmdAgents(conn, s)
default:
return false
}
@ -260,12 +240,13 @@ func (p *Plugin) cmdHelp(conn net.Conn) {
// ======== /status ========
func (p *Plugin) cmdStatus(conn net.Conn) {
if statusProv == nil {
func (p *Plugin) cmdStatus(conn net.Conn, s *sdk.PluginSDK) {
st := s.Status()
if st == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
return
}
ks := statusProv.GetKernelStatus()
ks := st.GetKernelStatus()
llmStatus := "不可用"
if ks.LLM.Available {
@ -294,19 +275,21 @@ func (p *Plugin) cmdStatus(conn net.Conn) {
// ======== /kernel ========
func (p *Plugin) cmdKernel(conn net.Conn) {
if statusProv == nil {
func (p *Plugin) cmdKernel(conn net.Conn, s *sdk.PluginSDK) {
st := s.Status()
if st == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
return
}
data, _ := json.MarshalIndent(statusProv.GetKernelStatus(), "", " ")
data, _ := json.MarshalIndent(st.GetKernelStatus(), "", " ")
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
}
// ======== /settings ========
func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
if cfgReg == nil {
func (p *Plugin) cmdSettings(conn net.Conn, parts []string, s *sdk.PluginSDK) {
sett := s.Settings()
if sett == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "config registry not available"})
return
}
@ -318,7 +301,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
}
key := parts[2]
val := strings.Join(parts[3:], " ")
if err := cfgReg.Set(key, val); err != nil {
if err := sett.SetCore(key, val); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
@ -330,7 +313,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
if len(parts) >= 2 {
prefix = parts[1]
}
keys := cfgReg.List(prefix)
keys, _ := sett.ListCore(prefix)
sort.Strings(keys)
if len(keys) == 0 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "无匹配配置项"})
@ -338,7 +321,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
}
var lines []string
for _, k := range keys {
v, _ := cfgReg.Get(k)
v, _ := sett.GetCore(k)
lines = append(lines, fmt.Sprintf(" %s = %v", k, v))
}
writeLine(conn, map[string]interface{}{
@ -409,20 +392,22 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin remove <name>"})
return
}
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
name := parts[2]
if pluginDir == "" {
dir := pmgr.PluginDir()
if dir == "" {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin dir not configured"})
return
}
dir := filepath.Join(pluginDir, name)
if err := os.RemoveAll(dir); err != nil {
if err := os.RemoveAll(filepath.Join(dir, name)); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
// 同步清理禁用表
if pmgr != nil {
pmgr.EnablePlugin(name)
}
_ = pmgr.EnablePlugin(name)
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)})
case "info":
@ -430,20 +415,33 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin info <name>"})
return
}
if pluginReg == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
plg := pluginReg.Get(parts[2])
if plg == nil {
if pmgr != nil && pmgr.IsPluginDisabled(parts[2]) {
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", parts[2])})
return
name := parts[2]
metas := pmgr.PluginMetas()
meta, hasMeta := metas[name]
loaded := false
for _, n := range pmgr.ListLoadedPlugins() {
if n == name {
loaded = true
break
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", parts[2])})
}
if loaded {
display := name
if hasMeta && meta.NameZh != "" {
display = meta.NameZh
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s (%s)\n状态: 已加载", display, name)})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s\n状态: 已加载", plg.Name())})
if pmgr.IsPluginDisabled(name) {
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", name)})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", name)})
case "disable":
if len(parts) < 3 {
@ -541,12 +539,13 @@ func (p *Plugin) cmdKnowledge(conn net.Conn, s *sdk.PluginSDK) {
// ======== /agents ========
func (p *Plugin) cmdAgents(conn net.Conn) {
if statusProv == nil {
func (p *Plugin) cmdAgents(conn net.Conn, s *sdk.PluginSDK) {
st := s.Status()
if st == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
return
}
ks := statusProv.GetKernelStatus()
ks := st.GetKernelStatus()
data, _ := json.MarshalIndent(map[string]string{"agent_id": ks.AgentID}, "", " ")
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
}

View File

@ -34,7 +34,12 @@ func (tc *toolCapture) RegisterAPI(name string) error { return nil }
func setupPlugin() (*Plugin, *toolCapture, error) {
p := New("cmd")
tc := newToolCapture()
sdk := sdk.New("cmd", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
sdk := sdk.New("cmd", sdk.SDKConfig{
RegTool: tc.RegisterTool,
RegStage: tc.RegisterStage,
RegAPI: tc.RegisterAPI,
Settings: sdk.NewSettings("cmd", nil),
})
if err := p.Start(sdk); err != nil {
return nil, nil, err
}

View File

@ -9,27 +9,10 @@ import (
"sync"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
var (
hcStageHost *agentCore.StageHost
hcIOMgr *agentIO.IOManager
hcPluginReg *plugin.Registry
hcMemory *memory.GraphDB
hcKnowledge *knowledge.Store
hcDocStore *doc.Store
hcProviderMgr *agentAPI.ProviderManager
hcStatusProvider agentCore.StatusProvider
)
type toolInfo struct {
Name string `json:"name"`
Source string `json:"source"`
@ -50,24 +33,9 @@ type llmReport struct {
Detail string `json:"detail,omitempty"`
}
func Configure(sh *agentCore.StageHost, iom *agentIO.IOManager, pr *plugin.Registry,
mem *memory.GraphDB, ks *knowledge.Store, ds *doc.Store, pm *agentAPI.ProviderManager, sp agentCore.StatusProvider) {
hcStageHost = sh
hcIOMgr = iom
hcPluginReg = pr
hcMemory = mem
hcKnowledge = ks
hcDocStore = ds
hcProviderMgr = pm
hcStatusProvider = sp
}
func init() {
plugin.RegisterPluginMeta("healthcheck", "健康检查", "Health Check")
plugin.RegisterFactory("healthcheck", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
if hcStageHost == nil {
return nil, nil
}
return New(name), nil
})
}
@ -215,7 +183,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
"properties": map[string]interface{}{},
},
}, func(args map[string]interface{}) (interface{}, error) {
return p.listAllTools()
return p.listAllTools(s)
})
p.selfToolNames["healthcheck_memory"] = true
@ -227,7 +195,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
"properties": map[string]interface{}{},
},
}, func(args map[string]interface{}) (interface{}, error) {
return p.checkMemory()
return p.checkMemory(s)
})
p.selfToolNames["healthcheck_report"] = true
@ -255,7 +223,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
return map[string]interface{}{"ok": true, "received": count}, nil
})
if hcStatusProvider != nil {
if s.Status() != nil {
p.selfToolNames["healthcheck_kernel"] = true
s.RegisterTool("healthcheck_kernel", sdk.ToolDef{
Name: "healthcheck_kernel",
@ -265,7 +233,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
"properties": map[string]interface{}{},
},
}, func(args map[string]interface{}) (interface{}, error) {
return hcStatusProvider.GetKernelStatus(), nil
return s.Status().GetKernelStatus(), nil
})
}
@ -300,9 +268,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.startAutoCheck(s, p.autoInterval)
}
log.Printf("[healthcheck] ready (stageHost=%v iom=%v reg=%v mem=%v ks=%v ds=%v pm=%v sp=%v)",
hcStageHost != nil, hcIOMgr != nil, hcPluginReg != nil,
hcMemory != nil, hcKnowledge != nil, hcDocStore != nil, hcProviderMgr != nil, hcStatusProvider != nil)
log.Printf("[healthcheck] ready (tool=%v mem=%v ks=%v ds=%v llm=%v plugins=%v status=%v)",
s.Tool() != nil, s.Memory() != nil, s.Knowledge() != nil,
s.DocMemory() != nil, s.LLM() != nil, s.PluginMgr() != nil, s.Status() != nil)
return nil
}
@ -367,36 +335,32 @@ func (p *Plugin) runAutoCheck(s *sdk.PluginSDK) {
func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) {
results := []checkResult{}
pluginResult := p.checkPluginsRaw()
pluginResult := p.checkPluginsRaw(s)
results = append(results, pluginResult...)
toolResult := p.checkToolsRaw()
toolResult := p.checkToolsRaw(s)
results = append(results, toolResult...)
if hcMemory != nil {
r := p.testMemoryRaw()
results = append(results, r)
if s.Memory() != nil {
results = append(results, p.testMemoryRaw(s))
} else {
results = append(results, checkResult{Name: "memory", Status: "skip", Detail: "图记忆未初始化", Pass: true})
}
if hcKnowledge != nil {
r := p.testKnowledgeRaw()
results = append(results, r)
if s.Knowledge() != nil {
results = append(results, p.testKnowledgeRaw(s))
} else {
results = append(results, checkResult{Name: "knowledge", Status: "skip", Detail: "知识库未初始化", Pass: true})
}
if hcDocStore != nil {
r := p.testDocStoreRaw()
results = append(results, r)
if s.DocMemory() != nil {
results = append(results, p.testDocStoreRaw(s))
} else {
results = append(results, checkResult{Name: "documents", Status: "skip", Detail: "文档记忆未初始化", Pass: true})
}
if hcProviderMgr != nil {
r := p.testLLMDriven()
results = append(results, r)
if s.LLM() != nil {
results = append(results, p.testLLMDriven(s))
} else {
results = append(results, checkResult{Name: "llm_discovery", Status: "skip", Detail: "LLM Provider 未初始化", Pass: true})
}
@ -424,7 +388,7 @@ func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) {
}
func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) {
results := p.checkPluginsRaw()
results := p.checkPluginsRaw(s)
return map[string]interface{}{
"status": "ok",
"plugins": results,
@ -432,12 +396,12 @@ func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) {
}, nil
}
func (p *Plugin) checkPluginsRaw() []checkResult {
if hcPluginReg == nil {
func (p *Plugin) checkPluginsRaw(s *sdk.PluginSDK) []checkResult {
if s.PluginMgr() == nil {
return []checkResult{{Name: "plugins", Status: "skip", Detail: "插件注册表未初始化", Pass: true}}
}
names := hcPluginReg.List()
names := s.PluginMgr().ListLoadedPlugins()
if names == nil {
names = []string{}
}
@ -449,8 +413,8 @@ func (p *Plugin) checkPluginsRaw() []checkResult {
}}
}
func (p *Plugin) listAllTools() (interface{}, error) {
tools := p.collectAllTools()
func (p *Plugin) listAllTools(s *sdk.PluginSDK) (interface{}, error) {
tools := p.collectAllTools(s)
return map[string]interface{}{
"status": "ok",
"count": len(tools),
@ -458,8 +422,8 @@ func (p *Plugin) listAllTools() (interface{}, error) {
}, nil
}
func (p *Plugin) checkToolsRaw() []checkResult {
tools := p.collectAllTools()
func (p *Plugin) checkToolsRaw(s *sdk.PluginSDK) []checkResult {
tools := p.collectAllTools(s)
return []checkResult{{
Name: "tools",
Status: "ok",
@ -468,7 +432,7 @@ func (p *Plugin) checkToolsRaw() []checkResult {
}}
}
func (p *Plugin) collectAllTools() []toolInfo {
func (p *Plugin) collectAllTools(s *sdk.PluginSDK) []toolInfo {
seen := map[string]bool{}
var tools []toolInfo
@ -480,14 +444,12 @@ func (p *Plugin) collectAllTools() []toolInfo {
tools = append(tools, toolInfo{Name: name, Source: source, Description: desc})
}
if hcStageHost != nil {
for _, def := range hcStageHost.GetToolDefs() {
if s.Tool() != nil {
for _, def := range s.Tool().GetToolDefs() {
addTool(def.Name, "plugin", def.Description)
}
}
if hcIOMgr != nil {
for _, def := range hcIOMgr.GetAllTools() {
for _, def := range s.Tool().GetAllTools() {
addTool(def.Name, "device", def.Description)
}
}
@ -495,23 +457,18 @@ func (p *Plugin) collectAllTools() []toolInfo {
return tools
}
func (p *Plugin) testMemoryRaw() checkResult {
func (p *Plugin) testMemoryRaw(s *sdk.PluginSDK) checkResult {
marker := fmt.Sprintf("_hc_%d", time.Now().UnixNano())
triples := []memory.Triple{
triples := []sdk.Triple{
{Subject: marker, Relation: "is", Object: "healthcheck_test", SubjectType: "System", ObjectType: "Flag"},
}
start := time.Now()
ec, rc, err := hcMemory.Commit(triples, "healthcheck", 0)
if err != nil {
if err := s.Memory().Commit(triples); err != nil {
return checkResult{Name: "memory_write", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
}
if _, _, err := hcMemory.Commit(triples, "healthcheck_cleanup", 0); err != nil {
log.Printf("[healthcheck] memory cleanup error: %v", err)
}
n, err := hcMemory.Purge(map[string]string{"subject_contains": marker}, "hard")
n, err := s.Memory().Purge(map[string]string{"subject_contains": marker}, "hard")
if err != nil {
return checkResult{Name: "memory_purge", Status: "fail", Detail: fmt.Sprintf("清理失败: %v", err), Pass: false}
}
@ -520,25 +477,29 @@ func (p *Plugin) testMemoryRaw() checkResult {
return checkResult{
Name: "memory",
Status: "ok",
Detail: fmt.Sprintf("写入 %d 实体/%d 关系, 清理 %d 条, 耗时 %v", ec, rc, n, elapsed.Round(time.Millisecond)),
Detail: fmt.Sprintf("写入+清理 %d 条, 耗时 %v", n, elapsed.Round(time.Millisecond)),
Pass: true,
}
}
func (p *Plugin) testKnowledgeRaw() checkResult {
func (p *Plugin) testKnowledgeRaw(s *sdk.PluginSDK) checkResult {
marker := fmt.Sprintf("_hc_knowledge_test_%d", time.Now().UnixNano())
start := time.Now()
if err := hcKnowledge.Add(marker, "健康检查测试标记,可忽略"); err != nil {
if err := s.Knowledge().Add(marker, "健康检查测试标记,可忽略"); err != nil {
return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
}
results := hcKnowledge.Search("健康检查测试标记", 3)
results, err := s.Knowledge().Search("健康检查测试标记", 3)
if err != nil {
s.Knowledge().Remove(marker)
return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("查询失败: %v", err), Pass: false}
}
elapsed := time.Since(start)
// 清理测试条目,避免积累
hcKnowledge.Remove(marker)
s.Knowledge().Remove(marker)
if len(results) > 0 {
return checkResult{
@ -557,20 +518,21 @@ func (p *Plugin) testKnowledgeRaw() checkResult {
}
}
func (p *Plugin) testDocStoreRaw() checkResult {
func (p *Plugin) testDocStoreRaw(s *sdk.PluginSDK) checkResult {
start := time.Now()
doc := &doc.Doc{
Summary: "健康检查测试文档",
doc := &sdk.Doc{
Title: fmt.Sprintf("健康检查测试文档 %d", time.Now().UnixNano()),
Content: "这是一条由 healthcheck 插件创建的测试文档,用于验证文档记忆系统是否正常工作。",
Tags: []string{"healthcheck", "test"},
Source: "healthcheck",
}
if err := hcDocStore.Insert(doc); err != nil {
if err := s.DocMemory().Insert(doc); err != nil {
return checkResult{Name: "documents", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
}
if doc.ID != "" {
hcDocStore.Remove(doc.ID)
// 清理测试文档,避免积累(SDK Insert 不回填 ID,经 Query 按标题定位)
for _, d := range s.DocMemory().Query("健康检查测试文档", 10) {
if d.ID != "" && strings.HasPrefix(d.Title, "健康检查测试文档") {
s.DocMemory().Remove(d.ID)
}
}
elapsed := time.Since(start)
@ -582,9 +544,9 @@ func (p *Plugin) testDocStoreRaw() checkResult {
}
}
func (p *Plugin) testLLMDriven() checkResult {
provider := hcProviderMgr.Default()
if provider == nil {
func (p *Plugin) testLLMDriven(s *sdk.PluginSDK) checkResult {
llmName := s.LLM().CurrentSource()
if llmName == "" {
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "无可用 LLM Provider", Pass: true}
}
@ -593,7 +555,7 @@ func (p *Plugin) testLLMDriven() checkResult {
defer cancel()
// 收集所有工具定义(排除健康检查自身的工具以避免循环测试)
toolDefs := p.collectToolDefsForLLM()
toolDefs := p.collectToolDefsForLLM(s)
if len(toolDefs) == 0 {
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "没有可测试的工具", Pass: true}
@ -608,15 +570,14 @@ func (p *Plugin) testLLMDriven() checkResult {
// 构建 prompt
prompt := p.buildDiscoveryPrompt(toolDefs)
msgs := []agentAPI.Message{{Role: "user", Content: prompt}}
msgs := []sdk.LLMMessage{{Role: "user", Content: prompt}}
tools := convertToolDefs(toolDefs)
llmName := provider.Name()
turnCount := 0
toolCallCount := 0
for turn := 0; turn < p.llmMaxTurns; turn++ {
resp, err := provider.Chat(ctx, &agentAPI.CompletionRequest{
resp, err := s.LLM().Chat(ctx, &sdk.LLMCompletionRequest{
Messages: msgs,
MaxTokens: p.llmMaxTokens,
Tools: tools,
@ -638,12 +599,12 @@ func (p *Plugin) testLLMDriven() checkResult {
break
}
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls})
msgs = append(msgs, sdk.LLMMessage{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls})
for _, tc := range resp.ToolCalls {
toolCallCount++
content := p.executeToolForLLM(tc)
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: content})
content := p.executeToolForLLM(s, tc)
msgs = append(msgs, sdk.LLMMessage{Role: "tool", ToolCallID: tc.ID, Content: content})
}
}
@ -666,7 +627,7 @@ func (p *Plugin) testLLMDriven() checkResult {
// collectToolDefsForLLM 收集全部已注册的工具定义供 LLM 发现和测试。
// 动态排除本插件自身注册的工具(通过 selfToolNames),避免 LLM 自我循环调用。
func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef {
func (p *Plugin) collectToolDefsForLLM(s *sdk.PluginSDK) []sdk.ToolDef {
seen := map[string]bool{}
var defs []sdk.ToolDef
@ -678,14 +639,12 @@ func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef {
defs = append(defs, d)
}
if hcStageHost != nil {
for _, d := range hcStageHost.GetToolDefs() {
if s.Tool() != nil {
for _, d := range s.Tool().GetToolDefs() {
addDef(d)
}
}
if hcIOMgr != nil {
for _, d := range hcIOMgr.GetAllTools() {
addDef(sdk.ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters})
for _, d := range s.Tool().GetAllTools() {
addDef(d)
}
}
@ -715,10 +674,10 @@ func (p *Plugin) buildDiscoveryPrompt(toolDefs []sdk.ToolDef) string {
}
// executeToolForLLM 在 LLM 工具循环中执行工具调用。
// healthcheck_report 通过 StageHost 路由到自身注册的 handler,负责收集 LLM 上报。
func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string {
if hcStageHost != nil {
result, err := hcStageHost.ExecuteTool(tc.Name, tc.Arguments)
// healthcheck_report 经 SDK ToolAPI 路由到自身注册的 handler,负责收集 LLM 上报。
func (p *Plugin) executeToolForLLM(s *sdk.PluginSDK, tc sdk.LLMToolCall) string {
if s.Tool() != nil {
result, err := s.Tool().ExecuteTool(tc.Name, tc.Arguments)
if err != nil {
return fmt.Sprintf("调用工具 %s 失败: %v", tc.Name, err)
}
@ -726,7 +685,7 @@ func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string {
return string(data)
}
return fmt.Sprintf("工具 %s 不可执行(StageHost 未初始化)", tc.Name)
return fmt.Sprintf("工具 %s 不可执行(工具注册表未初始化)", tc.Name)
}
func convertToolDefs(defs []sdk.ToolDef) []interface{} {
@ -744,11 +703,11 @@ func convertToolDefs(defs []sdk.ToolDef) []interface{} {
return tools
}
func (p *Plugin) checkMemory() (interface{}, error) {
if hcMemory == nil {
func (p *Plugin) checkMemory(s *sdk.PluginSDK) (interface{}, error) {
if s.Memory() == nil {
return map[string]interface{}{"status": "skip", "pass": true, "detail": "图记忆未初始化"}, nil
}
r := p.testMemoryRaw()
r := p.testMemoryRaw(s)
c := map[string]interface{}{
"status": r.Status,
"pass": r.Pass,

View File

@ -10,7 +10,6 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
@ -34,16 +33,28 @@ func (tc *toolCapture) RegisterTool(name string, def sdk.ToolDef, handler sdk.To
func (tc *toolCapture) RegisterStage(stage sdk.Stage, handler sdk.StageHandler) {}
func (tc *toolCapture) RegisterAPI(name string) error { return nil }
func setupPlugin() (*Plugin, *toolCapture, error) {
sh := agentCore.NewStageHost()
iom := agentIO.NewIOManager()
pr := plugin.NewRegistry()
func newTestSDK(cfg sdk.SDKConfig) *sdk.PluginSDK {
if cfg.Settings == nil {
cfg.Settings = sdk.NewSettings("healthcheck", nil)
}
if cfg.Tool == nil {
cfg.Tool = sdk.NewTool(agentCore.NewStageHost(), agentIO.NewIOManager())
}
return sdk.New("healthcheck", cfg)
}
Configure(sh, iom, pr, nil, nil, nil, nil, nil)
p := New("healthcheck")
func setupPlugin() (*Plugin, *toolCapture, error) {
return setupPluginWith(sdk.SDKConfig{})
}
func setupPluginWith(cfg sdk.SDKConfig) (*Plugin, *toolCapture, error) {
tc := newToolCapture()
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
if err := p.Start(sdk); err != nil {
cfg.RegTool = tc.RegisterTool
cfg.RegStage = tc.RegisterStage
cfg.RegAPI = tc.RegisterAPI
p := New("healthcheck")
s := newTestSDK(cfg)
if err := p.Start(s); err != nil {
return nil, nil, err
}
return p, tc, nil
@ -185,15 +196,8 @@ func TestHealthcheckWithMemory(t *testing.T) {
}
defer memDB.Close()
sh := agentCore.NewStageHost()
iom := agentIO.NewIOManager()
pr := plugin.NewRegistry()
Configure(sh, iom, pr, memDB, nil, nil, nil, nil)
p := New("healthcheck")
tc := newToolCapture()
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
if err := p.Start(sdk); err != nil {
_, tc, err := setupPluginWith(sdk.SDKConfig{Memory: sdk.NewGraphMemory(memDB)})
if err != nil {
t.Fatal(err)
}
@ -228,15 +232,8 @@ func TestHealthcheckWithKnowledge(t *testing.T) {
}
defer ks.Stop()
sh := agentCore.NewStageHost()
iom := agentIO.NewIOManager()
pr := plugin.NewRegistry()
Configure(sh, iom, pr, nil, ks, nil, nil, nil)
p := New("healthcheck")
tc := newToolCapture()
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
if err := p.Start(sdk); err != nil {
_, tc, err := setupPluginWith(sdk.SDKConfig{Knowledge: sdk.NewKnowledge(ks)})
if err != nil {
t.Fatal(err)
}
@ -282,15 +279,8 @@ func TestHealthcheckWithDocStore(t *testing.T) {
}
defer ds.Stop()
sh := agentCore.NewStageHost()
iom := agentIO.NewIOManager()
pr := plugin.NewRegistry()
Configure(sh, iom, pr, nil, nil, ds, nil, nil)
p := New("healthcheck")
tc := newToolCapture()
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
if err := p.Start(sdk); err != nil {
_, tc, err := setupPluginWith(sdk.SDKConfig{DocMemory: sdk.NewDocMemory(ds)})
if err != nil {
t.Fatal(err)
}
@ -340,11 +330,4 @@ func TestLLMReportCollection(t *testing.T) {
}
}
func TestConfigureNilStageHost(t *testing.T) {
Configure(nil, nil, nil, nil, nil, nil, nil, nil)
if hcStageHost != nil {
t.Fatal("expected hcStageHost to be nil")
}
}

View File

@ -10,15 +10,13 @@ import (
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
@ -77,11 +75,17 @@ func setupIntegrationWithProvider(t *testing.T, pm *agentAPI.ProviderManager) *t
t.Fatal(err)
}
pluginReg.SetStageHost(stageHost)
pluginReg.SetProviderManager(pm)
pluginReg.SetKnowledge(ks)
pluginReg.SetDocStore(docStore)
cli.DefaultSocket = filepath.Join(tmpDir, "cli.sock")
openclaw.SkillsDir = filepath.Join(tmpDir, "skills")
os.MkdirAll(openclaw.SkillsDir, 0755)
webui.Configure(":0", nil, memDB, nil, nil, nil, iom, nil, ks, nil, nil, pluginReg, nil, nil)
healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, pm, nil)
// 经 ConfigRegistry 装配内核路径配置(clawhubadapter/pluginmgr 等经 SDK settings 读取)
cfgReg := internalConfig.NewConfigRegistry("")
cfgReg.SeedDefaults(tmpDir)
pluginReg.SetConfigRegistry(cfgReg)
plgDir := filepath.Join(tmpDir, "plugins")
os.MkdirAll(plgDir, 0755)
@ -229,7 +233,7 @@ func TestIntegrationCmdRunStderr(t *testing.T) {
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
"command": "echo stderr_test >&2",
"command": "sh -c \"echo stderr_test >&2\"",
})
if err != nil {
t.Fatal(err)
@ -520,7 +524,7 @@ func TestIntegrationLLMDrivenDiscoveryWithRealKey(t *testing.T) {
Model: "deepseek-v4-flash",
BaseURL: "https://api.deepseek.com",
APIKey: apiKey,
}, vm, "deepseek"))
}, vm, "deepseek", "deepseek"))
// Setup — 加载所有真实内置插件
env := setupIntegrationWithProvider(t, pm)

View File

@ -66,11 +66,7 @@ var downloadClient = &http.Client{
},
}
var (
PluginDir string // 由 main.go 设置
Reg *plugin.Registry // 由 main.go 设置
HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 main.go 覆写或 settings 配置
)
var HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 settings 配置
func init() {
plugin.RegisterPluginMeta("pluginmgr", "插件管理", "Plugin Manager")
@ -80,12 +76,14 @@ func init() {
}
type Plugin struct {
name string
mu sync.Mutex
server *http.Server
mux *http.ServeMux
listen net.Listener
httpURL string
name string
mu sync.Mutex
server *http.Server
mux *http.ServeMux
listen net.Listener
httpURL string
sdk *sdk.PluginSDK
pluginDir string
}
func New(name string) *Plugin {
@ -96,6 +94,7 @@ func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
p.sdk = s
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "http_addr",
Default: HTTPAddr,
@ -111,6 +110,12 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}
}
if v, _ := s.Settings().GetCore("plugin.dir"); v != nil {
if dir, ok := v.(string); ok && dir != "" {
p.pluginDir = dir
}
}
p.registerTools(s)
if HTTPAddr != "" {
@ -374,7 +379,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) {
}, nil
}
dir := PluginDir
dir := p.pluginDir
if dir == "" {
return map[string]interface{}{"error": "plugin dir not configured"}, nil
}
@ -409,7 +414,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) {
}
func (p *Plugin) listPlugins() (interface{}, error) {
dir := PluginDir
dir := p.pluginDir
if dir == "" {
return []map[string]interface{}{}, nil
}
@ -446,7 +451,7 @@ func (p *Plugin) listPlugins() (interface{}, error) {
}
func (p *Plugin) removePlugin(name string) (interface{}, error) {
dir := filepath.Join(PluginDir, name)
dir := filepath.Join(p.pluginDir, name)
if _, err := os.Stat(dir); os.IsNotExist(err) {
return map[string]interface{}{"error": "plugin not found", "name": name}, nil
}
@ -456,8 +461,10 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
}
// 同步清理禁用表
if Reg != nil {
Reg.EnablePlugin(name)
if p.sdk != nil && p.sdk.PluginMgr() != nil {
if err := p.sdk.PluginMgr().EnablePlugin(name); err != nil {
log.Printf("[pluginmgr] enable %s after remove: %v", name, err)
}
}
return map[string]interface{}{
@ -468,7 +475,7 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
}
func (p *Plugin) pluginInfo(name string) (interface{}, error) {
dir := filepath.Join(PluginDir, name)
dir := filepath.Join(p.pluginDir, name)
m, err := plugin.ReadManifest(dir)
if err != nil {
return nil, fmt.Errorf("plugin %q not found", name)

View File

@ -9,28 +9,13 @@ import (
"io"
"log"
"net/http"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
@ -49,26 +34,24 @@ func init() {
}
type Handler struct {
supervisor *supervisor.Daemon
memory *memory.GraphDB
indexer *memory.Indexer
skills *skill.Manager
lua *luaVM.VM
config *types.Config
startTime time.Time
iom *agentIO.IOManager
textMem *text.Memory
knowledge *knowledge.Store
tracker *tracker.Tracker
cfgReg *internalConfig.ConfigRegistry
pluginReg *plugin.Registry
pluginMgr sdk.PluginManager
eventBus *events.Bus
statusProvider agentCore.StatusProvider
providerMgr *agentAPI.ProviderManager
baseAPIKey string
sessionMu sync.Mutex
sessions map[string]time.Time
sdk *sdk.PluginSDK
supervisor sdk.SupervisorAPI
memory sdk.MemoryAPI
indexer sdk.IndexerAPI
skills sdk.SkillAPI
adapter sdk.AdapterAPI
config sdk.ConfigAPI
startTime time.Time
textMem sdk.TextMemoryAPI
knowledge sdk.KnowledgeAPI
tracker sdk.TrackerAPI
settings sdk.SettingsAPI
pluginMgr sdk.PluginManager
status sdk.StatusAPI
llm sdk.LLMAPI
sessionMu sync.Mutex
sessions map[string]time.Time
chatMu sync.Mutex
chatHistory []ChatMsg
@ -104,51 +87,64 @@ type termState struct {
created time.Time
}
func (h *Handler) SetPluginMgr(mgr sdk.PluginManager) { h.pluginMgr = mgr }
const maxChatHistory = 200
const maxCmdHistory = 100
const maxTerminals = 50
func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string) *Handler {
var idx *memory.Indexer
if mem != nil {
idx = memory.NewIndexer(mem)
func NewHandler(s *sdk.PluginSDK) *Handler {
var (
sup sdk.SupervisorAPI
mem sdk.MemoryAPI
idx sdk.IndexerAPI
sk sdk.SkillAPI
ad sdk.AdapterAPI
cfg sdk.ConfigAPI
tm sdk.TextMemoryAPI
ks sdk.KnowledgeAPI
tr sdk.TrackerAPI
se sdk.SettingsAPI
pm sdk.PluginManager
st sdk.StatusAPI
llm sdk.LLMAPI
)
if s != nil {
sup, mem, idx = s.Supervisor(), s.Memory(), s.Indexer()
sk, ad, cfg = s.Skill(), s.Adapter(), s.Config()
tm, ks, tr = s.TextMemory(), s.Knowledge(), s.Tracker()
se, pm = s.Settings(), s.PluginMgr()
st, llm = s.Status(), s.LLM()
}
h := &Handler{
supervisor: sup,
memory: mem,
indexer: idx,
skills: sk,
lua: lua,
config: cfg,
startTime: time.Now(),
iom: iom,
textMem: tm,
knowledge: ks,
tracker: tr,
cfgReg: cr,
pluginReg: pr,
eventBus: evBus,
statusProvider: sp,
providerMgr: pm,
baseAPIKey: baseKey,
sessions: make(map[string]time.Time),
termStates: make(map[string]*termState),
sdk: s,
supervisor: sup,
memory: mem,
indexer: idx,
skills: sk,
adapter: ad,
config: cfg,
startTime: time.Now(),
textMem: tm,
knowledge: ks,
tracker: tr,
settings: se,
pluginMgr: pm,
status: st,
llm: llm,
sessions: make(map[string]time.Time),
termStates: make(map[string]*termState),
}
h.loadChatHistory()
if evBus != nil {
if s != nil {
go h.trackToolEvents()
}
return h
}
func (h *Handler) loadChatHistory() {
if h.cfgReg == nil {
if h.settings == nil {
return
}
ps := h.cfgReg.PluginConfig("webui")
v, err := ps.Get("chathistory")
v, err := h.settings.Get("chathistory")
if err != nil || v == nil {
return
}
@ -166,12 +162,15 @@ func (h *Handler) loadChatHistory() {
}
func (h *Handler) trackToolEvents() {
h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) {
if h.sdk == nil {
return
}
h.sdk.Subscribe(sdk.EventToolCall, func(ev *sdk.Event) {
h.handleToolEvent(ev)
})
}
func (h *Handler) handleToolEvent(ev *events.Event) {
func (h *Handler) handleToolEvent(ev *sdk.Event) {
payload := ev.Payload
tool, _ := payload["tool"].(string)
args, _ := payload["args"].(map[string]interface{})
@ -235,20 +234,19 @@ func getStr(m map[string]interface{}, key string) string {
func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
ttl = 24 * time.Hour
if h.cfgReg == nil {
if h.settings == nil {
return
}
ps := h.cfgReg.PluginConfig("webui")
if v, _ := ps.Get("api_key"); v != nil {
if v, _ := h.settings.Get("api_key"); v != nil {
apiKey, _ = v.(string)
}
if v, _ := ps.Get("username"); v != nil {
if v, _ := h.settings.Get("username"); v != nil {
username, _ = v.(string)
}
if v, _ := ps.Get("password"); v != nil {
if v, _ := h.settings.Get("password"); v != nil {
password, _ = v.(string)
}
if v, _ := ps.Get("session_ttl_hours"); v != nil {
if v, _ := h.settings.Get("session_ttl_hours"); v != nil {
switch n := v.(type) {
case float64:
if n > 0 { ttl = time.Duration(n) * time.Hour }
@ -433,12 +431,15 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
agents := h.supervisor.ListAgents()
agentCount := 0
if h.supervisor != nil {
agentCount = len(h.supervisor.ListAgents())
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "running",
"uptime": time.Since(h.startTime).Round(time.Second).String(),
"agents": len(agents),
"version": meta.Version,
"agents": agentCount,
"version": sdk.SDKVersion,
"startedAt": h.startTime,
})
}
@ -448,16 +449,20 @@ func (h *Handler) handleKernel(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.statusProvider == nil {
if h.status == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "kernel status provider not available"})
return
}
writeJSON(w, http.StatusOK, h.statusProvider.GetKernelStatus())
writeJSON(w, http.StatusOK, h.status.GetKernelStatus())
}
func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
agents := h.supervisor.ListAgents()
writeJSON(w, http.StatusOK, map[string]interface{}{"agents": agents})
case http.MethodPost:
@ -470,7 +475,10 @@ func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent id is required"})
return
}
h.config.Agents = append(h.config.Agents, cfg)
if h.config != nil {
kcfg := h.config.Get()
kcfg.Agents = append(kcfg.Agents, cfg)
}
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -484,7 +492,11 @@ func (h *Handler) handleAgentByID(w http.ResponseWriter, r *http.Request) {
if len(parts) == 1 {
switch r.Method {
case http.MethodGet:
status, err := h.supervisor.GetAgentStatus(agentID)
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
status, err := h.supervisor.GetAgentStatus(string(agentID))
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
return
@ -515,7 +527,11 @@ func (h *Handler) handleSnapshots(w http.ResponseWriter, r *http.Request, agentI
case http.MethodGet:
writeJSON(w, http.StatusOK, map[string]interface{}{"agent_id": agentID, "snapshots": []map[string]interface{}{}})
case http.MethodPost:
snap, err := h.supervisor.PreActionSnapshot(agentID)
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
snap, err := h.supervisor.PreActionSnapshot(string(agentID))
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
@ -531,8 +547,12 @@ func (h *Handler) handleRollback(w http.ResponseWriter, r *http.Request, agentID
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
snapID := types.SnapshotID(parts[2])
if err := h.supervisor.RollbackAgent(agentID, snapID); err != nil {
if err := h.supervisor.RollbackAgent(string(agentID), string(snapID)); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
@ -598,28 +618,25 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
if depth <= 0 {
depth = 2
}
result, err := h.memory.Recall(keywords, nil, depth, "")
entities, relations, err := h.memory.Recall(keywords, depth)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, result)
writeJSON(w, http.StatusOK, map[string]interface{}{"entities": entities, "relations": relations})
case http.MethodPost:
var req struct {
Triples []memory.Triple `json:"triples"`
SessionID string `json:"session_id"`
TurnID int `json:"turn_id"`
Triples []sdk.Triple `json:"triples"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
ec, rc, err := h.memory.Commit(req.Triples, req.SessionID, req.TurnID)
if err != nil {
if err := h.memory.Commit(req.Triples); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, map[string]int{"entities_created": ec, "relations_created": rc})
writeJSON(w, http.StatusCreated, map[string]interface{}{"status": "committed", "committed": len(req.Triples)})
case http.MethodDelete:
var req struct {
Criteria map[string]string `json:"criteria"`
@ -650,7 +667,11 @@ func (h *Handler) handleMemoryContext(w http.ResponseWriter, r *http.Request) {
return
}
userInput := r.URL.Query().Get("q")
injected := h.indexer.BuildContext(userInput)
injected, err := h.indexer.BuildContext(userInput)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"context": h.indexer.FormatContext(injected),
"summary": injected.Summary,
@ -701,12 +722,13 @@ func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) {
}
query := r.URL.Query().Get("q")
if query != "" {
results := h.knowledge.Search(query, 10)
results, _ := h.knowledge.Search(query, 10)
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
return
}
categories, _ := h.knowledge.List()
writeJSON(w, http.StatusOK, map[string]interface{}{
"categories": h.knowledge.List(),
"categories": categories,
"stats": h.knowledge.Stats(),
})
@ -795,13 +817,13 @@ func (h *Handler) handleTextMemory(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
if h.lua == nil {
if h.adapter == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
return
}
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.lua.ListAdapters()})
writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.adapter.List()})
case http.MethodPost:
var req struct {
Name string `json:"name"`
@ -811,12 +833,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), req.Name)
if err := os.WriteFile(path, []byte(req.Code), 0644); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if err := h.lua.LoadAdapter(path); err != nil {
if err := h.adapter.Load(req.Name, req.Code); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
@ -827,7 +844,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
if h.lua == nil {
if h.adapter == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
return
}
@ -838,7 +855,7 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
}
switch r.Method {
case http.MethodGet:
for _, a := range h.lua.ListAdapters() {
for _, a := range h.adapter.List() {
if a.Name == name {
writeJSON(w, http.StatusOK, a)
return
@ -846,12 +863,10 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
}
http.NotFound(w, r)
case http.MethodDelete:
path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), name)
if err := os.Remove(path); err != nil {
if err := h.adapter.Remove(name); err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"})
return
}
h.lua.RemoveAdapter(name)
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -865,7 +880,7 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"network_status": "monitoring",
"endpoints": h.config.Defaults.LLMEndpoints,
"endpoints": h.config.Get().Defaults.LLMEndpoints,
})
}
@ -876,10 +891,9 @@ func (h *Handler) addChatMsg(msg ChatMsg) {
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
}
// persist to webui config table as compact JSON
if h.cfgReg != nil {
ps := h.cfgReg.PluginConfig("webui")
if h.settings != nil {
b, _ := json.Marshal(h.chatHistory)
ps.Set("chathistory", string(b))
_ = h.settings.Set("chathistory", string(b))
}
h.chatMu.Unlock()
}
@ -929,7 +943,11 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
}
h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)})
resp := h.iom.InjectTextSync("cli", body.Message)
if h.sdk == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
return
}
resp := h.sdk.InjectTextSync("cli", "cli", body.Message)
if resp == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
return
@ -965,7 +983,7 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
flusher.Flush()
done := r.Context().Done()
if h.eventBus == nil {
if h.sdk == nil {
fmt.Fprintf(w, "event: error\ndata: {\"msg\":\"event bus unavailable\"}\n\n")
flusher.Flush()
return
@ -994,15 +1012,15 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
var unsubs []func()
for _, t := range subTypes {
t2 := t
unsub := h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
if evt.Type == events.EventToolCall {
unsub := h.sdk.Subscribe(sdk.EventType(t2), func(evt *sdk.Event) {
if evt.Type == sdk.EventToolCall {
toolName, _ := evt.Payload["tool"].(string)
log.Printf("[SSE] received tool_call event: tool=%s", toolName)
}
data, _ := json.Marshal(evt)
select {
case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)):
if evt.Type == events.EventToolCall {
if evt.Type == sdk.EventToolCall {
toolName, _ := evt.Payload["tool"].(string)
log.Printf("[SSE] wrote tool_call to writeCh: tool=%s", toolName)
}
@ -1031,16 +1049,20 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
if h.config == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "config not available"})
return
}
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, h.config)
writeJSON(w, http.StatusOK, h.config.Get())
case http.MethodPut:
var cfg types.Config
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid config"})
return
}
h.config = &cfg
h.config.Put(&cfg)
writeJSON(w, http.StatusOK, map[string]string{"status": "config_updated"})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -1048,7 +1070,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
if h.cfgReg == nil {
if h.settings == nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "config registry not available"})
return
}
@ -1056,30 +1078,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
prefix := r.URL.Query().Get("prefix")
values := make(map[string]interface{})
meta := make(map[string]*internalConfig.ConfigDef)
meta := make(map[string]*sdk.ConfigDef)
if strings.HasPrefix(prefix, "plugin.") {
// 插件配置:从插件自身 config_<name> 表读取
pluginName := prefix[7:]
ps := h.cfgReg.PluginConfig(pluginName)
keys, _ := ps.List("")
keys, _ := h.settings.ListPlugin(pluginName, "")
for _, k := range keys {
v, _ := ps.Get(k)
v, _ := h.settings.GetPlugin(pluginName, k)
fullKey := prefix + "." + k
values[fullKey] = v
if def := h.cfgReg.GetDef(fullKey); def != nil {
meta[fullKey] = def
}
}
for _, def := range h.settings.DefsPlugin(pluginName, "") {
fullKey := prefix + "." + def.Key
meta[fullKey] = def
}
} else {
// 核心配置:从 core config 表读取
keys := h.cfgReg.List(prefix)
for _, k := range keys {
v, _ := h.cfgReg.Get(k)
values[k] = v
// 核心配置:从 core config 表读取(键可为任意前缀,如 core.llm.*、webui.*)
all := h.settings.Dump()
var keys []string
for k := range all {
if strings.HasPrefix(k, prefix) {
keys = append(keys, k)
}
}
defs := h.cfgReg.ListDefs(prefix)
for _, d := range defs {
sort.Strings(keys)
for _, k := range keys {
values[k] = all[k]
}
for _, d := range h.settings.DefsCore(prefix) {
meta[d.Key] = d
// 有 def 但 DB 中尚无值的 key,用 default 填充以便在 WebUI 中显示和编辑
if _, exists := values[d.Key]; !exists {
@ -1087,29 +1114,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
}
}
// 无前缀时同时加载所有插件配置
if prefix == "" && h.pluginReg != nil {
for _, p := range h.pluginReg.List() {
ps := h.cfgReg.PluginConfig(p)
pkeys, _ := ps.List("")
if prefix == "" {
for _, p := range h.settings.Plugins() {
if p == "core" {
continue
}
pkeys, _ := h.settings.ListPlugin(p, "")
for _, k := range pkeys {
v, _ := ps.Get(k)
v, _ := h.settings.GetPlugin(p, k)
fullKey := "plugin." + p + "." + k
values[fullKey] = v
if def := h.cfgReg.GetDef(fullKey); def != nil {
meta[fullKey] = def
}
}
for _, def := range h.settings.DefsPlugin(p, "") {
fullKey := "plugin." + p + "." + def.Key
meta[fullKey] = def
}
}
}
}
plugins := []string{"core"}
pm := h.pluginReg.PluginMetas()
if h.pluginReg != nil {
for _, p := range h.pluginReg.List() {
for _, p := range h.settings.Plugins() {
if p != "core" {
plugins = append(plugins, "plugin."+p)
}
}
var pm map[string]sdk.PluginMeta
if h.pluginMgr != nil {
pm = h.pluginMgr.PluginMetas()
}
var disabledPlugins []sdk.DisabledPluginInfo
if h.pluginMgr != nil {
disabledPlugins = h.pluginMgr.ListDisabledPlugins()
@ -1134,20 +1167,21 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(body.Key, "plugin.") {
parts := strings.SplitN(body.Key, ".", 3)
if len(parts) >= 3 {
ps := h.cfgReg.PluginConfig(parts[1])
if err := ps.Set(parts[2], body.Value); err != nil {
if err := h.settings.SetPlugin(parts[1], parts[2], body.Value); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
} else {
if err := h.cfgReg.Set(body.Key, body.Value); err != nil {
if err := h.settings.SetCore(body.Key, body.Value); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
if strings.HasPrefix(body.Key, "core.llm.") && h.providerMgr != nil && h.lua != nil {
h.reloadLLMProviders()
if strings.HasPrefix(body.Key, "core.llm.") && h.llm != nil {
if err := h.llm.ReloadFromConfig(); err != nil {
log.Printf("[webui] failed to reload LLM providers: %v", err)
}
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
default:
@ -1155,36 +1189,13 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
}
}
func (h *Handler) reloadLLMProviders() {
cfg := h.cfgReg.ToConfig()
h.providerMgr.Reset()
for _, src := range cfg.LLM.Sources {
key := src.APIKey
if key == "" {
key = h.baseAPIKey
}
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
Model: src.Model,
BaseURL: src.BaseURL,
APIKey: key,
Temperature: cfg.LLM.Temperature,
MaxTokens: cfg.LLM.MaxTokens,
ContextWindow: src.ContextWindow,
}, h.lua, src.Adapter)
h.providerMgr.Register(src.Name, provider)
}
if cfg.LLM.Provider != "" {
_ = h.providerMgr.SetDefault(cfg.LLM.Provider)
}
}
func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.iom == nil {
if h.sdk == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"})
return
}
@ -1211,9 +1222,9 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request
return
}
response := h.iom.InjectTextSync("http", lastMsg.Content)
response := h.sdk.InjectTextSync("http", "http", lastMsg.Content)
if response == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "no response from agent"})
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"})
return
}
@ -1375,11 +1386,10 @@ func (h *Handler) handleTracker(w http.ResponseWriter, r *http.Request) {
func (h *Handler) pluginmgrAddr() string {
addr := "127.0.0.1:9876"
if h.cfgReg == nil {
if h.settings == nil {
return addr
}
ps := h.cfgReg.PluginConfig("pluginmgr")
if v, err := ps.Get("http_addr"); err == nil {
if v, err := h.settings.GetPlugin("pluginmgr", "http_addr"); err == nil {
if s, ok := v.(string); ok && s != "" {
addr = s
}
@ -1436,17 +1446,11 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
}
if path == "reload" && r.Method == http.MethodPost {
if h.pluginReg == nil {
if h.pluginMgr == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin registry not available"})
return
}
dir := ""
if h.cfgReg != nil {
if v, _ := h.cfgReg.Get("core.plugin.dir"); v != nil {
dir, _ = v.(string)
}
}
if _, err := h.pluginReg.Reload(dir); err != nil {
if _, err := h.pluginMgr.ReloadPlugins(); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}

View File

@ -17,12 +17,19 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
func testSDK(cfg sdk.SDKConfig) *sdk.PluginSDK {
if cfg.EventBus == nil {
cfg.EventBus = events.NewBus()
}
return sdk.New("webui", cfg)
}
func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
t.Helper()
cfg := &types.Config{
@ -34,7 +41,11 @@ func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
sup := supervisor.New(cfg)
sup.Start()
return NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, ""), sup
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Config: sdk.NewConfig(cfg),
})
return NewHandler(s), sup
}
func TestAuthMiddleware(t *testing.T) {
@ -44,9 +55,21 @@ func TestAuthMiddleware(t *testing.T) {
cfgReg.PluginConfig("webui").Set("password", "secret-pass")
cfgReg.PluginConfig("webui").Set("session_ttl_hours", "24")
h, sup := newTestHandler(t)
sup := supervisor.New(&types.Config{
Daemon: types.DaemonConfig{
CheckInterval: time.Minute,
HeartbeatInterval: 30 * time.Second,
},
})
sup.Start()
defer sup.Shutdown()
h.cfgReg = cfgReg
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Settings: sdk.NewSettings("webui", cfgReg),
Config: sdk.NewConfig(&types.Config{}),
})
h := NewHandler(s)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@ -194,7 +217,12 @@ func TestHandleKnowledgeSearch(t *testing.T) {
sup.Start()
defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Knowledge: sdk.NewKnowledge(ks),
Config: sdk.NewConfig(cfg),
})
h := NewHandler(s)
req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil)
w := httptest.NewRecorder()
@ -226,7 +254,12 @@ func TestHandleKnowledgeCreate(t *testing.T) {
sup.Start()
defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Knowledge: sdk.NewKnowledge(ks),
Config: sdk.NewConfig(cfg),
})
h := NewHandler(s)
body := `{"name":"new_doc","content":"fresh content"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body))
@ -291,7 +324,12 @@ func TestHandleTrackerStats(t *testing.T) {
sup.Start()
defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, tr, nil, nil, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Tracker: tr,
Config: sdk.NewConfig(cfg),
})
h := NewHandler(s)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil)
w := httptest.NewRecorder()
@ -305,8 +343,6 @@ func TestHandleTrackerStats(t *testing.T) {
func TestHandleOpenAICompletionsNoMessages(t *testing.T) {
h, sup := newTestHandler(t)
defer sup.Shutdown()
// 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验
h.iom = agentIO.NewIOManager()
body := `{"model":"test"}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
@ -322,8 +358,6 @@ func TestHandleOpenAICompletionsNoMessages(t *testing.T) {
func TestHandleOpenAICompletionsLastMsgNotUser(t *testing.T) {
h, sup := newTestHandler(t)
defer sup.Shutdown()
// 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验
h.iom = agentIO.NewIOManager()
body := `{"messages":[{"role":"assistant","content":"hi"}]}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
@ -392,9 +426,27 @@ func TestHandleConfigGet(t *testing.T) {
}
func TestRegisterRoutes(t *testing.T) {
h, sup := newTestHandler(t)
cfgReg := internalConfig.NewConfigRegistry("")
cfgReg.PluginConfig("webui").Set("api_key", "test-api-key")
cfgReg.PluginConfig("webui").Set("username", "admin")
cfgReg.PluginConfig("webui").Set("password", "secret-pass")
sup := supervisor.New(&types.Config{
Daemon: types.DaemonConfig{
CheckInterval: time.Minute,
HeartbeatInterval: 30 * time.Second,
},
})
sup.Start()
defer sup.Shutdown()
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Settings: sdk.NewSettings("webui", cfgReg),
Config: sdk.NewConfig(&types.Config{}),
})
h := NewHandler(s)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@ -407,7 +459,7 @@ func TestRegisterRoutes(t *testing.T) {
{"/api/v1/agents", http.MethodGet, http.StatusOK},
{"/api/v1/config", http.MethodGet, http.StatusOK},
{"/api/v1/network", http.MethodGet, http.StatusOK},
{"/", http.MethodGet, http.StatusOK},
{"/", http.MethodGet, http.StatusFound},
{"/api/v1/memory", http.MethodGet, http.StatusServiceUnavailable},
{"/api/v1/knowledge", http.MethodGet, http.StatusServiceUnavailable},
{"/api/v1/tracker", http.MethodGet, http.StatusServiceUnavailable},
@ -416,6 +468,9 @@ func TestRegisterRoutes(t *testing.T) {
for _, tt := range tests {
req := httptest.NewRequest(tt.method, tt.path, nil)
if strings.HasPrefix(tt.path, "/api/v1/") {
req.Header.Set("X-API-Key", "test-api-key")
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
@ -470,8 +525,12 @@ func TestSettingsAPIFlow(t *testing.T) {
sup.Start()
defer sup.Shutdown()
pluginReg := plugin.NewRegistry()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Settings: sdk.NewSettings("webui", cfgReg),
Config: sdk.NewConfig(&types.Config{}),
})
h := NewHandler(s)
t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
@ -578,7 +637,11 @@ func TestSettingsAPIFlow(t *testing.T) {
})
t.Run("settings_not_available_without_registry", func(t *testing.T) {
h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
s2 := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Config: sdk.NewConfig(&types.Config{}),
})
h2 := NewHandler(s2)
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
w := httptest.NewRecorder()
h2.handleSettings(w, req)
@ -603,8 +666,12 @@ func TestSettingsWithPluginRegistry(t *testing.T) {
sup.Start()
defer sup.Shutdown()
pluginReg := plugin.NewRegistry()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
Settings: sdk.NewSettings("webui", cfgReg),
Config: sdk.NewConfig(&types.Config{}),
})
h := NewHandler(s)
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
w := httptest.NewRecorder()
@ -633,7 +700,7 @@ type echoProvider struct{ name string }
func (p *echoProvider) Name() string { return p.name }
func (p *echoProvider) MaxContextTokens() int { return 8192 }
func (p *echoProvider) Chat(ctx context.Context, req *agentAPI.CompletionRequest) (*agentAPI.CompletionResponse, error) {
content := "echo: " + req.Messages[len(req.Messages)-1].Content
content := "echo: " + lastUserContent(req.Messages)
return &agentAPI.CompletionResponse{Content: content, FinishReason: "stop"}, nil
}
func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionRequest) (<-chan agentAPI.StreamChunk, error) {
@ -642,6 +709,15 @@ func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionR
return ch, nil
}
func lastUserContent(msgs []agentAPI.Message) string {
for i := len(msgs) - 1; i >= 0; i-- {
if msgs[i].Role == "user" {
return msgs[i].Content
}
}
return ""
}
func init() {
// 避免测试时自动输出
}
@ -656,19 +732,22 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
}
defer memDB.Close()
pm := agentAPI.NewProviderManager()
pm.Register("echo", &echoProvider{name: "echo"})
agent := agentCore.New(agentCore.AgentConfig{
ID: "test",
SystemPrompt: "你是测试助手",
Provider: &echoProvider{name: "echo"},
IO: iom,
Memory: memDB,
Indexer: nil,
ID: "test",
SystemPrompt: "你是测试助手",
Provider: &echoProvider{name: "echo"},
ProviderManager: pm,
IO: iom,
Memory: memDB,
Indexer: nil,
ContextSavePath: "",
})
agent.Start()
defer agent.Stop()
// Handler 需要 iom
sup := supervisor.New(&types.Config{
Daemon: types.DaemonConfig{
CheckInterval: time.Minute,
@ -678,7 +757,12 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
sup.Start()
defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, iom, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
s := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
IOManager: iom,
Config: sdk.NewConfig(&types.Config{}),
})
h := NewHandler(s)
t.Run("POST_chat_completions_returns_echo", func(t *testing.T) {
body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}`
@ -711,7 +795,10 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
})
t.Run("POST_chat_completions_no_iom_returns_503", func(t *testing.T) {
h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
s2 := testSDK(sdk.SDKConfig{
Supervisor: supervisor.NewSDKAdapter(sup),
})
h2 := NewHandler(s2)
body := `{"messages":[{"role":"user","content":"hi"}]}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")

View File

@ -7,117 +7,28 @@ import (
"log"
"net/http"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
// 包级依赖注入 — 由 main.go 在 Load() 前调用 Configure() 设置。
var (
webuiAddr string
webuiSup *supervisor.Daemon
webuiMem *memory.GraphDB
webuiSK *skill.Manager
webuiLua *luaVM.VM
webuiCfg *types.Config
webuiIOM *agentIO.IOManager
webuiTM *text.Memory
webuiKS *knowledge.Store
webuiTR *tracker.Tracker
webuiCR *internalConfig.ConfigRegistry
webuiPR *plugin.Registry
webuiEvBus *events.Bus
webuiStatusProvider agentCore.StatusProvider
webuiProviderMgr *agentAPI.ProviderManager
webuiBaseAPIKey string
)
// Configure 注入 WebUI 插件需要的内核依赖。必须在 Load() 之前调用。
func Configure(addr string,
sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
) {
webuiAddr = addr
webuiSup, webuiMem, webuiSK, webuiLua = sup, mem, sk, lua
webuiCfg, webuiIOM, webuiTM, webuiKS = cfg, iom, tm, ks
webuiTR, webuiCR, webuiPR, webuiEvBus = tr, cr, pr, evBus
webuiStatusProvider = sp
webuiProviderMgr = pm
webuiBaseAPIKey = baseKey
}
func init() {
plugin.RegisterPluginMeta("webui", "Web 控制台", "WebUI")
plugin.RegisterFactory("webui", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
if webuiSup == nil {
return nil, nil // 未 Configure 则跳过(不给日志警告)
}
addr := webuiAddr
if a, ok := config["addr"].(string); ok {
addr = a
}
return New(name, addr,
webuiSup, webuiMem, webuiSK, webuiLua,
webuiCfg, webuiIOM, webuiTM, webuiKS,
webuiTR, webuiCR, webuiPR, webuiEvBus, webuiStatusProvider,
webuiProviderMgr, webuiBaseAPIKey,
), nil
return New(name), nil
})
}
type Plugin struct {
name string
addr string
handler *Handler
server *http.Server
mux *http.ServeMux
sup *supervisor.Daemon
mem *memory.GraphDB
sk *skill.Manager
lua *luaVM.VM
cfg *types.Config
iom *agentIO.IOManager
tm *text.Memory
ks *knowledge.Store
tr *tracker.Tracker
cr *internalConfig.ConfigRegistry
pr *plugin.Registry
evBus *events.Bus
statusProvider agentCore.StatusProvider
providerMgr *agentAPI.ProviderManager
baseAPIKey string
}
func New(name, addr string,
sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
) *Plugin {
func New(name string) *Plugin {
return &Plugin{
name: name,
addr: addr,
mux: http.NewServeMux(),
sup: sup, mem: mem, sk: sk, lua: lua, cfg: cfg,
iom: iom, tm: tm, ks: ks, tr: tr, cr: cr, pr: pr, evBus: evBus,
statusProvider: sp, providerMgr: pm, baseAPIKey: baseKey,
}
}
@ -157,11 +68,18 @@ func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
addr := ":8080"
if v, _ := s.Settings().Get("addr"); v != nil {
if s2, ok := v.(string); ok && s2 != "" {
addr = s2
}
}
s.RegisterOutputChannel("webui", 1, "Web 控制台", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
payload, _ := args["payload"].(string)
if payload != "" {
p.evBus.Publish(&events.Event{
Type: events.EventAgentOutput,
s.Publish(&sdk.Event{
Type: sdk.EventAgentOutput,
Payload: map[string]interface{}{
"content": payload,
"channel": "webui",
@ -171,6 +89,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
return map[string]interface{}{"status": "ok"}, nil
})
s.Settings().RegisterDef(sdk.ConfigDef{Key: "addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "Web 控制台监听地址", Category: "webui"})
s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
@ -178,7 +97,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.ensureAuthBootstrap(s)
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}})
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}})
return nil
})
s.RegisterStage(sdk.StageBeforeToolcall, func(ctx *sdk.StageContext) error {
@ -186,22 +105,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
if len(ctx.ToolCalls) > 0 {
tool = ctx.ToolCalls[0].Name
}
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}})
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}})
return nil
})
s.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error {
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}})
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}})
return nil
})
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey)
h.SetPluginMgr(s.PluginMgr())
p.handler = h
h.RegisterRoutes(p.mux)
p.handler = NewHandler(s)
p.handler.RegisterRoutes(p.mux)
p.server = &http.Server{Addr: p.addr, Handler: p.mux}
p.server = &http.Server{Addr: addr, Handler: p.mux}
go func() {
log.Printf("[webui] HTTP server listening on %s", p.addr)
log.Printf("[webui] HTTP server listening on %s", addr)
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("[webui] server error: %v", err)
}

16
internal/sdk/adapter.go Normal file
View File

@ -0,0 +1,16 @@
package sdk
// AdapterAPI provides access to the Lua LLM protocol adapters.
type AdapterAPI interface {
List() []APIAdapter
// Load writes the adapter source file and hot-reloads it.
Load(name, code string) error
Remove(name string) error
AdapterDir() string
}
// APIAdapter describes a loaded Lua protocol adapter.
type APIAdapter struct {
Name string `json:"name"`
Version string `json:"version"`
}

View File

@ -0,0 +1,62 @@
package sdk
import (
"errors"
"os"
"path/filepath"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
)
// adapterImpl 桥接 Lua VM 的协议适配器管理。
type adapterImpl struct {
vm *luaVM.VM
}
func NewAdapter(vm *luaVM.VM) AdapterAPI {
return &adapterImpl{vm: vm}
}
func (a *adapterImpl) List() []APIAdapter {
if a.vm == nil {
return nil
}
got := a.vm.ListAdapters()
out := make([]APIAdapter, len(got))
for i, ad := range got {
out[i] = APIAdapter{Name: ad.Name, Version: ad.Version}
}
return out
}
func (a *adapterImpl) Load(name, code string) error {
if a.vm == nil {
return errors.New("lua vm not available")
}
path := filepath.Join(a.vm.AdapterDir(), name+".lua")
if err := os.WriteFile(path, []byte(code), 0644); err != nil {
return err
}
return a.vm.LoadAdapter(path)
}
func (a *adapterImpl) Remove(name string) error {
if a.vm == nil {
return errors.New("lua vm not available")
}
path := filepath.Join(a.vm.AdapterDir(), name+".lua")
if err := os.Remove(path); err != nil {
return err
}
a.vm.RemoveAdapter(name)
return nil
}
func (a *adapterImpl) AdapterDir() string {
if a.vm == nil {
return ""
}
return a.vm.AdapterDir()
}
var _ AdapterAPI = (*adapterImpl)(nil)

10
internal/sdk/config.go Normal file
View File

@ -0,0 +1,10 @@
package sdk
import "gitcode.com/JianFeeeee/HomeAgent/pkg/types"
// ConfigAPI provides read/write access to the runtime config object
// (unrelated to the config database).
type ConfigAPI interface {
Get() *types.Config
Put(cfg *types.Config)
}

View File

@ -0,0 +1,22 @@
package sdk
import "gitcode.com/JianFeeeee/HomeAgent/pkg/types"
// configImpl 持有运行期配置对象的引用(与配置数据库无关)。
type configImpl struct {
cfg *types.Config
}
func NewConfig(cfg *types.Config) ConfigAPI {
return &configImpl{cfg: cfg}
}
func (c *configImpl) Get() *types.Config {
return c.cfg
}
func (c *configImpl) Put(cfg *types.Config) {
c.cfg = cfg
}
var _ ConfigAPI = (*configImpl)(nil)

20
internal/sdk/events.go Normal file
View File

@ -0,0 +1,20 @@
package sdk
import "gitcode.com/JianFeeeee/HomeAgent/internal/events"
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
type EventType = events.EventType
type Event = events.Event
const (
EventRawInput = events.EventRawInput
EventAgentOutput = events.EventAgentOutput
EventAgentLLMChain = events.EventAgentLLMChain
EventToolCall = events.EventToolCall
EventReasoning = events.EventReasoning
EventStage = events.EventStage
EventSystem = events.EventSystem
EventAll = events.EventAll
)

17
internal/sdk/indexer.go Normal file
View File

@ -0,0 +1,17 @@
package sdk
// IndexerAPI exposes the memory context indexer.
type IndexerAPI interface {
BuildContext(q string) (*IndexContext, error)
FormatContext(ctx *IndexContext) string
GetToolDefinitions() []map[string]interface{}
BuildToolPrompt() string
}
// IndexContext is a neutral snapshot of the injected memory context.
type IndexContext struct {
Entities []Entity `json:"entities"`
Relations []Relation `json:"relations"`
Summary string `json:"summary"`
TokenEstimate int `json:"token_estimate"`
}

View File

@ -0,0 +1,71 @@
package sdk
import (
"errors"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
)
// indexerImpl 桥接 memory.Indexer 到中立的 IndexerAPI。
type indexerImpl struct {
idx *memory.Indexer
}
func NewIndexer(idx *memory.Indexer) IndexerAPI {
return &indexerImpl{idx: idx}
}
func (i *indexerImpl) BuildContext(q string) (*IndexContext, error) {
if i.idx == nil {
return nil, errors.New("indexer not available")
}
ctx := i.idx.BuildContext(q)
if ctx == nil {
return &IndexContext{}, nil
}
entities := make([]Entity, len(ctx.Entities))
for j, e := range ctx.Entities {
entities[j] = Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
}
relations := make([]Relation, len(ctx.Relations))
for j, r := range ctx.Relations {
relations[j] = Relation{SourceName: r.SourceName, TargetName: r.TargetName, RelationType: r.RelationType}
}
return &IndexContext{
Entities: entities,
Relations: relations,
Summary: ctx.Summary,
TokenEstimate: ctx.TokenEstimate,
}, nil
}
func (i *indexerImpl) FormatContext(ctx *IndexContext) string {
if i.idx == nil || ctx == nil {
return ""
}
entities := make([]memory.Entity, len(ctx.Entities))
for j, e := range ctx.Entities {
entities[j] = memory.Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
}
return i.idx.FormatContext(&memory.InjectedContext{
Entities: entities,
Summary: ctx.Summary,
TokenEstimate: ctx.TokenEstimate,
})
}
func (i *indexerImpl) GetToolDefinitions() []map[string]interface{} {
if i.idx == nil {
return nil
}
return i.idx.GetToolDefinitions()
}
func (i *indexerImpl) BuildToolPrompt() string {
if i.idx == nil {
return ""
}
return i.idx.BuildToolPrompt()
}
var _ IndexerAPI = (*indexerImpl)(nil)

View File

@ -2,5 +2,13 @@ package sdk
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type KnowledgeAPI = pubsdk.KnowledgeAPI
// KnowledgeAPI 是内置插件使用的全量知识库接口。
type KnowledgeAPI interface {
pubsdk.KnowledgeAPI
// Stats 返回知识库的运行统计。
Stats() map[string]interface{}
// Remove 按名称删除一条知识。
Remove(name string) error
}
type Knowledge = pubsdk.Knowledge

View File

@ -26,4 +26,14 @@ func (k *knowledgeImpl) List() ([]string, error) {
return k.ks.List(), nil
}
func (k *knowledgeImpl) Stats() map[string]interface{} {
if k.ks == nil { return map[string]interface{}{} }
return k.ks.Stats()
}
func (k *knowledgeImpl) Remove(name string) error {
if k.ks == nil { return nil }
return k.ks.Remove(name)
}
var _ KnowledgeAPI = (*knowledgeImpl)(nil)

View File

@ -1,5 +1,62 @@
package sdk
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
import (
"context"
type LLMAPI = pubsdk.LLMAPI
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// LLMAPI 是内置插件使用的全量 LLM Provider 接口。
// 内置 SDK 为全量接口,外部 SDK 仅暴露其中的安全子集(pubsdk.LLMAPI)。
type LLMAPI interface {
pubsdk.LLMAPI
// ReloadFromConfig 从内核配置重建所有 LLM Provider。
ReloadFromConfig() error
// Chat 向默认 Provider 发起一次补全调用(非记忆管线)。
Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error)
}
// LLMMessage 是面向 LLM Provider 的中立对话消息。
type LLMMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
}
// LLMToolCall 是中立的工具调用请求。
type LLMToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
}
// LLMTokenUsage 报告一次补全的 token 消耗。
type LLMTokenUsage struct {
Prompt int `json:"prompt"`
Completion int `json:"completion"`
Total int `json:"total"`
}
// LLMCompletionRequest 是中立的补全请求。
// Tools/ToolChoice 使用 OpenAI 风格载荷,便于 Provider 直接转发。
type LLMCompletionRequest struct {
Model string `json:"model,omitempty"`
Messages []LLMMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []interface{} `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
DisableThinking bool `json:"disable_thinking,omitempty"`
}
// LLMCompletionResponse 是中立的补全响应。
type LLMCompletionResponse struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
TokenUsage LLMTokenUsage `json:"token_usage,omitempty"`
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
}

View File

@ -1,26 +1,133 @@
package sdk
import agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
import (
"context"
"fmt"
type llmImpl struct{ mgr *agentAPI.ProviderManager }
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
)
func NewLLM(mgr *agentAPI.ProviderManager) LLMAPI { return &llmImpl{mgr: mgr} }
type llmImpl struct {
mgr *agentAPI.ProviderManager
cfgReg *internalConfig.ConfigRegistry
lua *luaVM.VM
baseAPIKey string
}
func NewLLM(mgr *agentAPI.ProviderManager, cfgReg *internalConfig.ConfigRegistry, lua *luaVM.VM, baseAPIKey string) LLMAPI {
return &llmImpl{mgr: mgr, cfgReg: cfgReg, lua: lua, baseAPIKey: baseAPIKey}
}
func (l *llmImpl) ListSources() []string {
if l.mgr == nil { return nil }
if l.mgr == nil {
return nil
}
return l.mgr.List()
}
func (l *llmImpl) SetSource(name string) error {
if l.mgr == nil { return nil }
if l.mgr == nil {
return nil
}
return l.mgr.SetDefault(name)
}
func (l *llmImpl) CurrentSource() string {
if l.mgr == nil { return "" }
if l.mgr == nil {
return ""
}
p := l.mgr.Default()
if p == nil { return "" }
if p == nil {
return ""
}
return p.Name()
}
func (l *llmImpl) Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error) {
if l.mgr == nil {
return nil, fmt.Errorf("llm: provider manager not available")
}
p := l.mgr.Default()
if p == nil {
return nil, fmt.Errorf("llm: no default provider")
}
apiReq := &agentAPI.CompletionRequest{
Model: req.Model,
Temperature: req.Temperature,
MaxTokens: req.MaxTokens,
Stream: req.Stream,
Tools: req.Tools,
ToolChoice: req.ToolChoice,
DisableThinking: req.DisableThinking,
}
if len(req.Messages) > 0 {
apiReq.Messages = make([]agentAPI.Message, len(req.Messages))
for i, m := range req.Messages {
msg := agentAPI.Message{
Role: m.Role,
Content: m.Content,
ReasoningContent: m.ReasoningContent,
ToolCallID: m.ToolCallID,
}
if len(m.ToolCalls) > 0 {
msg.ToolCalls = make([]agentAPI.ToolCall, len(m.ToolCalls))
for j, tc := range m.ToolCalls {
msg.ToolCalls[j] = agentAPI.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
}
}
apiReq.Messages[i] = msg
}
}
resp, err := p.Chat(ctx, apiReq)
if err != nil {
return nil, err
}
out := &LLMCompletionResponse{
Content: resp.Content,
ReasoningContent: resp.ReasoningContent,
FinishReason: resp.FinishReason,
TokenUsage: LLMTokenUsage{
Prompt: resp.TokenUsage.Prompt,
Completion: resp.TokenUsage.Completion,
Total: resp.TokenUsage.Total,
},
}
for _, tc := range resp.ToolCalls {
out.ToolCalls = append(out.ToolCalls, LLMToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments})
}
return out, nil
}
func (l *llmImpl) ReloadFromConfig() error {
if l.mgr == nil || l.cfgReg == nil || l.lua == nil {
return nil
}
cfg := l.cfgReg.ToConfig()
if cfg == nil {
return nil
}
l.mgr.Reset()
for _, src := range cfg.LLM.Sources {
key := src.APIKey
if key == "" {
key = l.baseAPIKey
}
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
Model: src.Model,
BaseURL: src.BaseURL,
APIKey: key,
Temperature: cfg.LLM.Temperature,
MaxTokens: cfg.LLM.MaxTokens,
ContextWindow: src.ContextWindow,
}, l.lua, src.Name, src.Adapter)
l.mgr.Register(src.Name, provider)
}
if cfg.LLM.Provider != "" {
_ = l.mgr.SetDefault(cfg.LLM.Provider)
}
return nil
}
var _ LLMAPI = (*llmImpl)(nil)

View File

@ -2,12 +2,26 @@ package sdk
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type MemoryAPI = pubsdk.MemoryAPI
// MemoryAPI 是内置插件使用的全量图记忆接口。
type MemoryAPI interface {
pubsdk.MemoryAPI
// GraphData 返回整个知识图谱的完整快照。
GraphData() (map[string]interface{}, error)
}
type Entity = pubsdk.Entity
type Relation = pubsdk.Relation
type Triple = pubsdk.Triple
type TextMemoryAPI = pubsdk.TextMemoryAPI
// TextMemoryAPI 是内置插件使用的全量文本记忆接口。
type TextMemoryAPI interface {
pubsdk.TextMemoryAPI
// RecentEvents 返回最近 n 条记忆事件。
RecentEvents(n int) ([]TextEvent, error)
// Stats 返回文本记忆的运行统计。
Stats() map[string]interface{}
}
type TextEvent = pubsdk.TextEvent
type DocMemoryAPI = pubsdk.DocMemoryAPI

View File

@ -50,6 +50,11 @@ func (m *graphMemory) Purge(criteria map[string]string, mode string) (int, error
return m.db.Purge(criteria, mode)
}
func (m *graphMemory) GraphData() (map[string]interface{}, error) {
if m.db == nil { return map[string]interface{}{}, nil }
return m.db.GraphData()
}
type textMemoryImpl struct{ tm *text.Memory }
func NewTextMemory(tm *text.Memory) TextMemoryAPI { return &textMemoryImpl{tm: tm} }
@ -61,6 +66,22 @@ func (m *textMemoryImpl) Append(evt TextEvent) error {
})
}
func (m *textMemoryImpl) RecentEvents(n int) ([]TextEvent, error) {
if m.tm == nil { return nil, nil }
got, err := m.tm.RecentEvents(n)
if err != nil { return nil, err }
out := make([]TextEvent, len(got))
for i, e := range got {
out[i] = TextEvent{Role: e.Source, Content: e.Input, Timestamp: e.Timestamp, Channel: e.AgentID}
}
return out, nil
}
func (m *textMemoryImpl) Stats() map[string]interface{} {
if m.tm == nil { return map[string]interface{}{} }
return m.tm.Stats()
}
type docMemoryImpl struct{ ds *doc.Store }
func NewDocMemory(ds *doc.Store) DocMemoryAPI { return &docMemoryImpl{ds: ds} }

View File

@ -57,6 +57,12 @@ type DisabledPluginInfo struct {
DisabledBy string `json:"disabled_by"`
}
// PluginMeta is the display-name metadata for a plugin (from plg.json / RegisterPluginMeta).
type PluginMeta struct {
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
}
type PluginManager interface {
ListLoadedPlugins() []string
ListDisabledPlugins() []DisabledPluginInfo
@ -64,18 +70,45 @@ type PluginManager interface {
DisablePlugin(name, by string) error
EnablePlugin(name string) error
ReloadPlugins() (string, error)
PluginMetas() map[string]PluginMeta
PluginDir() string
}
type PluginSDK struct {
*pubsdk.PluginSDK
settings SettingsAPI
memory MemoryAPI
textMem TextMemoryAPI
docMem DocMemoryAPI
know KnowledgeAPI
llm LLMAPI
iom *agentIO.IOManager
eventBus *events.Bus
logger *log.Logger
pluginMgr PluginManager
status StatusAPI
supervisor SupervisorAPI
skill SkillAPI
adapter AdapterAPI
tracker TrackerAPI
config ConfigAPI
tool ToolAPI
indexer IndexerAPI
}
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
// 以下访问器遮蔽公共 SDK 的同名方法,返回内置插件可用的全量接口。
func (s *PluginSDK) Settings() SettingsAPI { return s.settings }
func (s *PluginSDK) Memory() MemoryAPI { return s.memory }
func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem }
func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem }
func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know }
func (s *PluginSDK) LLM() LLMAPI { return s.llm }
// ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口,
// 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。
type ioAdapter struct{ iom *agentIO.IOManager }
@ -114,6 +147,15 @@ type SDKConfig struct {
RegOutput OutputChannelRegistrar
RegInput InputChannelRegistrar
PluginMgr PluginManager
Status StatusAPI
Supervisor SupervisorAPI
Skill SkillAPI
Adapter AdapterAPI
Tracker TrackerAPI
Config ConfigAPI
Tool ToolAPI
Indexer IndexerAPI
}
func New(name string, cfg SDKConfig) *PluginSDK {
@ -131,13 +173,38 @@ func New(name string, cfg SDKConfig) *PluginSDK {
base.SetLLMAPI(cfg.LLM)
return &PluginSDK{
PluginSDK: base,
settings: cfg.Settings,
memory: cfg.Memory,
textMem: cfg.TextMemory,
docMem: cfg.DocMemory,
know: cfg.Knowledge,
llm: cfg.LLM,
iom: cfg.IOManager,
eventBus: cfg.EventBus,
logger: log.Default(),
pluginMgr: cfg.PluginMgr,
status: cfg.Status,
supervisor: cfg.Supervisor,
skill: cfg.Skill,
adapter: cfg.Adapter,
tracker: cfg.Tracker,
config: cfg.Config,
tool: cfg.Tool,
indexer: cfg.Indexer,
}
}
func (s *PluginSDK) Status() StatusAPI { return s.status }
func (s *PluginSDK) Supervisor() SupervisorAPI { return s.supervisor }
func (s *PluginSDK) Skill() SkillAPI { return s.skill }
func (s *PluginSDK) Adapter() AdapterAPI { return s.adapter }
func (s *PluginSDK) Tracker() TrackerAPI { return s.tracker }
func (s *PluginSDK) Config() ConfigAPI { return s.config }
func (s *PluginSDK) Tool() ToolAPI { return s.tool }
func (s *PluginSDK) Indexer() IndexerAPI { return s.indexer }
func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) {
if s.iom != nil {
s.iom.InjectInputTo(source, channel, eventType, payload)

View File

@ -2,5 +2,13 @@ package sdk
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type SettingsAPI = pubsdk.SettingsAPI
// SettingsAPI 是内置插件使用的全量配置接口。
type SettingsAPI interface {
pubsdk.SettingsAPI
// DefsCore 返回核心配置表中匹配前缀的配置定义。
DefsCore(prefix string) []*ConfigDef
// DefsPlugin 返回另一个插件的配置定义。
DefsPlugin(plugin, prefix string) []*ConfigDef
}
type ConfigDef = pubsdk.ConfigDef

View File

@ -79,7 +79,7 @@ func (s *settingsImpl) Defs(prefix string) []*ConfigDef {
// ConfigDef = pubsdk.ConfigDef (type alias), so direct conversion works
cpy := ConfigDef{
Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description,
Category: d.Category, Options: d.Options,
Category: d.Category, Options: d.Options, Default: d.Default,
}
out[i] = &cpy
}
@ -97,6 +97,25 @@ func (s *settingsImpl) Plugins() []string {
result = append(result, names...)
return result
}
func (s *settingsImpl) DefsCore(prefix string) []*ConfigDef {
if s.reg == nil { return nil }
return mapDefs(s.reg.ListDefs(prefix))
}
func (s *settingsImpl) DefsPlugin(plugin, prefix string) []*ConfigDef {
if s.reg == nil { return nil }
return mapDefs(s.reg.PluginConfig(plugin).ListDefs(prefix))
}
func mapDefs(defs []*internalConfig.ConfigDef) []*ConfigDef {
out := make([]*ConfigDef, len(defs))
for i, d := range defs {
out[i] = &ConfigDef{
Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description,
Category: d.Category, Options: d.Options, Default: d.Default,
}
}
return out
}
func stringifyDefault(v interface{}) string {
if v == nil { return "" }

22
internal/sdk/skill.go Normal file
View File

@ -0,0 +1,22 @@
package sdk
// SkillAPI provides access to the skill manager.
type SkillAPI interface {
List() []*Skill
Get(name string) *Skill
Install(name, content string) error
Uninstall(name string) error
Toggle(name string, enabled bool) error
}
// Skill is a neutral description of an installed skill.
type Skill struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author,omitempty"`
Entry string `json:"entry,omitempty"`
Source string `json:"source,omitempty"`
Enabled bool `json:"enabled"`
RawContent string `json:"-"`
}

99
internal/sdk/status.go Normal file
View File

@ -0,0 +1,99 @@
package sdk
// StatusAPI provides a snapshot of the kernel runtime status.
type StatusAPI interface {
GetKernelStatus() *KernelStatus
}
// KernelStatus is the aggregated runtime snapshot of all kernel subsystems.
type KernelStatus struct {
Uptime string `json:"uptime"`
StartTime string `json:"start_time"`
AgentID string `json:"agent_id"`
Plugins []PluginInfo `json:"plugins"`
Tools []ToolDef `json:"tools"`
Channels []ChannelInfo `json:"channels"`
Memory MemoryStatus `json:"memory"`
Knowledge KnowledgeStatus `json:"knowledge"`
Documents DocumentStatus `json:"documents"`
TextMemory TextMemoryStatus `json:"text_memory"`
Social SocialStatus `json:"social"`
Skills SkillsStatus `json:"skills"`
LLM LLMStatus `json:"llm"`
Context ContextStatus `json:"context"`
Runtime RuntimeStatus `json:"runtime"`
Tracker TrackerStatus `json:"tracker"`
}
type PluginInfo struct {
Name string `json:"name"`
Loaded bool `json:"loaded"`
}
type ChannelInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Ready bool `json:"ready"`
}
type MemoryStatus struct {
Available bool `json:"available"`
EntityCount int `json:"entity_count"`
RelationCount int `json:"relation_count"`
EntityTypes int `json:"entity_types"`
}
type KnowledgeStatus struct {
Available bool `json:"available"`
ItemCount int `json:"item_count"`
Items []string `json:"items,omitempty"`
}
type DocumentStatus struct {
Available bool `json:"available"`
DocCount int `json:"doc_count"`
VectorCount int `json:"vector_count"`
}
type TextMemoryStatus struct {
Available bool `json:"available"`
FileCount int `json:"file_count"`
}
type SocialStatus struct {
Available bool `json:"available"`
PersonCount int `json:"person_count"`
}
type SkillsStatus struct {
Available bool `json:"available"`
SkillList []string `json:"skill_list,omitempty"`
}
type LLMStatus struct {
Available bool `json:"available"`
Provider string `json:"provider,omitempty"`
Sources int `json:"sources,omitempty"`
}
type ContextStatus struct {
EventCount int `json:"event_count,omitempty"`
}
type RuntimeStatus struct {
Goroutines int `json:"goroutines"`
MemoryMB int64 `json:"memory_mb"`
GoVersion string `json:"go_version"`
}
type TrackerStatus struct {
Available bool `json:"available"`
Dir string `json:"dir,omitempty"`
}

View File

@ -0,0 +1,25 @@
package sdk
import (
"time"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
// SupervisorAPI provides access to agent lifecycle management.
type SupervisorAPI interface {
ListAgents() []AgentStatus
GetAgentStatus(id string) (*AgentStatus, error)
PreActionSnapshot(id string) (*types.Snapshot, error)
RollbackAgent(id string, snapID string) error
}
// AgentStatus is a neutral snapshot of a supervised agent.
type AgentStatus struct {
ID types.AgentID `json:"id"`
State types.AgentState `json:"state"`
Health types.HealthStatus `json:"health"`
Uptime time.Duration `json:"uptime,omitempty"`
Network types.NetworkCheckResult `json:"network,omitempty"`
TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"`
}

20
internal/sdk/tool.go Normal file
View File

@ -0,0 +1,20 @@
package sdk
// ToolSource 描述内核工具注册表/执行器的只读视图(由 StageHost 实现)。
// 用接口而非具体类型,避免 sdk 依赖内核包(内核包反向依赖 sdk)。
type ToolSource interface {
GetToolDefs() []ToolDef
ToolDef(name string) *ToolDef
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
}
// ToolAPI exposes the kernel tool registry and executor
// (StageHost for plugin tools + IOManager for device/channel tools).
type ToolAPI interface {
// GetToolDefs returns all tools registered on the stage host (plugin tools).
GetToolDefs() []ToolDef
// GetAllTools returns all tools exposed by IO devices/channels.
GetAllTools() []ToolDef
// ExecuteTool executes a tool by name, resolving across the stage host first.
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
}

50
internal/sdk/tool_impl.go Normal file
View File

@ -0,0 +1,50 @@
package sdk
import (
"fmt"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
// toolImpl 桥接 StageHost(插件工具)与 IOManager(设备/通道工具)。
type toolImpl struct {
stageHost ToolSource
iom *agentIO.IOManager
}
func NewTool(sh ToolSource, iom *agentIO.IOManager) ToolAPI {
return &toolImpl{stageHost: sh, iom: iom}
}
func (t *toolImpl) GetToolDefs() []ToolDef {
if t.stageHost == nil {
return nil
}
return t.stageHost.GetToolDefs()
}
func (t *toolImpl) GetAllTools() []ToolDef {
if t.iom == nil {
return nil
}
defs := t.iom.GetAllTools()
out := make([]ToolDef, 0, len(defs))
for _, d := range defs {
out = append(out, ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters})
}
return out
}
func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
if t.stageHost != nil {
if def := t.stageHost.ToolDef(name); def != nil {
return t.stageHost.ExecuteTool(name, args)
}
}
if t.iom != nil {
return t.iom.ExecuteTool(name, args)
}
return nil, fmt.Errorf("tool %s not found", name)
}
var _ ToolAPI = (*toolImpl)(nil)

36
internal/sdk/tracker.go Normal file
View File

@ -0,0 +1,36 @@
package sdk
import "time"
// TrackerAPI provides access to the file change tracker / rollback.
type TrackerAPI interface {
ChangeSets() []*ChangeSet
Rollback() error
Stats() map[string]interface{}
HasChanges() bool
}
type ChangeType string
const (
ChangeFileCreated ChangeType = "created"
ChangeFileModified ChangeType = "modified"
ChangeFileDeleted ChangeType = "deleted"
)
type FileChange struct {
Path string `json:"path"`
Type ChangeType `json:"type"`
SizeBefore int64 `json:"size_before,omitempty"`
SizeAfter int64 `json:"size_after,omitempty"`
HashBefore string `json:"hash_before,omitempty"`
HashAfter string `json:"hash_after,omitempty"`
Content []byte `json:"-"` // 回滚用原始内容,不参与 JSON 序列化
}
type ChangeSet struct {
ID string `json:"id"`
Action string `json:"action"`
Timestamp time.Time `json:"timestamp"`
Files []FileChange `json:"files"`
}

View File

@ -8,18 +8,12 @@ import (
"sort"
"strings"
"sync"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
type Skill struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author,omitempty"`
Entry string `json:"entry,omitempty"`
Source string `json:"source,omitempty"`
Enabled bool `json:"enabled"`
RawContent string `json:"-"`
}
// Skill 已迁入内置 SDK,此处保留别名以兼容现有调用方。
type Skill = sdk.Skill
type Manager struct {
mu sync.RWMutex
@ -195,6 +189,9 @@ func (m *Manager) GetInjectedPrompt() string {
return strings.Join(parts, "\n\n")
}
// Manager 直接满足内置 SDK 的 SkillAPI(复用优先,无需独立适配器)。
var _ sdk.SkillAPI = (*Manager)(nil)
func extractDescription(content string) string {
lines := strings.Split(content, "\n")
for _, line := range lines {

View File

@ -9,6 +9,7 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/network"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
@ -224,11 +225,5 @@ func (d *Daemon) RollbackAgent(id types.AgentID, snapID types.SnapshotID) error
return fmt.Errorf("no tracker available for rollback")
}
type AgentStatus struct {
ID types.AgentID `json:"id"`
State types.AgentState `json:"state"`
Health types.HealthStatus `json:"health"`
Uptime time.Duration `json:"uptime,omitempty"`
Network types.NetworkCheckResult `json:"network,omitempty"`
TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"`
}
// AgentStatus 已迁入内置 SDK,此处保留别名以兼容现有调用方。
type AgentStatus = sdk.AgentStatus

View File

@ -0,0 +1,48 @@
package supervisor
import (
"errors"
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
// sdkAdapter 实现 sdk.SupervisorAPI,将字符串签名的中立接口桥接到
// Daemon 的强类型签名(types.AgentID / types.SnapshotID)。
type sdkAdapter struct {
d *Daemon
}
func NewSDKAdapter(d *Daemon) sdk.SupervisorAPI {
return &sdkAdapter{d: d}
}
func (a *sdkAdapter) ListAgents() []sdk.AgentStatus {
if a.d == nil {
return nil
}
return a.d.ListAgents()
}
func (a *sdkAdapter) GetAgentStatus(id string) (*sdk.AgentStatus, error) {
if a.d == nil {
return nil, errors.New("supervisor not available")
}
return a.d.GetAgentStatus(types.AgentID(id))
}
func (a *sdkAdapter) PreActionSnapshot(id string) (*types.Snapshot, error) {
if a.d == nil {
return nil, errors.New("supervisor not available")
}
return a.d.PreActionSnapshot(types.AgentID(id))
}
func (a *sdkAdapter) RollbackAgent(id string, snapID string) error {
if a.d == nil {
return errors.New("supervisor not available")
}
return a.d.RollbackAgent(types.AgentID(id), types.SnapshotID(snapID))
}
var _ sdk.SupervisorAPI = (*sdkAdapter)(nil)

View File

@ -7,32 +7,21 @@ import (
"os"
"path/filepath"
"time"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
type ChangeType string
// DTO 已迁入内置 SDK,此处保留别名以兼容现有调用方。
type ChangeType = sdk.ChangeType
const (
ChangeFileCreated ChangeType = "created"
ChangeFileModified ChangeType = "modified"
ChangeFileDeleted ChangeType = "deleted"
ChangeFileCreated = sdk.ChangeFileCreated
ChangeFileModified = sdk.ChangeFileModified
ChangeFileDeleted = sdk.ChangeFileDeleted
)
type FileChange struct {
Path string `json:"path"`
Type ChangeType `json:"type"`
SizeBefore int64 `json:"size_before,omitempty"`
SizeAfter int64 `json:"size_after,omitempty"`
HashBefore string `json:"hash_before,omitempty"`
HashAfter string `json:"hash_after,omitempty"`
Content []byte `json:"-"` // stored separately, not in JSON
}
type ChangeSet struct {
ID string `json:"id"`
Action string `json:"action"`
Timestamp time.Time `json:"timestamp"`
Files []FileChange `json:"files"`
}
type FileChange = sdk.FileChange
type ChangeSet = sdk.ChangeSet
func NewChangeSet(action string) *ChangeSet {
return &ChangeSet{

View File

@ -11,6 +11,8 @@ import (
"strings"
"sync"
"time"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
type Tracker struct {
@ -302,3 +304,6 @@ func (t *Tracker) Stats() map[string]interface{} {
"max_changeset_age": t.maxChangesetAge.String(),
}
}
// Tracker 直接满足内置 SDK 的 TrackerAPI(复用优先,无需独立适配器)。
var _ sdk.TrackerAPI = (*Tracker)(nil)