Files
inkreach-agent/docs/plugins/11-storage.md

3.9 KiB
Raw Permalink Blame History

L9 插件存储(SQLite)

每个插件一块私有持久化空间:一个 SQLite 库 + 一个自由文件目录。 KV 简单状态、SQL 结构化数据、文件随便放;schema 必须机器可读声明——它是 Agent 自我迭代与 UI 展示的同一份事实源。

1. 空间布局

userData/plugin-data/<plugin-id>/
  data.db        ← SQLite 库(WAL 模式):kv 表(宿主管理)+ 插件自建表
  data.db-wal    ← WAL 伴生文件
  <其余自由文件>  ← api.storage.dir() 指向这里,插件自行读写
  • 每插件一库(不是共享大库):导出/清除/损坏恢复都以插件为单位,跨插件天然隔离。
  • 卸载/移除插件不删数据(重装即恢复);删除只在设置页点「清除数据」(二次确认)时发生。

2. API(全部 async——底层在宿主共享 worker 线程执行,永不阻塞主进程)

export default definePlugin(async (api) => {
  // ① 声明机器可读 schema(使用 db 前必须声明;plugin:check 强制)
  api.storage.declareSchema({
    kv: { description: '会话级偏好:ui.<key> = 用户界面偏好' },
    tables: [
      {
        name: 'notes',
        description: '用户笔记',
        columns: [
          { name: 'id', type: 'TEXT', description: '主键,ulid' },
          { name: 'title', type: 'TEXT', description: '标题,非空' },
          { name: 'created_at', type: 'INTEGER', description: '创建时间,Unix 毫秒' }
        ]
      }
    ]
  })

  // ② KV:宿主管理的 kv 表(value 走 JSON 编码,适合设置类小状态)
  const last = await api.storage.kv.get<string>('ui.lastQuery')
  await api.storage.kv.set('ui.lastQuery', 'postgres')
  await api.storage.kv.delete('ui.lastQuery')
  const keys = await api.storage.kv.keys()

  // ③ 自建表:SQL 自由发挥
  await api.storage.db.exec('CREATE TABLE IF NOT EXISTS notes (...)')  // DDL,激活期执行
  await api.storage.db.run('INSERT INTO notes (id, title) VALUES (?, ?)', ['n1', '标题'])
  const rows = await api.storage.db.query('SELECT * FROM notes WHERE title LIKE ?', ['%标%'])
  // 原子批量:worker 端 BEGIN/COMMIT,任一失败整体回滚
  await api.storage.db.batch([
    { sql: 'INSERT INTO notes (id, title) VALUES (?, ?)', params: ['n2', 'a'] },
    { sql: 'UPDATE notes SET title = ? WHERE id = ?', params: ['b', 'n2'] }
  ])

  // ④ 自由文件(日志快照、导出等)
  const dir = api.storage.dir() // userData/plugin-data/<id>/
})

3. schema 声明为什么是硬性要求

插件会被 Agent 自我迭代。迭代前 Agent 需要知道「库里有什么」:

  • declareSchema 的内容随 plugins:list 下发——管理页「存储区」卡片与(未来的)Agent 内省工具读的是同一份数据;
  • 每张表、每个字段的 description 必填,plugin:check 会拒绝「用了 db 却不声明 schema」的插件;
  • 写给 Agent 读的 description 请具体(「创建时间,Unix 毫秒」而不是「时间」)。

4. 迁移(表结构变更)

宿主不解释 schema,迁移由插件自管,惯用法:

const row = await api.storage.db.query('PRAGMA user_version')
const v = Number(row[0]?.user_version ?? 0)
if (v < 1) {
  await api.storage.db.exec('ALTER TABLE notes ADD COLUMN tag TEXT')
  await api.storage.db.exec('PRAGMA user_version = 1')
}

5. 设置页管理

  • 二级页「存储区」卡片:schema 表格化展示(表/列/类型/描述)+ 数据目录(资源管理器打开)+ 清除数据(二次确认,连接关闭后整目录删除)。

6. 边界

  • 存储只在 main 层提供;renderer 组件 / iframe 页需要数据时,让 main 侧经状态胶囊/设置值转发(受控 IPC 通道属后续契约)。
  • KV 的 value 必须可 JSON 序列化(Date/Map 请先转换);大二进制放文件,路径存 KV。
  • 跨插件不直读对方库:需要别人的数据 = 声明依赖 + 走对方服务(见 L8)。