关于 product-update-logger
product-update-logger 是面向产品团队的技能,可将已发布的改动整理为更新日志条目和一组营销文案。它可从自由文本、本地 Git 提交或 GitHub PR 收集内容。
主要功能
- 01写入 docs/changelog.md
- 02读取自由文本、Git 和 GitHub PR
- 03生成推文串、LinkedIn 帖文、邮件摘要和一句话简介
- 04按规则分类并编写更新日志
适用场景
- 01将产品发布记录到持续更新的日志
- 02根据已发布改动准备社交媒体和邮件文案
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill product-update-logger安装后在 Claude Code 中输入/product-update-logger确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.env.exampleREADME.mdevals/evals.jsonreferences/changelog-format.mdcontent-rules.mdnoise-filter.mdscripts/gather.py详细介绍
product-update-logger
告诉这个技能你的产品发布了什么。它会将精心润色的更新日志条目写入 docs/changelog.md(持续维护的日志,最新条目置顶),同时生成一套内容:推文串、LinkedIn 帖子、邮件片段和一句话简介。
输入来源:消息中的自由文本、本地仓库中自动读取的 git 提交,或你提供仓库后获取的 GitHub PR。可任意组合。
参考文件
每次运行前读取这些文件:
cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md
步骤 1:检查设置
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- GitHub PR fetching disabled}"
echo "Git: $(git rev-parse --is-inside-work-tree 2>/dev/null && echo 'repo detected' || echo 'not a git repo')"
echo "Changelog: $(ls docs/changelog.md 2>/dev/null && echo 'exists' || echo 'will be created')"
记录 git 是否可用,以及是否已存在更新日志。这将决定版本标签的格式。
步骤 2:解析输入
从对话中收集:
items-- 对已发布内容的自由文本描述(多项时以竖线分隔)。如果 git 可用,则为可选项。since-- 向前回溯多少天。默认值:7。用户可能会说“过去 2 周”(14 天)或“自上次发布以来”。repo-- 用于获取 PR 的 GitHub“owner/repo”。可选。version_label-- 自定义标签,例如“v2.1.0”或“The Speed Update”。可选;默认按日期生成。
如果用户没有提及 items,且没有 git 仓库: 询问:“你发布了什么?请逐行列出功能、修复或改进。”
如果 git 可用,且用户没有提供具体内容: 在步骤 3 中通过 git 自动读取。向用户展示找到的内容,并在转换前征求确认。
写入解析后的输入:
python3 << 'PYEOF'
import json, os, re
inp = {
"items": "", # FILL: pipe-separated free text, or "" if none
"since": 7, # FILL: integer days
"repo": "", # FILL: "owner/repo" or ""
"version_label": "" # FILL: "" means auto (date-based), or custom string
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"Since: {inp['since']} days")
print(f"Free text items: {inp['items'] or 'none (will use git/GitHub)'}")
print(f"GitHub repo: {inp['repo'] or 'none'}")
print(f"Version label: {inp['version_label'] or 'auto (date-based)'}")
PYEOF
步骤 3:运行收集脚本
ls scripts/gather.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/gather.py not found"
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'Items found: {d[\"total_items\"]}')
print(f'Noise filtered: {d[\"noise_filtered\"]}')
print(f'Git available: {d[\"git_available\"]}')
print(f'GitHub available: {d[\"github_available\"]}')
print(f'Sources: 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('Items:')
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'Existing changelog format: {d[\"existing_changelog\"][\"format\"]}')
print(f'Last label: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'Today: {d[\"date\"]}')
"
AI 指令: 将每个原始项目从技术语言转换为面向用户的收益表述。遵循 references/changelog-format.md 中的转换规则和示例。
规则:
- 不要编造结果或指标。“快了 40%”必须有源数据支持。如果提交或 PR 中没有数字,就不要添加。
- 使用过去时:“已添加”“已修复”“已改进”——不要使用“添加”“修复”
- 为每个项目恰好分配一个类别: 新增、改进、修复或底层更新
- 底层更新: 仅在与开发者相关时包含(API 变更、重大变更)。省略空类别。
- 省略归属于测试变更、CI 变更或仅文档变更的内容
确定版本标签:
- 如果用户指定了标签:原样使用
- 如果
existing_changelog.format == "semver":根据变更递增版本号(仅修复则递增修订号;有任何新功能则递增次版本号) - 默认值:使用当天日期,格式为
Week of [Month Day, Year]
将条目写入 /tmp/pul-entry.json:
{
"label": "Week of April 23, 2026",
"date": "2026-04-23",
"new": [
{"title": "Dark mode", "description": "Toggle in Settings > Appearance. Works across all views."}
],
"improved": [
{"title": "API response time", "description": "40% faster on average. Dashboard now loads in under 1 second."}
],
"fixed": [
{"title": "CSV export", "description": "Exports no longer drop the last row."}
],
"under_the_hood": []
}
验证条目:
python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
e = json.load(f)
print(f'Label: {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: {total} items')
"
第 5 步:生成内容包
使用第 4 步中的变更日志条目,生成全部四项内容。严格遵循 references/content-rules.md。
一句话简介(最多 20 个词):用一句话概括最重要的 1–2 项。语言通俗,不用行话。
推文串(3–5 条推文):
- 推文 1:开场——“We shipped [N] things this week.”,或直接介绍最重要的功能
- 推文 2 到 N:每条推文介绍一项,最多 1–2 句话
- 最后一条推文:“Changelog: [link]”或“More next week.”(可选)
- 每条推文严格少于 280 个字符
- 不加话题标签。不用 em dash。使用主动语态。
LinkedIn 帖子:
- 不使用 Markdown(星号在 LinkedIn 上会原样显示)
- 不加话题标签
- 使用创始人口吻:“We shipped”,而不是“We are excited to announce”
- 段落简短(每段 1–2 句话),段落之间空一行
- 结尾用问题或观察,不用行动号召
- 总字数为 150–400 词
邮件片段:
- 主题:“What shipped this week: [biggest item] + [1 more]”
- 正文:50–100 词。以“Here's what we shipped this week:”开头,然后列出要点。
写入 /tmp/pul-content.json:
{
"one_liner": "Dark mode, faster API, and a fixed export bug.",
"tweet_thread": [
"We shipped 3 things this week.",
"Dark mode is live. Toggle it in Settings > Appearance. Works everywhere.",
"API response time is now 40% faster. Dashboard loads in under a second.",
"Fixed: CSV exports were dropping the last row. That's gone now.",
"Changelog: [link]"
],
"linkedin_post": "We shipped 3 updates this week.\n\nDark mode is live. Toggle it in Settings under Appearance. It works across every view.\n\nAPI response time is 40% faster on average. The dashboard now loads in under a second for most users.\n\nWe also fixed a bug where CSV exports were silently dropping the last row. If you hit this and stopped exporting, it's worth trying again.\n\nWhat feature have you been waiting for?",
"email_snippet": {
"subject": "What shipped this week: dark mode + faster API",
"body": "Here's what we shipped this week:\n\n- Dark mode: toggle in Settings > Appearance\n- API response time: 40% faster, dashboard loads under 1 second\n- Fixed: CSV exports no longer drop the last row\n\nFull changelog below."
}
}
第 6 步:自检
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
# Check 1: No em dashes
if chr(8212) in full_text:
print('FAIL: em dash found -- replace with hyphen')
fails += 1
else:
print('PASS: no em dashes')
# Check 2: Banned words
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'FAIL: banned words found: {found}')
fails += 1
else:
print('PASS: no banned words')
# Check 3: Tweet length
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'FAIL: tweets over 280 chars: {long_tweets}')
fails += 1
else:
print(f'PASS: all {len(thread)} tweets under 280 chars')
# Check 4: LinkedIn no hashtags
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('FAIL: hashtags found in LinkedIn post')
fails += 1
else:
print('PASS: no hashtags in LinkedIn')
# Check 5: No markdown in LinkedIn
if '**' in li or '__' in li:
print('FAIL: markdown formatting in LinkedIn (renders as literal asterisks)')
fails += 1
else:
print('PASS: no markdown in LinkedIn')
# Check 6: One-liner word count
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
print(f'FAIL: one-liner is {word_count} words (max 20)')
fails += 1
else:
print(f'PASS: one-liner is {word_count} words')
# Check 7: Item count
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'INFO: {entry_items} changelog items from {raw_total} raw items')
print()
print(f'Result: {\"PASS\" if fails == 0 else f\"FAIL ({fails} issues)\"}')
"
**如果任何一项检查未通过:**继续第 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)
# Build the new changelog section
lines = [f"## {entry['label']}", ""]
CAT_HEADERS = {
"new": "### New",
"improved": "### Improved",
"fixed": "### Fixed",
"under_the_hood": "### 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)
# Prepend to 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()
# Insert after the top-level heading (if any) or at the very top
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 = "# Changelog\n\n" + new_section
with open(changelog_path, "w") as f:
f.write(updated)
print(f"Changelog updated: {changelog_path}")
# Save content package
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"# Content Package: {entry['label']}",
"",
"## One-liner",
content.get('one_liner', ''),
"",
"## Tweet Thread",
"",
]
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 += [
"## LinkedIn Post",
"",
content.get('linkedin_post', ''),
"",
"## Email Snippet",
"",
f"Subject: {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 package: {content_path}")
PYEOF
第 8 步:清理并呈现
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "Done."
按以下顺序呈现给用户:
1. 变更日志条目(格式化后的 Markdown,而非原始 JSON):
## Week of April 23, 2026
### New
- **Dark mode** -- Toggle in Settings > Appearance. Works across all views.
### Improved
- **API response time** -- 40% faster on average. Dashboard now loads in under 1 second.
### Fixed
- **CSV export** -- Exports no longer drop the last row.
2. 内容包:
- 一句话简介:
[text] - 推文串:按编号列出推文
- LinkedIn 帖子:完整文本
- 邮件片段:主题行 + 正文
3. 保存的文件:
docs/changelog.md-- 已更新(新条目已置顶)docs/product-updates/[date]-content.md-- 已保存完整内容包
常见错误
| Agent 容易犯的错误 | 错误原因 |
|---|---|
| 编造结果或指标 | 每项说法都必须来自原始条目。“快了 40%”必须来自提交信息或 PR 正文。如果没有提供数字,就不要添加。 |
| 写“We are excited to announce” | 禁止使用。改用“We shipped”、“[Feature] is now live”,或直接陈述事实。 |
| 在 LinkedIn 上使用 Markdown 粗体(**) | LinkedIn 会将 ** 原样显示为星号。只能使用纯文本。 |
| 在 LinkedIn 或推文中添加话题标签 | 本技能从不使用话题标签。 |
| 把所有条目都放进“New” | Bug 应归入 Fixed,速度提升应归入 Improved。错误分类会削弱变更日志。 |
| 跳过第 3 步中的确认环节 | 始终先展示条目列表并请用户确认,然后再进行转换。这样可以避免处理错误分支的提交或过时条目。 |
| 加入空的“Under the hood”部分 | 如果没有内容就省略。宁可不说,也不要制造噪音。 |
| 把多个条目合并到一条推文中 | 每条推文介绍一项。具体胜于面面俱到。 |
| 用填充内容凑推文 | 如果只有一项真实内容,就写 2 条推文。不要为了凑够 5 条而填充内容。 |