mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
feat: restructure plugin system, add Lua plugin support, update docs
This commit is contained in:
@ -1,3 +1,5 @@
|
||||
//go:build linux
|
||||
|
||||
package agentcli
|
||||
|
||||
import (
|
||||
@ -155,18 +157,13 @@ func (t *TerminalSession) IsExpired() bool {
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
sessions map[string]*TerminalSession
|
||||
nextID int
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("agentcli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
name string
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
sessions map[string]*TerminalSession
|
||||
nextID int
|
||||
defaultTimeout time.Duration
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
@ -180,6 +177,22 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "default_timeout", Type: "string", DisplayName: "默认终端超时",
|
||||
Description: "终端自动关闭的默认时间,例如 5m, 10m, 30m, 1h(默认 5m)",
|
||||
Default: "5m",
|
||||
})
|
||||
if v, _ := s.Settings().Get("default_timeout"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if d, err := time.ParseDuration(s); err == nil {
|
||||
p.defaultTimeout = d
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.defaultTimeout <= 0 {
|
||||
p.defaultTimeout = DefaultTimeout
|
||||
}
|
||||
|
||||
s.RegisterTool("terminal_create", sdk.ToolDef{
|
||||
Name: "terminal_create",
|
||||
Description: "创建一个新的交互式终端会话。返回终端 ID,后续通过此 ID 进行读写操作。适用于运行交互式程序如 vim、ssh、top、nano 等。终端默认 5 分钟后自动关闭,可通过 timeout 参数调整。",
|
||||
@ -339,7 +352,7 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in
|
||||
}
|
||||
|
||||
timeoutStr, _ := args["timeout"].(string)
|
||||
timeout := DefaultTimeout
|
||||
timeout := p.defaultTimeout
|
||||
if timeoutStr != "" {
|
||||
if d, err := time.ParseDuration(timeoutStr); err == nil {
|
||||
timeout = d
|
||||
@ -795,6 +808,13 @@ func isTimeoutError(err error) bool {
|
||||
return strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "would block")
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("agentcli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
plugin.RegisterPluginMeta("agentcli", "终端交互", "Agent CLI")
|
||||
}
|
||||
|
||||
func sanitizePreview(s string) string {
|
||||
var buf bytes.Buffer
|
||||
for _, r := range s {
|
||||
|
||||
21
internal/plugins/agentcli/plugin_stub.go
Normal file
21
internal/plugins/agentcli/plugin_stub.go
Normal file
@ -0,0 +1,21 @@
|
||||
//go:build !linux
|
||||
|
||||
package agentcli
|
||||
|
||||
import (
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("agentcli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &stubPlugin{}, nil
|
||||
})
|
||||
plugin.RegisterPluginMeta("agentcli", "终端交互", "Agent CLI")
|
||||
}
|
||||
|
||||
type stubPlugin struct{}
|
||||
|
||||
func (p *stubPlugin) Name() string { return "agentcli" }
|
||||
func (p *stubPlugin) Start(sdk *sdk.PluginSDK) error { return nil }
|
||||
func (p *stubPlugin) Stop() error { return nil }
|
||||
@ -38,6 +38,7 @@ func Configure(pr *plugin.Registry, cr *internalConfig.ConfigRegistry, sp agentC
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("cli", "CLI", "CLI")
|
||||
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
sock := DefaultSocket
|
||||
if sock == "" {
|
||||
@ -69,6 +70,20 @@ func New(name, socketPath string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "api_key", Type: "password", DisplayName: "CLI API 密钥",
|
||||
Description: "CLI 客户端连接时需提供的认证密钥(留空则使用 WebUI 密钥)",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "socket_path", Type: "string", DisplayName: "Socket 管道路径",
|
||||
Description: "CLI Unix 域套接字监听路径(留空则使用默认路径)",
|
||||
})
|
||||
if v, _ := s.Settings().Get("socket_path"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
p.socket = s
|
||||
}
|
||||
}
|
||||
|
||||
dir := filepath.Dir(p.socket)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create socket dir: %w", err)
|
||||
@ -109,7 +124,7 @@ func (p *Plugin) handleConn(conn net.Conn, s *sdk.PluginSDK) {
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
|
||||
apiKey := p.webuiAPIKey()
|
||||
apiKey := p.cliAPIKey(s)
|
||||
if apiKey != "" {
|
||||
if !scanner.Scan() {
|
||||
return
|
||||
@ -156,6 +171,17 @@ func (p *Plugin) handleConn(conn net.Conn, s *sdk.PluginSDK) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) cliAPIKey(s *sdk.PluginSDK) string {
|
||||
if s != nil {
|
||||
if v, _ := s.Settings().Get("api_key"); v != nil {
|
||||
if k, ok := v.(string); ok && k != "" {
|
||||
return k
|
||||
}
|
||||
}
|
||||
}
|
||||
return p.webuiAPIKey()
|
||||
}
|
||||
|
||||
func (p *Plugin) webuiAPIKey() string {
|
||||
if cfgReg == nil {
|
||||
return ""
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
@ -41,13 +42,32 @@ func shellUnquote(s string) []string {
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("cmd", "命令执行", "Command")
|
||||
plugin.RegisterFactory("cmd", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
}
|
||||
|
||||
type cmdRecord struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Command string `json:"command"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Workdir string `json:"workdir"`
|
||||
Timeout string `json:"timeout"`
|
||||
Duration string `json:"duration"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
const maxHistory = 100
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
name string
|
||||
defaultTimeout string
|
||||
maxOutput int
|
||||
mu sync.Mutex
|
||||
history []cmdRecord
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
@ -57,6 +77,30 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "default_timeout", Type: "string", DisplayName: "默认命令超时",
|
||||
Description: "命令执行的默认超时时间,例如 30s, 1m, 5m(默认 30s)",
|
||||
Default: "30s",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "max_output_bytes", Type: "int", DisplayName: "最大输出字节数",
|
||||
Description: "命令输出的最大字节数,超出部分将被截断(默认 32000)",
|
||||
Default: "32000",
|
||||
})
|
||||
p.maxOutput = 32000
|
||||
if v, _ := s.Settings().Get("max_output_bytes"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 {
|
||||
}
|
||||
}
|
||||
}
|
||||
p.defaultTimeout = "30s"
|
||||
if v, _ := s.Settings().Get("default_timeout"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
p.defaultTimeout = s
|
||||
}
|
||||
}
|
||||
|
||||
s.RegisterTool("cmd_run", sdk.ToolDef{
|
||||
Name: "cmd_run",
|
||||
Description: "执行一条系统命令并返回输出。适用于查询系统信息、运行脚本、操作文件等单次命令场景。命令在临时 shell 中执行,不支持交互。如需交互式终端(如 vim、ssh、top),请使用 terminal_create 相关工具。",
|
||||
@ -86,7 +130,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
timeoutStr, _ := args["timeout"].(string)
|
||||
if timeoutStr == "" {
|
||||
timeoutStr = "30s"
|
||||
timeoutStr = p.defaultTimeout
|
||||
}
|
||||
timeout, err := time.ParseDuration(timeoutStr)
|
||||
if err != nil {
|
||||
@ -104,6 +148,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
}
|
||||
}
|
||||
|
||||
tStart := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
@ -120,22 +165,40 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
rec := cmdRecord{Timestamp: tStart, Command: command, Workdir: workdir, Timeout: timeoutStr}
|
||||
exitCode := -1
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
rec.Status = "timeout"
|
||||
rec.Stdout = p.truncateOutput(stdout.String())
|
||||
rec.Stderr = p.truncateOutput(stderr.String())
|
||||
rec.Duration = time.Since(tStart).Round(time.Millisecond).String()
|
||||
p.recordCmd(rec)
|
||||
return map[string]interface{}{
|
||||
"status": "timeout",
|
||||
"stdout": truncateOutput(stdout.String()),
|
||||
"stderr": truncateOutput(stderr.String()),
|
||||
"stdout": rec.Stdout,
|
||||
"stderr": rec.Stderr,
|
||||
"error": fmt.Sprintf("命令执行超时(%s)", timeoutStr),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
if cmd.ProcessState != nil {
|
||||
exitCode = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
|
||||
rec.Status = "ok"
|
||||
rec.Stdout = p.truncateOutput(stdout.String())
|
||||
rec.Stderr = p.truncateOutput(stderr.String())
|
||||
rec.ExitCode = exitCode
|
||||
rec.Duration = time.Since(tStart).Round(time.Millisecond).String()
|
||||
p.recordCmd(rec)
|
||||
|
||||
return map[string]interface{}{
|
||||
"status": "ok",
|
||||
"stdout": truncateOutput(stdout.String()),
|
||||
"stderr": truncateOutput(stderr.String()),
|
||||
"exit_code": cmd.ProcessState.ExitCode(),
|
||||
"stdout": rec.Stdout,
|
||||
"stderr": rec.Stderr,
|
||||
"exit_code": exitCode,
|
||||
"command": command,
|
||||
}, nil
|
||||
})
|
||||
@ -147,8 +210,20 @@ func (p *Plugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncateOutput(s string) string {
|
||||
const maxLen = 32000
|
||||
func (p *Plugin) recordCmd(r cmdRecord) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.history = append(p.history, r)
|
||||
if len(p.history) > maxHistory {
|
||||
p.history = p.history[len(p.history)-maxHistory:]
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) truncateOutput(s string) string {
|
||||
maxLen := p.maxOutput
|
||||
if maxLen <= 0 {
|
||||
maxLen = 32000
|
||||
}
|
||||
if len(s) > maxLen {
|
||||
return s[:maxLen] + fmt.Sprintf("\n... [输出被截断,共 %d 字节]", len(s))
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("files", "文件系统", "Files")
|
||||
plugin.RegisterFactory("files", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
|
||||
@ -63,6 +63,7 @@ func Configure(sh *agentCore.StageHost, iom *agentIO.IOManager, pr *plugin.Regis
|
||||
}
|
||||
|
||||
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
|
||||
@ -81,6 +82,12 @@ type Plugin struct {
|
||||
stopCh chan struct{}
|
||||
stopOnce sync.Once
|
||||
perfData PerfData
|
||||
|
||||
autoInterval time.Duration
|
||||
llmTimeout time.Duration
|
||||
llmMaxTurns int
|
||||
llmMaxTokens int
|
||||
perfHistory int
|
||||
}
|
||||
|
||||
type PerfData struct {
|
||||
@ -106,6 +113,74 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.autoInterval = 30 * time.Minute
|
||||
p.llmTimeout = 120 * time.Second
|
||||
p.llmMaxTurns = 20
|
||||
p.llmMaxTokens = 4096
|
||||
p.perfHistory = 100
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "auto_interval", Type: "string", DisplayName: "自动检查间隔",
|
||||
Description: "自动健康检查的执行间隔,例如 30m, 1h, 10m(设为 0 禁用)",
|
||||
Default: "30m",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "llm_timeout", Type: "string", DisplayName: "LLM 检查超时",
|
||||
Description: "LLM 驱动检查的超时时间,例如 120s, 3m, 5m(默认 120s)",
|
||||
Default: "120s",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "llm_max_turns", Type: "int", DisplayName: "LLM 最大对话轮数",
|
||||
Description: "LLM 工具发现的最大对话轮数(默认 20)",
|
||||
Default: "20",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "llm_max_tokens", Type: "int", DisplayName: "LLM 最大 Token",
|
||||
Description: "LLM 调用时的最大 Token 数(默认 4096)",
|
||||
Default: "4096",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "perf_history", Type: "int", DisplayName: "性能历史保留数",
|
||||
Description: "保留的历史检查记录条数(默认 100)",
|
||||
Default: "100",
|
||||
})
|
||||
|
||||
if v, _ := s.Settings().Get("auto_interval"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if d, err := time.ParseDuration(s); err == nil && d > 0 {
|
||||
p.autoInterval = d
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("llm_timeout"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if d, err := time.ParseDuration(s); err == nil && d > 0 {
|
||||
p.llmTimeout = d
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("llm_max_turns"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if n, err := fmt.Sscanf(s, "%d", &p.llmMaxTurns); err != nil || n < 1 {
|
||||
p.llmMaxTurns = 20
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("llm_max_tokens"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if n, err := fmt.Sscanf(s, "%d", &p.llmMaxTokens); err != nil || n < 1 {
|
||||
p.llmMaxTokens = 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("perf_history"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if n, err := fmt.Sscanf(s, "%d", &p.perfHistory); err != nil || n < 1 {
|
||||
p.perfHistory = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.selfToolNames["healthcheck"] = true
|
||||
s.RegisterTool("healthcheck", sdk.ToolDef{
|
||||
Name: "healthcheck",
|
||||
@ -220,7 +295,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
}, nil
|
||||
})
|
||||
|
||||
p.startAutoCheck(s, 30*time.Minute)
|
||||
if p.autoInterval > 0 {
|
||||
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,
|
||||
@ -278,8 +355,8 @@ func (p *Plugin) runAutoCheck(s *sdk.PluginSDK) {
|
||||
p.mu.Lock()
|
||||
p.perfData.LastCheck = pt.Time
|
||||
p.perfData.Checks = append(p.perfData.Checks, pt)
|
||||
if len(p.perfData.Checks) > 100 {
|
||||
p.perfData.Checks = p.perfData.Checks[len(p.perfData.Checks)-100:]
|
||||
if len(p.perfData.Checks) > p.perfHistory {
|
||||
p.perfData.Checks = p.perfData.Checks[len(p.perfData.Checks)-p.perfHistory:]
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
@ -511,7 +588,7 @@ func (p *Plugin) testLLMDriven() checkResult {
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), p.llmTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 收集所有工具定义(排除健康检查自身的工具以避免循环测试)
|
||||
@ -537,10 +614,10 @@ func (p *Plugin) testLLMDriven() checkResult {
|
||||
turnCount := 0
|
||||
toolCallCount := 0
|
||||
|
||||
for turn := 0; turn < 20; turn++ {
|
||||
for turn := 0; turn < p.llmMaxTurns; turn++ {
|
||||
resp, err := provider.Chat(ctx, &agentAPI.CompletionRequest{
|
||||
Messages: msgs,
|
||||
MaxTokens: 4096,
|
||||
MaxTokens: p.llmMaxTokens,
|
||||
Tools: tools,
|
||||
ToolChoice: "auto",
|
||||
})
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
@ -20,6 +21,7 @@ type serverConfig struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("mcp", "MCP 服务器", "MCP")
|
||||
plugin.RegisterFactory("mcp", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
@ -39,15 +41,6 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "servers",
|
||||
Default: "",
|
||||
Type: "text",
|
||||
DisplayName: "MCP 服务器配置",
|
||||
Description: "MCP 服务器列表,JSON 数组格式,包含 name、command/url、args、env 等字段",
|
||||
Category: "mcp",
|
||||
})
|
||||
|
||||
cfgs, err := p.loadConfig(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load mcp config: %w", err)
|
||||
@ -89,7 +82,49 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
}
|
||||
|
||||
func (p *Plugin) loadConfig(s *sdk.PluginSDK) ([]serverConfig, error) {
|
||||
// 优先从 skill.json(config map)读取
|
||||
// 优先从独立服务器配置键读取(servers.<name>.<field>)
|
||||
keys, _ := s.Settings().List("servers.")
|
||||
if len(keys) > 0 {
|
||||
serverNames := make(map[string]bool)
|
||||
for _, k := range keys {
|
||||
parts := strings.SplitN(k, ".", 3)
|
||||
if len(parts) >= 2 {
|
||||
serverNames[parts[1]] = true
|
||||
}
|
||||
}
|
||||
var cfgs []serverConfig
|
||||
for name := range serverNames {
|
||||
cfg := serverConfig{Name: name}
|
||||
if v, _ := s.Settings().Get("servers." + name + ".command"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
cfg.Command = s
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("servers." + name + ".url"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
cfg.URL = s
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("servers." + name + ".args"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
json.Unmarshal([]byte(s), &cfg.Args)
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("servers." + name + ".env"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
json.Unmarshal([]byte(s), &cfg.Env)
|
||||
}
|
||||
}
|
||||
if cfg.Command != "" || cfg.URL != "" {
|
||||
cfgs = append(cfgs, cfg)
|
||||
}
|
||||
}
|
||||
if len(cfgs) > 0 {
|
||||
return cfgs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:从旧版 JSON blob 读取
|
||||
raw, err := s.Settings().Get("servers")
|
||||
if err == nil {
|
||||
switch v := raw.(type) {
|
||||
@ -107,8 +142,6 @@ func (p *Plugin) loadConfig(s *sdk.PluginSDK) ([]serverConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 备用:从 JSON 文件读取
|
||||
// 没有配置时不报错,只返回空
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ var SkillsDir string
|
||||
var SimulatorDir string
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("openclaw", "开放式交互", "OpenClaw")
|
||||
plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
dir := SkillsDir
|
||||
if dir == "" {
|
||||
@ -69,6 +70,25 @@ func (p *Plugin) Name() string { return p.name }
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "skills_dir", Type: "string", DisplayName: "Skill 加载目录",
|
||||
Description: "OpenClaw 技能加载目录路径(留空则使用默认路径)",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "simulator_dir", Type: "string", DisplayName: "模拟器工作目录",
|
||||
Description: "OpenClaw 模拟器工作目录路径(留空则使用默认路径)",
|
||||
})
|
||||
if v, _ := s.Settings().Get("skills_dir"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
p.skillsDir = s
|
||||
}
|
||||
}
|
||||
if v, _ := s.Settings().Get("simulator_dir"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
p.simulatorDir = s
|
||||
}
|
||||
}
|
||||
|
||||
// Launch OC plugin manager first (handles OC-format plugin installation and lifecycle)
|
||||
os.MkdirAll(p.skillsDir, 0755)
|
||||
if err := p.launchManager(s); err != nil {
|
||||
|
||||
@ -41,6 +41,7 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("pluginmgr", "插件管理", "Plugin Manager")
|
||||
plugin.RegisterFactory("pluginmgr", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
@ -504,7 +505,7 @@ func validatePackage(data []byte) (*pluginPackage, error) {
|
||||
return nil, fmt.Errorf("entry %q not found in package", pkg.Entry)
|
||||
}
|
||||
|
||||
valid := map[string]bool{"plugin.so": true, "main.lua": true, "SKILL.md": true}
|
||||
valid := map[string]bool{"plugin.so": true, "plugin.dll": true, "main.lua": true, "SKILL.md": true}
|
||||
if !valid[pkg.Entry] {
|
||||
return nil, fmt.Errorf("unsupported entry: %q", pkg.Entry)
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("timer", "定时任务", "Timer")
|
||||
plugin.RegisterFactory("timer", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
@ -21,6 +22,7 @@ type Plugin struct {
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
maxDur time.Duration
|
||||
}
|
||||
|
||||
type timerTask struct {
|
||||
@ -38,6 +40,20 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.maxDur = 24 * time.Hour
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "max_duration", Type: "string", DisplayName: "最大定时时长",
|
||||
Description: "允许设置的最大定时时长,例如 24h, 7d, 1h(默认 24h)",
|
||||
Default: "24h",
|
||||
})
|
||||
if v, _ := s.Settings().Get("max_duration"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
if d, err := time.ParseDuration(s); err == nil && d > 0 {
|
||||
p.maxDur = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.RegisterTool("timer_set", sdk.ToolDef{
|
||||
Name: "timer_set",
|
||||
Description: "设置一个定时提醒。倒计时结束后通过中断通道通知 agent。",
|
||||
@ -69,6 +85,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("invalid duration %q: %v", durStr, err)}, nil
|
||||
}
|
||||
if dur > p.maxDur {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("duration %v exceeds max %v", dur, p.maxDur)}, nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.wg.Add(1)
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
internal/plugins/webui/dashboard2.html
Normal file
1
internal/plugins/webui/dashboard2.html
Normal file
File diff suppressed because one or more lines are too long
@ -15,6 +15,7 @@ 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"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
@ -61,16 +62,55 @@ type Handler struct {
|
||||
pluginReg *plugin.Registry
|
||||
eventBus *events.Bus
|
||||
statusProvider agentCore.StatusProvider
|
||||
providerMgr *agentAPI.ProviderManager
|
||||
baseAPIKey string
|
||||
sessionMu sync.Mutex
|
||||
sessions map[string]time.Time
|
||||
|
||||
chatMu sync.Mutex
|
||||
chatHistory []ChatMsg
|
||||
cmdMu sync.Mutex
|
||||
cmdHistory []CmdExec
|
||||
termMu sync.Mutex
|
||||
termStates map[string]*termState
|
||||
}
|
||||
|
||||
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) *Handler {
|
||||
type ChatMsg struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
type CmdExec struct {
|
||||
Command string `json:"command"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Status string `json:"status"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
type termState struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
Running bool `json:"running"`
|
||||
Output string `json:"output"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Uptime string `json:"uptime"`
|
||||
created time.Time
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return &Handler{
|
||||
h := &Handler{
|
||||
supervisor: sup,
|
||||
memory: mem,
|
||||
indexer: idx,
|
||||
@ -86,8 +126,83 @@ func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
|
||||
pluginReg: pr,
|
||||
eventBus: evBus,
|
||||
statusProvider: sp,
|
||||
providerMgr: pm,
|
||||
baseAPIKey: baseKey,
|
||||
sessions: make(map[string]time.Time),
|
||||
termStates: make(map[string]*termState),
|
||||
}
|
||||
if evBus != nil {
|
||||
go h.trackToolEvents()
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) trackToolEvents() {
|
||||
h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) {
|
||||
h.handleToolEvent(ev)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) handleToolEvent(ev *events.Event) {
|
||||
payload := ev.Payload
|
||||
tool, _ := payload["tool"].(string)
|
||||
args, _ := payload["args"].(map[string]interface{})
|
||||
status, _ := payload["status"].(string)
|
||||
ts := time.Now()
|
||||
|
||||
switch tool {
|
||||
case "cmd_run":
|
||||
exec := CmdExec{
|
||||
Command: getStr(args, "command"),
|
||||
Status: status,
|
||||
Time: ts.Format(time.RFC3339),
|
||||
}
|
||||
h.cmdMu.Lock()
|
||||
h.cmdHistory = append(h.cmdHistory, exec)
|
||||
if len(h.cmdHistory) > maxCmdHistory {
|
||||
h.cmdHistory = h.cmdHistory[len(h.cmdHistory)-maxCmdHistory:]
|
||||
}
|
||||
h.cmdMu.Unlock()
|
||||
|
||||
case "terminal_create":
|
||||
id := getStr(args, "id")
|
||||
cmd := getStr(args, "command")
|
||||
now := time.Now()
|
||||
term := &termState{
|
||||
ID: id,
|
||||
Command: cmd,
|
||||
Running: true,
|
||||
CreatedAt: now.Format(time.RFC3339),
|
||||
created: now,
|
||||
}
|
||||
h.termMu.Lock()
|
||||
h.termStates[id] = term
|
||||
if len(h.termStates) > maxTerminals {
|
||||
for k := range h.termStates {
|
||||
delete(h.termStates, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
h.termMu.Unlock()
|
||||
|
||||
case "terminal_close":
|
||||
id := getStr(args, "id")
|
||||
if id != "" {
|
||||
h.termMu.Lock()
|
||||
if t, ok := h.termStates[id]; ok {
|
||||
t.Running = false
|
||||
}
|
||||
h.termMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStr(m map[string]interface{}, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := m[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
|
||||
@ -204,6 +319,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/skills", h.requireAPI(h.handleSkills))
|
||||
mux.HandleFunc("/api/v1/memory", h.requireAPI(h.handleMemory))
|
||||
mux.HandleFunc("/api/v1/memory/", h.requireAPI(h.handleMemory))
|
||||
mux.HandleFunc("/api/v1/memory/graph", h.requireAPI(h.handleMemoryGraph))
|
||||
mux.HandleFunc("/api/v1/memory/context", h.requireAPI(h.handleMemoryContext))
|
||||
mux.HandleFunc("/api/v1/memory/tools", h.requireAPI(h.handleMemoryTools))
|
||||
mux.HandleFunc("/api/v1/memory/text", h.requireAPI(h.handleTextMemory))
|
||||
@ -218,7 +334,10 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/tracker", h.requireAPI(h.handleTracker))
|
||||
mux.HandleFunc("/api/v1/tracker/", h.requireAPI(h.handleTracker))
|
||||
mux.HandleFunc("/api/v1/chat", h.requireAPI(h.handleChat))
|
||||
mux.HandleFunc("/api/v1/chat/history", h.requireAPI(h.handleChatHistory))
|
||||
mux.HandleFunc("/api/v1/chat/events", h.requireAPI(h.handleChatEvents))
|
||||
mux.HandleFunc("/api/v1/terminals", h.requireAPI(h.handleTerminals))
|
||||
mux.HandleFunc("/api/v1/cmd/history", h.requireAPI(h.handleCmdHistory))
|
||||
mux.HandleFunc("/api/v1/kernel", h.requireAPI(h.handleKernel))
|
||||
mux.HandleFunc("/api/v1/plugins", h.requireAPI(h.handlePlugins))
|
||||
mux.HandleFunc("/api/v1/plugins/", h.requireAPI(h.handlePluginByID))
|
||||
@ -528,6 +647,23 @@ func (h *Handler) handleMemoryTools(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) handleMemoryGraph(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.memory == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "memory system not available"})
|
||||
return
|
||||
}
|
||||
data, err := h.memory.GraphData()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": data})
|
||||
}
|
||||
|
||||
func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
@ -652,7 +788,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.lua.ReloadAll(); err != nil {
|
||||
if err := h.lua.LoadAdapter(path); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@ -687,7 +823,7 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"})
|
||||
return
|
||||
}
|
||||
h.lua.ReloadAll()
|
||||
h.lua.RemoveAdapter(name)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
@ -705,6 +841,42 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) addChatMsg(msg ChatMsg) {
|
||||
h.chatMu.Lock()
|
||||
defer h.chatMu.Unlock()
|
||||
h.chatHistory = append(h.chatHistory, msg)
|
||||
if len(h.chatHistory) > maxChatHistory {
|
||||
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleChatHistory(w http.ResponseWriter, r *http.Request) {
|
||||
h.chatMu.Lock()
|
||||
result := make([]ChatMsg, len(h.chatHistory))
|
||||
copy(result, h.chatHistory)
|
||||
h.chatMu.Unlock()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"messages": result})
|
||||
}
|
||||
|
||||
func (h *Handler) handleTerminals(w http.ResponseWriter, r *http.Request) {
|
||||
h.termMu.Lock()
|
||||
terms := make([]*termState, 0, len(h.termStates))
|
||||
for _, ts := range h.termStates {
|
||||
ts.Uptime = time.Since(ts.created).Round(time.Second).String()
|
||||
terms = append(terms, ts)
|
||||
}
|
||||
h.termMu.Unlock()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"terminals": terms})
|
||||
}
|
||||
|
||||
func (h *Handler) handleCmdHistory(w http.ResponseWriter, r *http.Request) {
|
||||
h.cmdMu.Lock()
|
||||
result := make([]CmdExec, len(h.cmdHistory))
|
||||
copy(result, h.cmdHistory)
|
||||
h.cmdMu.Unlock()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"history": result})
|
||||
}
|
||||
|
||||
func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
@ -722,15 +894,22 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)})
|
||||
resp := h.iom.InjectTextSync("cli", body.Message)
|
||||
if resp == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
||||
return
|
||||
}
|
||||
content, _ := resp.Payload["content"].(string)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
reasoning, _ := resp.Payload["reasoning_content"].(string)
|
||||
result := map[string]interface{}{
|
||||
"response": content,
|
||||
})
|
||||
}
|
||||
if reasoning != "" {
|
||||
result["reasoning_content"] = reasoning
|
||||
}
|
||||
h.addChatMsg(ChatMsg{Role: "assistant", Content: content, ReasoningContent: reasoning, Time: time.Now().Format(time.RFC3339)})
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
||||
@ -771,15 +950,17 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}()
|
||||
|
||||
unsub := h.eventBus.Subscribe(events.EventAll, func(evt *events.Event) {
|
||||
data, _ := json.Marshal(evt)
|
||||
select {
|
||||
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
|
||||
default:
|
||||
}
|
||||
})
|
||||
defer unsub()
|
||||
|
||||
subTypes := []string{"agent_output", "reasoning", "agent_error"}
|
||||
for _, t := range subTypes {
|
||||
t2 := t
|
||||
_ = h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
|
||||
data, _ := json.Marshal(evt)
|
||||
select {
|
||||
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
|
||||
default:
|
||||
}
|
||||
})
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
@ -844,10 +1025,30 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
defs := h.cfgReg.ListDefs(prefix)
|
||||
for _, d := range defs {
|
||||
meta[d.Key] = d
|
||||
// 有 def 但 DB 中尚无值的 key,用 default 填充以便在 WebUI 中显示和编辑
|
||||
if _, exists := values[d.Key]; !exists {
|
||||
values[d.Key] = d.Default
|
||||
}
|
||||
}
|
||||
// 无前缀时同时加载所有插件配置
|
||||
if prefix == "" && h.pluginReg != nil {
|
||||
for _, p := range h.pluginReg.List() {
|
||||
ps := h.cfgReg.PluginConfig(p)
|
||||
pkeys, _ := ps.List("")
|
||||
for _, k := range pkeys {
|
||||
v, _ := ps.Get(k)
|
||||
fullKey := "plugin." + p + "." + k
|
||||
values[fullKey] = v
|
||||
if def := h.cfgReg.GetDef(fullKey); def != nil {
|
||||
meta[fullKey] = def
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins := []string{"core"}
|
||||
pm := h.pluginReg.PluginMetas()
|
||||
if h.pluginReg != nil {
|
||||
for _, p := range h.pluginReg.List() {
|
||||
plugins = append(plugins, "plugin."+p)
|
||||
@ -855,9 +1056,10 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
sort.Strings(plugins)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"settings": values,
|
||||
"meta": meta,
|
||||
"plugins": plugins,
|
||||
"settings": values,
|
||||
"meta": meta,
|
||||
"plugins": plugins,
|
||||
"plugin_meta": pm,
|
||||
})
|
||||
case http.MethodPut:
|
||||
var body struct {
|
||||
@ -883,12 +1085,37 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(body.Key, "core.llm.") && h.providerMgr != nil && h.lua != nil {
|
||||
h.reloadLLMProviders()
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}, 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)
|
||||
@ -1169,6 +1396,9 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) handleStatic(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.Write([]byte(dashboardHTML))
|
||||
return
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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"
|
||||
@ -39,6 +40,8 @@ var (
|
||||
webuiPR *plugin.Registry
|
||||
webuiEvBus *events.Bus
|
||||
webuiStatusProvider agentCore.StatusProvider
|
||||
webuiProviderMgr *agentAPI.ProviderManager
|
||||
webuiBaseAPIKey string
|
||||
)
|
||||
|
||||
// Configure 注入 WebUI 插件需要的内核依赖。必须在 Load() 之前调用。
|
||||
@ -47,16 +50,19 @@ func Configure(addr string,
|
||||
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,
|
||||
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 则跳过(不给日志警告)
|
||||
@ -69,6 +75,7 @@ func init() {
|
||||
webuiSup, webuiMem, webuiSK, webuiLua,
|
||||
webuiCfg, webuiIOM, webuiTM, webuiKS,
|
||||
webuiTR, webuiCR, webuiPR, webuiEvBus, webuiStatusProvider,
|
||||
webuiProviderMgr, webuiBaseAPIKey,
|
||||
), nil
|
||||
})
|
||||
}
|
||||
@ -93,6 +100,8 @@ type Plugin struct {
|
||||
pr *plugin.Registry
|
||||
evBus *events.Bus
|
||||
statusProvider agentCore.StatusProvider
|
||||
providerMgr *agentAPI.ProviderManager
|
||||
baseAPIKey string
|
||||
}
|
||||
|
||||
func New(name, addr string,
|
||||
@ -100,7 +109,7 @@ func New(name, addr string,
|
||||
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,
|
||||
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
|
||||
) *Plugin {
|
||||
return &Plugin{
|
||||
name: name,
|
||||
@ -108,7 +117,7 @@ func New(name, addr string,
|
||||
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,
|
||||
statusProvider: sp, providerMgr: pm, baseAPIKey: baseKey,
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +160,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "session_ttl_hours", Default: "24", Type: "int", DisplayName: "会话时长(小时)", Description: "登录 cookie 有效时长", Category: "webui"})
|
||||
p.ensureAuthBootstrap(s)
|
||||
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)
|
||||
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)
|
||||
p.handler = h
|
||||
h.RegisterRoutes(p.mux)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user