- vision:model.Router 绑 WithVisionModel(LLM_VISION_MODEL);vision.RegisterTools(VisualTool/OCRTool) 用同一 OpenAICompatibleAdapter(LLM_BASE_URL 为空则跳过并 Warn)。 - voice:voice.New(cfg.Voice) Chain(cloud→local→edge-tts/espeak→silent)存 a.voice。 - mcp:读 cfg.MCP.ServersJSON → 逐 Enabled server mcp.NewRegistry(...).Register(ctx)(Connect+ DiscoverTools+RegisterTool),注册 KindMCP 能力 mcp:<name> 供 adminListPlugins 枚举; close() 中 Unregister 防 stdio 子进程孤儿;每 server 15s connect 超时,失败 Warn 跳过。 - skills:LoadDir(configs/skills) → skills.Register 为 KindSkill(Runner.Run 仍 ErrRunPending, 执行留后续)。 - exec/terminal:新 terminalTool(sdk.Tool) 内部调 execProv.Execute,注册进 dispatcher; ExecSandbox.Enabled=false 默认拦截(授权默认关)。 - config:LLMConfig 增 VisionModel(LLM_VISION_MODEL);新增 MCPConfig/ServersJSON(MCPSERVERS_JSON)。 - 拆 5 个辅助文件(vision.go/voice.go/mcp.go/skills.go/terminal_tool.go)避免 wire.go 膨胀; wire.go 最终 535 行。 默认配置下各子系统干净跳过不阻断启动(LLM_BASE_URL 已配则 vision 用主模型降级)。 go build/vet/test 全绿(46 包),gofmt 干净。
88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/mcp"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
)
|
|
|
|
// mcpConnectTimeout bounds each MCP server's connect+discover handshake so a hung
|
|
// server degrades to a warning instead of blocking startup.
|
|
const mcpConnectTimeout = 15 * time.Second
|
|
|
|
// wireMCP reads cfg.MCP.ServersJSON, connects to every enabled server, registers
|
|
// its discovered tools into the capability registry (KindTool so the agent can
|
|
// call them), and surfaces a KindMCP capability so the admin plugin list
|
|
// enumerates the server. A server that fails to connect or is unconfigured
|
|
// degrades to a warning and never blocks startup.
|
|
func (a *app) wireMCP() {
|
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
|
servers, err := a.mcpServers()
|
|
if err != nil {
|
|
log.Warn("mcp config parse skipped", "error", err)
|
|
return
|
|
}
|
|
if len(servers) == 0 {
|
|
log.Debug("no mcp servers configured")
|
|
return
|
|
}
|
|
for _, srv := range servers {
|
|
if !srv.Enabled {
|
|
continue
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout)
|
|
err := a.registerMCPServer(ctx, srv)
|
|
cancel()
|
|
if err != nil {
|
|
log.Warn("mcp server skipped", "name", srv.Name, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// mcpServers parses the configured MCP servers from cfg.MCP.ServersJSON.
|
|
func (a *app) mcpServers() ([]config.MCPServer, error) {
|
|
raw := strings.TrimSpace(a.cfg.MCP.ServersJSON)
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
var servers []config.MCPServer
|
|
if err := json.Unmarshal([]byte(raw), &servers); err != nil {
|
|
return nil, err
|
|
}
|
|
return servers, nil
|
|
}
|
|
|
|
// registerMCPServer connects to one server, registers its tools, and publishes a
|
|
// KindMCP capability under "mcp:<name>". It returns the first error so callers
|
|
// can classify it; on partial registration it cleans up what it added.
|
|
func (a *app) registerMCPServer(ctx context.Context, srv config.MCPServer) error {
|
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
|
reg := mcp.NewRegistry(a.caps, mcp.MCPConfig{
|
|
Name: srv.Name,
|
|
Command: srv.Command,
|
|
Args: srv.Args,
|
|
URL: srv.URL,
|
|
}, mcp.WithLogger(a.log))
|
|
if err := reg.Register(ctx); err != nil {
|
|
_ = reg.Unregister(context.Background())
|
|
return err
|
|
}
|
|
if err := a.caps.Register(registry.Capability{
|
|
Name: "mcp:" + srv.Name,
|
|
Kind: registry.KindMCP,
|
|
Description: "MCP server " + srv.Name,
|
|
Value: reg,
|
|
}); err != nil {
|
|
log.Warn("mcp capability register skipped", "name", srv.Name, "error", err)
|
|
}
|
|
a.mcpRegs = append(a.mcpRegs, reg)
|
|
log.Info("mcp server registered", "name", srv.Name)
|
|
return nil
|
|
}
|