Files
agent-radar/scripts/publish_emlog.py
T

195 lines
7.2 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
"""把 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())