Publish agent-radar via gitea-publish skill

This commit is contained in:
gitea-publish
2026-09-01 15:14:54 +08:00
commit 78b87e8b2f
17 changed files with 2279 additions and 0 deletions
+194
View File
@@ -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())