Files
artex/server/platform_tools.go
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

327 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Autumn-27/artex/db"
actool "github.com/Autumn-27/norma/tool"
)
// 平台操作工具(给内置 Auto agent 用):建/改 skill、自定义工具、MCP。都是 host 工具,
// seed 进 tools 表、默认绑定 auto,经 hostTools 注入。复用现有 db/文件系统逻辑。
func (s *Server) platformTools() []actool.CoreTool {
return []actool.CoreTool{
s.toolCreateSkill(),
s.toolUpdateSkillFile(),
s.toolCreateCustomTool(),
s.toolUpdateCustomTool(),
s.toolCreateMCP(),
s.toolUpdateMCP(),
s.toolDeleteAssetsByHost(),
}
}
// platformToolKeys are the tool keys the Auto agent gets bound by default.
var platformToolKeys = []string{
"create_skill", "update_skill_file",
"create_custom_tool", "update_custom_tool",
"create_mcp", "update_mcp",
"delete_assets_by_host",
}
// ---- assets ----
// toolDeleteAssetsByHost hard-deletes every asset tied to one host (exact match).
// Platform-level (not a per-task tool): operates on the global, cross-task asset库.
func (s *Server) toolDeleteAssetsByHost() actool.CoreTool {
return wrTool("delete_assets_by_host",
"按 host 精确删除资产:删掉该 host 的域名/子域名,以及其下的服务(service)、接口(endpoint)。\n"+
"host 完全匹配(小写、去空格),不是模糊/通配。\n"+
"传根域名(如 example.com)会连带删除它的子域名及其服务/接口;传子域名(如 a.example.com)或 IP 只删该 host 自身及其服务/接口。\n"+
"⚠️ 硬删除、作用于全局资产库(跨任务共享)、不可撤销。",
objSchema(map[string]any{
"host": strParam("要删除的 host:域名/子域名/IP。完全匹配,如 example.com 或 a.example.com 或 1.2.3.4"),
}, "host"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
as := s.assetStore()
if as == nil {
return actool.Errorf("资产库未初始化"), nil
}
var a struct {
Host string `json:"host"`
}
_ = json.Unmarshal(in, &a)
if strings.TrimSpace(a.Host) == "" {
return actool.Errorf("host 不能为空"), nil
}
counts, err := as.DeleteByHost(a.Host)
if err != nil {
return actool.Errorf("删除失败: " + err.Error()), nil
}
var total int64
for _, n := range counts {
total += n
}
return jsonResult(map[string]any{
"host": a.Host,
"deleted": total,
"deleted_by_type": counts,
})
})
}
// ---- skills ----
func (s *Server) toolCreateSkill() actool.CoreTool {
return wrTool("create_skill",
"创建一个新 skill(写 SKILL.md,agentskills.io 规范)。name 小写字母/数字/连字符。",
objSchema(map[string]any{
"name": strParam("skill 名(小写字母开头,字母/数字/连字符)"),
"description": strParam("skill 描述(必填,说明它做什么/何时用)"),
"instructions": strParam("Markdown 正文说明(可选)"),
}, "name", "description"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a struct{ Name, Description, Instructions string }
_ = json.Unmarshal(in, &a)
if !validSkillName(a.Name) {
return actool.Errorf("skill 名不合法(小写字母开头,仅字母/数字/连字符,≤64)"), nil
}
if strings.TrimSpace(a.Description) == "" {
return actool.Errorf("description 必填"), nil
}
path := filepath.Join(s.skillDir, a.Name)
if _, err := os.Stat(path); err == nil {
return actool.Errorf("skill 已存在: " + a.Name), nil
}
if err := os.MkdirAll(path, 0o755); err != nil {
return actool.Errorf(err.Error()), nil
}
var b strings.Builder
b.WriteString("---\n")
fmt.Fprintf(&b, "name: %s\n", a.Name)
fmt.Fprintf(&b, "description: %s\n", a.Description)
b.WriteString("---\n")
if strings.TrimSpace(a.Instructions) != "" {
b.WriteString(a.Instructions)
} else {
fmt.Fprintf(&b, "## %s\n\n1. \n2. \n3. \n", a.Name)
}
if err := os.WriteFile(filepath.Join(path, "SKILL.md"), []byte(b.String()), 0o644); err != nil {
_ = os.RemoveAll(path)
return actool.Errorf(err.Error()), nil
}
return actool.Text("skill created: " + a.Name), nil
})
}
func (s *Server) toolUpdateSkillFile() actool.CoreTool {
return wrTool("update_skill_file",
"写/覆盖某个 skill 内的一个文件(默认 SKILL.md)。用于修改技能内容或加脚本/引用。",
objSchema(map[string]any{
"name": strParam("skill 名"),
"file": strParam("相对路径(可选,默认 SKILL.md,如 scripts/run.py)"),
"content": strParam("文件完整内容"),
}, "name", "content"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a struct{ Name, File, Content string }
_ = json.Unmarshal(in, &a)
if !validSkillName(a.Name) {
return actool.Errorf("skill 名不合法"), nil
}
skillPath := filepath.Join(s.skillDir, a.Name)
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
return actool.Errorf("skill 不存在: " + a.Name), nil
}
rel := strings.TrimSpace(a.File)
if rel == "" {
rel = "SKILL.md"
}
clean, msg := skillRelPath(rel)
if msg != "" {
return actool.Errorf("非法路径: " + msg), nil
}
full := filepath.Join(skillPath, clean)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return actool.Errorf(err.Error()), nil
}
if err := os.WriteFile(full, []byte(a.Content), 0o644); err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text("skill file written: " + a.Name + "/" + clean), nil
})
}
// ---- custom tools ----
type customToolToolInput struct {
Key string `json:"key"`
Description string `json:"description"`
Kind string `json:"kind"`
Exec json.RawMessage `json:"exec"`
Schema json.RawMessage `json:"schema"`
Agents []string `json:"agents"`
Deferred bool `json:"deferred"`
Enabled *bool `json:"enabled"`
}
func customToolSchema(keyDesc string) map[string]any {
return objSchema(map[string]any{
"key": strParam(keyDesc),
"description": strParam("发给模型的描述"),
"kind": strParam("shell | command | script(仅Python) | http。shell=bash 环境声明(仅告知模型该工具可在 bash 中直接调用,无需 exec/schema);其余三种需提供 exec"),
"exec": map[string]any{"type": "object", "description": "执行规格(shell 类型不需要): command→{command}; script→{code}; http→{method,url,headers,body,proxy,use_recording_proxy}"},
"schema": map[string]any{"type": "object", "description": "参数 JSON-Schema(shell/command/script 可留空; http 必填且需含 properties)"},
"agents": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "绑定的 agent key(可选)"},
"deferred": map[string]any{"type": "boolean", "description": "是否延迟(shell 类型无效;仅 command/script/http 的不常用工具才开)"},
"enabled": map[string]any{"type": "boolean", "description": "是否启用(默认 true)"},
}, "key", "kind")
}
func toDBTool(a customToolToolInput) *db.Tool {
enabled := true
if a.Enabled != nil {
enabled = *a.Enabled
}
return &db.Tool{
Key: a.Key, Description: a.Description, Schema: a.Schema, Agents: a.Agents,
Enabled: enabled, Kind: a.Kind, Exec: a.Exec, Deferred: a.Deferred,
}
}
func (s *Server) toolCreateCustomTool() actool.CoreTool {
return wrTool("create_custom_tool", "【重要】当安装一些平台没有的工具时,调用该工具将安装的工具放入平台中,让平台可以调用!创建一个自定义工具(shell/command/script/http)。shell=bash 环境声明,只需 key+description+agents,无需 exec/schema。",
customToolSchema("工具 key(小写字母开头,字母/数字/下划线)"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a customToolToolInput
_ = json.Unmarshal(in, &a)
a.Key = strings.TrimSpace(a.Key)
if !reToolKey.MatchString(a.Key) {
return actool.Errorf("key 需小写字母开头,仅含小写字母/数字/下划线"), nil
}
if a.Kind != "shell" && a.Kind != "command" && a.Kind != "script" && a.Kind != "http" {
return actool.Errorf("kind 需为 shell / command / script / http"), nil
}
if a.Kind == "http" && !hasSchemaProps(a.Schema) {
return actool.Errorf("http 工具必须提供参数 JSON Schema(不能留空)"), nil
}
if exist, _ := s.m.pg.GetTool(a.Key); exist != nil {
return actool.Errorf("该 key 已存在: " + a.Key), nil
}
if err := s.m.pg.CreateCustomTool(toDBTool(a)); err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text("custom tool created: " + a.Key), nil
})
}
func (s *Server) toolUpdateCustomTool() actool.CoreTool {
return wrTool("update_custom_tool", "修改一个已有的自定义工具(按 key)。",
customToolSchema("要修改的自定义工具 key"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a customToolToolInput
_ = json.Unmarshal(in, &a)
existing, _ := s.m.pg.GetTool(a.Key)
if existing == nil || existing.System {
return actool.Errorf("只能修改自定义工具: " + a.Key), nil
}
if a.Kind != "shell" && a.Kind != "command" && a.Kind != "script" && a.Kind != "http" {
return actool.Errorf("kind 需为 shell / command / script / http"), nil
}
if a.Kind == "http" && !hasSchemaProps(a.Schema) {
return actool.Errorf("http 工具必须提供参数 JSON Schema(不能留空)"), nil
}
if err := s.m.pg.UpdateCustomTool(toDBTool(a)); err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text("custom tool updated: " + a.Key), nil
})
}
// ---- MCP ----
type mcpToolInput struct {
ID int64 `json:"id"`
Name string `json:"name"`
Transport string `json:"transport"`
Command string `json:"command"`
Args json.RawMessage `json:"args"`
Env json.RawMessage `json:"env"`
URL string `json:"url"`
Enabled *bool `json:"enabled"`
Insecure *bool `json:"insecure"`
}
func mcpSchema(withID bool) map[string]any {
props := map[string]any{
"name": strParam("MCP 服务器名"),
"transport": strParam("stdio | http / sse"),
"command": strParam("stdio 的启动命令(如 npx)"),
"args": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "命令参数数组"},
"env": map[string]any{"type": "object", "description": "环境变量 {KEY:VALUE}"},
"url": strParam("http/sse 的 URL"),
"enabled": map[string]any{"type": "boolean", "description": "是否启用(默认 true)"},
"insecure": map[string]any{"type": "boolean", "description": "http: 跳过 TLS 证书校验(自签证书时置 true, 默认 false)"},
}
required := []string{"name", "transport"}
if withID {
props["id"] = map[string]any{"type": "integer", "description": "要修改的 MCP 服务器 id"}
required = []string{"id", "name", "transport"}
}
return objSchema(props, required...)
}
func (a mcpToolInput) toDB() *db.MCPServer {
enabled := true
if a.Enabled != nil {
enabled = *a.Enabled
}
insecure := false
if a.Insecure != nil {
insecure = *a.Insecure
}
return &db.MCPServer{
ID: a.ID, Name: a.Name, Transport: a.Transport, Command: a.Command,
Args: a.Args, Env: a.Env, URL: a.URL, Enabled: enabled, Insecure: insecure,
}
}
func (s *Server) toolCreateMCP() actool.CoreTool {
return wrTool("create_mcp", "创建一个 MCP 服务器(stdio/http/sse)。创建后其工具需按 agent 可见性授权。",
mcpSchema(false),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a mcpToolInput
_ = json.Unmarshal(in, &a)
a.ID = 0
if strings.TrimSpace(a.Name) == "" || strings.TrimSpace(a.Transport) == "" {
return actool.Errorf("name / transport 必填"), nil
}
id, err := s.m.pg.SaveMCP(a.toDB())
if err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text(fmt.Sprintf("mcp created: id=%d name=%s", id, a.Name)), nil
})
}
func (s *Server) toolUpdateMCP() actool.CoreTool {
return wrTool("update_mcp", "修改一个已有的 MCP 服务器(按 id)。",
mcpSchema(true),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a mcpToolInput
_ = json.Unmarshal(in, &a)
if a.ID == 0 {
return actool.Errorf("id 必填"), nil
}
if _, err := s.m.pg.SaveMCP(a.toDB()); err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text(fmt.Sprintf("mcp updated: id=%d", a.ID)), nil
})
}