89 lines
3.9 KiB
Markdown
89 lines
3.9 KiB
Markdown
# 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 线程执行,永不阻塞主进程)
|
||
|
||
```ts
|
||
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,迁移由插件自管,惯用法:
|
||
|
||
```ts
|
||
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)。
|