mcpField
P
product-update-logger

product-update-logger

github.com/varnan-tech营销与增长68744 次安装2 周前更新
代码仓库

关于 product-update-logger

product-update-logger 是面向产品团队的技能,可将已发布的改动整理为更新日志条目和一组营销文案。它可从自由文本、本地 Git 提交或 GitHub PR 收集内容。

主要功能

  1. 01写入 docs/changelog.md
  2. 02读取自由文本、Git 和 GitHub PR
  3. 03生成推文串、LinkedIn 帖文、邮件摘要和一句话简介
  4. 04按规则分类并编写更新日志

适用场景

  1. 01将产品发布记录到持续更新的日志
  2. 02根据已发布改动准备社交媒体和邮件文案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/varnan-tech/opendirectory --skill product-update-logger

安装后在 Claude Code 中输入/product-update-logger确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.env.example
README.md
evals/
evals.json
references/
changelog-format.md
content-rules.md
noise-filter.md
scripts/
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 条而填充内容。

评论

支持 Markdown