refactor: move webui.listen_addr from core.daemon to webui category

This commit is contained in:
JianFeeeee
2026-07-14 11:22:46 +08:00
parent 28dc095dc5
commit c7e22fbe30
15 changed files with 1813 additions and 379 deletions

View File

@ -42,7 +42,7 @@ import (
func main() { func main() {
dataDir := flag.String("data", "", "data directory (default: auto-detect next to binary)") dataDir := flag.String("data", "", "data directory (default: auto-detect next to binary)")
httpAddr := flag.String("webui", ":8080", "webui listen address") httpAddr := flag.String("webui", "", "webui listen address (default: webui.listen_addr from config)")
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)") cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
flag.Parse() flag.Parse()
@ -395,7 +395,11 @@ func main() {
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂) // 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
cli.DefaultSocket = *cliSocket cli.DefaultSocket = *cliSocket
openclaw.SkillsDir = filepath.Join(cfg.Daemon.DataDir, "skills") openclaw.SkillsDir = filepath.Join(cfg.Daemon.DataDir, "skills")
webui.Configure(*httpAddr, webuiListenAddr := *httpAddr
if webuiListenAddr == "" {
webuiListenAddr = cfgReg.GetString("webui.listen_addr", ":8080")
}
webui.Configure(webuiListenAddr,
sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg, evBus, agent, sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg, evBus, agent,
providerMgr, baseAPIKey, providerMgr, baseAPIKey,
) )

View File

@ -299,6 +299,37 @@ s.LLM().ListSources() []SourceInfo
s.LLM().SetSource(name string) error s.LLM().SetSource(name string) error
``` ```
#### Event Subscription (built-in plugins)
```go
// Subscribe to system events, returns unsubscribe function
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
log.Printf("Tool was called: %v", evt.Payload)
})
defer unsub()
// Publish event
s.Publish(&events.Event{
Type: "custom_event",
Payload: map[string]interface{}{"key": "value"},
})
```
#### IO Channel Management (built-in plugins)
```go
// Register a channel (bind device driver)
s.RegisterChannel("mydevice", deviceImpl)
// Unregister a channel
s.UnregisterChannel("mydevice")
// List all channels
channels := s.ListChannels()
```
> **Note**: `Subscribe`, `Publish`, `RegisterChannel`, `UnregisterChannel`, `ListChannels`, `InjectInput`, `InjectInputSync`, `InjectInterrupt`, `InjectTextSync`, `InjectTextSyncNoMemory`, `OutputChan` are only available in built-in plugins (`internal/sdk` package). External dynamic plugins should use the public APIs: `InjectText`, `InjectInterruptText`, `InjectTextNoMemory`.
--- ---
## 3. Lua Plugin Development in Detail ## 3. Lua Plugin Development in Detail
@ -367,6 +398,8 @@ When running inside the kernel, `sdk.*` global variables are injected by the Go
| `sdk.http.get(url)` | HTTP GET request (`-- !impl`) | | `sdk.http.get(url)` | HTTP GET request (`-- !impl`) |
| `sdk.http.post(url, body, content_type)` | HTTP POST request (`-- !impl`) | | `sdk.http.post(url, body, content_type)` | HTTP POST request (`-- !impl`) |
> **Note**: Lua plugin's `sdk.register_stage` callback currently only receives `raw_message`, `user_id`, `phase` fields. The functionality is limited. For complex stage handling logic, use Go plugins.
--- ---
## 4. Built-in Plugins ## 4. Built-in Plugins

View File

@ -300,6 +300,37 @@ s.LLM().ListSources() []SourceInfo
s.LLM().SetSource(name string) error s.LLM().SetSource(name string) error
``` ```
#### 事件订阅(内置插件)
```go
// 订阅系统事件,返回取消订阅函数
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
log.Printf("工具被调用: %v", evt.Payload)
})
defer unsub()
// 发布事件
s.Publish(&events.Event{
Type: "custom_event",
Payload: map[string]interface{}{"key": "value"},
})
```
#### IO 通道管理(内置插件)
```go
// 注册通道(绑定设备驱动)
s.RegisterChannel("mydevice", deviceImpl)
// 注销通道
s.UnregisterChannel("mydevice")
// 列出所有通道
channels := s.ListChannels()
```
> **注意**`Subscribe`、`Publish`、`RegisterChannel`、`UnregisterChannel`、`ListChannels`、`InjectInput`、`InjectInputSync`、`InjectInterrupt`、`InjectTextSync`、`InjectTextSyncNoMemory`、`OutputChan` 这些方法仅在内置插件中可用(`internal/sdk` 包),外部动态插件无法访问。外部插件请使用 `InjectText`、`InjectInterruptText`、`InjectTextNoMemory` 等公共 API。
--- ---
## 三、Lua 插件开发详解 ## 三、Lua 插件开发详解
@ -368,6 +399,8 @@ lua main.lua
| `sdk.http.get(url)` | HTTP GET 请求(`-- !impl` | | `sdk.http.get(url)` | HTTP GET 请求(`-- !impl` |
| `sdk.http.post(url, body, content_type)` | HTTP POST 请求(`-- !impl` | | `sdk.http.post(url, body, content_type)` | HTTP POST 请求(`-- !impl` |
> **注意**Lua 插件的 `sdk.register_stage` 阶段回调目前仅传递 `raw_message`、`user_id`、`phase` 三个字段,功能受限。复杂的阶段处理逻辑建议使用 Go 插件。
--- ---
## 四、内置插件 ## 四、内置插件

2
go.mod
View File

@ -11,6 +11,8 @@ require (
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 // direct require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 // direct
replace gitcode.com/JianFeeeee/homeagent-sdk => ../homeagentsdk
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect

View File

@ -1522,6 +1522,9 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
prompt += "\n\n" + a.indexer.BuildToolPrompt() prompt += "\n\n" + a.indexer.BuildToolPrompt()
} }
// 动态工具目录
prompt += a.buildToolCatalog()
return prompt return prompt
} }
@ -1549,6 +1552,41 @@ func cleanParams(params map[string]interface{}) map[string]interface{} {
return cleaned return cleaned
} }
func (a *Agent) buildToolCatalog() string {
defs := a.buildToolDefs()
if len(defs) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("\n\n【可用工具列表】")
seen := make(map[string]bool)
for _, d := range defs {
t, ok := d.(map[string]interface{})
if !ok {
continue
}
fn, ok := t["function"].(map[string]interface{})
if !ok {
continue
}
name, _ := fn["name"].(string)
if name == "" || seen[name] {
continue
}
seen[name] = true
desc, _ := fn["description"].(string)
sb.WriteString(fmt.Sprintf("\n- %s", name))
if desc != "" {
// only first 80 chars of description
if len(desc) > 80 {
desc = desc[:80] + "..."
}
sb.WriteString(": " + desc)
}
}
return sb.String()
}
func (a *Agent) buildToolDefs() []interface{} { func (a *Agent) buildToolDefs() []interface{} {
var tools []interface{} var tools []interface{}

View File

@ -2,6 +2,7 @@ package core
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
@ -85,6 +86,7 @@ func inferToolPlugin(name string) string {
// - 只读操作先调用 ctx.RLock() / defer ctx.RUnlock() // - 只读操作先调用 ctx.RLock() / defer ctx.RUnlock()
// - 写操作(如设置 ctx.Response先调用 ctx.Lock() / defer ctx.Unlock() // - 写操作(如设置 ctx.Response先调用 ctx.Lock() / defer ctx.Unlock()
// 如果任意 handler 设置了 Response后续 handler 可通过 ctx.IsResponded() 判断后提前返回。 // 如果任意 handler 设置了 Response后续 handler 可通过 ctx.IsResponded() 判断后提前返回。
// handler 返回的 error 会被收集到 ctx.Errors 中并记录日志,不会中断其他 handler 的执行。
func (h *StageHost) RunStage(stage sdk.Stage, ctx *sdk.StageContext) { func (h *StageHost) RunStage(stage sdk.Stage, ctx *sdk.StageContext) {
h.mu.RLock() h.mu.RLock()
handlers := h.stages[stage] handlers := h.stages[stage]
@ -93,14 +95,29 @@ func (h *StageHost) RunStage(stage sdk.Stage, ctx *sdk.StageContext) {
return return
} }
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, len(handlers))
for _, handler := range handlers { for _, handler := range handlers {
wg.Add(1) wg.Add(1)
go func(fn sdk.StageHandler) { go func(fn sdk.StageHandler) {
defer wg.Done() defer wg.Done()
fn(ctx) if err := fn(ctx); err != nil {
errCh <- err
}
}(handler) }(handler)
} }
wg.Wait() wg.Wait()
close(errCh)
var errs []string
for err := range errCh {
errs = append(errs, err.Error())
log.Printf("[stage] %s handler error: %v", stage, err)
}
if len(errs) > 0 {
ctx.Lock()
ctx.Errors = append(ctx.Errors, errs...)
ctx.Unlock()
}
} }
func (h *StageHost) ToolCount() int { func (h *StageHost) ToolCount() int {

View File

@ -68,32 +68,32 @@ func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
} }
type MemoryStatus struct { type MemoryStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
EntityCount int `json:"entity_count,omitempty"` EntityCount int `json:"entity_count"`
RelationCount int `json:"relation_count,omitempty"` RelationCount int `json:"relation_count"`
EntityTypes int `json:"entity_types,omitempty"` EntityTypes int `json:"entity_types"`
} }
type KnowledgeStatus struct { type KnowledgeStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
ItemCount int `json:"item_count,omitempty"` ItemCount int `json:"item_count"`
Items []string `json:"items,omitempty"` Items []string `json:"items,omitempty"`
} }
type DocumentStatus struct { type DocumentStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
DocCount int `json:"doc_count,omitempty"` DocCount int `json:"doc_count"`
VectorCount int `json:"vector_count,omitempty"` VectorCount int `json:"vector_count"`
} }
type TextMemoryStatus struct { type TextMemoryStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
FileCount int `json:"file_count,omitempty"` FileCount int `json:"file_count"`
} }
type SocialStatus struct { type SocialStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
PersonCount int `json:"person_count,omitempty"` PersonCount int `json:"person_count"`
} }
type SkillsStatus struct { type SkillsStatus struct {

View File

@ -298,7 +298,7 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) {
set := func(k, v string) { stmt.Exec(k, v) } set := func(k, v string) { stmt.Exec(k, v) }
set("core.daemon.listen_addr", ":8080") set("webui.listen_addr", ":8080")
set("core.daemon.data_dir", dataDir) set("core.daemon.data_dir", dataDir)
set("core.daemon.heartbeat_interval", "15s") set("core.daemon.heartbeat_interval", "15s")
set("core.daemon.check_interval", "30s") set("core.daemon.check_interval", "30s")
@ -349,26 +349,11 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) {
set("core.agent.distill_interval", "30m") set("core.agent.distill_interval", "30m")
set("core.agent.workdir", "") set("core.agent.workdir", "")
set("core.agent.system_prompt", `你是 HomeAgent一个持续运行的个人管家。 set("core.agent.system_prompt", `你是 HomeAgent一个持续运行的个人管家。
你的每次回复会自动发送到当前输出通道(默认=输入源),无需额外工具。 你的回复默认发送到用户的输入来源,无需额外工具。
如需切换回复通道,使用 output_set_channel 如需异步发送消息到其他通道,使用 output_send
如需异步发送消息或通知,使用 output_send 指定通道和内容 使用 output_list_channels 查看可用通道
使用 output_list_channels 查看可用通道及其能力。
可用工具列表会由系统自动传入,按需使用即可。以下是你尤其需要关注的几类工具 当用户上传图片或音频时,系统自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具
- memory_* — 图记忆(长期记忆,记录和查询个人信息/事实)
- knowledge_* — 知识库(查阅预设知识文档)
- doc_* — 文档记忆(近期对话的存档,查询后自动清除)
- person_* — 人物特质与社交关系网
- llm_* — LLM 源管理(列出/切换模型提供商)
- output_* — 输出通道管理(切换/发送消息)
- timer_set — 设置定时提醒
- plgreload — 热重载插件
- spawn_child — 生成子 Agent 执行独立任务
- describe_image — 描述用户上传的图片
- transcribe_audio — 转写用户上传的音频
- ocr_image — 识别图片中的文字
当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请使用上述工具。
回复你的真实想法,用自然语言与用户交流。`) 回复你的真实想法,用自然语言与用户交流。`)
@ -387,7 +372,7 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) {
func (r *ConfigRegistry) seedCoreDefs(dataDir string) { func (r *ConfigRegistry) seedCoreDefs(dataDir string) {
reg := func(d ConfigDef) { r.defs[d.Key] = &d } reg := func(d ConfigDef) { r.defs[d.Key] = &d }
reg(ConfigDef{Key: "core.daemon.listen_addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "WebUI HTTP 监听地址", Category: "daemon"}) reg(ConfigDef{Key: "webui.listen_addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "WebUI HTTP 监听地址", Category: "webui"})
reg(ConfigDef{Key: "core.daemon.data_dir", Default: dataDir, Type: "string", DisplayName: "数据目录", Description: "数据存储根目录", Category: "daemon"}) reg(ConfigDef{Key: "core.daemon.data_dir", Default: dataDir, Type: "string", DisplayName: "数据目录", Description: "数据存储根目录", Category: "daemon"})
reg(ConfigDef{Key: "core.daemon.heartbeat_interval", Default: "15s", Type: "duration", DisplayName: "心跳间隔", Description: "Agent 心跳检查间隔", Category: "daemon"}) reg(ConfigDef{Key: "core.daemon.heartbeat_interval", Default: "15s", Type: "duration", DisplayName: "心跳间隔", Description: "Agent 心跳检查间隔", Category: "daemon"})
reg(ConfigDef{Key: "core.daemon.check_interval", Default: "30s", Type: "duration", DisplayName: "检查间隔", Description: "网络状态检查间隔", Category: "daemon"}) reg(ConfigDef{Key: "core.daemon.check_interval", Default: "30s", Type: "duration", DisplayName: "检查间隔", Description: "网络状态检查间隔", Category: "daemon"})
@ -568,7 +553,6 @@ func (r *ConfigRegistry) ToConfig() *types.Config {
return b return b
} }
cfg.Daemon.ListenAddr = read("core.daemon.listen_addr", cfg.Daemon.ListenAddr)
cfg.Daemon.DataDir = read("core.daemon.data_dir", cfg.Daemon.DataDir) cfg.Daemon.DataDir = read("core.daemon.data_dir", cfg.Daemon.DataDir)
cfg.Daemon.HeartbeatInterval = readDur("core.daemon.heartbeat_interval", cfg.Daemon.HeartbeatInterval) cfg.Daemon.HeartbeatInterval = readDur("core.daemon.heartbeat_interval", cfg.Daemon.HeartbeatInterval)
cfg.Daemon.CheckInterval = readDur("core.daemon.check_interval", cfg.Daemon.CheckInterval) cfg.Daemon.CheckInterval = readDur("core.daemon.check_interval", cfg.Daemon.CheckInterval)

View File

@ -168,8 +168,8 @@ func TestSeedDefaultsToConfig(t *testing.T) {
// Reconstruct config from DB // Reconstruct config from DB
cfg2 := r.ToConfig() cfg2 := r.ToConfig()
if cfg2.Daemon.ListenAddr != ":8080" { if v := r.GetString("webui.listen_addr", ""); v != ":8080" {
t.Fatalf("expected :8080, got %s", cfg2.Daemon.ListenAddr) t.Fatalf("expected :8080, got %s", v)
} }
if cfg2.LLM.Provider != "deepseek" { if cfg2.LLM.Provider != "deepseek" {
t.Fatalf("expected deepseek, got %s", cfg2.LLM.Provider) t.Fatalf("expected deepseek, got %s", cfg2.LLM.Provider)

View File

@ -4,7 +4,6 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -324,42 +323,24 @@ func (v *VM) GetAdapterHeaders(name string) map[string]string {
} }
func (v *VM) writeBundledAdapters() error { func (v *VM) writeBundledAdapters() error {
// Try multiple paths for compatibility known := []string{
tryPaths := []string{"adapters", ".", "lua/adapters"} "openai", "anthropic", "deepseek", "gemini",
var entries []fs.DirEntry "github", "groq", "mistral", "ollama",
var err error
for _, p := range tryPaths {
entries, err = bundledAdapters.ReadDir(p)
if err == nil && len(entries) > 0 {
break
}
} }
if err != nil || len(entries) == 0 { for _, name := range known {
return nil srcPath := "adapters/" + name + ".lua"
} dstPath := filepath.Join(v.adapterDir, name+".lua")
for _, entry := range entries {
if entry.IsDir() {
continue
}
if filepath.Ext(entry.Name()) != ".lua" {
continue
}
dstPath := filepath.Join(v.adapterDir, entry.Name())
if _, err := os.Stat(dstPath); err == nil { if _, err := os.Stat(dstPath); err == nil {
continue continue
} }
data, err := bundledAdapters.ReadFile(filepath.Join("adapters", entry.Name())) data, err := bundledAdapters.ReadFile(srcPath)
if err != nil { if err != nil {
// try alternative paths continue
data, err = bundledAdapters.ReadFile(entry.Name())
if err != nil {
continue
}
} }
if err := os.WriteFile(dstPath, data, 0644); err != nil { if err := os.WriteFile(dstPath, data, 0644); err != nil {
return fmt.Errorf("write %s: %w", entry.Name(), err) return fmt.Errorf("write %s: %w", name+".lua", err)
} }
fmt.Printf("[lua] installed bundled adapter: %s\n", entry.Name()) fmt.Printf("[lua] installed bundled adapter: %s\n", name+".lua")
} }
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -13,6 +14,9 @@ import (
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
) )
// isWindows 缓存运行时检测结果
var isWindows = runtime.GOOS == "windows"
// shellUnquote 拆解命令字符串,处理单引号/双引号包裹的参数 // shellUnquote 拆解命令字符串,处理单引号/双引号包裹的参数
func shellUnquote(s string) []string { func shellUnquote(s string) []string {
var args []string var args []string
@ -152,7 +156,12 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
parts := shellUnquote(command) // Windows 上预置 chcp 65001 确保控制台输出为 UTF-8避免 GBK 乱码
execCmd := command
if isWindows {
execCmd = "chcp 65001>nul & " + command
}
parts := shellUnquote(execCmd)
if len(parts) == 0 { if len(parts) == 0 {
return map[string]interface{}{"error": "command is required"}, nil return map[string]interface{}{"error": "command is required"}, nil
} }

File diff suppressed because one or more lines are too long

View File

@ -950,7 +950,7 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
} }
}() }()
subTypes := []string{"agent_output", "reasoning", "agent_error"} subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call"}
for _, t := range subTypes { for _, t := range subTypes {
t2 := t t2 := t
_ = h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) { _ = h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {

View File

@ -34,7 +34,7 @@ func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
sup := supervisor.New(cfg) sup := supervisor.New(cfg)
sup.Start() sup.Start()
return NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, events.NewBus(), nil), sup return NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, ""), sup
} }
func TestAuthMiddleware(t *testing.T) { func TestAuthMiddleware(t *testing.T) {
@ -194,7 +194,7 @@ func TestHandleKnowledgeSearch(t *testing.T) {
sup.Start() sup.Start()
defer sup.Shutdown() defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -226,7 +226,7 @@ func TestHandleKnowledgeCreate(t *testing.T) {
sup.Start() sup.Start()
defer sup.Shutdown() defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
body := `{"name":"new_doc","content":"fresh content"}` body := `{"name":"new_doc","content":"fresh content"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body)) req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body))
@ -291,7 +291,7 @@ func TestHandleTrackerStats(t *testing.T) {
sup.Start() sup.Start()
defer sup.Shutdown() defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, tr, nil, nil, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, tr, nil, nil, events.NewBus(), nil, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -458,7 +458,7 @@ func TestSettingsAPIFlow(t *testing.T) {
cfgReg := internalConfig.NewConfigRegistry("") cfgReg := internalConfig.NewConfigRegistry("")
cfgReg.Register("core.llm.model", "deepseek-v4-flash") cfgReg.Register("core.llm.model", "deepseek-v4-flash")
cfgReg.Register("core.llm.base_url", "https://api.deepseek.com") cfgReg.Register("core.llm.base_url", "https://api.deepseek.com")
cfgReg.Register("core.daemon.listen_addr", ":8080") cfgReg.Register("webui.listen_addr", ":8080")
cfgReg.Register("plugin.qq.access_token", "secret123") cfgReg.Register("plugin.qq.access_token", "secret123")
sup := supervisor.New(&types.Config{ sup := supervisor.New(&types.Config{
@ -471,7 +471,7 @@ func TestSettingsAPIFlow(t *testing.T) {
defer sup.Shutdown() defer sup.Shutdown()
pluginReg := plugin.NewRegistry() pluginReg := plugin.NewRegistry()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) { t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
@ -505,7 +505,7 @@ func TestSettingsAPIFlow(t *testing.T) {
}) })
t.Run("GET_settings_with_prefix", func(t *testing.T) { t.Run("GET_settings_with_prefix", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings?prefix=core.daemon", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings?prefix=webui", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
h.handleSettings(w, req) h.handleSettings(w, req)
@ -513,11 +513,11 @@ func TestSettingsAPIFlow(t *testing.T) {
json.NewDecoder(w.Body).Decode(&resp) json.NewDecoder(w.Body).Decode(&resp)
settings := resp["settings"].(map[string]interface{}) settings := resp["settings"].(map[string]interface{})
if _, ok := settings["core.daemon.listen_addr"]; !ok { if _, ok := settings["webui.listen_addr"]; !ok {
t.Fatal("expected core.daemon.listen_addr in filtered results") t.Fatal("expected webui.listen_addr in filtered results")
} }
if _, ok := settings["core.llm.model"]; ok { if _, ok := settings["core.llm.model"]; ok {
t.Fatal("core.llm.model should not be in core.daemon filtered results") t.Fatal("core.llm.model should not be in webui filtered results")
} }
}) })
@ -578,7 +578,7 @@ func TestSettingsAPIFlow(t *testing.T) {
}) })
t.Run("settings_not_available_without_registry", func(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) h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
h2.handleSettings(w, req) h2.handleSettings(w, req)
@ -604,7 +604,7 @@ func TestSettingsWithPluginRegistry(t *testing.T) {
defer sup.Shutdown() defer sup.Shutdown()
pluginReg := plugin.NewRegistry() pluginReg := plugin.NewRegistry()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -677,7 +677,7 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
sup.Start() sup.Start()
defer sup.Shutdown() defer sup.Shutdown()
h := NewHandler(sup, nil, nil, nil, &types.Config{}, iom, nil, nil, nil, nil, nil, events.NewBus(), nil) h := NewHandler(sup, nil, nil, nil, &types.Config{}, iom, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
t.Run("POST_chat_completions_returns_echo", func(t *testing.T) { t.Run("POST_chat_completions_returns_echo", func(t *testing.T) {
body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}` body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}`
@ -710,7 +710,7 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
}) })
t.Run("POST_chat_completions_no_iom_returns_503", func(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) h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
body := `{"messages":[{"role":"user","content":"hi"}]}` body := `{"messages":[{"role":"user","content":"hi"}]}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)) req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")

View File

@ -150,7 +150,6 @@ type Config struct {
} }
type DaemonConfig struct { type DaemonConfig struct {
ListenAddr string `json:"listen_addr"`
DataDir string `json:"data_dir"` DataDir string `json:"data_dir"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"` HeartbeatInterval time.Duration `json:"heartbeat_interval"`
CheckInterval time.Duration `json:"check_interval"` CheckInterval time.Duration `json:"check_interval"`