Publish agent-radar via gitea-publish skill
This commit is contained in:
Executable
+194
@@ -0,0 +1,194 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user