_deepseek_export.py
· 12 KiB · Python
Исходник
#!/usr/bin/env python3
"""
Export DeepSeek shared conversation to Markdown.
Requires uv — the fast Python package installer.
Install: curl -LsSf https://astral.sh/uv/install.sh | sh
Usage (just run it — venv is created automatically):
python deepseek_export.py <share_url_or_id> [-o output.md] [--json] [--txt] [--token TOKEN]
Examples:
python deepseek_export.py https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj
python deepseek_export.py nvy7v2ps1r6e2wyoyj -o my_chat.md
python deepseek_export.py nvy7v2ps1r6e2wyoyj --json -o chat.json
python deepseek_export.py nvy7v2ps1r6e2wyoyj --txt
First run will create .venv/ and install requests via uv.
"""
# ── auto-venv bootstrap ─────────────────────────────────────────────────────
import os
import shutil
import subprocess
import sys
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
_VENV_DIR = _SCRIPT_DIR / ".venv"
_VENV_PYTHON = _VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
_USAGE_TEXT = (
"Export DeepSeek shared conversations to Markdown/JSON/text.\n"
"Run with -h for full help."
)
def _find_uv() -> str | None:
return shutil.which("uv")
def _install_uv() -> bool:
print("[boot] uv is not installed.", flush=True)
answer = input("Install uv now? [Y/n] ").strip().lower()
if answer and answer != "y":
print("Aborted. Install uv manually: curl -LsSf https://astral.sh/uv/install.sh | sh")
return False
print("[boot] Installing uv …", flush=True)
try:
subprocess.check_call(
["curl", "-LsSf", "https://astral.sh/uv/install.sh", "|", "sh"],
cwd=_SCRIPT_DIR,
)
except Exception:
# curl ... | sh doesn't work via subprocess.check_call, use shell
subprocess.check_call("curl -LsSf https://astral.sh/uv/install.sh | sh", shell=True)
if not _find_uv():
print("[boot] uv installation failed. Install manually: curl -LsSf https://astral.sh/uv/install.sh | sh")
return False
return True
def _ensure_venv():
"""Create venv + install deps if missing, then re-exec inside it."""
if sys.prefix != sys.base_prefix:
return # already inside a venv
print(_USAGE_TEXT, flush=True)
print(flush=True)
uv = _find_uv()
if not uv:
if not _install_uv():
sys.exit(1)
uv = _find_uv()
if not uv:
sys.exit(1)
if not _VENV_PYTHON.exists():
print("[boot] Creating .venv …", flush=True)
subprocess.check_call([uv, "venv", str(_VENV_DIR)], cwd=_SCRIPT_DIR)
print("[boot] Installing dependencies …", flush=True)
subprocess.check_call(
[uv, "pip", "install", "--python", str(_VENV_PYTHON), "requests"],
cwd=_SCRIPT_DIR,
)
os.execv(str(_VENV_PYTHON), [str(_VENV_PYTHON), *sys.argv])
_ensure_venv()
# ── end bootstrap ────────────────────────────────────────────────────────────
import argparse
import json
import re
from datetime import datetime, timezone
import requests
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
SHARE_URL_RE = re.compile(
r"(?:https?://chat\.deepseek\.com)?/share/(?P<id>[A-Za-z0-9_-]+)"
)
def extract_share_id(raw: str) -> str:
m = SHARE_URL_RE.search(raw)
if m:
return m.group("id")
if re.fullmatch(r"[A-Za-z0-9_-]{6,}", raw):
return raw
raise ValueError(f"Cannot extract share id from: {raw}")
HEADERS = {
"accept": "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9",
"referer": "https://chat.deepseek.com/",
"user-agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
),
}
# ---------------------------------------------------------------------------
# Strategy 1 — plain requests (works when WAF is not aggressive)
# ---------------------------------------------------------------------------
def fetch_via_requests(share_id: str, token: str | None = None) -> dict:
s = requests.Session()
s.headers.update(HEADERS)
s.get("https://chat.deepseek.com/", timeout=15)
if token:
s.headers["authorization"] = f"Bearer {token}"
url = f"https://chat.deepseek.com/api/v0/share/content?share_id={share_id}"
r = s.get(url, timeout=15)
r.raise_for_status()
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"API error: {data.get('msg', data)}")
return data
# ---------------------------------------------------------------------------
# Strategy 2 — Playwright headless browser (bypasses WAF reliably)
# ---------------------------------------------------------------------------
def fetch_via_playwright(share_id: str, token: str | None = None) -> dict:
try:
from playwright.sync_api import sync_playwright
except ImportError:
raise RuntimeError(
"playwright not installed. Run:\n"
" uv pip install playwright && uv run playwright install chromium"
)
captured = {}
def on_response(response):
if "/api/v0/share/content" in response.url:
try:
captured["data"] = response.json()
except Exception:
pass
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
ctx = browser.new_context(
user_agent=HEADERS["user-agent"],
viewport={"width": 1280, "height": 800},
)
page = ctx.new_page()
page.on("response", on_response)
if token:
ctx.add_cookies([{
"name": "ds_session_id",
"value": token,
"domain": "chat.deepseek.com",
"path": "/",
}])
page.goto(
f"https://chat.deepseek.com/share/{share_id}",
wait_until="networkidle",
timeout=30000,
)
page.wait_for_timeout(3000)
browser.close()
if not captured.get("data"):
raise RuntimeError("Playwright could not capture the share API response")
data = captured["data"]
if data.get("code") != 0:
raise RuntimeError(f"API error: {data.get('msg', data)}")
return data
# ---------------------------------------------------------------------------
# Main fetch (tries requests first, falls back to playwright)
# ---------------------------------------------------------------------------
def fetch_share(share_id: str, token: str | None = None) -> dict:
try:
print("[1/2] Trying direct API…", end=" ", flush=True)
data = fetch_via_requests(share_id, token)
print("OK")
return data
except Exception as e:
print(f"failed ({e})")
print("[2/2] Falling back to Playwright…", end=" ", flush=True)
try:
data = fetch_via_playwright(share_id, token)
print("OK")
return data
except Exception as e:
print(f"failed ({e})")
raise RuntimeError(
"Both strategies failed. If WAF is blocking you, install Playwright:\n"
" uv pip install playwright && uv run playwright install chromium\n"
"Or pass your token with --token."
)
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
def _ts(ts) -> str:
if not ts:
return ""
try:
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
except Exception:
return str(ts)
def to_markdown(data: dict) -> str:
biz = data.get("data", {}).get("biz_data", {})
messages = biz.get("messages", []) or biz.get("chat_messages", [])
title = biz.get("title") or "DeepSeek Conversation"
model = biz.get("model_type") or biz.get("model") or ""
parts = [f"# {title}\n"]
if model:
parts.append(f"**Model:** {model}")
parts.append("")
if not messages:
parts.append("_No messages found._\n")
return "\n".join(parts)
for msg in messages:
role = (msg.get("role") or "").upper()
content = msg.get("content") or msg.get("message") or ""
if not content and not role:
continue
thinking = msg.get("thinking_content") or msg.get("reasoning_content") or ""
elapsed = msg.get("thinking_elapsed_secs")
search = msg.get("search_results") or []
if role == "USER":
header = "You"
elif role in ("ASSISTANT", "AI"):
header = "DeepSeek"
elif role == "SYSTEM":
header = "System"
else:
header = role.title()
parts.append(f"## {header}\n")
if thinking:
label = f"Thinking ({elapsed}s)" if elapsed else "Thinking"
parts.append(f"<details><summary>{label}</summary>\n\n{thinking}\n\n</details>\n")
if search:
refs = []
for item in search:
t = item.get("title") or ""
u = item.get("url") or ""
if u:
refs.append(f"- [{t}]({u})" if t else f"- {u}")
if refs:
parts.append("**Sources:**\n" + "\n".join(refs) + "\n")
if content:
parts.append(f"{content}\n")
parts.append("---\n")
return "\n".join(parts)
def to_plain_text(data: dict) -> str:
biz = data.get("data", {}).get("biz_data", {})
messages = biz.get("messages", []) or biz.get("chat_messages", [])
title = biz.get("title") or "DeepSeek Conversation"
parts = [f"=== {title} ===\n"]
for msg in messages:
role = (msg.get("role") or "").upper()
content = msg.get("content") or msg.get("message") or ""
thinking = msg.get("thinking_content") or msg.get("reasoning_content") or ""
if role == "USER":
parts.append(f"[User]\n{content}\n")
elif role in ("ASSISTANT", "AI"):
if thinking:
parts.append(f"[Thinking]\n{thinking}\n")
parts.append(f"[DeepSeek]\n{content}\n")
else:
parts.append(f"[{role}]\n{content}\n")
return "\n".join(parts)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
class _ArgumentParser(argparse.ArgumentParser):
def error(self, message):
self.print_usage(sys.stderr)
sys.stderr.write("\n")
sys.stderr.write(f"{self.prog}: error: {message}\n")
sys.exit(2)
def main():
ap = _ArgumentParser(
description="Export a DeepSeek shared conversation",
epilog="Example: %(prog)s https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj",
)
ap.add_argument("link", help="Share URL or share ID")
ap.add_argument("-o", "--output", help="Output file path (default: auto-named)")
ap.add_argument("--json", action="store_true", help="Dump raw JSON instead of Markdown")
ap.add_argument("--txt", action="store_true", help="Dump plain text")
ap.add_argument("--token", help="Bearer token for auth (optional, needed for private shares)")
args = ap.parse_args()
share_id = extract_share_id(args.link)
print(f"Share ID: {share_id}")
data = fetch_share(share_id, args.token)
if args.output:
out = Path(args.output)
elif args.json:
out = Path(f"export_{share_id}.json")
elif args.txt:
out = Path(f"export_{share_id}.txt")
else:
out = Path(f"export_{share_id}.md")
if args.json:
content = json.dumps(data, indent=2, ensure_ascii=False)
elif args.txt:
content = to_plain_text(data)
else:
content = to_markdown(data)
out.write_text(content, encoding="utf-8")
print(f"Saved to {out} ({len(content)} bytes)")
if __name__ == "__main__":
main()
| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | Export DeepSeek shared conversation to Markdown. |
| 4 | |
| 5 | Requires uv — the fast Python package installer. |
| 6 | Install: curl -LsSf https://astral.sh/uv/install.sh | sh |
| 7 | |
| 8 | Usage (just run it — venv is created automatically): |
| 9 | python deepseek_export.py <share_url_or_id> [-o output.md] [--json] [--txt] [--token TOKEN] |
| 10 | |
| 11 | Examples: |
| 12 | python deepseek_export.py https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj |
| 13 | python deepseek_export.py nvy7v2ps1r6e2wyoyj -o my_chat.md |
| 14 | python deepseek_export.py nvy7v2ps1r6e2wyoyj --json -o chat.json |
| 15 | python deepseek_export.py nvy7v2ps1r6e2wyoyj --txt |
| 16 | |
| 17 | First run will create .venv/ and install requests via uv. |
| 18 | """ |
| 19 | |
| 20 | # ── auto-venv bootstrap ───────────────────────────────────────────────────── |
| 21 | import os |
| 22 | import shutil |
| 23 | import subprocess |
| 24 | import sys |
| 25 | from pathlib import Path |
| 26 | |
| 27 | _SCRIPT_DIR = Path(__file__).resolve().parent |
| 28 | _VENV_DIR = _SCRIPT_DIR / ".venv" |
| 29 | _VENV_PYTHON = _VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python") |
| 30 | |
| 31 | _USAGE_TEXT = ( |
| 32 | "Export DeepSeek shared conversations to Markdown/JSON/text.\n" |
| 33 | "Run with -h for full help." |
| 34 | ) |
| 35 | |
| 36 | |
| 37 | def _find_uv() -> str | None: |
| 38 | return shutil.which("uv") |
| 39 | |
| 40 | |
| 41 | def _install_uv() -> bool: |
| 42 | print("[boot] uv is not installed.", flush=True) |
| 43 | answer = input("Install uv now? [Y/n] ").strip().lower() |
| 44 | if answer and answer != "y": |
| 45 | print("Aborted. Install uv manually: curl -LsSf https://astral.sh/uv/install.sh | sh") |
| 46 | return False |
| 47 | print("[boot] Installing uv …", flush=True) |
| 48 | try: |
| 49 | subprocess.check_call( |
| 50 | ["curl", "-LsSf", "https://astral.sh/uv/install.sh", "|", "sh"], |
| 51 | cwd=_SCRIPT_DIR, |
| 52 | ) |
| 53 | except Exception: |
| 54 | # curl ... | sh doesn't work via subprocess.check_call, use shell |
| 55 | subprocess.check_call("curl -LsSf https://astral.sh/uv/install.sh | sh", shell=True) |
| 56 | if not _find_uv(): |
| 57 | print("[boot] uv installation failed. Install manually: curl -LsSf https://astral.sh/uv/install.sh | sh") |
| 58 | return False |
| 59 | return True |
| 60 | |
| 61 | |
| 62 | def _ensure_venv(): |
| 63 | """Create venv + install deps if missing, then re-exec inside it.""" |
| 64 | if sys.prefix != sys.base_prefix: |
| 65 | return # already inside a venv |
| 66 | |
| 67 | print(_USAGE_TEXT, flush=True) |
| 68 | print(flush=True) |
| 69 | |
| 70 | uv = _find_uv() |
| 71 | if not uv: |
| 72 | if not _install_uv(): |
| 73 | sys.exit(1) |
| 74 | uv = _find_uv() |
| 75 | if not uv: |
| 76 | sys.exit(1) |
| 77 | |
| 78 | if not _VENV_PYTHON.exists(): |
| 79 | print("[boot] Creating .venv …", flush=True) |
| 80 | subprocess.check_call([uv, "venv", str(_VENV_DIR)], cwd=_SCRIPT_DIR) |
| 81 | print("[boot] Installing dependencies …", flush=True) |
| 82 | subprocess.check_call( |
| 83 | [uv, "pip", "install", "--python", str(_VENV_PYTHON), "requests"], |
| 84 | cwd=_SCRIPT_DIR, |
| 85 | ) |
| 86 | |
| 87 | os.execv(str(_VENV_PYTHON), [str(_VENV_PYTHON), *sys.argv]) |
| 88 | |
| 89 | |
| 90 | _ensure_venv() |
| 91 | # ── end bootstrap ──────────────────────────────────────────────────────────── |
| 92 | |
| 93 | import argparse |
| 94 | import json |
| 95 | import re |
| 96 | from datetime import datetime, timezone |
| 97 | |
| 98 | import requests |
| 99 | |
| 100 | # --------------------------------------------------------------------------- |
| 101 | # Helpers |
| 102 | # --------------------------------------------------------------------------- |
| 103 | |
| 104 | SHARE_URL_RE = re.compile( |
| 105 | r"(?:https?://chat\.deepseek\.com)?/share/(?P<id>[A-Za-z0-9_-]+)" |
| 106 | ) |
| 107 | |
| 108 | |
| 109 | def extract_share_id(raw: str) -> str: |
| 110 | m = SHARE_URL_RE.search(raw) |
| 111 | if m: |
| 112 | return m.group("id") |
| 113 | if re.fullmatch(r"[A-Za-z0-9_-]{6,}", raw): |
| 114 | return raw |
| 115 | raise ValueError(f"Cannot extract share id from: {raw}") |
| 116 | |
| 117 | |
| 118 | HEADERS = { |
| 119 | "accept": "application/json, text/plain, */*", |
| 120 | "accept-language": "en-US,en;q=0.9", |
| 121 | "referer": "https://chat.deepseek.com/", |
| 122 | "user-agent": ( |
| 123 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " |
| 124 | "(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" |
| 125 | ), |
| 126 | } |
| 127 | |
| 128 | |
| 129 | # --------------------------------------------------------------------------- |
| 130 | # Strategy 1 — plain requests (works when WAF is not aggressive) |
| 131 | # --------------------------------------------------------------------------- |
| 132 | |
| 133 | def fetch_via_requests(share_id: str, token: str | None = None) -> dict: |
| 134 | s = requests.Session() |
| 135 | s.headers.update(HEADERS) |
| 136 | s.get("https://chat.deepseek.com/", timeout=15) |
| 137 | |
| 138 | if token: |
| 139 | s.headers["authorization"] = f"Bearer {token}" |
| 140 | |
| 141 | url = f"https://chat.deepseek.com/api/v0/share/content?share_id={share_id}" |
| 142 | r = s.get(url, timeout=15) |
| 143 | r.raise_for_status() |
| 144 | data = r.json() |
| 145 | |
| 146 | if data.get("code") != 0: |
| 147 | raise RuntimeError(f"API error: {data.get('msg', data)}") |
| 148 | return data |
| 149 | |
| 150 | |
| 151 | # --------------------------------------------------------------------------- |
| 152 | # Strategy 2 — Playwright headless browser (bypasses WAF reliably) |
| 153 | # --------------------------------------------------------------------------- |
| 154 | |
| 155 | def fetch_via_playwright(share_id: str, token: str | None = None) -> dict: |
| 156 | try: |
| 157 | from playwright.sync_api import sync_playwright |
| 158 | except ImportError: |
| 159 | raise RuntimeError( |
| 160 | "playwright not installed. Run:\n" |
| 161 | " uv pip install playwright && uv run playwright install chromium" |
| 162 | ) |
| 163 | |
| 164 | captured = {} |
| 165 | |
| 166 | def on_response(response): |
| 167 | if "/api/v0/share/content" in response.url: |
| 168 | try: |
| 169 | captured["data"] = response.json() |
| 170 | except Exception: |
| 171 | pass |
| 172 | |
| 173 | with sync_playwright() as p: |
| 174 | browser = p.chromium.launch(headless=True) |
| 175 | ctx = browser.new_context( |
| 176 | user_agent=HEADERS["user-agent"], |
| 177 | viewport={"width": 1280, "height": 800}, |
| 178 | ) |
| 179 | page = ctx.new_page() |
| 180 | page.on("response", on_response) |
| 181 | |
| 182 | if token: |
| 183 | ctx.add_cookies([{ |
| 184 | "name": "ds_session_id", |
| 185 | "value": token, |
| 186 | "domain": "chat.deepseek.com", |
| 187 | "path": "/", |
| 188 | }]) |
| 189 | |
| 190 | page.goto( |
| 191 | f"https://chat.deepseek.com/share/{share_id}", |
| 192 | wait_until="networkidle", |
| 193 | timeout=30000, |
| 194 | ) |
| 195 | page.wait_for_timeout(3000) |
| 196 | browser.close() |
| 197 | |
| 198 | if not captured.get("data"): |
| 199 | raise RuntimeError("Playwright could not capture the share API response") |
| 200 | data = captured["data"] |
| 201 | if data.get("code") != 0: |
| 202 | raise RuntimeError(f"API error: {data.get('msg', data)}") |
| 203 | return data |
| 204 | |
| 205 | |
| 206 | # --------------------------------------------------------------------------- |
| 207 | # Main fetch (tries requests first, falls back to playwright) |
| 208 | # --------------------------------------------------------------------------- |
| 209 | |
| 210 | def fetch_share(share_id: str, token: str | None = None) -> dict: |
| 211 | try: |
| 212 | print("[1/2] Trying direct API…", end=" ", flush=True) |
| 213 | data = fetch_via_requests(share_id, token) |
| 214 | print("OK") |
| 215 | return data |
| 216 | except Exception as e: |
| 217 | print(f"failed ({e})") |
| 218 | |
| 219 | print("[2/2] Falling back to Playwright…", end=" ", flush=True) |
| 220 | try: |
| 221 | data = fetch_via_playwright(share_id, token) |
| 222 | print("OK") |
| 223 | return data |
| 224 | except Exception as e: |
| 225 | print(f"failed ({e})") |
| 226 | raise RuntimeError( |
| 227 | "Both strategies failed. If WAF is blocking you, install Playwright:\n" |
| 228 | " uv pip install playwright && uv run playwright install chromium\n" |
| 229 | "Or pass your token with --token." |
| 230 | ) |
| 231 | |
| 232 | |
| 233 | # --------------------------------------------------------------------------- |
| 234 | # Formatting |
| 235 | # --------------------------------------------------------------------------- |
| 236 | |
| 237 | def _ts(ts) -> str: |
| 238 | if not ts: |
| 239 | return "" |
| 240 | try: |
| 241 | return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") |
| 242 | except Exception: |
| 243 | return str(ts) |
| 244 | |
| 245 | |
| 246 | def to_markdown(data: dict) -> str: |
| 247 | biz = data.get("data", {}).get("biz_data", {}) |
| 248 | messages = biz.get("messages", []) or biz.get("chat_messages", []) |
| 249 | |
| 250 | title = biz.get("title") or "DeepSeek Conversation" |
| 251 | model = biz.get("model_type") or biz.get("model") or "" |
| 252 | |
| 253 | parts = [f"# {title}\n"] |
| 254 | if model: |
| 255 | parts.append(f"**Model:** {model}") |
| 256 | parts.append("") |
| 257 | |
| 258 | if not messages: |
| 259 | parts.append("_No messages found._\n") |
| 260 | return "\n".join(parts) |
| 261 | |
| 262 | for msg in messages: |
| 263 | role = (msg.get("role") or "").upper() |
| 264 | content = msg.get("content") or msg.get("message") or "" |
| 265 | if not content and not role: |
| 266 | continue |
| 267 | |
| 268 | thinking = msg.get("thinking_content") or msg.get("reasoning_content") or "" |
| 269 | elapsed = msg.get("thinking_elapsed_secs") |
| 270 | search = msg.get("search_results") or [] |
| 271 | |
| 272 | if role == "USER": |
| 273 | header = "You" |
| 274 | elif role in ("ASSISTANT", "AI"): |
| 275 | header = "DeepSeek" |
| 276 | elif role == "SYSTEM": |
| 277 | header = "System" |
| 278 | else: |
| 279 | header = role.title() |
| 280 | |
| 281 | parts.append(f"## {header}\n") |
| 282 | |
| 283 | if thinking: |
| 284 | label = f"Thinking ({elapsed}s)" if elapsed else "Thinking" |
| 285 | parts.append(f"<details><summary>{label}</summary>\n\n{thinking}\n\n</details>\n") |
| 286 | |
| 287 | if search: |
| 288 | refs = [] |
| 289 | for item in search: |
| 290 | t = item.get("title") or "" |
| 291 | u = item.get("url") or "" |
| 292 | if u: |
| 293 | refs.append(f"- [{t}]({u})" if t else f"- {u}") |
| 294 | if refs: |
| 295 | parts.append("**Sources:**\n" + "\n".join(refs) + "\n") |
| 296 | |
| 297 | if content: |
| 298 | parts.append(f"{content}\n") |
| 299 | parts.append("---\n") |
| 300 | |
| 301 | return "\n".join(parts) |
| 302 | |
| 303 | |
| 304 | def to_plain_text(data: dict) -> str: |
| 305 | biz = data.get("data", {}).get("biz_data", {}) |
| 306 | messages = biz.get("messages", []) or biz.get("chat_messages", []) |
| 307 | title = biz.get("title") or "DeepSeek Conversation" |
| 308 | |
| 309 | parts = [f"=== {title} ===\n"] |
| 310 | for msg in messages: |
| 311 | role = (msg.get("role") or "").upper() |
| 312 | content = msg.get("content") or msg.get("message") or "" |
| 313 | thinking = msg.get("thinking_content") or msg.get("reasoning_content") or "" |
| 314 | if role == "USER": |
| 315 | parts.append(f"[User]\n{content}\n") |
| 316 | elif role in ("ASSISTANT", "AI"): |
| 317 | if thinking: |
| 318 | parts.append(f"[Thinking]\n{thinking}\n") |
| 319 | parts.append(f"[DeepSeek]\n{content}\n") |
| 320 | else: |
| 321 | parts.append(f"[{role}]\n{content}\n") |
| 322 | return "\n".join(parts) |
| 323 | |
| 324 | |
| 325 | # --------------------------------------------------------------------------- |
| 326 | # CLI |
| 327 | # --------------------------------------------------------------------------- |
| 328 | |
| 329 | class _ArgumentParser(argparse.ArgumentParser): |
| 330 | def error(self, message): |
| 331 | self.print_usage(sys.stderr) |
| 332 | sys.stderr.write("\n") |
| 333 | sys.stderr.write(f"{self.prog}: error: {message}\n") |
| 334 | sys.exit(2) |
| 335 | |
| 336 | |
| 337 | def main(): |
| 338 | ap = _ArgumentParser( |
| 339 | description="Export a DeepSeek shared conversation", |
| 340 | epilog="Example: %(prog)s https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj", |
| 341 | ) |
| 342 | ap.add_argument("link", help="Share URL or share ID") |
| 343 | ap.add_argument("-o", "--output", help="Output file path (default: auto-named)") |
| 344 | ap.add_argument("--json", action="store_true", help="Dump raw JSON instead of Markdown") |
| 345 | ap.add_argument("--txt", action="store_true", help="Dump plain text") |
| 346 | ap.add_argument("--token", help="Bearer token for auth (optional, needed for private shares)") |
| 347 | args = ap.parse_args() |
| 348 | |
| 349 | share_id = extract_share_id(args.link) |
| 350 | print(f"Share ID: {share_id}") |
| 351 | |
| 352 | data = fetch_share(share_id, args.token) |
| 353 | |
| 354 | if args.output: |
| 355 | out = Path(args.output) |
| 356 | elif args.json: |
| 357 | out = Path(f"export_{share_id}.json") |
| 358 | elif args.txt: |
| 359 | out = Path(f"export_{share_id}.txt") |
| 360 | else: |
| 361 | out = Path(f"export_{share_id}.md") |
| 362 | |
| 363 | if args.json: |
| 364 | content = json.dumps(data, indent=2, ensure_ascii=False) |
| 365 | elif args.txt: |
| 366 | content = to_plain_text(data) |
| 367 | else: |
| 368 | content = to_markdown(data) |
| 369 | |
| 370 | out.write_text(content, encoding="utf-8") |
| 371 | print(f"Saved to {out} ({len(content)} bytes)") |
| 372 | |
| 373 | |
| 374 | if __name__ == "__main__": |
| 375 | main() |
| 376 |