Storage
OpenCode 使用键值存储系统来持久化数据。
概述
Storage 基于文件系统实现持久化,提供简单的键值 API。所有数据存储在 ~/.opencode/data/ 目录下。
存储位置
~/.opencode/data/
├── project/
│ └── <project-id>.json
├── session/
│ └── <project-id>/
│ ├── <session-id>.json
│ └── session_diff/
│ └── <session-id>.json
├── message/
│ └── <session-id>/
│ └── <message-id>.json
├── part/
│ └── <message-id>/
│ └── <part-id>.json
├── permission/
│ └── <project-id>.json
├── permission_approval/
│ └── <project-id>.json
├── permission_pending/
│ └── <project-id>/
│ └── <session-id>.json
└── share/
└── <session-id>.json
API
读取
async function read<T>(key: string[]): Promise<T>
从存储中读取值。
示例:
const session = await Storage.read<Session.Info>(["session", projectID, sessionID])
写入
async function write<T>(key: string[], value: T): Promise<void>
写入值到存储。
示例:
await Storage.write(["session", projectID, sessionID], session)
更新
async function update<T>(key: string[], updater: (draft: T) => void): Promise<T>
读取、修改并写回值。
示例:
const updated = await Storage.update(["session", projectID, sessionID], (draft) => {
draft.title = "New title"
draft.time.updated = Date.now()
})
移除
async function remove(key: string[]): Promise<void>
从存储中删除值。
示例:
await Storage.remove(["session", projectID, sessionID])
列出
async function list(prefix: string[]): Promise<string[][]>
列出所有具有给定前缀的键。
示例:
const messages = await Storage.list(["message", sessionID])
for (const key of messages) {
const messageID = key[key.length - 1]
console.log(`Message: ${messageID}`)
}
存储键结构
核心对象
| 对象 | 键模式 |
|---|---|
| Project | ["project", projectID] |
| Session | ["session", projectID, sessionID] |
| Message | ["message", sessionID, messageID] |
| Part | ["part", messageID, partID] |
元数据
| 对象 | 键模式 |
|---|---|
| SessionDiff | ["session_diff", sessionID] |
| Share | ["share", sessionID] |
| Permission | ["permission", projectID] |
权限
| 对象 | 键模式 |
|---|---|
| Approval | ["permission_approval", projectID] |
| Pending | ["permission_pending", projectID, sessionID] |
数据格式
所有存储的数据都是 JSON 格式。
项目示例
{
"id": "abc123",
"worktree": "/home/user/project",
"vcs": "git",
"time": {
"created": 1640995200000,
"updated": 1640995200000
},
"sandboxes": []
}
会话示例
{
"id": "sess_abc123",
"slug": "xyz789",
"projectID": "abc123",
"directory": "/home/user/project",
"title": "New session",
"version": "1.0.0",
"time": {
"created": 1640995200000,
"updated": 1640995200000
}
}
并发安全
Storage 使用文件锁确保并发安全:
// 内部实现
async function withLock<T>(key: string[], fn: () => Promise<T>): Promise<T> {
const lockFile = getLockPath(key)
// 等待锁
await waitForLock(lockFile)
try {
// 执行操作
return await fn()
} finally {
// 释放锁
await releaseLock(lockFile)
}
}
错误处理
文件不存在
try {
const value = await Storage.read(["key"])
} catch (error) {
if (error.code === "ENOENT") {
// 文件不存在
}
}
解析错误
try {
const value = await Storage.read<T>(key)
} catch (error) {
if (error.name === "ZodError") {
// JSON 格式不匹配
}
}
性能考虑
批量读取
// 读取多个消息
const messages = await Promise.all(messageIDs.map((id) => Storage.read(["message", sessionID, id])))
批量写入
// 写入多个 parts
await Promise.all(parts.map((part) => Storage.write(["part", part.messageID, part.id], part)))
数据迁移
当存储结构变更时,需要迁移旧数据:
async function migrateV1ToV2() {
const projects = await Storage.list(["project"])
for (const key of projects) {
const project = await Storage.read(key)
// 检查是否需要迁移
if (!project.sandboxes) {
// 添加新字段
project.sandboxes = []
// 写回
await Storage.write(key, project)
}
}
}
清理策略
过期数据
// 清理 30 天前的会话
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000
const sessions = await Storage.list(["session", projectID])
for (const key of sessions) {
const session = await Storage.read(key)
if (session.time.created < cutoff) {
// 删除过期会话
await Session.remove(session.id)
}
}
存储监控
使用事件
Bus.subscribeAll(async (event) => {
console.log(`Event: ${event.type}`)
console.log(`Data: ${JSON.stringify(event.properties)}`)
})
关系图
最佳实践
- 使用完整键路径: 避免使用通配符
- 批量操作: 使用 Promise.all 进行批量读写
- 错误处理: 总是处理 ENOENT 和解析错误
- 数据验证: 使用 Zod schema 验证读取的数据
- 性能: 避免频繁的小写入,考虑批量更新
变更历史
| 版本 | 变更内容 | 日期 |
|---|---|---|
| v1 | 初始 Storage 实现 | - |
| v1.1 | 添加文件锁支持 | - |
| v1.2 | 添加并发安全 | - |