Files
artex/db/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

233 lines
8.0 KiB
Go

package db
import (
"database/sql"
"encoding/json"
)
// Tool is one row of the built-in tool catalog. key + handler live in code; this
// row carries only the page-editable surface: description, parameter schema
// (structure read-only, per-param description/default editable), agent binding,
// and the on/off switch. See schema.sql §H and agent/toolcatalog.go.
type Tool struct {
Key string `json:"key"`
System bool `json:"system"`
Description string `json:"description"`
Schema json.RawMessage `json:"schema"`
Agents []string `json:"agents"`
Enabled bool `json:"enabled"`
Kind string `json:"kind"` // builtin | command | script | http
Exec json.RawMessage `json:"exec"` // 自定义工具执行规格(kind!=builtin)
Deferred bool `json:"deferred"` // schema 延迟(走 SearchExtraTools/ExecuteExtraTool)
Calls int `json:"calls"` // runtime ledger aggregate; not stored in tools
}
const toolCols = `key, system, description, schema, agents, enabled, kind, exec, deferred`
// SeedTool inserts a built-in tool's code-defined defaults ONCE. ON CONFLICT DO
// NOTHING: an existing row (possibly edited in the UI) is never overwritten on
// startup — that's what keeps page edits from being wiped every restart. Use
// UpsertToolForce for an explicit "reset to code default".
func (d *DB) SeedTool(key, desc string, schema, agents json.RawMessage) error {
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
if len(agents) == 0 {
agents = json.RawMessage("[]")
}
_, err := d.Exec(`
INSERT INTO tools(key, system, description, schema, agents, enabled)
VALUES ($1, true, $2, $3, $4, true)
ON CONFLICT (key) DO NOTHING`, key, desc, schema, agents)
return err
}
// AddAgentToToolBinding adds an agent key to the given tools' `agents` arrays if
// not already present (idempotent). Used to give the built-in Auto agent its
// default toolset on existing DBs.
func (d *DB) AddAgentToToolBinding(agentKey string, keys []string) error {
for _, k := range keys {
if _, err := d.Exec(`UPDATE tools SET agents = agents || to_jsonb($1::text) WHERE key=$2 AND NOT (agents ? $1)`, agentKey, k); err != nil {
return err
}
}
return nil
}
// RemoveAgentFromToolBindings strips an agent key from every tool's `agents`
// JSONB array — called when a custom agent is deleted so no tool keeps a dangling
// binding. Uses jsonb `-` (remove array element) guarded by `?` (membership).
func (d *DB) RemoveAgentFromToolBindings(agentKey string) error {
_, err := d.Exec(`UPDATE tools SET agents = agents - $1 WHERE agents ? $1`, agentKey)
return err
}
// RemoveAgentFromTool strips one agent key from a SINGLE tool's `agents` array —
// used by one-time migrations that change a tool's default binding on existing DBs
// (SeedTool is first-insert-only, so a changed default never reaches a seeded row).
func (d *DB) RemoveAgentFromTool(agentKey, toolKey string) error {
_, err := d.Exec(`UPDATE tools SET agents = agents - $1 WHERE key=$2 AND agents ? $1`, agentKey, toolKey)
return err
}
// UpsertToolForce overwrites a tool row with the given code-default values (used by
// the per-tool "reset" action). It resets description/schema/agents and re-enables
// the tool, but keeps system=true.
func (d *DB) UpsertToolForce(key, desc string, schema, agents json.RawMessage) error {
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
if len(agents) == 0 {
agents = json.RawMessage("[]")
}
_, err := d.Exec(`
INSERT INTO tools(key, system, description, schema, agents, enabled)
VALUES ($1, true, $2, $3, $4, true)
ON CONFLICT (key) DO UPDATE
SET description = EXCLUDED.description,
schema = EXCLUDED.schema,
agents = EXCLUDED.agents,
enabled = true`, key, desc, schema, agents)
return err
}
// RefreshToolDefaults updates a system tool's model-facing description + schema to the
// code defaults, PRESERVING the user's agent binding + enabled flag. Used by one-time
// migrations to propagate a code schema change (SeedTool is first-insert-only, so a new
// parameter added in code otherwise never reaches an already-seeded row). No-op for
// custom tools or unknown keys.
func (d *DB) RefreshToolDefaults(key, desc string, schema json.RawMessage) error {
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
_, err := d.Exec(`UPDATE tools SET description=$2, schema=$3, updated_at=now() WHERE key=$1 AND system`, key, desc, schema)
return err
}
func scanTool(rows interface{ Scan(...any) error }) (*Tool, error) {
var t Tool
var agents []byte
if err := rows.Scan(&t.Key, &t.System, &t.Description, &t.Schema, &agents, &t.Enabled, &t.Kind, &t.Exec, &t.Deferred); err != nil {
return nil, err
}
if len(agents) > 0 {
_ = json.Unmarshal(agents, &t.Agents)
}
if t.Agents == nil {
t.Agents = []string{}
}
return &t, nil
}
// ListTools returns the whole tool catalog, ordered by key.
func (d *DB) ListTools() ([]*Tool, error) {
rows, err := d.Query(`SELECT ` + toolCols + ` FROM tools ORDER BY key`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*Tool
for rows.Next() {
t, err := scanTool(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
// GetTool fetches one tool row (nil if absent).
func (d *DB) GetTool(key string) (*Tool, error) {
row := d.QueryRow(`SELECT `+toolCols+` FROM tools WHERE key=$1`, key)
t, err := scanTool(row)
if err == sql.ErrNoRows {
return nil, nil
}
return t, err
}
// CreateCustomTool inserts a user-defined tool (system=false) with an execution
// spec. Fails if the key already exists.
func (d *DB) CreateCustomTool(t *Tool) error {
schema := t.Schema
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
exec := t.Exec
if len(exec) == 0 {
exec = json.RawMessage("{}")
}
agents, _ := json.Marshal(t.Agents)
if len(agents) == 0 {
agents = json.RawMessage("[]")
}
_, err := d.Exec(`
INSERT INTO tools(key, system, description, schema, agents, enabled, kind, exec, deferred)
VALUES ($1, false, $2, $3, $4, $5, $6, $7, $8)`,
t.Key, t.Description, schema, agents, t.Enabled, t.Kind, exec, t.Deferred)
return err
}
// UpdateCustomTool updates a custom tool's editable fields (kind/exec/deferred +
// desc/schema/agents/enabled). Only touches system=false rows.
func (d *DB) UpdateCustomTool(t *Tool) error {
schema := t.Schema
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
exec := t.Exec
if len(exec) == 0 {
exec = json.RawMessage("{}")
}
agents, _ := json.Marshal(t.Agents)
if len(agents) == 0 {
agents = json.RawMessage("[]")
}
_, err := d.Exec(`
UPDATE tools SET description=$2, schema=$3, agents=$4, enabled=$5, kind=$6, exec=$7, deferred=$8
WHERE key=$1 AND system=false`,
t.Key, t.Description, schema, agents, t.Enabled, t.Kind, exec, t.Deferred)
return err
}
// DeleteCustomTool removes a custom tool (system=false only; built-ins protected).
func (d *DB) DeleteCustomTool(key string) error {
_, err := d.Exec(`DELETE FROM tools WHERE key=$1 AND system=false`, key)
return err
}
// ListCustomTools returns only the user-defined (system=false) tools.
func (d *DB) ListCustomTools() ([]*Tool, error) {
rows, err := d.Query(`SELECT ` + toolCols + ` FROM tools WHERE system=false ORDER BY key`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*Tool
for rows.Next() {
t, err := scanTool(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
// UpdateTool saves the page-editable fields. key is never changed (it is welded to
// the Go handler). system tools: the caller must keep the schema structure — only
// per-param description/default and the agent binding are meant to move.
func (d *DB) UpdateTool(key, desc string, schema, agents json.RawMessage, enabled bool) error {
if len(schema) == 0 {
schema = json.RawMessage("{}")
}
if len(agents) == 0 {
agents = json.RawMessage("[]")
}
_, err := d.Exec(`
UPDATE tools SET description=$2, schema=$3, agents=$4, enabled=$5 WHERE key=$1`,
key, desc, schema, agents, enabled)
return err
}