195 lines
7.2 KiB
Python
Executable File
195 lines
7.2 KiB
Python
Executable File
#!/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. **update 前必须先 detail 取回全文**。
|
||
emlog 的 article_update 接口在 content 为空时会把正文清空 —— 这是接口行为,
|
||
不是 bug。总纲文每次回填都要更新,所以这条保护是刚需。
|
||
2. **文章 ID 回写**。
|
||
项目文回写到 data/projects/<id>.yaml 的 article 块;
|
||
总纲文回写到 articles/00-overview.meta.yaml。
|
||
有了 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 # 可巴蒂
|
||
OVERVIEW_ID = "00-overview"
|
||
META_PATH = ARTICLES / f"{OVERVIEW_ID}.meta.yaml"
|
||
|
||
|
||
# ---------------------------------------------------------------- 转换
|
||
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 read_emlog_id(aid: str) -> int | None:
|
||
if aid == OVERVIEW_ID:
|
||
if not META_PATH.exists():
|
||
return None
|
||
return (yaml.safe_load(META_PATH.read_text(encoding="utf-8")) or {}).get("emlog_id")
|
||
p = PROJECTS / f"{aid}.yaml"
|
||
if not p.exists():
|
||
return None
|
||
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
|
||
|
||
|
||
def write_emlog_id(aid: str, emlog_id: int) -> None:
|
||
url = f"{emlog.BLOG_URL}/?post={emlog_id}"
|
||
if aid == OVERVIEW_ID:
|
||
META_PATH.write_text(
|
||
f"# 总纲文发布元数据(由 scripts/publish_emlog.py 维护)\n"
|
||
f"emlog_id: {emlog_id}\nurl: \"{url}\"\nlast_published: \"{today()}\"\n",
|
||
encoding="utf-8",
|
||
)
|
||
print(f" 已写入 {META_PATH.name}")
|
||
return
|
||
|
||
p = PROJECTS / f"{aid}.yaml"
|
||
if not p.exists():
|
||
print(f" [warn] 档案不存在,未回写 ID: {p}")
|
||
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())
|