chore: 以本地工作区为准全量快照同步 GitHub

包含小程序、管理端、soul-api、脚本与静态资源等当前本地全部已跟踪与新增文件(排除 .DS_Store 与 .obsidian)。

Made-with: Cursor
This commit is contained in:
卡若
2026-04-13 11:49:38 +08:00
parent 83375cc388
commit b5f5180654
142 changed files with 10684 additions and 2670 deletions

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量上传《卡若的IP财富旅程》PART1-5 到小程序。
part_id : part-new-17755513182
part_title: 卡若的IP财富旅程
各 PART 对应独立 chapter:
PART1 -> chapter-ip-1 | PART1 | 我少年时期的互联网启蒙
PART2 -> chapter-ip-2 | PART2 | 从0-1的过程
PART3 -> chapter-ip-3 | PART3 | 我的创业旅程
PART4 -> chapter-ip-4 | PART4 | 私域流量到AI时代的商业进化
PART5 -> chapter-ip-5 | PART5 | 未来的一些思考
ID 格式: ip<part_num>.<section_num> 例: ip1.1, ip2.10, ip3.5
"""
from __future__ import annotations
import importlib.util
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
BOOK_DIR = Path("/Users/karuo/Documents/个人/2、我写的书/《卡若的IP财富旅程》")
PART_ID = "part-new-17755513182"
PART_TITLE = "卡若的IP财富旅程"
CHAPTERS = {
1: ("chapter-ip-1", "PART1 | 我少年时期的互联网启蒙"),
2: ("chapter-ip-2", "PART2 | 从0-1的过程"),
3: ("chapter-ip-3", "PART3 | 我的创业旅程"),
4: ("chapter-ip-4", "PART4 | 私域流量到AI时代的商业进化"),
5: ("chapter-ip-5", "PART5 | 未来的一些思考"),
}
# 匹配 ✅1.5-标题 或 ✅1.5- 标题 形式的文件名,提取 section_num 和 title
FILENAME_RE = re.compile(
r"^[✅⬜]?\s*(\d+)\.(\d+)[- ]+(.+?)(?:\.md)?$",
re.UNICODE,
)
def load_db():
mig = ROOT / "scripts" / "migrate_2026_sections.py"
spec = importlib.util.spec_from_file_location("_mig_db", mig)
mod = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(mod)
return mod.DB_CONFIG
def strip_md_title_line(text: str) -> str:
lines = text.splitlines()
if lines and lines[0].lstrip().startswith("#"):
return "\n".join(lines[1:]).lstrip("\n")
return text
def for_miniprogram_body(text: str) -> str:
out_lines: list[str] = []
for line in text.splitlines():
if line.strip() == "---":
out_lines.append("")
else:
out_lines.append(line)
body = "\n".join(out_lines)
body = body.replace("**", "")
body = re.sub(r"\n{3,}", "\n\n", body)
return body.strip() + "\n"
def collect_files(part_num: int) -> list[tuple[str, str, Path]]:
"""返回 [(ip_id, title, path)] 已去重且按节号排序"""
part_dir = BOOK_DIR / f"PART{part_num}"
if not part_dir.exists():
print(f" ⚠ 目录不存在: {part_dir}", file=sys.stderr)
return []
seen_sections: dict[str, tuple[str, str, Path]] = {}
for f in part_dir.iterdir():
name = f.name
# 去掉 .md 后缀再匹配(处理双后缀 .md.md
clean_name = re.sub(r"\.md(\.md)*$", "", name, flags=re.IGNORECASE)
m = FILENAME_RE.match(clean_name)
if not m:
print(f" ⚠ 跳过无法解析的文件: {name}")
continue
p_num, s_num, title_raw = m.group(1), m.group(2), m.group(3).strip()
if int(p_num) != part_num:
print(f" ⚠ PART号不匹配跳过: {name}")
continue
# 把文件名里的冒号/括号等清理出来作为标题
title = title_raw.strip(": \t")
# 还原完整标题(包含冒号)
title = title_raw
ip_id = f"ip{part_num}.{s_num}"
section_key = f"{part_num}.{s_num}"
# 优先选有 .md 后缀的文件(去重)
if section_key in seen_sections:
existing_path = seen_sections[section_key][2]
# 若新文件有 .md 后缀,替换
if str(f).lower().endswith(".md") and not str(existing_path).lower().endswith(".md"):
seen_sections[section_key] = (ip_id, title, f)
else:
seen_sections[section_key] = (ip_id, title, f)
# 按节号排序
result = list(seen_sections.values())
result.sort(key=lambda x: float(re.sub(r"ip\d+\.", "", x[0])))
return result
def upsert_chapter(cur, chapter_id: str, chapter_title: str, ip_id: str, title: str, body: str, price: float):
cur.execute("SELECT id FROM chapters WHERE id = %s", (ip_id,))
if cur.fetchone():
cur.execute(
"UPDATE chapters SET section_title=%s, content=%s, part_id=%s, part_title=%s, "
"chapter_id=%s, chapter_title=%s, price=%s WHERE id=%s",
(title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price, ip_id),
)
return "updated"
else:
cur.execute(
"INSERT INTO chapters (id, section_title, content, part_id, part_title, chapter_id, chapter_title, price) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
(ip_id, title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price),
)
return "created"
def main():
try:
import pymysql
except ImportError:
print("需要: pip install pymysql", file=sys.stderr)
sys.exit(1)
dry_run = "--dry-run" in sys.argv
price = 1.0
cfg = load_db()
conn = pymysql.connect(**cfg)
cur = conn.cursor()
total_ok = 0
total_skip = 0
total_err = 0
for part_num, (chapter_id, chapter_title) in CHAPTERS.items():
print(f"\n{'='*60}")
print(f"PART{part_num}{chapter_id} | {chapter_title}")
print(f"{'='*60}")
files = collect_files(part_num)
if not files:
print(" 无文件,跳过")
continue
for ip_id, title, path in files:
try:
raw = path.read_text(encoding="utf-8")
except Exception as e:
print(f" ✗ 读取失败 {path.name}: {e}")
total_err += 1
continue
body = for_miniprogram_body(strip_md_title_line(raw))
if dry_run:
print(f" [dry] {ip_id} {title[:30]} ({path.name})")
total_ok += 1
continue
try:
action = upsert_chapter(cur, chapter_id, chapter_title, ip_id, title, body, price)
conn.commit()
print(f" ✓ [{action}] {ip_id} {title[:40]}")
total_ok += 1
except Exception as e:
conn.rollback()
print(f" ✗ 上传失败 {ip_id} ({path.name}): {e}")
total_err += 1
conn.close()
print(f"\n{'='*60}")
print(f"完成:成功 {total_ok} | 跳过 {total_skip} | 失败 {total_err}")
if dry_run:
print("dry-run 模式,未实际写库)")
if __name__ == "__main__":
main()