opencode SQLite 数据库结构与查询手册

opencode SQLite 数据库结构与查询手册

数据库文件位置

opencode 使用 XDG Base Directory 标准存储数据:

系统 默认路径
macOS(XDG 环境) ~/.local/share/opencode/opencode.db
macOS(无 XDG) ~/Library/Application Support/opencode/opencode.db
Linux ~/.local/share/opencode/opencode.db

脚本会自动检测两个路径,取第一个存在的。

自定义路径: 设置环境变量 OPENCODE_DB 可覆盖默认路径(绝对路径或 :memory:)。

安装渠道隔离: 非 latest/beta/prod 渠道的安装版本,数据库文件名为 opencode-{channel}.db


数据库配置(PRAGMA)

ini 复制代码
PRAGMA journal_mode = WAL;       -- 预写式日志,提升并发
PRAGMA synchronous = NORMAL;     -- 性能与安全的平衡
PRAGMA busy_timeout = 5000;      -- 5 秒锁等待超时
PRAGMA cache_size = -64000;      -- 64MB 内存缓存
PRAGMA foreign_keys = ON;        -- 启用外键约束

表结构总览

csharp 复制代码
project(项目)
  ├── permission(权限规则)
  ├── workspace(工作空间)
  └── session(会话)
        ├── message(消息)
        │     └── part(消息片段:文本/工具调用/工具结果/压缩...)
        ├── todo(待办项)
        ├── session_share(会话分享)
        └── session_entry(会话条目/事件)

account(账户)
  └── account_state(账户状态)

event_sequence(事件序列)
  └── event(事件)

表详细说明

project --- 项目

字段 类型 说明
id TEXT PK 项目唯一 ID
worktree TEXT Git worktree 路径
vcs TEXT 版本控制系统类型
name TEXT 项目名称
icon_url TEXT 项目图标 URL
icon_url_override TEXT 覆盖图标 URL
icon_color TEXT 图标颜色
sandboxes TEXT(JSON) 沙箱配置列表
commands TEXT(JSON) 项目命令({start?: string}
time_created INTEGER 创建时间戳(毫秒)
time_updated INTEGER 更新时间戳(毫秒)
time_initialized INTEGER 初始化时间戳

session --- 会话

字段 类型 说明
id TEXT PK 会话唯一 ID
project_id TEXT FK→project 所属项目
workspace_id TEXT FK→workspace 所属工作空间(可选)
parent_id TEXT FK→session 父会话 ID(子 agent 场景)
slug TEXT URL 友好别名
directory TEXT 工作目录
path TEXT 文件路径(可选)
title TEXT 会话标题
version TEXT 会话版本号
share_url TEXT 分享链接
summary_additions INTEGER 代码增加行数
summary_deletions INTEGER 代码删除行数
summary_files INTEGER 修改文件数
summary_diffs TEXT(JSON) 文件差异快照
revert TEXT(JSON) 回滚信息 {messageID, partID?, snapshot?, diff?}
permission TEXT(JSON) 权限规则集
time_created INTEGER 创建时间戳(毫秒)
time_updated INTEGER 更新时间戳(毫秒)
time_compacting INTEGER 正在压缩的时间戳
time_archived INTEGER 归档时间戳

message --- 消息

字段 类型 说明
id TEXT PK 消息唯一 ID
session_id TEXT FK→session 所属会话
time_created INTEGER 创建时间戳(毫秒)
time_updated INTEGER 更新时间戳(毫秒)
data TEXT(JSON) 消息信息(见下方 JSON 结构)

data 字段 JSON 结构(MessageV2.Info):

json 复制代码
// role = "user"
{
  "id": "msg_xxx",
  "role": "user",
  "sessionID": "ses_xxx",
  "model": { "providerID": "anthropic", "modelID": "claude-sonnet-4-6" }
}

// role = "assistant"
{
  "id": "msg_xxx",
  "role": "assistant",
  "sessionID": "ses_xxx",
  "parentID": "msg_yyy",        // 对应的 user 消息 ID
  "model": { "providerID": "...", "modelID": "..." },
  "agent": "general",           // 使用的 agent 名称
  "summary": true,              // 是否为压缩摘要(compaction 产出)
  "finish": "stop",             // 结束原因:stop / tool-calls / length / error
  "tokens": {
    "input": 1234,
    "output": 567,
    "cache": { "read": 0, "write": 0 }
  },
  "error": { ... }              // 若出错,包含错误信息
}

part --- 消息片段

每条消息由一个或多个 part 组成,每个 part 是消息的一个组成单元。

字段 类型 说明
id TEXT PK Part 唯一 ID
message_id TEXT FK→message 所属消息
session_id TEXT 所属会话(冗余存储,加速查询)
time_created INTEGER 创建时间戳(毫秒)
time_updated INTEGER 更新时间戳(毫秒)
data TEXT(JSON) Part 数据(见下方 JSON 结构)

data 字段的 type 取值:

json 复制代码
// type = "text"(普通文本回复)
{
  "type": "text",
  "id": "part_xxx",
  "messageID": "msg_xxx",
  "sessionID": "ses_xxx",
  "text": "这是 LLM 的回复文本"
}

// type = "tool"(工具调用请求)
{
  "type": "tool",
  "id": "part_xxx",
  "tool": "read",               // 工具名称
  "toolCallID": "call_xxx",
  "input": { "file_path": "/path/to/file" },
  "state": {
    "status": "completed",      // pending / running / completed / error
    "output": "文件内容...",
    "metadata": { ... },
    "time": {
      "start": 1700000000000,
      "end":   1700000001000,
      "compacted": null          // 若被 prune 裁剪则有时间戳
    }
  }
}

// type = "compaction"(压缩请求标记)
{
  "type": "compaction",
  "id": "part_xxx",
  "auto": true,                  // 是否自动触发
  "overflow": false,             // 是否因 API overflow 触发
  "tail_start_id": "msg_yyy"    // tail 开始消息 ID
}

// type = "step-start"(agent 步骤开始标记)
{
  "type": "step-start",
  "id": "part_xxx"
}

// type = "error"
{
  "type": "error",
  "error": { "name": "...", "message": "..." }
}

todo --- 待办项

字段 类型 说明
session_id TEXT FK→session 所属会话
content TEXT 待办内容
status TEXT 状态(pending/completed 等)
priority TEXT 优先级
position INTEGER 排序位置
time_created INTEGER 创建时间戳
time_updated INTEGER 更新时间戳

主键: (session_id, position)


session_entry --- 会话条目

记录会话过程中的结构化事件(如权限请求、文件变更等)。

字段 类型 说明
id TEXT PK 条目唯一 ID
session_id TEXT FK→session 所属会话
type TEXT 条目类型
data TEXT(JSON) 条目数据
time_created INTEGER 创建时间戳
time_updated INTEGER 更新时间戳

session_share --- 会话分享

字段 类型 说明
session_id TEXT PK FK→session 所属会话
id TEXT 分享 ID
secret TEXT 分享密钥
url TEXT 分享链接
time_created INTEGER 创建时间戳
time_updated INTEGER 更新时间戳

workspace --- 工作空间

字段 类型 说明
id TEXT PK 工作空间 ID
project_id TEXT FK→project 所属项目
type TEXT 类型
name TEXT 名称
branch TEXT Git 分支
directory TEXT 工作目录
extra TEXT(JSON) 额外配置

account / account_state --- 账户

字段(account) 类型 说明
id TEXT PK 账户 ID
email TEXT 邮箱
url TEXT 服务端 URL
access_token TEXT OAuth access token
refresh_token TEXT OAuth refresh token
token_expiry INTEGER token 过期时间戳
字段(account_state) 类型 说明
id INTEGER PK 单例记录 ID
active_account_id TEXT FK→account 当前激活账户
active_org_id TEXT 当前激活组织 ID

event / event_sequence --- 事件溯源

字段(event) 类型 说明
id TEXT PK 事件 ID
aggregate_id TEXT FK→event_sequence 聚合根 ID
seq INTEGER 序列号
type TEXT 事件类型
data TEXT(JSON) 事件数据

常用 SQL 查询

连接数据库(命令行):

javascript 复制代码
sqlite3 ~/Library/Application\ Support/opencode/opencode.db

查询所有 Session 列表

sql 复制代码
SELECT
  s.id,
  s.title,
  s.directory,
  datetime(s.time_created / 1000, 'unixepoch', 'localtime') AS created_at,
  datetime(s.time_updated / 1000, 'unixepoch', 'localtime') AS updated_at,
  s.summary_files,
  s.summary_additions,
  s.summary_deletions
FROM session s
ORDER BY s.time_created DESC;

查询某个 Session 的完整对话(消息 + 文本内容)

scss 复制代码
-- 替换 'your-session-id' 为实际 session ID
SELECT
  m.id                                          AS message_id,
  datetime(m.time_created / 1000, 'unixepoch', 'localtime') AS time,
  json_extract(m.data, '$.role')                AS role,
  json_extract(m.data, '$.agent')               AS agent,
  json_extract(m.data, '$.finish')              AS finish,
  json_extract(m.data, '$.tokens.input')        AS tokens_in,
  json_extract(m.data, '$.tokens.output')       AS tokens_out,
  p.id                                          AS part_id,
  json_extract(p.data, '$.type')                AS part_type,
  json_extract(p.data, '$.text')                AS text_content
FROM message m
LEFT JOIN part p ON p.message_id = m.id
WHERE m.session_id = 'your-session-id'
  AND json_extract(p.data, '$.type') = 'text'
ORDER BY m.time_created, p.time_created;

查询所有工具调用记录

sql 复制代码
SELECT
  p.session_id,
  m.id                                               AS message_id,
  datetime(p.time_created / 1000, 'unixepoch', 'localtime') AS time,
  json_extract(p.data, '$.tool')                     AS tool_name,
  json_extract(p.data, '$.input')                    AS input_json,
  json_extract(p.data, '$.state.status')             AS status,
  json_extract(p.data, '$.state.time.start')         AS start_ms,
  json_extract(p.data, '$.state.time.end')           AS end_ms,
  (json_extract(p.data, '$.state.time.end') -
   json_extract(p.data, '$.state.time.start'))       AS duration_ms,
  CASE WHEN json_extract(p.data, '$.state.time.compacted') IS NOT NULL
       THEN 'pruned' ELSE 'intact' END               AS prune_status
FROM part p
JOIN message m ON m.id = p.message_id
WHERE json_extract(p.data, '$.type') = 'tool'
ORDER BY p.time_created DESC
LIMIT 200;

按工具名称统计调用次数和平均耗时

sql 复制代码
SELECT
  json_extract(p.data, '$.tool')                     AS tool_name,
  COUNT(*)                                           AS call_count,
  COUNT(CASE WHEN json_extract(p.data, '$.state.status') = 'error' THEN 1 END) AS error_count,
  AVG(json_extract(p.data, '$.state.time.end') -
      json_extract(p.data, '$.state.time.start'))    AS avg_duration_ms,
  MAX(json_extract(p.data, '$.state.time.end') -
      json_extract(p.data, '$.state.time.start'))    AS max_duration_ms
FROM part p
WHERE json_extract(p.data, '$.type') = 'tool'
  AND json_extract(p.data, '$.state.status') = 'completed'
GROUP BY tool_name
ORDER BY call_count DESC;

查询 Token 消耗统计(按 Session)

sql 复制代码
SELECT
  s.title,
  s.id AS session_id,
  datetime(s.time_created / 1000, 'unixepoch', 'localtime') AS created_at,
  SUM(json_extract(m.data, '$.tokens.input'))   AS total_input_tokens,
  SUM(json_extract(m.data, '$.tokens.output'))  AS total_output_tokens,
  SUM(json_extract(m.data, '$.tokens.input') +
      json_extract(m.data, '$.tokens.output'))  AS total_tokens,
  COUNT(DISTINCT m.id) AS message_count
FROM session s
JOIN message m ON m.session_id = s.id
WHERE json_extract(m.data, '$.role') = 'assistant'
  AND json_extract(m.data, '$.tokens.input') IS NOT NULL
GROUP BY s.id
ORDER BY total_tokens DESC;

查询压缩(Compaction)记录

sql 复制代码
SELECT
  p.session_id,
  m.id                                               AS compact_req_msg,
  datetime(m.time_created / 1000, 'unixepoch', 'localtime') AS time,
  json_extract(p.data, '$.auto')                     AS auto_triggered,
  json_extract(p.data, '$.overflow')                 AS was_overflow,
  json_extract(p.data, '$.tail_start_id')            AS tail_start_id
FROM part p
JOIN message m ON m.id = p.message_id
WHERE json_extract(p.data, '$.type') = 'compaction'
ORDER BY m.time_created DESC;

查询压缩摘要内容

sql 复制代码
SELECT
  m.session_id,
  m.id                                               AS summary_msg_id,
  datetime(m.time_created / 1000, 'unixepoch', 'localtime') AS time,
  p.id                                               AS part_id,
  json_extract(p.data, '$.text')                     AS summary_text
FROM message m
JOIN part p ON p.message_id = m.id
WHERE json_extract(m.data, '$.summary') = 1
  AND json_extract(p.data, '$.type') = 'text'
ORDER BY m.time_created DESC;

查询被 Prune 裁剪的工具输出

sql 复制代码
SELECT
  p.session_id,
  json_extract(p.data, '$.tool')                     AS tool_name,
  datetime(json_extract(p.data, '$.state.time.compacted') / 1000,
           'unixepoch', 'localtime')                 AS pruned_at
FROM part p
WHERE json_extract(p.data, '$.type') = 'tool'
  AND json_extract(p.data, '$.state.time.compacted') IS NOT NULL
ORDER BY json_extract(p.data, '$.state.time.compacted') DESC;

查询 Todo 列表

vbnet 复制代码
SELECT
  t.session_id,
  s.title AS session_title,
  t.position,
  t.content,
  t.status,
  t.priority,
  datetime(t.time_created / 1000, 'unixepoch', 'localtime') AS created_at
FROM todo t
JOIN session s ON s.id = t.session_id
ORDER BY t.session_id, t.position;

导出数据到 Excel

方式一:按 Session ID 导出完整数据(推荐)

同时生成 Excel(4 sheets)和 JSON,输出到脚本同级的 excels/ 目录。

安装依赖:

复制代码
pip install pandas openpyxl

运行:

bash 复制代码
# 修改脚本顶部的 SESSION_ID,然后运行
python docs/export_session.py

输出文件:

文件 说明
docs/excels/session_<id8>.xlsx Excel,含 4 个 sheet
docs/excels/session_<id8>_messages.json JSON,与插件 save-messages.js 格式一致

Excel Sheet 结构:

Sheet 内容
Messages 每行一条消息:role、agent、finish、token、parts(JSON 字符串)
Session session 基本信息、项目名、代码变更统计
ToolStats 按工具名汇总(调用次数、成功/失败/pruned、平均/最大耗时)
TokenUsage 每轮 assistant token 消耗时序(input/output/cache)

脚本完整代码:

python 复制代码
import sqlite3
import pandas as pd
import os
import sys
import json
from datetime import datetime
from collections import defaultdict
from urllib.request import pathname2url

# ── 配置 ──────────────────────────────────────────────────
SESSION_ID = "ses_29276303affePFCDRVSqdzdE2B"   # ← 替换为目标 session ID

_candidates = [
    os.path.expanduser("~/.local/share/opencode/opencode.db"),
    os.path.expanduser("~/Library/Application Support/opencode/opencode.db"),
]
DB_PATH = next((p for p in _candidates if os.path.exists(p)), None)
if DB_PATH is None:
    print("找不到 opencode 数据库,请手动设置 DB_PATH")
    sys.exit(1)
print(f"使用数据库: {DB_PATH}")

_output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "excels")
os.makedirs(_output_dir, exist_ok=True)
OUTPUT_PATH      = os.path.join(_output_dir, f"session_{SESSION_ID[:8]}.xlsx")
OUTPUT_JSON_PATH = os.path.join(_output_dir, f"session_{SESSION_ID[:8]}_messages.json")

conn = sqlite3.connect(f"file:{pathname2url(DB_PATH)}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row


def ts(ms):
    """毫秒时间戳 → 可读字符串"""
    if ms is None:
        return ""
    return datetime.fromtimestamp(ms / 1000).strftime("%Y-%m-%d %H:%M:%S")


messages_raw = conn.execute("""
    SELECT
        m.id, m.time_created,
        json_extract(m.data, '$.role')           AS role,
        json_extract(m.data, '$.agent')          AS agent,
        json_extract(m.data, '$.finish')         AS finish,
        json_extract(m.data, '$.summary')        AS is_summary,
        json_extract(m.data, '$.tokens.input')   AS tokens_in,
        json_extract(m.data, '$.tokens.output')  AS tokens_out
    FROM message m
    WHERE m.session_id = ?
    ORDER BY m.time_created, m.id
""", (SESSION_ID,)).fetchall()

parts_raw = conn.execute("""
    SELECT
        p.id, p.message_id, p.time_created,
        json_extract(p.data, '$.type')                    AS type,
        json_extract(p.data, '$.text')                    AS text,
        json_extract(p.data, '$.tool')                    AS tool,
        json_extract(p.data, '$.input')                   AS input_json,
        json_extract(p.data, '$.state.status')            AS status,
        json_extract(p.data, '$.state.output')            AS output,
        json_extract(p.data, '$.state.time.start')        AS tool_start,
        json_extract(p.data, '$.state.time.end')          AS tool_end,
        json_extract(p.data, '$.state.time.compacted')    AS compacted,
        json_extract(p.data, '$.auto')                    AS compact_auto,
        json_extract(p.data, '$.overflow')                AS compact_overflow
    FROM part p
    WHERE p.session_id = ?
    ORDER BY p.time_created, p.id
""", (SESSION_ID,)).fetchall()

# 按 message_id 分组 parts
parts_by_msg = defaultdict(list)
for p in parts_raw:
    parts_by_msg[p["message_id"]].append(p)


def build_part(p):
    """将 sqlite Row 转为与插件格式对齐的 part dict"""
    t = p["type"]
    if t == "text":
        return {"type": "text", "text": p["text"] or ""}
    elif t == "tool":
        try:
            inp = json.loads(p["input_json"]) if p["input_json"] else None
        except Exception:
            inp = p["input_json"]
        return {
            "type": "tool",
            "tool": p["tool"],
            "input": inp,
            "state": {
                "status": p["status"],
                "output": "[compacted]" if p["compacted"] else (p["output"] or ""),
                "time": {
                    "start": p["tool_start"],
                    "end":   p["tool_end"],
                },
            },
        }
    elif t == "compaction":
        return {
            "type":     "compaction",
            "auto":     p["compact_auto"],
            "overflow": p["compact_overflow"],
        }
    elif t == "error":
        return {"type": "error", "text": p["text"] or ""}
    else:
        return {"type": t}


# ── 构建 messages 结构(每条消息 → { info, parts })────────
messages_list = []
for msg in messages_raw:
    info = {
        "role":         msg["role"],
        "sessionID":    SESSION_ID,
        "messageID":    msg["id"],
        "agent":        msg["agent"],
        "finish":       msg["finish"],
        "is_summary":   bool(msg["is_summary"]),
        "time":         ts(msg["time_created"]),
        "time_created": msg["time_created"],
        "tokens": {
            "input":  msg["tokens_in"],
            "output": msg["tokens_out"],
        },
    }
    parts_for_msg = [
        build_part(p)
        for p in parts_by_msg.get(msg["id"], [])
        if p["type"] != "step-start"
    ]
    messages_list.append({"info": info, "parts": parts_for_msg})

# Messages sheet:每行一条消息,parts 序列化为 JSON 字符串
df_messages = pd.DataFrame([
    {
        "message_id":  m["info"]["messageID"],
        "time":        m["info"]["time"],
        "role":        m["info"]["role"],
        "agent":       m["info"]["agent"] or "",
        "finish":      m["info"]["finish"] or "",
        "is_summary":  m["info"]["is_summary"],
        "tokens_in":   m["info"]["tokens"]["input"] or "",
        "tokens_out":  m["info"]["tokens"]["output"] or "",
        "parts":       json.dumps(m["parts"], ensure_ascii=False),
    }
    for m in messages_list
])

df_session = pd.read_sql_query("""
    SELECT
        s.id, s.title, s.directory, s.version,
        datetime(s.time_created/1000, 'unixepoch', 'localtime') AS created_at,
        datetime(s.time_updated/1000, 'unixepoch', 'localtime') AS updated_at,
        s.summary_files, s.summary_additions, s.summary_deletions,
        p.worktree, p.name AS project_name
    FROM session s
    LEFT JOIN project p ON p.id = s.project_id
    WHERE s.id = ?
""", conn, params=(SESSION_ID,))

df_tool_stats = pd.read_sql_query("""
    SELECT
        json_extract(p.data, '$.tool')                              AS tool_name,
        COUNT(*)                                                     AS call_count,
        SUM(CASE WHEN json_extract(p.data,'$.state.status')='completed' THEN 1 ELSE 0 END) AS success,
        SUM(CASE WHEN json_extract(p.data,'$.state.status')='error'     THEN 1 ELSE 0 END) AS errors,
        SUM(CASE WHEN json_extract(p.data,'$.state.time.compacted') IS NOT NULL THEN 1 ELSE 0 END) AS pruned,
        ROUND(AVG(
            json_extract(p.data,'$.state.time.end') -
            json_extract(p.data,'$.state.time.start')), 0)          AS avg_duration_ms,
        MAX(
            json_extract(p.data,'$.state.time.end') -
            json_extract(p.data,'$.state.time.start'))              AS max_duration_ms
    FROM part p
    WHERE p.session_id = ?
      AND json_extract(p.data,'$.type') = 'tool'
    GROUP BY tool_name
    ORDER BY call_count DESC
""", conn, params=(SESSION_ID,))

df_tokens = pd.read_sql_query("""
    SELECT
        m.id                                                         AS message_id,
        datetime(m.time_created/1000, 'unixepoch', 'localtime')     AS time,
        json_extract(m.data, '$.agent')                             AS agent,
        json_extract(m.data, '$.tokens.input')                      AS tokens_in,
        json_extract(m.data, '$.tokens.output')                     AS tokens_out,
        json_extract(m.data, '$.tokens.cache.read')                 AS cache_read,
        json_extract(m.data, '$.tokens.cache.write')                AS cache_write,
        (
            COALESCE(json_extract(m.data,'$.tokens.input'), 0) +
            COALESCE(json_extract(m.data,'$.tokens.output'), 0) +
            COALESCE(json_extract(m.data,'$.tokens.cache.read'), 0) +
            COALESCE(json_extract(m.data,'$.tokens.cache.write'), 0)
        )                                                            AS total_tokens
    FROM message m
    WHERE m.session_id = ?
      AND json_extract(m.data, '$.role') = 'assistant'
      AND json_extract(m.data, '$.tokens.input') IS NOT NULL
    ORDER BY m.time_created
""", conn, params=(SESSION_ID,))

conn.close()

if df_session.empty:
    print(f"Session not found: {SESSION_ID}")
    sys.exit(1)

# ── 写入 Excel ────────────────────────────────────────────
with pd.ExcelWriter(OUTPUT_PATH, engine="openpyxl") as writer:
    df_messages.to_excel(  writer, sheet_name="Messages",   index=False)
    df_session.to_excel(   writer, sheet_name="Session",    index=False)
    df_tool_stats.to_excel(writer, sheet_name="ToolStats",  index=False)
    df_tokens.to_excel(    writer, sheet_name="TokenUsage", index=False)

# ── 写入 JSON(与插件 save-messages.js 格式一致)────────────
json_payload = {
    "sessionId": SESSION_ID,
    "title":     df_session["title"].iloc[0],
    "messages":  messages_list,
}
with open(OUTPUT_JSON_PATH, "w", encoding="utf-8") as f:
    json.dump(json_payload, f, ensure_ascii=False, indent=2)

print(f"\n导出完成")
print(f"  Excel → {OUTPUT_PATH}")
print(f"  JSON  → {OUTPUT_JSON_PATH}")
print(f"  Session : {df_session['title'].iloc[0]}")
print(f"  消息数  : {len(messages_list)}")

方式二:导出所有 Session 汇总

ini 复制代码
import sqlite3
import pandas as pd
import os
from urllib.request import pathname2url

_candidates = [
    os.path.expanduser("~/.local/share/opencode/opencode.db"),
    os.path.expanduser("~/Library/Application Support/opencode/opencode.db"),
]
DB_PATH = next((p for p in _candidates if os.path.exists(p)), None)
OUTPUT_PATH = os.path.expanduser("~/Desktop/opencode_all.xlsx")

conn = sqlite3.connect(f"file:{pathname2url(DB_PATH)}?mode=ro", uri=True)


def q(sql):
    return pd.read_sql_query(sql, conn)


sessions = q("""
    SELECT
        s.id, s.title, s.directory,
        datetime(s.time_created/1000, 'unixepoch', 'localtime') AS created_at,
        datetime(s.time_updated/1000, 'unixepoch', 'localtime') AS updated_at,
        s.summary_files, s.summary_additions, s.summary_deletions,
        p.name AS project_name
    FROM session s
    LEFT JOIN project p ON p.id = s.project_id
    ORDER BY s.time_created DESC
""")

tool_stats = q("""
    SELECT
        p.session_id,
        json_extract(p.data, '$.tool')  AS tool_name,
        COUNT(*)                         AS call_count,
        ROUND(AVG(
            json_extract(p.data, '$.state.time.end') -
            json_extract(p.data, '$.state.time.start')
        ), 0)                            AS avg_duration_ms
    FROM part p
    WHERE json_extract(p.data, '$.type') = 'tool'
    GROUP BY p.session_id, tool_name
    ORDER BY p.session_id, call_count DESC
""")

token_summary = q("""
    SELECT
        m.session_id,
        SUM(json_extract(m.data,'$.tokens.input'))  AS total_input,
        SUM(json_extract(m.data,'$.tokens.output')) AS total_output,
        COUNT(*)                                     AS assistant_msgs
    FROM message m
    WHERE json_extract(m.data,'$.role') = 'assistant'
      AND json_extract(m.data,'$.tokens.input') IS NOT NULL
    GROUP BY m.session_id
""")

conn.close()

with pd.ExcelWriter(OUTPUT_PATH, engine="openpyxl") as writer:
    sessions.to_excel(     writer, sheet_name="Sessions",    index=False)
    tool_stats.to_excel(   writer, sheet_name="ToolStats",   index=False)
    token_summary.to_excel(writer, sheet_name="TokenSummary",index=False)

print(f"导出完成 → {OUTPUT_PATH},共 {len(sessions)} 个 Session")

方式三:sqlite3 CLI 导出 CSV

bash 复制代码
# 进入 sqlite3
sqlite3 ~/Library/Application\ Support/opencode/opencode.db

# 设置 CSV 模式
.mode csv
.headers on

# 导出 Session 列表
.output /tmp/sessions.csv
SELECT id, title, directory,
       datetime(time_created/1000,'unixepoch','localtime') AS created_at
FROM session ORDER BY time_created DESC;

# 导出工具调用
.output /tmp/tool_calls.csv
SELECT p.session_id,
       json_extract(p.data,'$.tool') AS tool,
       json_extract(p.data,'$.state.status') AS status,
       datetime(p.time_created/1000,'unixepoch','localtime') AS time
FROM part p
WHERE json_extract(p.data,'$.type') = 'tool';

.quit

之后用 Excel 直接打开 CSV 文件(文件→导入)。


方式四:GUI 工具(推荐日常使用)

工具 平台 说明
DB Browser for SQLite macOS/Win/Linux 免费,支持直接执行 SQL 并导出 CSV/Excel
TablePlus macOS/Win 商业软件,界面美观
DBeaver 全平台 免费,支持 JSON 字段可视化

打开数据库文件路径:~/Library/Application Support/opencode/opencode.db


注意事项

  1. JSON 字段message.datapart.data 等字段存储 JSON 文本,SQLite 通过 json_extract() 函数查询内部字段。
  2. 时间戳 :所有时间字段均为毫秒级 Unix 时间戳,转可读时间用 datetime(field/1000, 'unixepoch', 'localtime')
  3. 只读建议:直接查询数据库时建议以只读模式连接,避免意外修改运行中的数据。
  4. WAL 模式 :若 opencode 正在运行,WAL 日志文件(.db-wal.db-shm)可能存在,Python 的 sqlite3 可以安全读取,不影响正常运行。
  5. part 与 message 关系 :一条 message 对应多个 parts,通过 message_id 关联。文本内容在 type='text' 的 part 里;工具调用在 type='tool' 的 part 里。
相关推荐
Cando学算法1 小时前
中位数定理:到所有点的距离之和最小的点就是中位数
c++·算法·学习方法
nlpming1 小时前
opencode 上下文压缩(Compaction)机制
算法
anew___1 小时前
算法刷题避坑指南:从数据规模到易错点的实战总结
算法
HZY1618yzh1 小时前
洛谷题解:P16304 [蓝桥杯 2026 省 Java C 组] 抽奖活动
java·c++·算法·蓝桥杯
智者知已应修善业2 小时前
【51单片机从奇数始再转偶数逐一点亮并循环】2023-9-8
c++·经验分享·笔记·算法·51单片机
倔强的猴子(翻版)2 小时前
我用 Python 写了个排序库,一亿数据量下比 C 级 np.sort() 快 7 倍
人工智能·python·算法·阿里云·文心一言
郝学胜-神的一滴2 小时前
深入理解回归损失函数:MSE、L1 与 Smooth L1 的设计哲学
人工智能·python·程序人生·算法·机器学习·数据挖掘·回归
iCxhust2 小时前
在 emu8086 中可以直接编译运行的完整汇编程序,演示数组的定义、遍历、求和、求最大值。
开发语言·前端·javascript·汇编·单片机·嵌入式硬件·算法
Jinkxs2 小时前
LoadBalancer- 常见负载均衡算法:轮询 / 加权轮询 / 最少连接等基础实现
运维·算法·负载均衡