← 返回
未分类

导出cursor聊天记录

从 Cursor 编辑器导出指定项目的聊天记录,支持按日期过滤。使用方法:用户提供项目目录和日期范围,skill 会自动导出对应的 Cursor 聊天记录为 Markdown 格式。
从 Cursor 编辑器导出指定项目的聊天记录,支持按日期过滤。使用方法:用户提供项目目录和日期范围,skill 会自动导出对应的 Cursor 聊天记录为 Markdown 格式。
lastyear
未分类 community v1.0.1 2 版本 100000 Key: 无需
★ 1
Stars
📥 103
下载
💾 4
安装
2
版本
#latest

概述

Cursor 聊天记录导出技能

概述

从 Cursor 编辑器的本地 SQLite 数据库(state.vscdb)中提取指定项目的聊天记录,支持按日期过滤。

项目路径

  • 导出脚本C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py
  • Cursor 数据库%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: 创建时间

使用方式

用户需要提供:

  1. 项目目录(必选):Cursor 中打开的项目路径
  2. 日期范围(可选):默认今天

调用方式

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

执行流程

步骤 0:环境检查与准备(每次执行前必须)

0.1 检查 Python 环境

先检查系统是否有 Python:

python --version

如果 Python 不存在(命令报错或找不到),使用 install_binary 工具安装:

install_binary(type="python", version="3.12.0")

安装完成后再次验证:

python --version

Python 版本要求:Python 3.9+(脚本使用了 fromisoformat 等新特性)

0.2 检查导出脚本是否存在

检查脚本文件是否存在:

dir "C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\cursor_chat_export.py"

如果脚本不存在,必须先创建目录并写入脚本文件:

  1. 创建目录:C:\Users\lastyear\.codebuddy\skills\cursor-chat-export\
  2. cursor_chat_export.py 的完整源代码写入该路径(脚本源代码见下方附录)

> ⚠️ 重要:脚本文件是 skill 正常运行的前提,每次执行 skill 前都必须检查。如果用户手动删除了脚本,skill 会自动重建。

步骤 1:确认参数

用户消息中提取:

  • 项目目录:用户给出的项目路径
  • 日期范围
  • 用户说"今天" → --date today
  • 用户说"昨天" → --date yesterday
  • 用户说"最近N天" → --last N
  • 用户说具体日期 → --date YYYY-MM-DD
  • 用户说"X到Y" → --from X --to Y
  • 未指定 → --date today

步骤 2:执行导出脚本

使用 --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

步骤 3:展示结果

将导出结果展示给用户,包含:

  • 导出的对话数量和消息数量
  • 保存的文件列表
  • 每个文件的简要内容(标题、消息数)

注意事项

  1. 只读访问:脚本以只读模式连接 SQLite,不会修改 Cursor 数据
  2. 项目路径匹配:通过 workspaceUris 精确匹配项目目录
  3. 性能:数据库较大(~6GB)时查询可能需要几秒
  4. 无需关闭 Cursor:只读模式不会锁库

错误处理

  • 数据库文件不存在 → 提示路径错误
  • 项目无匹配记录 → 提示"没有找到聊天记录"
  • 日期格式错误 → 提示正确格式
  • Python 不存在 → 自动安装 Python 3.12.0

附录:cursor_chat_export.py 完整源代码

当脚本不存在时,将以下内容写入 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 个版本

  • v1.0.1 新版 Cursor 不再在 bubble 中存储 workspaceUris/workspaceProjectDir 新增了 Strategy 4(通过 raw JSON 中包含项目名匹配)。 当前
    2026-04-21 13:27 安全 安全
  • v1.0.0 Initial release
    2026-04-21 12:42 安全

安全检测

腾讯云安全 (Keen)

安全,无风险
查看报告

腾讯云安全 (Sanbu)

安全,无风险
查看报告

🔗 相关推荐

dev-programming

Github

steipete
使用 `gh` CLI 与 GitHub 交互,通过 `gh issue`、`gh pr`、`gh run` 和 `gh api` 管理议题、PR、CI 运行及高级查询。
★ 677 📥 326,919
dev-programming

CodeConductor.ai

larsonreever
AI驱动平台,提供快速全栈开发、智能体、工作流自动化及低代码AI集成的可扩展产品创建。
★ 72 📥 181,717
dev-programming

Mcporter

steipete
使用 mcporter CLI 直接列出、配置、认证及调用 MCP 服务器/工具(支持 HTTP 或 stdio),涵盖临时服务器、配置编辑及 CLI/类型生成功能。
★ 195 📥 67,593