feat(wire): 地基④ wire.go 接 vision/voice/mcp/skill/exec 五个子系统,从死代码变可用
- 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 干净。
This commit is contained in:
1 parent
9b207a31e8
commit
eddb9b9891
8 files changed
+362
-1
No files matched your search
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec"
|
||||
)
|
||||
|
||||
// terminalToolName is the registered name of the exec/terminal tool.
|
||||
const terminalToolName = "terminal"
|
||||
|
||||
// terminalTool is an sdk.Tool that runs a command through the grant-based exec
|
||||
// sandbox (exec.Provider). It is registered even when the sandbox is closed:
|
||||
// Execute then returns exec.ErrExecutionDisabled, so the agent gains the ability
|
||||
// to run code only after the owner explicitly enables the sandbox (default off).
|
||||
type terminalTool struct {
|
||||
prov exec.Provider
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// Name returns the tool identifier.
|
||||
func (t *terminalTool) Name() string { return terminalToolName }
|
||||
|
||||
// Description explains what the tool does.
|
||||
func (t *terminalTool) Description() string {
|
||||
return "Run a terminal command through the sandboxed exec provider and return its stdout, stderr and exit code."
|
||||
}
|
||||
|
||||
// Parameters returns the input JSON Schema.
|
||||
func (t *terminalTool) Parameters() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The command line to run; split into argv by whitespace (no shell interpretation).",
|
||||
},
|
||||
"workdir": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional working directory, confined to an allowed sandbox root.",
|
||||
},
|
||||
"timeout": map[string]any{
|
||||
"type": "number",
|
||||
"description": "Optional timeout in seconds (default 30).",
|
||||
},
|
||||
},
|
||||
"required": []any{"command"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute runs the command via the exec provider and returns its outcome. When
|
||||
// the sandbox is closed the provider returns exec.ErrExecutionDisabled, which the
|
||||
// dispatcher wraps into a tool error safe to feed back to the LLM.
|
||||
func (t *terminalTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
m, ok := args.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("terminal: args must be an object")
|
||||
}
|
||||
cmd, _ := m["command"].(string)
|
||||
argv := strings.Fields(cmd)
|
||||
if len(argv) == 0 {
|
||||
return nil, fmt.Errorf("terminal: command is required")
|
||||
}
|
||||
if t.log != nil {
|
||||
t.log.Debug("terminal tool invoked", "cmd", strings.Join(argv, " "))
|
||||
}
|
||||
req := exec.ExecRequest{Command: argv, Timeout: timeoutArg(m["timeout"])}
|
||||
if wd, ok := m["workdir"].(string); ok {
|
||||
req.WorkDir = wd
|
||||
}
|
||||
res, err := t.prov.Execute(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"stdout": res.Stdout,
|
||||
"stderr": res.Stderr,
|
||||
"exit": res.ExitCode,
|
||||
"truncated": res.Truncated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// timeoutArg converts the optional timeout argument (seconds) to a duration. A
|
||||
// missing or non-positive value returns zero, letting the provider apply its own
|
||||
// default.
|
||||
func timeoutArg(v any) time.Duration {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
if n > 0 {
|
||||
return time.Duration(n * float64(time.Second))
|
||||
}
|
||||
case int:
|
||||
if n > 0 {
|
||||
return time.Duration(n) * time.Second
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// wireTerminalTool registers the terminal tool with the dispatcher so the agent
|
||||
// can issue sandboxed terminal commands. It no-ops (with a warning) when no exec
|
||||
// provider is wired, which keeps callers safe.
|
||||
func (a *app) wireTerminalTool() {
|
||||
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||
if a.execProv == nil {
|
||||
log.Warn("terminal tool skipped: no exec provider")
|
||||
return
|
||||
}
|
||||
if err := a.dispatcher.Register(&terminalTool{
|
||||
prov: a.execProv,
|
||||
log: logger.WithSource(a.log, "cmd/cyrene"),
|
||||
}); err != nil {
|
||||
log.Warn("terminal tool register skipped", "error", err)
|
||||
return
|
||||
}
|
||||
log.Info("terminal tool registered", "sandbox_enabled", a.cfg.ExecSandbox.Enabled)
|
||||
}
|
||||
Reference in new issue
Block a user