从 Cursor 编辑器的本地 SQLite 数据库(state.vscdb)中提取指定项目的聊天记录,支持按日期过滤。
C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py%APPDATA%\Cursor\User\globalStorage\state.vscdb(自动检测)Cursor 将所有聊天记录存储在全局 SQLite 数据库 state.vscdb 中:
| Key 模式 | 说明 |
|---|---|
| --------- | ------ |
composerData:{conversationId} | 对话元信息(创建时间等) |
bubbleId:{conversationId}:{bubbleId} | 单条消息(含项目路径信息) |
每条 bubble(消息)包含:
workspaceUris: 实际项目路径 URI(如 file:///c%3A/Users/.../MagiCenter)workspaceProjectDir: Cursor 内部项目路径type: 1=用户消息, 2=AI 回复text / richText: 消息内容createdAt: 创建时间用户需要提供:
python "C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py" \
--project "<项目目录>" \
--date <日期>
| 参数 | 必选 | 说明 |
|---|---|---|
| ------ | ------ | ------ |
--project / -p | ✅ | 项目目录路径 |
--date / -d | ❌ | 指定日期 (YYYY-MM-DD / today / yesterday),默认 today |
--from | ❌ | 起始日期(与 --to 搭配) |
--to | ❌ | 结束日期(与 --from 搭配) |
--last | ❌ | 最近 N 天 |
--format / -f | ❌ | 输出格式:markdown(默认)/ json / yaml |
--output / -o | ❌ | 输出文件路径(默认输出到控制台) |
--db | ❌ | 指定 Cursor 数据库路径(默认自动检测) |
# 导出今天的聊天记录
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter"
# 导出指定日期
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter" --date 2026-04-21
# 导出昨天
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter" --date yesterday
# 导出最近 7 天
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter" --last 7
# 导出日期范围
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter" --from 2026-04-15 --to 2026-04-21
# 导出为 JSON 格式并保存到文件
python cursor_chat_export.py -p "C:\Users\lastyear\Desktop\project\win\MagiCenter" --last 7 -f json -o output.json
先检查系统是否有 Python:
python --version
如果 Python 不存在(命令报错或找不到),使用 install_binary 工具安装:
install_binary(type="python", version="3.12.0")
安装完成后再次验证:
python --version
Python 版本要求:Python 3.9+(脚本使用了 fromisoformat 等新特性)
检查脚本文件是否存在:
dir "C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py"
如果脚本不存在,必须先创建目录并写入脚本文件:
C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py 的完整源代码写入该路径(脚本源代码见下方附录)> ⚠️ 重要:脚本文件是 skill 正常运行的前提,每次执行 skill 前都必须检查。如果用户手动删除了脚本,skill 会自动重建。
用户消息中提取:
--date today--date yesterday--last N--date YYYY-MM-DD--from X --to Y--date today使用 --output-dir 参数,将每个对话导出为单独的 Markdown 文件到桌面 cursor_logs 目录:
python "C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py" \
--project "<项目目录>" \
<日期参数> \
--output-dir "C:\Users\lastyear\Desktop\cursor_logs"
输出文件命名格式:{日期}_{会话ID前8位}.md
例如:
C:\Users\lastyear\Desktop\cursor_logs\
├── 2025-12-01_c76efbcf.md
├── 2025-12-01_1e703d2e.md
└── 2025-12-02_a3b4c5d6.md
将导出结果展示给用户,包含:
workspaceUris 精确匹配项目目录当脚本不存在时,将以下内容写入 C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py:
#!/usr/bin/env python3
r"""
Cursor Chat History Exporter - Export cursor chat logs by project and date.
Usage:
python cursor_chat_export.py -p "C:/Users/lastyear/Desktop/project/win/MagiCenter" --date 2026-04-21
python cursor_chat_export.py -p "C:/Users/lastyear/Desktop/project/win/MagiCenter" --last 7
"""
import sqlite3
import json
import os
import sys
import argparse
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import unquote
CURSOR_DB_FILENAME = "state.vscdb"
def get_default_db_path():
if sys.platform == "darwin":
return os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/state.vscdb")
elif sys.platform.startswith("win"):
return os.path.join(os.environ.get("APPDATA", ""), "Cursor", "User", "globalStorage", "state.vscdb")
else:
return os.path.expanduser("~/.config/Cursor/User/globalStorage/state.vscdb")
def connect_db_readonly(db_path):
import urllib.parse
uri = f"file:{urllib.parse.quote(db_path)}?mode=ro"
return sqlite3.connect(uri, uri=True)
def normalize_path(path):
return os.path.normpath(path).lower().replace("\\", "/")
def extract_path_from_uri(uri):
if not uri:
return ""
if uri.startswith("file:///"):
decoded = unquote(uri[8:])
if decoded.startswith("/") and len(decoded) > 2 and decoded[2] == ":":
decoded = decoded[1:]
return normalize_path(decoded)
return uri.lower()
def match_project(workspace_uris, workspace_project_dir, attached_files, target_norm):
"""Match bubble to target project using multiple strategies."""
target_base = target_norm.split("/")[-1]
# Strategy 1: workspaceUris
for uri in (workspace_uris or []):
extracted = extract_path_from_uri(uri)
if extracted and (extracted == target_norm or extracted.endswith("/" + target_base)):
return True
# Strategy 2: workspaceProjectDir
if workspace_project_dir:
wpd = normalize_path(workspace_project_dir)
parts = wpd.split("/")
if len(parts) > 0:
last = parts[-1]
if last == target_base:
return True
target_flat = target_norm.replace("/", "-")
if last in target_flat or target_flat.endswith(last):
return True
# Strategy 3: attachedFileCodeChunksMetadataOnly paths
if attached_files:
for f in attached_files:
rpath = f.get("relativeWorkspacePath", "") if isinstance(f, dict) else str(f)
if rpath:
full_path = os.path.join(target_norm, rpath)
if os.path.exists(full_path.replace("/", os.sep)):
return True
return False
def parse_date(date_str):
tz = timezone(timedelta(hours=8))
today = datetime.now(tz)
if date_str == "today":
return today.replace(hour=0, minute=0, second=0, microsecond=0)
elif date_str == "yesterday":
d = today - timedelta(days=1)
return d.replace(hour=0, minute=0, second=0, microsecond=0)
try:
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=tz)
except ValueError:
raise ValueError(f"Cannot parse date: {date_str}. Use YYYY-MM-DD, today, or yesterday.")
def parse_timestamp(ts):
"""Parse various timestamp formats to datetime."""
if not ts:
return None
tz = timezone(timedelta(hours=8))
try:
if isinstance(ts, (int, float)):
if ts > 1e12:
ts = ts / 1000
return datetime.fromtimestamp(ts, tz=tz)
elif isinstance(ts, str):
if ts.endswith("Z"):
ts = ts[:-1] + "+00:00"
return datetime.fromisoformat(ts).astimezone(tz)
except (ValueError, TypeError, OSError):
pass
return None
def export_chats(db_path, project_path, date_start, date_end):
"""Main export function."""
conn = connect_db_readonly(db_path)
c = conn.cursor()
target_norm = normalize_path(project_path)
target_base = target_norm.split("/")[-1]
# Step 1: Find all conversation IDs from composerData
c.execute("""
SELECT key FROM cursorDiskKV
WHERE key LIKE 'composerData:%' AND key IS NOT NULL
""")
all_cids = set()
for row in c.fetchall():
cid = row[0][len("composerData:"):]
if cid:
all_cids.add(cid)
# Step 2: For each conversation, find matching bubbles
conversations = {}
for cid in all_cids:
c.execute("""
SELECT key, value FROM cursorDiskKV
WHERE key LIKE ? AND key IS NOT NULL
""", (f"bubbleId:{cid}:%",))
bubbles = c.fetchall()
if not bubbles:
continue
project_matched = False
messages = []
conv_created_at = None
for bkey, bval in bubbles:
try:
d = json.loads(bval)
except (json.JSONDecodeError, TypeError):
continue
btype = d.get("type")
text = d.get("text", "") or ""
rich_text = d.get("richText", "") or ""
ts = d.get("createdAt")
wu = d.get("workspaceUris", [])
wpd = d.get("workspaceProjectDir", "")
attached = d.get("attachedFileCodeChunksMetadataOnly", [])
msg_dt = parse_timestamp(ts)
if not conv_created_at and msg_dt:
conv_created_at = msg_dt
if not project_matched:
project_matched = match_project(wu, wpd, attached, target_norm)
content = text or rich_text
role = "user" if btype == 1 else "assistant"
if content.strip():
if msg_dt and (msg_dt < date_start or msg_dt >= date_end):
continue
messages.append({
"role": role,
"content": content,
"timestamp": ts,
"parsed_time": msg_dt.isoformat() if msg_dt else None,
})
if project_matched and messages:
conversations[cid] = {
"conversation_id": cid,
"created_at": conv_created_at.isoformat() if conv_created_at else None,
"messages": messages,
}
conn.close()
result = sorted(
conversations.values(),
key=lambda x: x.get("created_at") or ""
)
return result
def format_markdown(conversations, project_path, date_desc):
lines = []
lines.append("# Cursor Chat History Export")
lines.append("")
lines.append(f"- **Project**: `{project_path}`")
lines.append(f"- **Date**: {date_desc}")
lines.append(f"- **Export Time**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"- **Conversations**: {len(conversations)}")
total_msgs = sum(len(c["messages"]) for c in conversations)
lines.append(f"- **Total Messages**: {total_msgs}")
lines.append("")
lines.append("---")
lines.append("")
for idx, conv in enumerate(conversations, 1):
lines.append(f"## Conversation {idx}")
lines.append("")
lines.append(f"- **ID**: `{conv['conversation_id'][:16]}...`")
lines.append(f"- **Created**: {conv.get('created_at', 'unknown')}")
lines.append(f"- **Messages**: {len(conv['messages'])}")
lines.append("")
for msg in conv["messages"]:
icon = "👤" if msg["role"] == "user" else "🤖"
label = "User" if msg["role"] == "user" else "Assistant"
lines.append(f"### {icon} {label}")
lines.append("")
lines.append(f"> {msg.get('parsed_time') or msg.get('timestamp', 'unknown')}")
lines.append("")
lines.append(msg["content"])
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Export Cursor chat history by project and date")
parser.add_argument("--project", "-p", required=True, help="Project directory path")
parser.add_argument("--date", "-d", help="Date (YYYY-MM-DD / today / yesterday)")
parser.add_argument("--from", dest="from_date", help="Start date (YYYY-MM-DD)")
parser.add_argument("--to", dest="to_date", help="End date (YYYY-MM-DD)")
parser.add_argument("--last", type=int, help="Last N days")
parser.add_argument("--db", help="Cursor database path (auto-detect)")
parser.add_argument("--format", "-f", choices=["markdown", "json"], default="markdown")
parser.add_argument("--output", "-o", help="Output file path")
parser.add_argument("--output-dir", help="Output directory: each conversation saved as {date}_{hash}.md")
args = parser.parse_args()
tz = timezone(timedelta(hours=8))
today = datetime.now(tz)
if args.date:
date_start = parse_date(args.date)
date_end = date_start + timedelta(days=1)
date_desc = args.date
elif args.from_date and args.to_date:
date_start = datetime.strptime(args.from_date, "%Y-%m-%d").replace(tzinfo=tz)
date_end = datetime.strptime(args.to_date, "%Y-%m-%d").replace(tzinfo=tz) + timedelta(days=1)
date_desc = f"{args.from_date} ~ {args.to_date}"
elif args.last:
date_start = (today - timedelta(days=args.last)).replace(hour=0, minute=0, second=0, microsecond=0)
date_end = today + timedelta(days=1)
date_desc = f"Last {args.last} days ({date_start.strftime('%Y-%m-%d')} ~ {(date_end - timedelta(days=1)).strftime('%Y-%m-%d')})"
else:
date_start = today.replace(hour=0, minute=0, second=0, microsecond=0)
date_end = date_start + timedelta(days=1)
date_desc = f"Today ({date_start.strftime('%Y-%m-%d')})"
db_path = args.db or get_default_db_path()
if not os.path.exists(db_path):
print(f"Error: Cursor database not found: {db_path}", file=sys.stderr)
sys.exit(1)
project_path = os.path.abspath(args.project)
try:
conversations = export_chats(db_path, project_path, date_start, date_end)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if not conversations:
print(f"No conversations found.")
print(f" Project: {project_path}")
print(f" Date: {date_desc}")
sys.exit(0)
if args.format == "json":
output = json.dumps(conversations, ensure_ascii=False, indent=2)
else:
output = format_markdown(conversations, project_path, date_desc)
if args.output:
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
f.write(output)
total_msgs = sum(len(c["messages"]) for c in conversations)
print(f"Exported to: {args.output}")
print(f" Conversations: {len(conversations)}")
print(f" Messages: {total_msgs}")
elif args.output_dir:
out_dir = os.path.abspath(args.output_dir)
os.makedirs(out_dir, exist_ok=True)
total_msgs = 0
saved_files = []
for conv in conversations:
cid = conv["conversation_id"]
created = conv.get("created_at", "")
if created:
try:
dt = datetime.fromisoformat(created)
date_str = dt.strftime("%Y-%m-%d")
except (ValueError, TypeError):
date_str = "unknown-date"
else:
date_str = "unknown-date"
filename = f"{date_str}_{cid[:8]}.md"
filepath = os.path.join(out_dir, filename)
single_conv_md = format_markdown([conv], project_path, date_desc)
with open(filepath, "w", encoding="utf-8") as f:
f.write(single_conv_md)
saved_files.append(filename)
total_msgs += len(conv["messages"])
print(f"Exported {len(conversations)} conversations to: {out_dir}")
print(f" Total messages: {total_msgs}")
print(f" Files:")
for fn in saved_files:
print(f" {fn}")
else:
sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', closefd=False)
print(output)
if __name__ == "__main__":
main()
共 2 个版本