产品更新日志
告诉此技能你的产品发布了什么。它会为 docs/changelog.md(一个持续记录、最新条目在最前的日志文件)撰写一条精炼的变更日志条目,并同时生成一个内容包:推文、领英帖子、邮件摘要和一句话简介。
输入来源:来自你消息的任意文本、本地仓库的Git提交自动读取,或提供仓库时的GitHub PR。支持任意组合。
参考文件
每次运行前请阅读以下文件:
cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md
步骤 1:环境检查
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-未设置 -- GitHub PR 获取已禁用}"
echo "Git: $(git rev-parse --is-inside-work-tree 2>/dev/null && echo '检测到仓库' || echo '不是 Git 仓库')"
echo "变更日志: $(ls docs/changelog.md 2>/dev/null && echo '已存在' || echo '将创建')"
注意 Git 是否可用以及变更日志是否已存在。这决定了版本标签的格式。
步骤 2:解析输入
从对话中收集:
items-- 发布内容的自由文本描述(多个项用管道符分隔)。如果 Git 可用,则为可选。since-- 回溯的天数。默认值:7。用户可以说“过去两周”(14)或“自上次发布以来”。repo-- 用于获取 PR 的 GitHub “所有者/仓库”。可选。version_label-- 自定义标签,如“v2.1.0”或“速度更新”。可选;默认为基于日期。
如果用户未提及任何项目且没有 Git 仓库: 询问“你发布了什么?列出功能、修复或改进——每行一个。”
如果 Git 可用且用户未指定具体内容: 继续执行步骤 3 的 Git 自动读取。向用户展示发现的内容,并在转换前请求确认。
写入解析后的输入:
python3 << 'PYEOF'
import json, os, re
inp = {
"items": "", # 填写:管道符分隔的自由文本,若无则留空
"since": 7, # 填写:整数天数
"repo": "", # 填写:“所有者/仓库” 或 “”
"version_label": "" # 填写:“” 表示自动(基于日期),或自定义字符串
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"回溯天数: {inp['since']} 天")
print(f"自由文本项目: {inp['items'] or '无(将使用 Git/GitHub)'}")
print(f"GitHub 仓库: {inp['repo'] or '无'}")
print(f"版本标签: {inp['version_label'] or '自动(基于日期)'}")
PYEOF
步骤 3:运行收集脚本
ls scripts/gather.py 2>/dev/null && echo "脚本已找到" || echo "错误:scripts/gather.py 未找到"
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/gather.py \
--since "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['since'])")" \
--repo "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['repo'])")" \
--items "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['items'])")" \
--output /tmp/pul-raw.json
验证输出:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(f'找到的项目数量: {d[\"total_items\"]}')
print(f'过滤的噪声数量: {d[\"noise_filtered\"]}')
print(f'Git 可用: {d[\"git_available\"]}')
print(f'GitHub 可用: {d[\"github_available\"]}')
print(f'来源: git={sum(1 for i in d[\"items\"] if i[\"source\"]==\"git_commit\")}, '
f'prs={sum(1 for i in d[\"items\"] if i[\"source\"]==\"github_pr\")}, '
f'text={sum(1 for i in d[\"items\"] if i[\"source\"]==\"free_text\")}')
print()
print('项目:')
for item in d['items']:
print(f' [{item[\"source\"]}] {item[\"subject\"]}')
"
如果 total_items == 0: 停止。告知用户:“未找到已发布的项目。请描述你发布了什么,指向一个包含最近提交的 Git 仓库,或添加一个 GitHub 仓库(repo: owner/repo)和 GITHUB_TOKEN。”
向用户展示项目列表并询问:“这些是我找到的项目。在我写入变更日志之前,需要添加或删除什么吗?”
等待用户确认或编辑。如果用户说“看起来不错”、“继续”,或未做任何更改,则继续执行。如果用户添加或删除了项目,则在步骤 4 之前相应更新 /tmp/pul-raw.json。
步骤 4:生成变更日志条目
打印项目以供参考:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(json.dumps(d['items'], indent=2))
print()
print(f'现有变更日志格式: {d[\"existing_changelog\"][\"format\"]}')
print(f'最后一个标签: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'今天日期: {d[\"date\"]}')
"
AI 指令: 将每个原始项目从技术语言转换为面向用户的收益语言。遵循 references/changelog-format.md 中的转换规则和示例。
规则:
- 不要编造成果或指标。 “快 40%” 必须来自源数据。如果提交或 PR 中没有数字,就不要添加。
- 使用过去时: “添加了”、“修复了”、“改进了”——而不是“添加”、“修复”
- 为每个项目分配恰好一个类别: 新功能、改进、修复、或底层优化
- 底层优化: 仅在与开发者相关时包含(API 更改、破坏性更改)。省略空段落。
- 省略 任何映射到:测试更改、CI 更改、仅文档提交的内容
确定版本标签:
- 如果用户指定了:直接使用
- 如果
existing_changelog.format == "semver":基于更改递增(仅修复为 patch,任何新功能为 minor) - 默认:使用今天的日期,格式为
Week of [Month Day, Year]
将条目写入 /tmp/pul-entry.json:
{
"label": "2026年4月23日周",
"date": "2026-04-23",
"new": [
{"title": "深色模式", "description": "在设置 > 外观中切换。适用于所有视图。"}
],
"improved": [
{"title": "API 响应时间", "description": "平均快 40%。仪表板现在可在 1 秒内加载。"}
],
"fixed": [
{"title": "CSV 导出", "description": "导出不再丢失最后一行。"}
],
"under_the_hood": []
}
验证条目:
python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
e = json.load(f)
print(f'标签: {e[\"label\"]}')
total = 0
for cat in ['new', 'improved', 'fixed', 'under_the_hood']:
items = e.get(cat, [])
if items:
print(f'{cat.replace(\"_\", \" \").title()} ({len(items)}):')
for item in items:
print(f' - {item[\"title\"]}: {item[\"description\"]}')
total += len(items)
print(f'总计: {total} 个项目')
"
步骤 5:生成内容包
使用步骤 4 中的变更日志条目,生成全部四个内容片段。严格遵循 references/content-rules.md。
一句话摘要(最多 20 词):一句话涵盖最重大的 1-2 个项目。使用平实语言,无术语。
推文线程(3-5 条推文):
- 推文 1:钩子——“本周我们发布了 [N] 项内容。”或从最大的功能开始
- 推文 2-N:每条推文一个项目,最多 1-2 句话
- 最后一条推文:“变更日志:[链接]”或“下周更多。”(可选)
- 每条推文严格不超过 280 个字符
- 无标签。无破折号。主动语态。
领英帖子:
- 无 Markdown(LinkedIn 上星号会作为文字显示)
- 无标签
- 创始人语气:“我们发布了”,而不是“我们很高兴地宣布”
- 短段落(每段 1-2 句话),段落之间用空行分隔
- 以问题或观察结尾,而非行动号召
- 总计 150-400 字
邮件摘要:
- 主题:“本周发布内容:[最大项目] + [再一个]”
- 正文:50-100 词。“以下是本周发布的内容:” 然后使用项目符号列表。
写入 /tmp/pul-content.json:
{
"one_liner": "深色模式、更快的 API 和修复的导出错误。",
"tweet_thread": [
"本周我们发布了 3 项内容。",
"深色模式已上线。在设置 > 外观中切换。适用于所有地方。",
"API 响应时间现在快了 40%。仪表板在一秒内加载。",
"修复了:CSV 导出会丢失最后一行。现在已修复。",
"变更日志:[链接]"
],
"linkedin_post": "本周我们发布了 3 项更新。\n\n深色模式已上线。在设置的“外观”下切换。它适用于每一个视图。\n\nAPI 响应时间平均快了 40%。现在大多数用户的仪表板在一秒内加载。\n\n我们还修复了一个错误,即 CSV 导出会悄悄丢弃最后一行。如果你遇到此问题并停止了导出,现在值得再试一次。\n\n你一直期待的功能是什么?",
"email_snippet": {
"subject": "本周发布内容:深色模式 + 更快的 API",
"body": "以下是本周我们发布的内容:\n\n- 深色模式:在设置 > 外观中切换\n- API 响应时间:快 40%,仪表板在 1 秒内加载\n- 修复:CSV 导出不再丢失最后一行\n\n完整变更日志如下。"
}
}
步骤 6:自我 QA
python3 -c "
import json, re
with open('/tmp/pul-raw.json') as f:
raw = json.load(f)
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
full_text = json.dumps(entry) + json.dumps(content)
fails = 0
# 检查 1:无破折号
if chr(8212) in full_text:
print('失败:发现破折号——请替换为连字符')
fails += 1
else:
print('通过:无破折号')
# 检查 2:禁用词汇
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize', 'excited to announce',
'pleased to announce', 'we are thrilled', 'cutting-edge', 'best-in-class',
'world-class', 'unlock', 'delightful']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'失败:发现禁用词汇:{found}')
fails += 1
else:
print('通过:无禁用词汇')
# 检查 3:推文长度
thread = content.get('tweet_thread', [])
long_tweets = [(i+1, len(t)) for i, t in enumerate(thread) if len(t) > 280]
if long_tweets:
print(f'失败:超过 280 字符的推文:{long_tweets}')
fails += 1
else:
print(f'通过:所有 {len(thread)} 条推文都小于 280 字符')
# 检查 4:领英帖子无标签
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('失败:领英帖子中发现标签')
fails += 1
else:
print('通过:领英帖子无标签')
# 检查 5:领英帖子无 Markdown
if '**' in li or '__' in li:
print('失败:领英帖子中有 Markdown 格式(星号会显示为文字)')
fails += 1
else:
print('通过:领英帖子无 Markdown')
# 检查 6:一句话摘要字数统计
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
print(f'失败:一句话摘要为 {word_count} 个词(最多 20)')
fails += 1
else:
print(f'通过:一句话摘要为 {word_count} 个词')
# 检查 7:项目数量
entry_items = (len(entry.get('new', [])) + len(entry.get('improved', [])) +
len(entry.get('fixed', [])) + len(entry.get('under_the_hood', [])))
raw_total = raw['total_items']
print(f'信息:{entry_items} 个变更日志项目来自 {raw_total} 个原始项目')
print()
print(f'结果:{\"通过\" if fails == 0 else f\"失败 ({fails} 个问题)\"}')
"
如果任何检查失败: 在继续步骤 7 之前修复相关临时文件中的问题。修复后重新运行检查。
步骤 7:追加到变更日志并保存内容
python3 << 'PYEOF'
import json, os, re
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
# 构建新的变更日志部分
lines = [f"## {entry['label']}", ""]
CAT_HEADERS = {
"new": "### 新功能",
"improved": "### 改进",
"fixed": "### 修复",
"under_the_hood": "### 底层优化",
}
for cat, header in CAT_HEADERS.items():
items = entry.get(cat, [])
if items:
lines.append(header)
for item in items:
lines.append(f"- **{item['title']}** -- {item['description']}")
lines.append("")
lines.append("---")
lines.append("")
new_section = "\n".join(lines)
# 前置添加到 docs/changelog.md
os.makedirs("docs", exist_ok=True)
changelog_path = "docs/changelog.md"
if os.path.exists(changelog_path):
existing = open(changelog_path).read()
# 插入到顶级标题之后(如果有的话)或最顶部
if existing.startswith("# "):
end_of_heading = existing.index("\n") + 1
updated = existing[:end_of_heading] + "\n" + new_section + existing[end_of_heading:]
else:
updated = new_section + existing
else:
updated = "# 变更日志\n\n" + new_section
with open(changelog_path, "w") as f:
f.write(updated)
print(f"变更日志已更新:{changelog_path}")
# 保存内容包
date = entry['date']
content_dir = "docs/product-updates"
os.makedirs(content_dir, exist_ok=True)
content_path = f"{content_dir}/{date}-content.md"
content_lines = [
f"# 内容包:{entry['label']}",
"",
"## 一句话摘要",
content.get('one_liner', ''),
"",
"## 推文线程",
"",
]
thread = content.get('tweet_thread', [])
for i, tweet in enumerate(thread, 1):
content_lines.append(f"[{i}/{len(thread)}] {tweet}")
content_lines.append("")
content_lines += [
"## 领英帖子",
"",
content.get('linkedin_post', ''),
"",
"## 邮件摘要",
"",
f"主题:{content.get('email_snippet', {}).get('subject', '')}",
"",
content.get('email_snippet', {}).get('body', ''),
"",
]
with open(content_path, "w") as f:
f.write("\n".join(content_lines))
print(f"内容包:{content_path}")
PYEOF
步骤 8:清理并展示
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "完成。"
按以下顺序向用户展示:
1. 变更日志条目(格式化的 Markdown,而非原始 JSON):
## 2026年4月23日周
### 新功能
- **深色模式** -- 在设置 > 外观中切换。适用于所有视图。
### 改进
- **API 响应时间** -- 平均快 40%。仪表板现在可在 1 秒内加载…