diff --git a/cmd/homed/main.go b/cmd/homed/main.go index 077045c..c4a8335 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "path/filepath" + "strconv" "syscall" "time" @@ -15,6 +16,7 @@ import ( agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent" "gitcode.com/JianFeeeee/HomeAgent/internal/api" "gitcode.com/JianFeeeee/HomeAgent/config" + internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" "gitcode.com/JianFeeeee/HomeAgent/internal/events" "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" luapkg "gitcode.com/JianFeeeee/HomeAgent/internal/lua" @@ -105,6 +107,21 @@ func main() { return onebot.NewDevice(name, wsURL, accessToken, iom), nil }) + // === Config Registry (统一配置中心,供插件读写) === + cfgReg := internalConfig.NewConfigRegistry(filepath.Join(cfg.Daemon.DataDir, "settings.json")) + cfgReg.Register("core.daemon.listen_addr", cfg.Daemon.ListenAddr) + cfgReg.Register("core.daemon.data_dir", cfg.Daemon.DataDir) + cfgReg.Register("core.daemon.heartbeat_interval", cfg.Daemon.HeartbeatInterval.String()) + cfgReg.Register("core.daemon.check_interval", cfg.Daemon.CheckInterval.String()) + cfgReg.Register("core.daemon.log_level", cfg.Daemon.LogLevel) + cfgReg.Register("core.llm.provider", "lua_deepseek") + cfgReg.Register("core.llm.model", cfg.LLM.Model) + cfgReg.Register("core.llm.base_url", cfg.LLM.BaseURL) + cfgReg.Register("core.llm.temperature", strconv.FormatFloat(cfg.LLM.Temperature, 'f', 2, 64)) + cfgReg.Register("core.llm.max_tokens", strconv.Itoa(cfg.LLM.MaxTokens)) + pluginReg.SetConfigRegistry(cfgReg) + log.Printf("[homed] config registry active with %d keys", len(cfgReg.List(""))) + // === Supervisor === sup := supervisor.New(cfg) if err := sup.Start(); err != nil { @@ -300,6 +317,9 @@ func main() { if trk != nil { trk.Stop() } + if err := cfgReg.Flush(); err != nil { + log.Printf("[homed] flush config: %v", err) + } sup.Shutdown() log.Printf("[homed] stopped") } diff --git a/internal/config/registry.go b/internal/config/registry.go new file mode 100644 index 0000000..8ee314b --- /dev/null +++ b/internal/config/registry.go @@ -0,0 +1,133 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "sync" +) + +type ConfigRegistry struct { + mu sync.RWMutex + values map[string]interface{} + persistPath string + dirty bool +} + +func NewConfigRegistry(persistPath string) *ConfigRegistry { + r := &ConfigRegistry{ + values: make(map[string]interface{}), + persistPath: persistPath, + } + r.load() + return r +} + +func (r *ConfigRegistry) Register(key string, value interface{}) { + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.values[key]; !exists { + r.values[key] = value + } +} + +func (r *ConfigRegistry) RegisterDefault(key string, value interface{}) { + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.values[key]; !exists { + r.values[key] = value + } +} + +func (r *ConfigRegistry) Get(key string) (interface{}, error) { + r.mu.RLock() + defer r.mu.RUnlock() + v, ok := r.values[key] + if !ok { + return nil, fmt.Errorf("config key %q not found", key) + } + return v, nil +} + +func (r *ConfigRegistry) Set(key string, value interface{}) error { + r.mu.Lock() + defer r.mu.Unlock() + r.values[key] = value + r.dirty = true + return nil +} + +func (r *ConfigRegistry) List(prefix string) []string { + r.mu.RLock() + defer r.mu.RUnlock() + var keys []string + for k := range r.values { + if prefix == "" || strings.HasPrefix(k, prefix) { + keys = append(keys, k) + } + } + sort.Strings(keys) + return keys +} + +func (r *ConfigRegistry) Delete(key string) error { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.values, key) + r.dirty = true + return nil +} + +func (r *ConfigRegistry) Dump() map[string]interface{} { + r.mu.RLock() + defer r.mu.RUnlock() + cp := make(map[string]interface{}) + for k, v := range r.values { + cp[k] = v + } + return cp +} + +func (r *ConfigRegistry) Flush() error { + r.mu.RLock() + if !r.dirty { + r.mu.RUnlock() + return nil + } + r.mu.RUnlock() + + r.mu.Lock() + defer r.mu.Unlock() + + if r.persistPath == "" { + return nil + } + os.MkdirAll(filepath.Dir(r.persistPath), 0755) + data, err := json.MarshalIndent(r.values, "", " ") + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + if err := os.WriteFile(r.persistPath, data, 0644); err != nil { + return fmt.Errorf("write config: %w", err) + } + r.dirty = false + return nil +} + +func (r *ConfigRegistry) load() { + if r.persistPath == "" { + return + } + data, err := os.ReadFile(r.persistPath) + if err != nil { + return + } + var vals map[string]interface{} + if err := json.Unmarshal(data, &vals); err != nil { + return + } + r.values = vals +} diff --git a/internal/config/registry_test.go b/internal/config/registry_test.go new file mode 100644 index 0000000..57d06b1 --- /dev/null +++ b/internal/config/registry_test.go @@ -0,0 +1,105 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRegistryBasic(t *testing.T) { + r := NewConfigRegistry("") + r.Register("core.llm.model", "deepseek-v4-flash") + r.Register("plugin.qq.access_token", "abc123") + + val, err := r.Get("core.llm.model") + if err != nil { + t.Fatalf("Get error: %v", err) + } + if v, ok := val.(string); !ok || v != "deepseek-v4-flash" { + t.Fatalf("expected deepseek-v4-flash, got %v", val) + } + + keys := r.List("core") + if len(keys) != 1 || keys[0] != "core.llm.model" { + t.Fatalf("expected [core.llm.model], got %v", keys) + } + + r.Set("core.llm.model", "gpt-4") + val, _ = r.Get("core.llm.model") + if v, _ := val.(string); v != "gpt-4" { + t.Fatalf("expected gpt-4, got %v", val) + } +} + +func TestRegistryPersist(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "settings.json") + + r := NewConfigRegistry(path) + r.Register("core.log_level", "debug") + r.Set("plugin.test.key", 42) + if err := r.Flush(); err != nil { + t.Fatalf("Flush: %v", err) + } + + r2 := NewConfigRegistry(path) + val, err := r2.Get("plugin.test.key") + if err != nil { + t.Fatalf("Get after reload: %v", err) + } + if v, _ := val.(float64); v != 42 { + t.Fatalf("expected 42, got %v", val) + } +} + +func TestRegistryDelete(t *testing.T) { + r := NewConfigRegistry("") + r.Register("a.b", "1") + r.Register("a.c", "2") + r.Delete("a.b") + keys := r.List("a") + if len(keys) != 1 || keys[0] != "a.c" { + t.Fatalf("expected [a.c], got %v", keys) + } +} + +func TestRegistryDump(t *testing.T) { + r := NewConfigRegistry("") + r.Register("x", 1) + r.Register("y", "two") + dump := r.Dump() + if len(dump) != 2 { + t.Fatalf("expected 2 keys, got %d", len(dump)) + } +} + +func TestRegistryUnknownKey(t *testing.T) { + r := NewConfigRegistry("") + _, err := r.Get("nonexistent") + if err == nil { + t.Fatal("expected error for unknown key") + } +} + +func TestRegistryFlush(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "settings.json") + r := NewConfigRegistry(path) + r.Set("k", "v") + if err := r.Flush(); err != nil { + t.Fatalf("Flush: %v", err) + } + data, _ := os.ReadFile(path) + if len(data) == 0 { + t.Fatal("expected persisted data") + } +} + +func TestRegistryFlushIdempotent(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "settings.json") + r := NewConfigRegistry(path) + r.Set("k", "v") + r.Flush() + r.Flush() // second flush should not error +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 741a3e9..385cdc3 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -12,6 +12,7 @@ import ( "time" agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" + internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/sdk" ) @@ -149,6 +150,14 @@ type Registry struct { ioMgr *agentIO.IOManager factories map[string]NativeFactory // 名称匹配的插件使用原生实现 sdkAPIs map[string]*sdkAPI // SDK 插件 API 实例 + cfgReg *internalConfig.ConfigRegistry +} + +// SetConfigRegistry 注入全局配置注册表,使 SDK 插件可访问配置 +func (r *Registry) SetConfigRegistry(cr *internalConfig.ConfigRegistry) { + r.mu.Lock() + defer r.mu.Unlock() + r.cfgReg = cr } type sdkAPI struct { @@ -172,6 +181,15 @@ func (r *Registry) RegisterPluginAPI(api *sdk.PluginAPI) error { if _, ok := r.sdkAPIs[api.Name]; ok { return fmt.Errorf("sdk api %s already registered", api.Name) } + + // 注入 SettingsAPI(读取/修改核心与其他插件配置) + if r.cfgReg != nil { + api.SetSettings(&pluginSettings{ + reg: r.cfgReg, + name: api.Name, + }) + } + r.sdkAPIs[api.Name] = &sdkAPI{ api: api, tools: api.Tools(), @@ -187,6 +205,24 @@ func (r *Registry) RegisterPluginAPI(api *sdk.PluginAPI) error { return nil } +// pluginSettings 实现 SettingsAPI,以插件名为命名空间 +type pluginSettings struct { + reg *internalConfig.ConfigRegistry + name string +} + +func (s *pluginSettings) Get(key string) (interface{}, error) { + return s.reg.Get(key) +} + +func (s *pluginSettings) Set(key string, value interface{}) error { + return s.reg.Set(key, value) +} + +func (s *pluginSettings) List(prefix string) ([]string, error) { + return s.reg.List(prefix), nil +} + // GetAllSDKToolDefs 收集所有 SDK 插件的工具定义 func (r *Registry) GetAllSDKToolDefs() []sdk.ToolDef { r.mu.RLock() diff --git a/internal/plugin/sdk/api.go b/internal/plugin/sdk/api.go index f8d393c..536a472 100644 --- a/internal/plugin/sdk/api.go +++ b/internal/plugin/sdk/api.go @@ -72,6 +72,12 @@ type ToolDef struct { Parameters map[string]interface{} `json:"parameters"` } +type SettingsAPI interface { + Get(key string) (interface{}, error) + Set(key string, value interface{}) error + List(prefix string) ([]string, error) +} + type MemoryAPI interface { Recall(query string, topK int) ([]MemItem, error) Commit(triples []map[string]string) error @@ -98,6 +104,7 @@ type PluginAPI struct { eventBus EventBus memAPI MemoryAPI knowAPI KnowledgeAPI + settAPI SettingsAPI } func NewPluginAPI(name, version string, bus EventBus, mem MemoryAPI, know KnowledgeAPI) *PluginAPI { @@ -153,8 +160,10 @@ func (p *PluginAPI) StageHandlers(stage Stage) []StageHandler { return p.stages[stage] } -func (p *PluginAPI) Memory() MemoryAPI { return p.memAPI } +func (p *PluginAPI) Memory() MemoryAPI { return p.memAPI } func (p *PluginAPI) Knowledge() KnowledgeAPI { return p.knowAPI } +func (p *PluginAPI) Settings() SettingsAPI { return p.settAPI } +func (p *PluginAPI) SetSettings(s SettingsAPI) { p.settAPI = s } func AllStages() map[Stage]bool { return map[Stage]bool{