Files
agent-radar/scripts/publish_emlog.py
T
gitea-publish e5cdb1801e track: 新增 OpenWork / Orca / Maka 三项目,样本达到 4 个
样本
- openwork (different-ai/openwork) 23.2k★ 能力分发层,DCO + evals + worlds
- orca (stablyai/orca) 58.8k★ 并行编排外壳,5000 issue 实为团队自开任务板
- maka (apache/maka) 4.4k★ 执行记录层,Apache 孵化但无正式 release

档案
- 三份五维档案填完,risk.claim_vs_reality 均已核实到证据
- ipollowork 象限坐标从占位 9/9/9/9 校准为 8/7/7/7(与新样本同一标尺)

文章
- 新增横评文《四个 Agent 工作台的路线分野:把「多引擎中立」拆开看》(emlog id=99)
- 总纲文回填四项目矩阵 + 双视图象限图 (emlog id=98)

镜像
- gitinbox/maka 已推送(public,source-only 快照 12 MB)
- openwork(291MB) / orca(671MB) 受 50KB/s 带宽约束,档案标 mode: pending

规则
- license-tiers.yaml 新增「目录切分许可」降级流程(OpenWork 判例:
  排除 ee/ 后为纯 MIT → public,完整镜像 → private)
- registry.yaml openwork 可见性改 public + mirror_excludes: ["ee/"]

脚本修复
- _common.py 新增 upsert_block:手工整块覆盖 ecosystem 删掉 metrics 时能自愈
- mirror_sync.sh:SOURCE_ONLY 未初始化(set -u 下偶发炸);
  浅仓推送被服务端拒绝 → 自动 --unshallow,失败降级快照;
  量体积前先 repack(此前新 commit 全是 loose object,恒打印 0 MB)
- build_matrix.py:成熟度长文本移出表格(新增 maturity_level 短标签);
  象限图标签靠右时翻转到左侧防溢出;SVG 加 max-width 防窄屏溢出;
  文章索引改为自动生成(原先手抄,横评文发了索引里却没有)
- publish_emlog.py:ID 回写按「有无同名档案」分流,
  00-* 跨项目文章落到 articles/<id>.meta.yaml(此前 ID 无处可写)
2026-09-01 19:27:21 +08:00

204 lines
7.9 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. **防空保护点放在"本地正文非空",不是"先 detail 取回线上全文"**。
emlog 的 article_update 在 content 为空时会把正文清空,所以必须有保护。
但保护不能建立在 detail 上,原因有二:
a) 本脚本的 content 永远来自本地 Markdown 源文,不存在"忘了传"的情况;
b) detail 接口**查不到草稿**(实测:草稿 id 返回 400 Article not found,
已发布文章正常)。若以 detail 为门槛,草稿将永远无法转正。
因此 detail 降级为可选的健全性检查:取得到就对比长度,取不到不阻断。
2. **文章 ID 回写**。
有同名项目档案(data/projects/<id>.yaml)→ 回写到档案的 article 块;
没有(总纲文 00-overview、横评文 00-comparison 这类跨项目文章)
→ 回写到 articles/<id>.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())