package skills import ( "context" "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // Store 把技能定义持久化进 SQLite 的 skills 表。它并发安全(底层 *sql.DB 串行化 // 写),且所有查询都参数化 —— 值永远不拼进 SQL。 type Store struct { db *sql.DB log *slog.Logger } // createSkillsTable 创建 skills 表结构。id 是不透明主键;name 是稳定且唯一的技能 // 标识(Upsert/Get/Delete 都按它查)。desc 列加双引号是因为 DESC 在排序语境里是 // SQL 关键字。spec_json 承载 design/09 §6.2 的新增字段(版本/调用三态/工具上限/ // 上下文模式/元数据等)——**必须落盘**:丢掉 allowed-tools 等于把能力收缩放宽, // 那是安全红线,不允许「读回来就变成不限」。 const createSkillsTable = ` CREATE TABLE IF NOT EXISTS skills ( id TEXT PRIMARY KEY, name TEXT NOT NULL, "desc" TEXT NOT NULL DEFAULT '', prompt TEXT NOT NULL DEFAULT '', steps_json TEXT NOT NULL DEFAULT '[]', refs_json TEXT NOT NULL DEFAULT '[]', guardrails_json TEXT NOT NULL DEFAULT '[]', spec_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_skills_name ON skills (name); ` // upsertSkillSQL 插入一条技能;name 已存在时更新该行,created_at 保持不变。 const upsertSkillSQL = ` INSERT INTO skills (id, name, "desc", prompt, steps_json, refs_json, guardrails_json, spec_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(name) DO UPDATE SET "desc" = excluded."desc", prompt = excluded.prompt, steps_json = excluded.steps_json, refs_json = excluded.refs_json, guardrails_json = excluded.guardrails_json, spec_json = excluded.spec_json, updated_at = excluded.updated_at ` // skillColumns 是读取技能行时的列清单(禁止 SELECT *)。 const skillColumns = `name, "desc", prompt, steps_json, refs_json, guardrails_json, spec_json` const ( getSkillSQL = `SELECT ` + skillColumns + ` FROM skills WHERE name = ?` listSkillsSQL = `SELECT ` + skillColumns + ` FROM skills ORDER BY name` deleteSkillSQL = `DELETE FROM skills WHERE name = ?` ) // idByteLen 是技能行随机 id 的字节数(128 位)。 const idByteLen = 16 // specColumnName 是 spec_json 列名;ensureSkillSpecColumn 用它做幂等补列。 const specColumnName = "spec_json" // NewStore 构造 Store,并在需要时建表/补列。db 为 nil 时返回 ErrNilDB。 // ctx 用于建表语句。 func NewStore(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) { if db == nil { return nil, fmt.Errorf("skills: store: %w", ErrNilDB) } s := &Store{db: db, log: sourcedLog(log)} if _, err := db.ExecContext(ctx, createSkillsTable); err != nil { return nil, fmt.Errorf("skills: store: create skills table: %w", err) } if err := s.ensureSpecColumn(ctx); err != nil { return nil, err } s.log.Info("skills table ready") return s, nil } // ensureSpecColumn 给既有的 skills 表补上 spec_json 列(SQLite 支持 ADD COLUMN 但 // 不支持 IF NOT EXISTS,因此先读 pragma 再决定)。CREATE TABLE IF NOT EXISTS 对旧库 // 不会改结构,缺这一列就会让写入报错,故必须幂等补齐。 func (s *Store) ensureSpecColumn(ctx context.Context) error { rows, err := s.db.QueryContext(ctx, `SELECT name FROM pragma_table_info('skills')`) if err != nil { return fmt.Errorf("skills: store: read skills columns: %w", err) } defer rows.Close() for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return fmt.Errorf("skills: store: scan skills column: %w", err) } if name == specColumnName { return nil } } if err := rows.Err(); err != nil { return fmt.Errorf("skills: store: read skills columns: %w", err) } if _, err := s.db.ExecContext(ctx, `ALTER TABLE skills ADD COLUMN spec_json TEXT NOT NULL DEFAULT '{}'`); err != nil { return fmt.Errorf("skills: store: add column %s: %w", specColumnName, err) } s.log.Info("skills table migrated", "column", specColumnName) return nil } // Upsert 写入技能:插入新行,或更新同名的既有行(created_at 保持)。skill 为 nil // 或名字为空时返回 ErrInvalidSkill。实现了 sdk.SkillSpec 的技能会连 Spec 一起落盘; // 只实现基础 sdk.Skill 的技能按各字段的缺省值落盘。 func (s *Store) Upsert(ctx context.Context, skill sdk.Skill) error { if skill == nil || strings.TrimSpace(skill.Name()) == "" { return fmt.Errorf("skills: store: upsert: %w", ErrInvalidSkill) } name := skill.Name() stepsJSON, err := marshalJSON(skill.Steps()) if err != nil { return fmt.Errorf("skills: store: upsert %q: steps: %w", name, err) } refsJSON, err := marshalJSON(skill.Refs()) if err != nil { return fmt.Errorf("skills: store: upsert %q: refs: %w", name, err) } guardrailsJSON, err := marshalJSON(skill.Guardrails()) if err != nil { return fmt.Errorf("skills: store: upsert %q: guardrails: %w", name, err) } specJSON, err := marshalJSON(specOf(sdk.SkillView(skill))) if err != nil { return fmt.Errorf("skills: store: upsert %q: spec: %w", name, err) } now := time.Now().UTC().Format(time.RFC3339Nano) if _, err := s.db.ExecContext(ctx, upsertSkillSQL, newID(), name, skill.Description(), skill.Prompt(), stepsJSON, refsJSON, guardrailsJSON, specJSON, now, now, ); err != nil { return fmt.Errorf("skills: store: upsert %q: %w", name, err) } s.log.Info("skill upserted", "skill", name) return nil } // Get 返回名为 name 的技能;不存在时返回 ErrNotFound。 func (s *Store) Get(ctx context.Context, name string) (*Skill, error) { row := s.db.QueryRowContext(ctx, getSkillSQL, name) skill, err := scanSkill(row, name) if err != nil { return nil, err } return skill, nil } // List 返回全部已存技能,按名字升序。 func (s *Store) List(ctx context.Context) ([]*Skill, error) { rows, err := s.db.QueryContext(ctx, listSkillsSQL) if err != nil { return nil, fmt.Errorf("skills: store: list: %w", err) } defer rows.Close() out := make([]*Skill, 0) for rows.Next() { skill, err := scanSkill(rows, "") if err != nil { return nil, err } out = append(out, skill) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("skills: store: list: iterate: %w", err) } return out, nil } // Delete 删除名为 name 的技能;不存在时返回 ErrNotFound。 func (s *Store) Delete(ctx context.Context, name string) error { res, err := s.db.ExecContext(ctx, deleteSkillSQL, name) if err != nil { return fmt.Errorf("skills: store: delete %q: %w", name, err) } n, err := res.RowsAffected() if err != nil { return fmt.Errorf("skills: store: delete %q: rows affected: %w", name, err) } if n == 0 { return fmt.Errorf("skills: store: delete %q: %w", name, ErrNotFound) } s.log.Info("skill deleted", "skill", name) return nil } // rowScanner 是 scanSkill 需要的 *sql.Row / *sql.Rows 子集。 type rowScanner interface{ Scan(dest ...any) error } // scanSkill 读一行 skills 记录并构造 *Skill。key 只用于错误文案里的查找名。 func scanSkill(sc rowScanner, key string) (*Skill, error) { var name, desc, prompt, stepsJSON, refsJSON, guardrailsJSON, specJSON string if err := sc.Scan(&name, &desc, &prompt, &stepsJSON, &refsJSON, &guardrailsJSON, &specJSON); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("skills: store: get %q: %w", key, ErrNotFound) } return nil, fmt.Errorf("skills: store: scan skill: %w", err) } steps, err := unmarshalSteps(stepsJSON) if err != nil { return nil, fmt.Errorf("skills: store: skill %q: steps: %w", name, err) } refs, err := unmarshalRefs(refsJSON) if err != nil { return nil, fmt.Errorf("skills: store: skill %q: refs: %w", name, err) } guardrails, err := unmarshalGuardrails(guardrailsJSON) if err != nil { return nil, fmt.Errorf("skills: store: skill %q: guardrails: %w", name, err) } spec, err := unmarshalSpec(specJSON) if err != nil { return nil, fmt.Errorf("skills: store: skill %q: spec: %w", name, err) } skill, err := New(name, desc, prompt, steps, refs, guardrails, WithSpec(spec)) if err != nil { return nil, fmt.Errorf("skills: store: skill %q: %w", name, err) } return skill, nil } func marshalJSON(v any) (string, error) { b, err := json.Marshal(v) if err != nil { return "", err } return string(b), nil } func unmarshalSteps(s string) ([]sdk.Step, error) { var out []sdk.Step if err := json.Unmarshal([]byte(s), &out); err != nil { return nil, err } return out, nil } func unmarshalRefs(s string) ([]sdk.Ref, error) { var out []sdk.Ref if err := json.Unmarshal([]byte(s), &out); err != nil { return nil, err } return out, nil } func unmarshalGuardrails(s string) ([]sdk.Guardrail, error) { var out []sdk.Guardrail if err := json.Unmarshal([]byte(s), &out); err != nil { return nil, err } return out, nil } // unmarshalSpec 解析 spec_json;空串按空定义对待(旧行默认 '{}'),非法 JSON 报错 // 而不是静默丢掉「工具上限」这类安全相关字段。 func unmarshalSpec(s string) (Spec, error) { if strings.TrimSpace(s) == "" { return Spec{}, nil } var out Spec if err := json.Unmarshal([]byte(s), &out); err != nil { return Spec{}, err } return out, nil } // newID 返回技能行的随机 128 位十六进制标识。 func newID() string { var b [idByteLen]byte if _, err := rand.Read(b[:]); err != nil { // crypto/rand 实际上不会失败;退化成纳秒时间戳,别让存储因为拿不到 id 卡住。 return fmt.Sprintf("%d", time.Now().UnixNano()) } return hex.EncodeToString(b[:]) }