3.9 KiB
3.9 KiB
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)。