产品更新日志
一封定期邮件,像真人一样告诉用户产品发生了什么变化,安全发送,然后用于社交媒体帖子。邮件是每一期的唯一信源;所有其他渠道都基于它精简而成。
该技能如何运行
- 一次性询问,提前完成。 首次运行时(或配置缺失、某必需值为空时),一次性询问所有设置问题,写入
config.yaml,然后继续执行,不再提问。 - 然后自动执行所有操作。 在
autonomy.mode: autonomous(默认模式)下,你会选择内容、起草、lint、自我审查、截图、发送测试、读取测试结果、构建受众、执行测试性发送、向所有人发送、验证、拉取退订、编写并发布社交媒体帖子,以及归档。无需审批步骤。每个向外操作都先通过自我审查关卡。 - 只有涉及金钱才需审批。 任何需要花钱的操作都会等待用户在聊天中明确同意,在任何模式下都如此:发送量超过提供商计划包含的配额(
provider.monthly_quota)、升级提供商计划、购买域名、付费工具或许可(例如付费的Remotion公司许可)。其他操作无需同意。 - 硬停不等于审批。 硬停仅中止该操作。完成其他安全可执行的操作,然后报告。参见硬停。
- 最后生成报告。 一个摘要:已发出的主题和完整文本、计数(已排队、已发送、失败、已排除、已退订)、送达样本、测试回读结果、每个帖子及其链接,以及任何被跳过的内容及原因。
review模式 保留旧有的逐操作审批关卡。参见审查模式。
每个脚本默认都是测试性运行。真正的发送需要显式标志(--send、--yes-send-to-everyone、--yes-send-broadcast)。在自主模式下,你自行传递这些标志,且仅在匹配的自我审查关卡通过之后。切勿在脚本以外发送广告邮件,因为脚本是强制执行退订的保证。
步骤0:加载配置
- 在当前项目根目录查找
config.yaml,然后在本技能文件夹(或$CHANGELOG_CONFIG)中查找。 - 如果没有,将
config.example.yaml复制到项目根目录的config.yaml,并在一条消息中询问:- 产品名称、网站、仪表板和文档URL
- 发件人姓名和头衔、发件地址、回复地址
- 测试收件人(姓名和地址)
- 提供商:Resend受众ID、保存API密钥的环境变量、以及每月包含邮件数(如知道,
provider.monthly_quota,可选) - 退订模式(
broadcast或own_endpoint) - 图片托管:截图存储位置及其公共基础URL
- 可选:发送频率、品牌链接颜色、以及每个社交渠道的发布方式(
social.post_via) 不要猜测任何值。将答案写入config.yaml,然后继续。
- 任何仍设置为大写占位符或
yourproduct.example地址的值都视为缺失。 - 读取
autonomy.mode(默认autonomous)、voice.notes和voice.banned_phrases。语音设置覆盖参考中的默认值。 - 密钥从不出现在配置或聊天中。API密钥和退订签名密钥存在于环境变量中。
后续版本无需询问。唯一可选的问题是:“本次是否有Git之外发布的(定价、合作、文档)?” 如果用户不在,则跳过,从Git和发布说明中工作。
步骤1:选择内容
首先阅读references/voice-and-format.md。
- 在
paths.issues_dir中找到最后一期(文件夹命名为NNN-YYYY-MM-DD-slug)。最新一期作为下一期的模板。 - 收集该日期后的候选内容:
git log --since=<date> --no-merges --oneline、已合并的Pull Request、发布说明,以及用户提及的任何内容。 - 只保留用户可见的变更。3到5项,按对读者的有用性排序。以最有用的变更开头,即大多数读者今天能立即使用的。
- 对照线上产品和网站检查每项声明。切勿编造功能、数字或日期。如果某项声明无法验证,则删除该项。
- 将列表和顺序写入该版本的
NOTES.md,然后继续。
步骤2:起草邮件
创建paths.issues_dir/NNN-YYYY-MM-DD-slug/并写入:
subject.txt:以利益为导向、产品优先(如“您现在可以将报告导出到Sheets”)。切勿使用“Changelog #N”。email.html:基于templates/email.html(或上一期),填充每个来自配置和内容的[[...]]标记。email.txt:与HTML相同的内容,纯文本,带有裸URL。
保留{{NAME}}和{{UNSUB_URL}}作为文字占位符;发送脚本会在收件人级别填充它们。
核心规则(详细信息见参考文档):
Hi {{NAME}},,然后是从配置中获取的一行开头语,直接进入主导项。- 每一项:变化了什么,为什么重要,用流畅的句子表达。无标记列表。
- 短句,简单词,使用“您现在可以”而非“我们构建了”。
- 每段最多一个粗体强调。
- 每项最多一张截图;主导项始终配有一张,并链接到该功能。
- 极简HTML样式:600px宽包裹器,系统字体,内联样式,无按钮或横幅。
- 不使用“激动地宣布”及其他禁用短语。如果启用了
voice.no_dashes,则不使用em破折号或en破折号。 - 可选的非正式附注(“顺便说一句,...”)。
- 来自配置的签名。退订行是最后一行,小字号且颜色柔和。
步骤3:截图
遵循references/screenshots.md:在演示账户上搭建真实状态(无客户数据),使用Playwright以2倍分辨率截取,裁剪到面板范围,命名文件为<issue>-<slug>.png,托管到paths.image_base_url,并在任何发送前确认每个URL返回200。
步骤4:Lint和自我审查(关卡1:文案)
python3 scripts/lint_email.py ISSUE_DIR --check-links
修复所有失败项,然后逐项检查清单。修复并重新运行,直到每个检查点通过。
-
lint_email.py --check-links退出码为0。 - 以忙碌客户的身份从上到下阅读:最有用的变更在5秒内清晰可见。
- 每一项都是用户今天可以看到或使用的。
- 每项声明、数字和链接都与线上产品和网站一致。
- 内容不像新闻稿;听起来像是发件人在说话。
- 主题不超过60个字符,以利益为导向,不使用“Changelog #N”。
- 每张图片都有替代文本,且托管URL返回200(或在步骤7之前部署)。
步骤5:测试发送和回读(关卡2:已送达副本)
python3 scripts/send_test.py ISSUE_DIR --send # 仅发送给测试收件人,加[TEST]前缀
python3 scripts/send_test.py ISSUE_DIR --readback # 一分钟后
测试副本仅发送给test_recipients,如果图片尚未部署,则内联嵌入。--readback从提供商API拉取每封已送达邮件,检查最新事件、主题、渲染的问候语和退订行,并确保没有存活占位符。同时,亲自阅读返回的HTML一次。如有任何问题,修复后重新lint、重新测试。
在own_endpoint模式下,打开测试副本中的退订链接,确认退订已记录,然后重新订阅测试地址。
步骤6:构建受众
详细信息见references/sending.md。
- 从用户数据库新鲜构建
ISSUE_DIR/audience.jsonl(每行{"email", "name"})。切勿重用旧列表。 - 从退订表构建
ISSUE_DIR/unsubs.txt。在广播模式下,首先运行python3 scripts/broadcast_unsubs.py --out <issues_dir>/unsubs.txt并将其复制进来。 - 创始人、测试和演示账户在配置
exclude中;它们改为接收测试副本。
步骤7:向所有人发送(关卡3:受众)
根据unsubscribe.mode选择路径。先运行测试性发送并检查计数。
广播模式(提供商托管退订):
python3 scripts/broadcast_sync.py ISSUE_DIR/audience.jsonl # 计划
python3 scripts/broadcast_sync.py ISSUE_DIR/audience.jsonl --apply # 更新联系人
python3 scripts/broadcast_create.py ISSUE_DIR --create # 仅草稿
python3 scripts/broadcast_create.py --send BROADCAST_ID # 显示收件人数量,拒绝
python3 scripts/broadcast_create.py --send BROADCAST_ID --yes-send-broadcast
自有端点模式(签名链接到自己的页面):
python3 scripts/blast.py ISSUE_DIR # 测试运行并显示计数
python3 scripts/blast.py ISSUE_DIR --send --yes-send-to-everyone
受众检查清单,在发送标志前必须全部通过:
- 关卡1和2在此确切副本上通过(自回读以来无任何更改)。
- 退订列表在此次运行中已拉取或构建,且如果产品曾有退订,则非空。
- 测试运行计数合理:队列数量接近用户数减去退订数减去排除数;与上一期的
NOTES.md相比无意外跳跃。 - 广播模式:
broadcast_sync.py报告无意外过期联系人。 - 每个托管图片URL返回200。
- 配额:如果设置了
provider.monthly_quota,则传递--sent-this-month N(累计本月各期NOTES.md中的已发送数量,以及同一计划上的其他邮件)。如果脚本显示发送将超过配额,停止并询问用户;仅在用户明确同意后传递--yes-over-quota。
逐一发送是可恢复的:如果停止,重新运行同一命令,它会跳过已发送的地址。
步骤8:验证和同步
- 逐一发送:
python3 scripts/verify_delivery.py ISSUE_DIR --sample 40。广播:通过提供商API或仪表板执行GET /broadcasts/{id}。 - 一天后拉取退订(
broadcast_unsubs.py)。如果用户在自己的数据库中维护退订,并且你有写权限,则导入它们;否则在报告中说明。
步骤9:社交媒体帖子
根据最终邮件编写,遵循references/social.md:
- X线程:钩子推文以
social.hook_prefix开头,列出所有项目,无链接,最强截图。然后每个项目一个编号推文,包含其链接和截图。加上一条创始人的单行个人引用推文。 - LinkedIn:一个帖子,钩子第一行,每个项目一段短文,纯文本,图片库。
- 社区(Discord或Slack):开头行,每个项目一行带裸链接,上线后的线程链接。
最多4到5项,无政策通知,声明与线上网站一致。在social.md上运行相同的禁用短语和破折号检查(lint_email.py会读取它)。然后,对于social.post_via中配置了工具的每个渠道,自行发布并记录帖子URL。对于没有工具的渠道,向用户提供最终文本和图片。创始人的引用推文来自个人账户,因此除非为该账户配置了工具,否则将其交给用户。
步骤10:视频(可选,仅按请求)
如果用户要求,遵循references/video.md:15到25秒的动态片段,使用版本截图,无标题幻灯片,每个项目大标题,截图主导,结尾带产品URL。对于超过一定团队规模,Remotion需要付费公司许可;这是一个关于钱的询问。
步骤11:归档和报告
在版本文件夹中保留:subject.txt、email.html、email.txt、截图、social.md、test_results.jsonl,以及包含项目列表、发送日期、使用的路径、受众规模、已发送和失败计数、广播ID、帖子URL和一天后的退订数量的NOTES.md。如果产品有公开的网络更新日志页面(product.changelog_page)且其源代码在此仓库中,则也将该版本添加到那里。当用户更正语音或格式时,将经验添加到配置的voice.notes中,以便下一期从中开始。然后发送开头描述的那个摘要。
硬停
停止该操作,完成所有其他安全操作,并报告停止的内容和原因。切勿绕过停止。
- 无法修复的Lint失败。
- 无法修复的损坏链接或图片(部署或更正后仍不是200)。
- 测试回读失败且无法找到原因。
- 退出过滤失败:无法拉取或构建退订列表、受众查询出错、或脚本因
unsubs.txt而拒绝。切勿在没有退订的情况下发送。 - 发送过程中提供商错误超过阈值(
blast.py在达到provider.max_failures或provider.max_failure_rate时退出码为2)。报告错误;一旦修复即可恢复发送。 - 任何会超过配额的发送、计划升级或其他支出:询问用户(金钱规则)。
- 登录、2FA或重新认证提示、CAPTCHA、平台警告或发布工具上的速率限制通知、支付或账单表单。
- 任何可能违反退订或反垃圾邮件法律的行为(缺少退订行、重新订阅已退订的人)。
- 无法验证的声明。放弃而非猜测。
审查模式
当autonomy.mode: review时,三个关卡还需要用户的确认:
- 草稿关卡。 在聊天中显示主题、邮件和项目列表。在用户批准文案之前,不发送任何内容。
- 测试关卡。 仅发送给
test_recipients。用户在真实收件箱中阅读,并说“继续”或给出修复。 - 发送关卡。 显示测试运行计数(已排队、已排除、已退订)。仅当用户在聊天中对此特定发送说“是”时,才向所有人发送。对先前步骤的同意并不代表对此步骤的同意。
社交媒体帖子在发布前显示,项目列表在编写前提出。在匹配的关卡之前,切勿传递发送标志。
回答“有多少订阅者?”
订阅者 = 用户数减去退订数。从用户的退订表或unsubs.txt(在广播模式下,先运行broadcast_unsubs.py)中计算退订数。同时给出最新版本的退订率。
文件
config.example.yaml:每个公司特定的值,加上autonomy、provider.monthly_quota和social.post_via。templates/email.html、templates/email.txt:起始布局。scripts/:lint_email.py、send_test.py(带--readback)、`...