product-update-logger
告訴這個技能你發布的產品內容。它會將一篇精美的變更日誌條目寫入 docs/changelog.md(一個持續更新的日誌,最新條目在最前面),並同時產生一組內容套件:推文串、LinkedIn 貼文、Email 片段以及一句話摘要。
輸入來源:來自你訊息的自由文字、從本地儲存庫自動讀取的 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 "Changelog: $(ls docs/changelog.md 2>/dev/null && echo '已存在' || echo '將會建立')"
注意 Git 是否可用,以及變更日誌是否已存在。這會影響版本標籤的格式。
步驟 2:解析輸入
從對話中收集以下資訊:
items-- 你交付內容的自由文字描述(若有多項,請以管道符分隔)。如果 Git 可用,此項可選。since-- 往回查看的天數。預設值:7。使用者可以說「過去兩週」(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": "", # 填入:以管道符分隔的自由文字,若無則為 ""
"since": 7, # 填入:整數天數
"repo": "", # 填入:"owner/repo" 或 ""
"version_label": "" # 填入:"" 表示自動(基於日期),或自訂字串
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"Since: {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 儲存庫,或使用 repo: owner/repo 加上 GITHUB_TOKEN 來添加 GitHub 儲存庫。」
將項目列表顯示給使用者,並詢問:「這些是我找到的項目。在我寫入變更日誌之前,有任何要添加或刪除的嗎?」
等待確認或編輯。如果使用者說「看起來很好」「請繼續」或未做任何更改,則繼續。如果使用者添加或刪除了項目,請在步驟 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":根據變更進行版本遞增(僅修復則補丁版本,有任何新功能則次版本) - 預設值:使用今天的日期的
[月 日, 年] 該週
將條目寫入 /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 個字元以內
- 無主題標籤。無破折號。使用主動語態。
LinkedIn 貼文:
- 無 Markdown(星號在 LinkedIn 上會顯示為文字)
- 無主題標籤
- 創辦人口吻:「我們交付了」,而非「我們很興奮地宣布」
- 短段落(每段 1-2 句),段落間空行
- 以問題或觀察結尾,而非行動呼籲
- 總字數 150-400
Email 片段:
- 主旨:「本週交付內容:[最大項目] + [額外 1 項]」
- 內文: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:自我品質檢查
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 = ['強大', '穩健', '無縫', '創新', '顛覆性',
'精簡', '槓桿', '轉型', '革命', '興奮地宣布',
'愉快地宣布', '我們很激動', '尖端', '同級最佳',
'世界級', '解鎖', '令人愉悅']
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:LinkedIn 無主題標籤
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('失敗:LinkedIn 貼文中發現主題標籤')
fails += 1
else:
print('通過:LinkedIn 中無主題標籤')
# 檢查 5:LinkedIn 中無 Markdown
if '**' in li or '__' in li:
print('失敗:LinkedIn 中的 Markdown 格式(會顯示為文字星號)')
fails += 1
else:
print('通過:LinkedIn 中無 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 += [
"## LinkedIn 貼文",
"",
content.get('linkedin_post', ''),
"",
"## Email 片段",
"",
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 秒。
### 修正
- **CSV 匯出** -- 匯出不再遺漏最後一行。
2. 一句話摘要(純文字) 3. 推文串(格式化為清單) 4. LinkedIn 貼文(純文字,無 Markdown) 5. Email 片段(主旨 + 內文)