mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
feat(skills): merge release announcement workflow
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: release-announcement
|
||||
description: 从本项目最新的 release-notes Markdown 制作微信群发布短文案与竖版长图,核验贡献者 GitHub ID 并嵌入真实头像。用于版本发布宣传,不负责创建或发布 GitHub Release。
|
||||
---
|
||||
|
||||
# Release announcement
|
||||
|
||||
在本仓库调用 `$release-announcement` 时,交付一张可发微信群的 PNG 长图和一段很短的群文案。默认使用 [版式与文案要求](references/editorial-brief.md);用户另有要求时以用户要求为准。
|
||||
|
||||
调用方式:Codex 使用 `$release-announcement`;Claude Code Haha 的终端输入 `/release-announcement`。这两个工具都从项目的 `.agents/skills/` 读取同一份技能。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 在仓库根目录运行 `python3 .agents/skills/release-announcement/scripts/release_poster.py scan --repo . --out artifacts/release-posters/latest-scan.json`。它按语义版本比较工作区、当前 Git 历史、本地 `main` 和已有的 `origin/main`,不切换分支、不抓取远程。读取 JSON 中的中文发布说明、来源和 `@GitHubID` 提及;`credit_candidate` 仅表示有明确署名格式,仍需核对上下文。如果来源不是工作区文件,明确告知用户。
|
||||
复测旧版本时加 `--version 0.6.6` 并使用独立输出路径,避免覆盖最新版本的 scan 结果。
|
||||
2. 仅依据该版本的中文 release note 提炼主要功能和关键修复;纯修复版的 `features` 留空,纯功能版的 `fixes` 留空,绝不为了填版式虚构内容。有重要模型或供应商更新时可加一个简短区块。每条信息必须能追溯到原文,压缩表述时保留条件和边界。安装提醒只写原文确有的信息。不要把英文译文重复计数。
|
||||
3. 对 `credit_candidate: true` 的账号逐个核对发布说明里的贡献项;普通 `@ID` 提及(例如 PR reviewer)不算代码署名。`render` 会通过 GitHub 公共用户 API 校验 ID 并下载真实头像;无法核验时停止并报告,不能臆造头像、错配姓名,不能让图像生成模型重绘头像。正式生成不能使用 `avatar_path` 跳过核验。
|
||||
4. 按 [数据格式](references/poster-spec.md) 写 `poster-spec.json`,版本必须与 scan 结果一致。运行 `python3 .agents/skills/release-announcement/scripts/release_poster.py render --scan artifacts/release-posters/latest-scan.json --spec <poster-spec.json> --out <poster.png>`。建议输出到 `artifacts/release-posters/v<version>/`,保留已有文件,文件名加版本或修订号。此脚本固定使用紧凑版式绘制准确中文和头像;不要用 imagegen 绘制正文。
|
||||
5. 写一段适合微信群的短文案,保存为同目录的 `wechat-copy.txt`:版本号 + 1 句主要变化 + 1 句感谢,具体细节交给图片。有本次明确署名的贡献者时感谢代码贡献;没有时只感谢反馈与支持,不暗示本版有特定群友提交。文案不要宣称未核实的下载地址或公开发布状态,也不要自动向群发送。若需要下载入口,先检查对应 GitHub Release 是否存在。
|
||||
6. 打开成图检查标题、文字换行、头像和 ID 对应、正文是否从顶部很快开始。最后提供 PNG、短文案、所用 release note 来源和任何未核实项。生成物放在被 Git 忽略的 `artifacts/`,不要提交生成图、头像缓存或中间 JSON。
|
||||
|
||||
## 图像与环境
|
||||
|
||||
默认长图不需要生成式插画:顶部仅有约 230px 的版本标题,直接进入实际有内容的功能或修复区。这样版式与中文文字最稳定。用户明确想增加插画时,可用 imagegen 只生成无字装饰素材,再用脚本绘制全部文字与真实头像;不能让装饰占去首屏。
|
||||
|
||||
`scan` 只用 Python 标准库;`render` 需要 Pillow 和可用的中文系统字体。缺依赖时先查看项目已有运行环境,不为一次制图修改生产依赖。GitHub 头像获取是公开只读请求;失败时给出具体账号和原因。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "版本发布长图"
|
||||
short_description: "从最新 release note 生成微信群短文案、长图和贡献者头像"
|
||||
default_prompt: "用 $release-announcement 读取本项目最新发布说明,制作微信群短文案和发布长图。"
|
||||
@@ -0,0 +1,9 @@
|
||||
# 发布图与群文案提示词
|
||||
|
||||
使用最新 release markdown 的**中文原文**作为唯一内容依据。写给已经在使用 Claude Code Haha 的微信群用户,突出他们最能感受到的变化,避免把每条 commit 都挤进图里。
|
||||
|
||||
**长图视觉提示词:**1080px 宽、适合手机纵向阅读的简体中文版本更新图。顶部深炭色 Hero 约 230px,只放产品名、版本号和「新版本发布」,无插画、无大图、无大块留白;紧接着进入本版实际有内容的功能或修复区。主体为暖白纸色,深色标题、陶土红小标记、简洁双列功能卡、单列修复清单。纯修复版不要出现「这次新增」或空功能卡。贡献者区展示从 GitHub 实际取得的圆形头像、准确 ID 和对应贡献。正文由确定性字体排版完成,不能依赖生成模型拼写文字或绘制头像。按内容自然增长高度,不压缩字号来塞入更多条目。
|
||||
|
||||
**提炼提示词:**从发布说明选最多 4 项核心功能、最多 9 项用户能理解的修复;没有的类别保持为空。每项标题尽量不超过 14 个汉字,说明用一句话讲清“现在能做什么”或“之前的问题如何改善”。优先考虑群友直接感受到的变化,避免用开发者内部修复挤掉有用的用户功能。保留必要条件,例如“在设置中开启后”、超时选项、适用平台、临时性或签名状态。模型更新可合并为一个区块。未署名的修复不强行归给贡献者。
|
||||
|
||||
**群文案提示词:**用自然、简短的群聊语气,最多三句话;写出版本号和最重要的两项变化,最后引导看图。有本版署名贡献者时感谢代码贡献;没有时感谢反馈与支持。只有用户明确说这些贡献者是群成员时才称他们为“群友”。不要把图内清单再抄一遍;不要虚构下载链接或给未核实的 GitHub ID 加 `@`。
|
||||
@@ -0,0 +1,29 @@
|
||||
# `poster-spec.json` 格式
|
||||
|
||||
运行 `scan` 后,以最新 release note 填写 JSON。字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.6.7",
|
||||
"product": "Claude Code Haha",
|
||||
"features": [
|
||||
{"title": "侧边对话", "body": "输入 /btw,在右侧开启独立的临时对话,不打断主任务。"}
|
||||
],
|
||||
"extras": [
|
||||
{"title": "模型与供应商", "headline": "新版模型与映射", "body": "依据本版说明压缩成一句话。"}
|
||||
],
|
||||
"fixes": [
|
||||
{"title": "超大会话打不开", "body": "超过 20MB 的历史可以继续打开。"}
|
||||
],
|
||||
"contributors": [
|
||||
{"login": "example-user", "contribution": "此人本次被署名的贡献"}
|
||||
],
|
||||
"install_note": "仅在发布说明明确写有安装提醒时填写。",
|
||||
"footer_url": "github.com/NanmiCoder/cc-haha"
|
||||
}
|
||||
```
|
||||
|
||||
- `version`、`features`、`fixes`、`contributors` 必填;`extras`、`install_note`、`footer_url` 可省略。纯修复版填 `"features": []`,纯功能版填 `"fixes": []`,两者不能同时为空。没有署名贡献者时 `contributors` 为 `[]`。
|
||||
- `render --scan` 会校验版本及贡献者 ID 必须是 release note 中文部分的明确署名候选。普通提及不算署名;不要把示例 ID 写入真实结果。
|
||||
- 测试或离线排版时,贡献者可以另给 `avatar_path` 并显式传 `--allow-offline-avatars`;正式生成禁止该选项,必须实时核验公开 GitHub 账号与头像。
|
||||
- 正式图保留所有已核验、已署名的贡献者;如果名字很多,可以缩短每人的贡献概述,不能遗漏账号或用假头像填位。
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from release_poster import check_spec, chinese_release_section, scan, wrap_lines
|
||||
|
||||
|
||||
class ReleasePosterTest(unittest.TestCase):
|
||||
def test_latest_semver_and_chinese_credits(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
repo = Path(directory)
|
||||
notes = repo / "release-notes"
|
||||
notes.mkdir()
|
||||
(notes / "v0.6.9.md").write_text("# v0.6.9\n本次版本修复桌面端加载问题 **@latest-user**\n<details>\n@english-only\n", encoding="utf-8")
|
||||
(notes / "v0.6.8.md").write_text("# v0.6.8\n本次版本修复桌面端加载问题 **@old-user**\n", encoding="utf-8")
|
||||
result = scan(repo)
|
||||
self.assertEqual(result["version"], "0.6.9")
|
||||
self.assertEqual([person["login"] for person in result["mentions"]], ["latest-user"])
|
||||
older = scan(repo, "v0.6.8")
|
||||
self.assertEqual(older["version"], "0.6.8")
|
||||
self.assertEqual([person["login"] for person in older["mentions"]], ["old-user"])
|
||||
|
||||
def test_chinese_section_in_either_order_and_without_details(self):
|
||||
title = "# Claude Code Haha v0.5.4\n"
|
||||
english_first = title + "English changes here.\n<details>\n<summary>中文版本</summary>\n" + title + "这里是中文修复内容和版本更新。\n</details>"
|
||||
chinese_first = title + "这里是中文修复内容和版本更新。\n<details>\n<summary>English</summary>\n" + title + "English changes here.\n</details>"
|
||||
no_details = title + "这里是中文修复内容和版本更新。\n" + title + "English changes here.\n"
|
||||
for markdown in (english_first, chinese_first, no_details):
|
||||
selected = chinese_release_section(markdown)
|
||||
self.assertIn("这里是中文修复内容", selected)
|
||||
self.assertNotIn("English changes", selected)
|
||||
|
||||
def test_incidental_mention_is_not_contributor_credit(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
repo = Path(directory)
|
||||
notes = repo / "release-notes"
|
||||
notes.mkdir()
|
||||
(notes / "v0.2.0.md").write_text(
|
||||
"# Claude Code Haha v0.2.0\n本次修复桌面问题。\n- PR 模板默认要求 @dosubot review。\n- 修复连接问题 **@actual-author**(#42)。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = scan(repo)
|
||||
candidates = {person["login"]: person["credit_candidate"] for person in result["mentions"]}
|
||||
self.assertEqual(candidates, {"dosubot": False, "actual-author": True})
|
||||
spec = {
|
||||
"version": "0.2.0", "features": [], "fixes": [{"title": "修复", "body": "说明"}],
|
||||
"contributors": [{"login": "dosubot", "contribution": "代码贡献"}],
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "未在 release note 中文部分明确署名"):
|
||||
check_spec(spec, result)
|
||||
|
||||
def test_cannot_credit_unmentioned_account(self):
|
||||
source = {"version": "0.6.7", "mentions": [{"login": "actual-author", "credit_candidate": True}]}
|
||||
spec = {
|
||||
"version": "0.6.7",
|
||||
"features": [{"title": "功能", "body": "说明"}],
|
||||
"fixes": [{"title": "修复", "body": "说明"}],
|
||||
"contributors": [{"login": "different-author", "contribution": "贡献"}],
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "未在 release note 中文部分明确署名"):
|
||||
check_spec(spec, source)
|
||||
|
||||
def test_offline_avatar_requires_explicit_test_mode(self):
|
||||
source = {"version": "0.6.7", "mentions": [{"login": "actual-author", "credit_candidate": True}]}
|
||||
spec = {
|
||||
"version": "0.6.7", "features": [], "fixes": [{"title": "修复", "body": "说明"}],
|
||||
"contributors": [{"login": "actual-author", "contribution": "修复", "avatar_path": "fake.png"}],
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "仅供显式离线测试"):
|
||||
check_spec(spec, source)
|
||||
check_spec(spec, source, allow_offline_avatars=True)
|
||||
|
||||
def test_chinese_punctuation_does_not_start_a_wrapped_line(self):
|
||||
width = 4
|
||||
measure = len
|
||||
for value in ("这是测试;继续", "这是测试。继续", "这是测试、继续", "测试「功能描述"):
|
||||
lines = wrap_lines(value, width, measure)
|
||||
self.assertTrue(all(not line.startswith((";", "。", "、")) for line in lines[1:]))
|
||||
self.assertTrue(all(not line.endswith("「") for line in lines[:-1]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user