Files
inspiration-collector/scripts/sync_cards.py
2026-06-16 00:33:14 +08:00

194 lines
5.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
sync_cards.py — Memos 卡片备份脚本(基于 uid 去重)
每30分钟由 cron 触发,将当前用户的所有 Memos 卡片备份到 cards/ 目录,
并推送到 Gitea inspiration-collector 仓库。
去重方式:按 memo.uid 比对,不重复写入。
"""
import os
import sys
import re
import subprocess
from datetime import datetime, timezone, timedelta
import requests
# ── 配置 ──
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CARDS_DIR = os.path.join(BASE_DIR, "cards")
GIT_DIR = BASE_DIR
MEMOS_URL = os.environ.get("MEMOS_URL", "http://localhost:5230")
MEMOS_TOKEN = os.environ.get("MEMOS_ACCESS_TOKEN", "")
TZ_BEIJING = timezone(timedelta(hours=8))
KNOWN_TAGS = {"#功能完善", "#90后四级副职培训班", "#灵感收集器"}
class MemosClient:
def __init__(self, base_url, token):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
})
def list_all_memos(self):
all_memos = []
page_token = ""
while True:
params = {"pageSize": 100}
if page_token:
params["pageToken"] = page_token
resp = self.session.post(
f"{self.base_url}/memos.api.v1.MemoService/ListMemos",
json=params, timeout=15
)
resp.raise_for_status()
data = resp.json()
all_memos.extend(data.get("memos", []))
next_token = data.get("nextPageToken", "")
if not next_token:
break
page_token = next_token
return all_memos
def sanitize_filename(text):
text = re.sub(r'[<>:"/\\|?*]', "", text)
text = re.sub(r'\s+', "_", text.strip())
return text[:80] or "memo"
def extract_tags(content):
"""提取内容中的 #标签 """
return [w for w in content.split() if w.startswith("#") and w in KNOWN_TAGS]
def load_existing_uids(cards_dir):
"""扫描现有卡片文件,收集所有 uid + 文件名作为去重依据"""
uids = set()
fnames = set()
if not os.path.isdir(cards_dir):
return uids, fnames
for fname in os.listdir(cards_dir):
if not fname.endswith(".md"):
continue
fnames.add(fname)
fpath = os.path.join(cards_dir, fname)
try:
with open(fpath, "r", encoding="utf-8") as f:
head = f.read(500)
# 匹配 uid: "xxx" 或 id: xxx 或 uid: xxx无引号
m = re.search(r'^(?:uid|id):\s*"?([^"\n]+)"?', head, re.MULTILINE)
if m:
val = m.group(1).strip()
if val:
uids.add(val)
except Exception:
continue
return uids, fnames
def memo_to_markdown(memo):
uid = memo.get("uid", "")
content = memo.get("content", "")
create_time = memo.get("createTime", "")
try:
dt = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
dt_bj = dt.astimezone(TZ_BEIJING)
time_str = dt_bj.strftime("%Y-%m-%dT%H:%M:%S+08:00")
file_ts = dt_bj.strftime("%Y%m%d%H%M%S")
except Exception:
time_str = create_time
file_ts = uid[:14] if uid else "unknown"
first_line = content.strip().split("\n")[0][:60]
filename = f"{file_ts}_{sanitize_filename(first_line)}.md"
filepath = os.path.join(CARDS_DIR, filename)
md = f"""---
title: "{first_line}"
date: {time_str}
uid: "{uid}"
source: "memos"
---
{content}
"""
return filepath, md
def git_push(directory, message):
try:
subprocess.run(["git", "add", "-A"], cwd=directory, check=True,
capture_output=True, timeout=30)
result = subprocess.run(
["git", "diff", "--cached", "--quiet"],
cwd=directory, capture_output=True, timeout=15
)
if result.returncode == 0:
return
subprocess.run(
["git", "commit", "-m", message],
cwd=directory, check=True, capture_output=True, timeout=30
)
subprocess.run(
["git", "push", "origin", "main"],
cwd=directory, check=True, capture_output=True, timeout=60
)
print(f"[OK] 已推送: {message}")
except subprocess.CalledProcessError as e:
print(f"[WARN] git 操作失败: {e.stderr.decode()[:200]}")
except Exception as e:
print(f"[WARN] git 异常: {e}")
def main():
if not MEMOS_TOKEN:
print("[ERROR] MEMOS_ACCESS_TOKEN 未设置")
sys.exit(1)
os.makedirs(CARDS_DIR, exist_ok=True)
# 加载已有 uid + 文件名
existing_uids, existing_fnames = load_existing_uids(CARDS_DIR)
print(f"[INFO] 已有 {len(existing_fnames)} 个文件,{len(existing_uids)} 个 uid")
client = MemosClient(MEMOS_URL, MEMOS_TOKEN)
print(f"[INFO] 正在从 Memos 拉取...")
all_memos = client.list_all_memos()
print(f"[INFO] 共获取 {len(all_memos)} 条卡片")
new_count = 0
for memo in all_memos:
uid = memo.get("uid", "")
if uid and uid in existing_uids:
continue # uid 已存在,跳过
filepath, md = memo_to_markdown(memo)
fname = os.path.basename(filepath)
if fname in existing_fnames:
continue # 文件名已存在,跳过(兼容旧版无 uid 的卡片)
with open(filepath, "w", encoding="utf-8") as f:
f.write(md)
new_count += 1
print(f" [+] {fname}")
if uid:
existing_uids.add(uid)
existing_fnames.add(fname)
if new_count > 0:
print(f"[INFO] 新增 {new_count} 条卡片,推送至 Gitea...")
git_push(GIT_DIR, f"cards: 备份 {new_count} 条 Memos 卡片")
else:
print(f"[INFO] 无新增卡片")
if __name__ == "__main__":
main()