mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
refactor: move webui.listen_addr from core.daemon to webui category
This commit is contained in:
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
2
go.mod
@ -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
|
||||||
|
|||||||
@ -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{}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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) {
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user