#!/usr/bin/env python3 """把 articles/ 下的 Markdown 源文发布(或更新)到 emlog 可巴蒂栏目。 用法 ---- python3 scripts/publish_emlog.py --article ipollowork python3 scripts/publish_emlog.py --article 00-overview python3 scripts/publish_emlog.py --article ipollowork --draft y # 存草稿 两个关键设计 ------------ 1. **防空保护点放在"本地正文非空",不是"先 detail 取回线上全文"**。 emlog 的 article_update 在 content 为空时会把正文清空,所以必须有保护。 但保护不能建立在 detail 上,原因有二: a) 本脚本的 content 永远来自本地 Markdown 源文,不存在"忘了传"的情况; b) detail 接口**查不到草稿**(实测:草稿 id 返回 400 Article not found, 已发布文章正常)。若以 detail 为门槛,草稿将永远无法转正。 因此 detail 降级为可选的健全性检查:取得到就对比长度,取不到不阻断。 2. **文章 ID 回写**。 有同名项目档案(data/projects/.yaml)→ 回写到档案的 article 块; 没有(总纲文 00-overview、横评文 00-comparison 这类跨项目文章) → 回写到 articles/.meta.yaml。 有了 ID 才能做"更新"而不是每次都发一篇新的。 判断顺序不能反:先按"是不是项目文"分流,再决定落哪里, 否则新增一篇横评类文章时 ID 会无处可写,下次发布就变成重复发一篇。 """ from __future__ import annotations import argparse import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, "/Users/figmarlite/.workbuddy/skills/emlog-blog-publisher/scripts") import yaml # noqa: E402 from _common import ARTICLES, PROJECTS, today, update_block # noqa: E402 import publish_article as emlog # noqa: E402 DEFAULT_SORT_ID = 17 # 可巴蒂 # ---------------------------------------------------------------- 转换 def md_to_html(md: str) -> str: """Markdown → HTML。emlog 只吃 HTML。""" try: import markdown return markdown.markdown( md, extensions=["tables", "fenced_code", "toc", "attr_list", "sane_lists"] ) except ImportError: sys.exit("缺少 markdown 库:pip install markdown") def split_title(md: str) -> tuple[str, str]: """取第一个一级标题作为文章标题,其余作为正文。""" lines = md.lstrip().split("\n") if lines and lines[0].startswith("# "): return lines[0][2:].strip(), "\n".join(lines[1:]).lstrip("\n") return "未命名", md def to_excerpt(md: str, limit: int = 110) -> str: """摘一段纯文本作为摘要:跳过标题与表格,取第一段实质内容。""" for line in md.split("\n"): s = line.strip() if not s or s.startswith(("#", "|", ">", "-", "*", "!")) or s.startswith("<"): continue s = re.sub(r"[*`_]", "", s) return s[:limit] return "" # ---------------------------------------------------------------- ID 读写 def meta_path(aid: str) -> Path: return ARTICLES / f"{aid}.meta.yaml" def read_emlog_id(aid: str) -> int | None: p = PROJECTS / f"{aid}.yaml" if p.exists(): d = yaml.safe_load(p.read_text(encoding="utf-8")) or {} v = (d.get("article") or {}).get("emlog_id") return int(v) if v else None mp = meta_path(aid) if mp.exists(): return (yaml.safe_load(mp.read_text(encoding="utf-8")) or {}).get("emlog_id") return None def write_emlog_id(aid: str, emlog_id: int) -> None: url = f"{emlog.BLOG_URL}/?post={emlog_id}" p = PROJECTS / f"{aid}.yaml" if not p.exists(): # 跨项目文章(总纲 / 横评 / 专题):没有档案可挂,单独存 meta。 mp = meta_path(aid) mp.write_text( f"# 发布元数据(由 scripts/publish_emlog.py 维护)\n" f"# 本文不属于单个项目,ID 不落在 data/projects/ 下\n" f"emlog_id: {emlog_id}\nurl: \"{url}\"\nlast_published: \"{today()}\"\n", encoding="utf-8", ) print(f" 已写入 {mp.name}") return text = p.read_text(encoding="utf-8") try: text = update_block(text, "article", { "emlog_id": emlog_id, "published_at": today(), "url": url, }) p.write_text(text, encoding="utf-8") print(f" 已回写 emlog_id 到档案") except ValueError as e: print(f" [warn] 回写失败:{e}") # ---------------------------------------------------------------- 发布 def do_publish(aid: str, title: str, html: str, sort_id: int, tags: str, draft: str, excerpt: str) -> None: res = emlog.publish_article( title=title, content=html, sort_id=sort_id, excerpt=excerpt, tags=tags, draft=draft, ) if res.get("code") != 0: print(f" 发布失败:{res.get('msg')}") sys.exit(1) new_id = int(res.get("data", {}).get("article_id") or 0) print(f" 已发布 id={new_id} {emlog.BLOG_URL}/?post={new_id}") write_emlog_id(aid, new_id) def do_update(aid: str, emlog_id: int, title: str, html: str, sort_id: int, tags: str, draft: str, excerpt: str) -> None: """更新既有文章。 关于"emlog update 不传 content 会清空正文"这个坑: 保护点应该是**本地正文非空**,而不是"先 detail 取回线上全文"。 两个理由—— 1. 本脚本的 content 永远来自本地 Markdown 源文,不存在"忘了传"的情况; 2. detail 接口查不到**草稿**(实测:草稿 id 返回 400 Article not found, 已发布文章正常)。若把保护建立在 detail 上,草稿就永远无法转正。 所以 detail 降级为可选的健全性检查:取得到就对比一下长度,取不到也不阻断。 """ print(f" 发现已有文章 id={emlog_id},走更新流程") if not html.strip(): print(" 本地正文为空,拒绝更新(那会清空线上内容)") sys.exit(1) det = emlog.get_article_detail(emlog_id) if det.get("code") == 0: # 注意返回结构是 data.article.content,不是 data.content art = (det.get("data") or {}).get("article") or {} existing = art.get("content") or "" print(f" 线上现有正文 {len(existing)} 字符 → 将覆盖为 {len(html)} 字符") else: print(f" 未能取回线上正文({det.get('msg', '未知')});" f"本地正文非空,继续更新") res = emlog.update_article( article_id=emlog_id, title=title, content=html, sort_id=sort_id, excerpt=excerpt, tags=tags, draft=draft, ) if res.get("code") != 0: print(f" 更新失败:{res.get('msg')}") sys.exit(1) print(f" 已更新 {emlog.BLOG_URL}/?post={emlog_id}") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--article", required=True, help="articles/ 下的文件名(不含 .md)") ap.add_argument("--sort-id", type=int, default=DEFAULT_SORT_ID) ap.add_argument("--tags", default="") ap.add_argument("--draft", default="n", choices=["y", "n"]) args = ap.parse_args() md_path = ARTICLES / f"{args.article}.md" if not md_path.exists(): print(f"文章不存在:{md_path}") return 1 md = md_path.read_text(encoding="utf-8") title, body = split_title(md) html = md_to_html(body) print(f"==> {args.article}") print(f" 标题:{title}") print(f" HTML {len(html)} 字符") # 摘要要从正文里摘,不是从标题 —— 传标题进去等于没写摘要 excerpt = to_excerpt(body) emlog_id = read_emlog_id(args.article) if emlog_id: do_update(args.article, emlog_id, title, html, args.sort_id, args.tags, args.draft, excerpt) else: do_publish(args.article, title, html, args.sort_id, args.tags, args.draft, excerpt) return 0 if __name__ == "__main__": raise SystemExit(main())