关于 migrate-placements
migrate-placements 是将 Adapty 付费墙位置迁移到 Flow 位置的技能。它盘点现有位置和 Flow,创建新位置并整理代码变更交接,不修改已有记录。
主要功能
- 01盘点位置和已有 Flow
- 02将 Flow 与付费墙位置匹配
- 03创建新的 Flow 位置
- 04生成新旧 ID 对照
- 05保留已有位置不变
适用场景
- 01将付费墙位置迁移到 Flow
- 02继续已进行一部分的迁移
- 03为应用发布准备位置变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ adaptyteam/ adapty-skills --skill migrate-placements安装后在 Claude Code 中输入/migrate-placements确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-surface.mdmigrate.pystub-flow.json详细介绍
migrate-placements
你的表达方式
用户读到的每一行,包括固定区块:
- 用「我」描述你做的事,用「你」描述用户做的事。 先说结果;最后给出一个下一步。不要开场白,也不要复述用户的请求。
- 每个区块最多加粗一处。 固定区块的标题就是那一处;App、Flows 或 Permanent 等标签保持普通文本,选项列表中的路线名称(Reuse、Convert、Build、Stub)也保持普通文本——四个加粗的列表引导语就是四处加粗,而不是一处。
- 只有当用户需要输入某个值,或能在别处看到该值时,才显示原始值。 Placement 的开发者 ID 使用代码格式——用户会将其输入应用。提及应用时使用其标题,提及 flow 时使用其名称;如果有多个同名 flow,则加上最后编辑日期,并链接到 flows 页面;绝不使用应用或 flow 的 UUID。数量和状态用普通文字表示(“1 个,现已发布”),不要使用代码格式。
- 只询问会改变计划的问题。 如果所有答案都会得出相同结果,就用一句话说明,不要提问。
- 每件事只说一次。 最终报告不要重复审批环节中已经说明的内容。
- 提供完整、可点击的链接。 使用用户的语言回复,固定区块也一样。
这项技能的作用及其能力边界
对于 Adapty 文档页面中这项技能不会链接的内容——例如 placements、audiences 或 flows 在控制面板中的行为——请使用 adapty-docs 技能查找。绝不要根据主题名称拼接文档 URL。
两个事实决定了这项技能的整体方案,而且都有实测依据 (api-surface.md):
创建后无法更改 placement 的内容类型。 在部署了 audience 联合类型的环境中,针对 paywall placement 使用 flow audience 执行 placements update 会被拒绝,并返回 Placement type can not be changed.,因此这是后端规则,并非 CLI 功能缺失。无法转换。 每次迁移都是一次 placements create,现有 paywall placement 保持原样。
Placement ID 永久有效。 文档原文如下:"Placement IDs are unique across every placement in the app, whatever the type, so the same ID can't serve a flow in one place and a paywall in another."(placements.md)删除 placement 不在处理范围内,因此新 placement 无法重复使用旧 ID,错误创建的 placement 会永远留在应用中。创建任何内容之前,都会完整显示并审批每个 ID。
因此,交付内容是 placements 加上代码更改交接,不会在控制面板中完成。 这项技能会创建新的 flow placements,并生成一张表,列出 old paywall placement ID → new flow placement ID。只有应用发布了调用变更,用户才能看到任何内容——这一点也值得明确说明,作为安全保障:在此之前,旧 placements 会继续提供服务,因此不发布任何内容就是回滚方式。
而且有些工作可能已经完成,所以先检查再创建。 已经开始迁移的用户,可能已经为部分 paywalls 创建了 flows;如果运行时不先检查,就会创建第二个内容更少的 flow——而且会永久存在,因为没有 flows delete。第 2 阶段会读取这些 flows;第 4 阶段会决定如何处理每一个。
范围边界。这项技能不负责设计或构建 flow 内容——那是 flow-generator 的职责,由它负责 flows config update、预览循环和发布。它不负责将 paywall 转换为 flow——那是上方的控制面板按钮,由用户点击,没有对应的 CLI 命令;这项技能的任务是找到它生成的 flow,并为其添加 placement。它不负责实现应用侧的调用位置——那是 adapty-integration 的职责。它不会删除或修改任何现有 placement、paywall 或 flow。
阶段 1 — 解析并探测
此迁移需要 flows publish。像同级技能那样解析一次 $ADAPTY —— 只安装一次 latest,之后一直使用它;仅当全局前缀不可写时才回退到 npx,然后检查该命令是否存在:
npm i -g adapty@latest >/dev/null 2>&1 \
&& ADAPTY="adapty" \
|| ADAPTY="npx --yes adapty@latest" # fallback: prefix not writable
$ADAPTY --version
$ADAPTY auth whoami
不要根据你从某处看到的版本号来设置门槛——检查命令本身。 $ADAPTY flows publish --help 要么会显示命令说明,要么不会,这是唯一不会过时的测试;版本号下限以前就错过一次,因为 flows publish 在 0.8.3-beta.0 和 0.8.3-beta.1 中发布,在 0.8.3 中缺失,之后又重新出现。如果安装 latest 后命令仍缺失,请尝试一次 npx --yes adapty@beta;如果那里也没有,那么该路径尚未发布:请如实说明,并把用户引导至仪表板步骤
(迁移到 flows),而不是让用户追逐版本号。
只安装一次;不要让每次调用都套上 npx。 与已安装时的 0.07 秒相比,包装器每次调用约多花 1 秒,而此技能对每个版位只调用一次。在 npx 回退方案中,--yes 至关重要:没有它,npx 会停下来询问是否允许安装,而无头运行时没有人可以回答。在 zsh(macOS 默认 Shell)中,多词的 $ADAPTY 不会按空格拆分,因此要在同一个 Shell 中运行一次 setopt shwordsplit;command not found: npx --yes adapty@latest 是 Shell 问题,绝不是 CLI 缺失。如果 auth whoami 失败,$ADAPTY auth login 会打开浏览器——需要用户完成登录。然后运行 $ADAPTY apps list --json 获取应用 UUID。
然后先探测此功能,再花费 152 次读取去盘点一个你无法操作的清单。 只读判别依据是 content_type:已部署受众联合类型的环境会在每个受众上返回该字段,而未部署的环境会完全省略此字段。因此,探测时需要选一个有受众的版位——任意选择一个并不能证明任何事,因为 audiences 数组为空时,返回“缺失”是有其他原因的,与是否部署无关:
$ADAPTY placements list --app "$APP" --page-size 20 --json # summaries only — no audiences here
$ADAPTY placements get --app "$APP" <ID> --json # repeat until one has a non-empty audiences[]
读取几次后就停止执行 get;目的是找到一个佐证,而不是做全面调查。共有三种结果,第三种不同于第二种:
- 存在
content_type⇒ 联合类型已部署。继续。 - 在一个确实存在的受众上缺少
content_type⇒ 确认未部署。停止。说明写入会返回audiences.0.paywall_id: Field required,并且在此部署环境中,迁移必须通过仪表板操作:https://app.adapty.io/placements。**有两个条件会阻止它,且都与账户无关**——上文提到的**CLI 版本和API 部署**;因此,该部署上的所有账户都会遇到相同情况,切换账户没有帮助。**这已不再是预期情况:**生产环境支持受众联合类型,因此如果来到这里,说明你连接的服务器版本落后,而不是此功能不存在。 - 没有任何版位包含受众——这是新应用,或所有受众列表都为空 ⇒ 无法确定。不要停止:阶段 2 只读,始终可以安全执行。运行该阶段,说明此功能尚未确认,让第一次写入来确定结果。(如果应用没有版位,那就没有可迁移的内容——因这个原因结束,而不是因为无法确定。)
这个信号只是相关性,并非证明,因此相同的结果也可能在之后才出现——而且后续停止保护的是版位,不是流程。如果阶段 5 中 flows publish 返回 http_404——该路由尚未验证,因此应实际运行,而不是自行假定结果——那么每个不同的 paywall 都已经运行过一次 flows create,而且没有 flows delete:这些流程记录会保留下来,必须从仪表板中删除。之后才出现的阶段 7 audiences.0.paywall_id: Field required 错误,则意味着这些流程已经创建并发布。两种停止都能保护的是一件完全无法撤销的事:没有创建任何版位,且 paywall 版位未受影响。
阶段 2 — 盘点
只读且全面。references/migrate.py 负责分页循环,因为 --page-size
默认值为 20,而响应中的 meta.pagination {count, page, pages} 是唯一能说明这一点的内容:
python3 <skill>/references/migrate.py inventory --app "$APP" --adapty "$ADAPTY" \
--scope active --out inventory.json # or --scope all; see below
绝不要只读取第 1 页。
--page-size的默认值为 20,因此只调用一次并统计返回的行数,会让一个有 150 个 placement 的应用被悄悄少报 130 个,输出看起来就像是个小应用。
如何解读 migrate.py 的退出码,适用于此处及之后的每个阶段:2 表示未能完成,且未能明确判定原因为内部问题——消息会说明已知信息,因此请仔细阅读并检查输入;3 表示脚本内部的不变量遭到破坏,输入几乎肯定没有问题,因此请停止并报告,而不是编辑文件。退出码为 2 时出现含糊的消息,是脚本拒绝猜测,并非 bug。
placements list 会返回 {developer_id, id, title},不包含受众——只有 placements get 会返回受众,因此对 N 个 placement 进行分类需要 N 次 GET,而且没有批量读取接口
(api-surface.md)。
同一条命令还会读取 flows list 和 paywalls list——读取账户已有的 flow,以及用于匹配它们的 paywall 标题,因为受众包含 paywall_id,但没有名称(api-surface.md)。无论账户规模多大,这都是两次分页读取,此外还要执行已经在进行的 N 次 GET。--no-existing 会跳过这些读取;仅在它们失败时才使用,然后说明这个问题没有得到解答,而不是报告数量为零。
传入 --scope active,并根据实际返回的内容来读取,而不是事先推断。 在生产环境中,placements list 和 placements get 都会返回 is_active(api-surface.md),因此过滤器确实会筛选结果,通常会是以下这一行:
30 placement(s) read -> inventory.json
scope=active: 30 kept, 120 filtered out (30 active, 118 inactive, 2 unknown) -- 2 placement(s)
carry no readable is_active and were withheld as unknown, not as inactive; offer --scope all
尚未更新的部署仍会回退,而回退是结果,不是故障:
150 placement(s) read -> inventory.json
scope=active: 150 kept, 0 filtered out (0 active, 0 inactive, 150 unknown) -- fell back to all:
no placement carries is_active, so activity is unknown rather than inactive; --scope active was
ignored and every placement kept
缺少 is_active 不等于 false。 遇到这种情况时,应说明账户无法进行筛选,并使用下方的规模门槛——届时它是唯一可用于限定范围的依据。不要报告迁移结果为空;工具不会返回这种结果,你也不应该这样报告。没有运行命令时,绝不要报告上述任一行。
向用户报告该行的两个部分。 过滤器隐藏工作比没有过滤器更糟:如果 is_active 的范围比文档所述的 placement 状态更窄,被隐藏的行就是需要迁移的 placement,却无人能看到。plan 会在 summary.scope 中重复显示被隐藏的数量——不要在消息中漏掉它。
混合情况有自己的标志,因此无需自行识别。 is_active 不可读取的行在 active 范围下会被隐藏,但会与非活跃项分开计数,因为 “你停用了这些项” 和 “我无法判断” 是需要告诉用户的两件不同的事。只要存在这类行,范围信息块就会将 unknown_withheld: true 与原因一并设定;该标志会触发阶段 4 提出扩大范围的选项。
使用 --scope 进行筛选,不要筛选输出。 list 摘要中会包含 is_active,而受众需要每个 placement 各执行一次 get,因此在 GET 循环之前筛选,才能把 2 + 150 次调用减少为 2 + 30;之后再筛选则毫无节省。
规模门槛。 如果存在该字段,就不需要这个门槛;如果某个部署中没有该字段,它就是唯一选项。请注意,它限定的是先按 placement 遍历的范围——不要把 paywalls placements 当作更便宜的窄范围路径,因为它只会筛选处于 live 状态且类型为 paywall 的行,因此完全看不到非活动 placement
(api-surface.md)。先读取 meta.pagination.count。如果 placement 超过 25 个,请说明成本——每个 placement 都要调用一次 get——并在付出这项成本前主动提出缩小范围:使用 developer_id 子串,或明确列出 placement。“全部”由此成为知情后的选择,而不是无意间全面扫描。
无论如何,先缩小范围。 之后再扩大范围意味着必须完整重读——无法续读,且
--out会覆盖文件——但读取成本低且幂等;真正重要的数量是扩大范围后新增的 flow 数量:每个额外的不同 paywall 对应一个 flow,都需要有人手动填写。第 4 阶段会说明这对用户意味着什么。
阶段 3 — 分类和分组
python3 <skill>/references/migrate.py plan --inventory inventory.json
针对每个受众:paywall_id → 可迁移;flow_id → 已完成;A/B 测试条目 → 跳过
(见下文);两者都没有 → 跳过。可迁移受众
按 paywall_id 分组,而这个分组就是流程计划:每个不同的付费墙创建一个可复用流程,供使用它的每个展示位置和受众共享,这样用户之后只需完善一个流程,而不是多个流程。
报告数量和分组,绝不要逐行列出展示位置——可迁移的展示位置、 可迁移的受众、不同的付费墙、已有流程、无受众、跳过的 A/B 测试。150 行 转储数据不算报告。 将不同付费墙的数量称为下一阶段将创建的流程数。
当 summary.unwritable_multi_segment 不为零时,在此报告该数量,并将这些展示位置视为
受阻。 携带多个 segment_id 的受众是可读但不可写的——API 在写入时将每个条目限制为一个,而其读取路径会刻意绕过此限制,以便旧版行数据能够往返保留(api-surface.md)。
对于此类展示位置,plan 不会生成 command,并会说明原因。不要提议拆分或删除
segment:这会改变谁能看到什么,应由用户决定。说明必须先在控制面板中解决此问题,并且迁移的其余部分可以继续进行。
跳过 A/B 测试,且 plan 会逐一列出(skipped: A/B test <title>,stderr)。默认受众上的测试会跳过整个展示位置(skipped_placements:没有 ID、没有 command、不会构建流程)。绝不会更改源展示位置。每次跳过都报告一次
(api-surface.md)。如果部署版本早于 A/B 测试读取功能,则测试会被静默遗漏:说明这些数量是 API 返回的数量,并在控制面板中检查一个已知正在运行测试的展示位置。
并在同一行报告 summary.scope(如果存在)——库存中有多少个展示位置被排除,以及排除原因。以上数量描述的是你读取的内容;而该区块是唯一描述未读取内容的部分。
summary.exposure 是第三个区块,与前两者并不重复:它表示此计划将创建的展示位置上的活动分布。此处不要输出它——它在阶段 6 才会用到,而且只有在那里,它的数值才有意义。
summary.existing 是第四个区块,这个区块需要报告,因为它会影响阶段 4 的提问。 每一行 flows_needed 都带有付费墙的 paywall_title;如果账户中存在名称匹配的流程,还会带有 existing_flow_candidates——每个候选项都包含其 status,以及 next_step:attach(流程已 published)或 publish_then_attach(转换后的流程仍为 draft)。说明不同付费墙中有多少个已有候选流程,并列出名称。
每个候选项都只是一个提议,用户必须逐一确认。 匹配仅依据名称——API 没有记录某个流程来自哪个付费墙——因此名称匹配可能只是巧合;而且,没有匹配并不能证明没有内容被转换,因为流程重命名后就无法匹配。请用户在备好流程列表后进行核对——https://app.adapty.io/flows。
阶段 4 — 两个问题
在一条消息中提出两个问题,然后停止。第二个问题针对每个付费墙,因此请用一张表格,为每一行推荐一条路径——不要针对每个付费墙分别发消息,也不要为整个账户只给出一条路径。
1. 范围。 当存在 is_active 时,这是一个真正的三选一问题,而不是“枚举全部内容再取消选择”:
| 选项 | 筛选条件 | 适用场景 |
|---|---|---|
| 仅启用项 | is_active: true | 默认建议——迁移实际正在提供服务的内容 |
| 全部 | 无筛选条件 | 迁移整个账户,包括已停用的展示位置 |
| 指定列表 | 用户提供的 ID | 分阶段迁移,或仅迁移某个应用区域 |
| 仅当选项有所不同时才询问。 如果账户中的所有版位都处于启用状态(或 | ||
--scope active 筛选出的集合与全部版位相同),答案不会改变任何内容:用一行说明“全部 | ||
| 个版位都已启用,我会全部处理”,然后只询问第 2 个问题。 |
建议仅处理启用的版位,并说明原因: 已停用的版位不会服务任何人,因此为它制作流程只是在做用户看不到结果的工作;而范围内每增加一个不同的付费墙,就意味着有人需要手动填写另一个流程。说明扩大范围的代价,不要说它是免费的: 使用 --scope all 重新运行会重新读取整个账户,因为无法续跑,而且 --out 会覆盖已有内容。
如果范围信息块中有 unknown_withheld: true,请明确询问用户是否要扩大范围,并说明未知状态的数量。这些行被暂时排除,是因为无法读取其状态,而不是因为有人停用了它们;这也是用户最可能希望撤销的一项排除。
第 2 阶段已经应用了 --scope active,但这并不代表你替用户做了决定——只有读取后才能知道该字段是否存在以及有多少个版位已停用,因此无法在读取之前让用户有效地做出选择。当前尚未写入任何内容;用户可以在此处选择扩大范围。
如果由于字段缺失而回退了清单读取方式,请说明无法按账户筛选,并改为询问最初的问题:处理全部版位(并显示数量),还是指定某个子集。这是在回答第 2 阶段的规模确认问题,并非再次询问同一件事。
2. 每个不同付费墙的流程内容来源——请逐个付费墙询问,而不是对整个账户只问一次,因为第 3 阶段刚刚告知你,有些付费墙已有流程,有些则没有。按以下顺序提供四种方式:
- 复用——使用账户中已有的流程,该流程来自
existing_flow_candidates,或由用户指定。只要存在候选流程,就优先提供此选项,因为这是唯一不产生费用的方式,而且复用的流程是用户自己的工作成果。与用户确认流程后,重新读取flows get,并根据其状态采取行动:published状态的流程直接关联;其他状态的流程则先发布(执行第 5 阶段中发布的部分,不执行其中的flows create部分)。 - 转换——在付费墙自己的概览页面中,通过控制面板上的 Move to new builder 一键将其重新创建为草稿流程:包括布局、所有语言区域中的文案,以及作为变量的产品和价格(文档)。对于使用旧版 Paywall Builder 构建的付费墙,这是正确的默认选项——Adapty 自己建议转换,而不是重建;这是由用户点击执行的操作,不是你来操作:没有对应的 CLI 命令。告诉用户在哪里操作,然后等待;完成后重新运行
inventory,并对新出现的内容采用复用方式。构建器会打开 Import review 面板,列出未能迁移的内容;用户也需要自行处理该面板中的内容。 - 构建——将付费墙交给
flow-generator,由它读取付费墙并设计流程。适用于无法通过转换处理的付费墙,或用户希望重新设计而不是照原样复现的屏幕。 - 临时占位——使用
references/stub-flow.json创建一个最简、可发布的流程。这是最后的手段,不是捷径。 它会向用户展示占位内容,因此在第 6 阶段必须告知用户并取得其同意;只有在前三种方式都不可行,或对应的版位无人使用时,才提供此选项。
无法通过 CLI 判断是否可以转换。 paywalls list/get 只会返回 {id, title, product_ids},不会提供付费墙由哪个构建器创建的信息,而且 Move to new builder 只会出现在使用旧版 Paywall Builder 创建的付费墙上。因此,不要告诉用户一定能看到这个按钮——应说明按钮的作用和查找位置,让用户查看后告诉你结果。
绝不要替他们做选择。这四种方式会影响用户看到的内容,这不该由 Agent 决定。你不能为已有流程的付费墙创建流程,除非用户已拒绝复用现有流程:这会在账户中留下重复项,因为没有 flows delete。
存在 is_active 时的演练顺序
这不是一个阶段,而是在大型迁移或首次迁移时,建议只提出一次:先处理非活动展示位。 这样,整个 create → publish → attach 链路就能从头到尾跑通,且不会向真实用户展示。同时,最终留下的是实际的账户状态,而不是模拟结果——此技能依赖的调用 placements create(带已发布流程)目前仍未经验证,因此首次实际运行最好选在出错也不会让任何人错失购买机会的地方。然后再处理活动展示位,此时操作机制已经验证可用。
代价需要如实说明,而且值得一提:这要分两轮执行,并且无论如何,创建的每个展示位都是永久的。提出这个方案,但不要强加于用户。如果没有该字段,就无从确定处理顺序,因此不要猜测哪些展示位没有流量,直接跳过这一步。
前提是
is_active: false表示没有用户会看到内容,而且该字段的含义只有一个:API 源码所记录的激活状态——Live 或 Inactive,也就是控制面板自己的开关,没有更细的含义。它无法告诉你已发布的应用是否仍在调用该展示位。非活动展示位不会展示任何付费墙,因此在其上放置占位项不会让任何人看到占位内容;但如果用户说某个展示位承载着实时流量,就相信用户的说法,不要相信该标记,并将其视为活动状态。
第 5 阶段 — 实现流程
用户选择复用的付费墙会跳过本阶段的大部分内容。 无需创建任何内容,也无需写入任何内容:重新读取 flows get,如果状态为 published,则将其记录到台账并继续;如果不是,则运行以下发布和轮询操作,不做其他任何事。flows create 和 config update 用于尚无流程的付费墙——对复用的流程运行这些命令,就会产生这条完整路线旨在避免的重复项,而且没有 flows delete 可用于撤销。等待用户点击 Move to new builder 的付费墙尚未准备好进入本阶段:不要将其列入台账,并在下次 inventory 时再处理。
按以下顺序,为每个需要新流程的不同付费墙执行操作。此顺序由测量结果决定:指定草稿流程的展示位置会被拒绝——Cannot attach a draft flow to a placement — publish it first.,退出码为 2;或者在较旧的 CLI 中,后端会返回 Flow must be published before placing in a placement.——而发布是异步的。
$ADAPTY flows create --app "$APP" --name "<paywall title> (flow)" --json # row only; draft
$ADAPTY flows config validate <FLOW> --app "$APP" \
--config-file <skill>/references/stub-flow.json --json # expect valid: true
$ADAPTY flows config update <FLOW> --app "$APP" \
--config-file <skill>/references/stub-flow.json --json # or flow-generator's config
$ADAPTY flows publish --app "$APP" <FLOW> --yes
$ADAPTY flows get <FLOW> --app "$APP" --json # poll until published
验证会在写入前对本地文件执行,绝不会在写入后执行——写入后,验证检查的是已保存的字节。create 仍须先执行,因为验证需要解析流程 ID。
flows publish 返回 status: publishing,绝不会返回 published——因此,轮询 flows get,直到状态显示为 published;不要根据发布响应报告流程已上线。该命令会输出轮询本身,并同时输出诊断调用;如果状态为 publication_failed,flows config get 会包含 publication_status、transform_error 和 publication_error,其中 transform_error 是转换服务用自己的措辞给出的异议——请原样引用,不要臆造原因。此处传入 --yes,仅仅是因为用户在第 4 阶段选择流程内容,而展示位置要到第 6 阶段才会放行;否则,非 TTY 运行会以退出码 2 拒绝执行,而不是挂起。flow-generator 负责发布约定、400 路径和失败诊断——请委托给它处理,而不要重新推导这些内容。
如果 flows publish 返回 http_404,就在此处停止运行,而且这不是正常结束。 此部署没有启用该路由,因此切换账号也无济于事;但此付费墙已经运行过 flows create,而且没有 flows delete,所以请说明存在多少条流程记录,并说明必须从仪表板中将其删除——https://app.adapty.io/flows。发布返回 404 后,不要继续为其余付费墙创建流程。
请按原样使用随附的 references/stub-flow.json,不要手动编写更小的版本。 已知它会从真实转换服务返回 valid: true;低于这个下限时,失败信息会是未指明位置的 Generated JSON failed schema validation,不会指出任何字段(api-surface.md)。
流程账本 — flows.json
一个文件,边操作边写入,而且这是阶段 5 唯一使用的账本。 一个 JSON 对象,将
paywall_id 映射到 flow_id;每个流程达到 published 后,都要完整重写一次——不要等到
最后再写:
{
"9f3c1a20-...": "6b41e0d7-...",
"c07e4b19-...": "a2d55f81-..."
}
增量写入才是关键。 flows create 不会去重,而且没有 flows delete,因此,如果一次运行在流程 3 和流程 4 之间中断,必须留下 3 条记录——否则重新运行时会为每个付费墙创建第二个流程,而每个重复流程都会成为永久条目,只能由人手动移除。在确认流程已 published 的 flows get 之后写入,这样文件中的每一行都代表一个确实可以附加的流程。
复用或转换的流程也和其他流程一样记入账本。 账本记录的是阶段 7 要附加的内容,而不是本阶段创建的内容。因此,用户已有的流程一旦经 flows get 确认为 published,就要写入同一个文件;这样阶段 7 就无法区分它们,而这正是我们要的结果。
重新进入时,先读取账本。 对于已存在的每个 paywall_id,使用
flows get <FLOW> --app "$APP" 确认记录的流程仍显示为 published,然后跳过该付费墙。记录的 ID 若状态不是 published,就不能跳过——应完成发布,不要创建第二个流程。
将 flows.json 与 inventory.json 放在工作目录中,并在输出进度时注明路径。阶段 7 会读取它;其他阶段都不会读取。
阶段 6 — 确认关卡
输出以下内容,填入具体信息,然后等待用户明确回复同意。在获得同意之前,不要对任何内容添加 --yes。回显已解析的值——绝不要只用文字提醒 ID 是永久的。**这句同意仅授权执行
placements create,不授权其他操作:**阶段 5 已经运行,因此此处应说明尚未发生的事情,而不是声称什么都还没发生。
目前还没有 placement——用户的这句同意将授权创建它们。
应用: 要创建的 placement: 个——这是不可逆的一步 流程:<flow line — pick by path, below>
现有付费墙 placement 新流程 placement 流程 <developer_id><proposed developer_id><test line, only if
planskipped a test>永久有效:placement 无法删除。如果这些 ID 中有任何一个不正确, 它就会永远留在应用中——回复同意前,请仔细阅读中间列。
不会触碰现有的付费墙 placement。它们会继续生效,直到你的应用 发布下面的更改;这也是回滚方式:什么都不发布。
<undo line — stub and build paths only, below>
用户暂时还不会受到影响。你的应用必须在原来调用旧 placement 的地方调用
getFlow("<new developer_id>")。
测试行的内容格式如下,不要照抄示例文字(也要注明整个 placement 被跳过的情况):
“未纳入:A/B 测试 <title>(在 <developer_id> 上);它会继续通过旧 placement 运行。”
这两个位置根据一个事实启用:本次运行是否创建了流程? Stub 和 build 会创建;existing 不会——用户提供的是已经发布的流程,因此本次运行没有创建任何内容。“现在已在你的账户中,只能在控制面板中移除”会错误地描述本次运行所做的事情。
-
Stub 或 build。 流程行和撤销行必须逐字使用:
已创建并发布 个,每个不同的付费墙对应一个——现已在你的账户中,只能在控制面板中移除
回复不同意不会撤销这些流程。无论如何,这 个流程都会保留;没有 CLI 命令可以删除流程。
-
Existing。 填写流程行,并完全省略撤销位置——回复不同意后,账户会保持本次运行开始时的状态,因此无需发出警告:
个,即你所选的那些,现已发布——本次运行未创建任何流程
此区块中的其他内容均无条件适用:永久性说明、不会触碰现有 placement 的说明以及调用变更说明,在这三种情况下都成立。
建议的 ID 来自 migrate.py 的 propose_developer_id——<original-id>-flow,并已与清单中的每个 developer_id 预先核对,因为冲突是永久性的。显示完整列表;绝不要将其缩写为数量。
如果流程内容是占位内容,也要打印这段说明并单独征求同意。 基于占位内容创建位置并非较小规模的迁移——这对用户来说是另一种结果。
当 is_active 已知时,应将影响范围表述为数量,而不是要求用户盲目确认——机械式防护优于附带说明,这正是本仓库的偏好。非活跃位置完全没有这类代价。
从
summary.exposure读取数量,绝不要从summary.scope读取。scope按list返回的结果划分整个账户;exposure划分此计划实际会创建的位置,排除所有已有流程的位置和所有空位置。在此处引用scope会夸大实际影响范围,而这正是本段要防止的问题。
此段有两种形式,只打印其中一种。 由 exposure.status_readable 决定打印哪一种,其他条件均无关:
exposure.status_readable | 打印 | 插槽 |
|---|---|---|
true | 数量形式(紧接在下方) | <n> = exposure.placements,<a> = exposure.active |
false | 无数量形式(本段之后的第二个区块) | 无;逐字照用 |
数量形式:
占位内容是只有一行的占位屏幕——一旦你的应用指向它,它 就是实际内容:用户会看到占位内容,且无法购买。
你正在迁移的
<n>个位置中,有<a>个处于活跃状态。在你的应用发布调用 更改的那一刻,给这些位置附加占位内容就会停止这些位置的购买, 直到你补全流程。其余<n-a>个位置处于非活跃状态,没有用户使用,因此 在这些位置使用占位内容没有代价。只有当你会在发布调用更改之前补全流程时,才回答是。 否则请回答否——这些流程已经发布,因此我会在此停止,不创建任何位置。补全流程, 再次发布,然后准备好后再回答是——位置只能附加到已发布的流程。
无数量形式。 当 exposure.status_readable 为 false 时,无法说明数量,只能征求确认。请逐字使用以下措辞,不要用猜测的数字替代:
占位内容是只有一行的占位屏幕。 一旦你的应用指向它,它 就是实际内容——用户会看到占位内容,且无法购买。对于尚无任何调用的位置, 这样做没有问题;但对于你即将发布的位置,这会导致收入中断。 只有当你会在发布调用更改之前补全流程时,才回答是。 否则请回答否——这些流程已经发布,因此我会在此停止,不创建任何位置。补全流程, 再次发布,然后准备好后再回答是——位置只能附加到已发布的流程。
两种形式都要保留“再次发布”。 补全占位内容会将流程标记为 dirty,而 dirty 流程能否附加尚未验证
(api-surface.md),因此只有 published 状态会被视为可附加。返回时,应像阶段 5 那样重新读取 flows get,不要假定编辑后状态未变。
阶段 7 — 创建
不要手动输入 placements create。使用账本重新运行 plan,并执行它生成的内容。
python3 <skill>/references/migrate.py plan --inventory inventory.json --flows flows.json > plan.json
使用 --flows 时,每个 placement 行都会新增一个 command — 由
build_create_command 根据 to_flow_audience 构建的受众生成的确切 argv。将它们逐行打印出来,并
原样运行:
python3 - plan.json <<'PY'
import json, shlex, sys
for row in json.load(open(sys.argv[1]))['placements']:
cmd = row.get('command')
print(shlex.join(cmd) if cmd else '# NO COMMAND: ' + row['command_unavailable'])
PY
# then, one at a time, prefixed with $ADAPTY:
$ADAPTY placements create --app ... --title ... --developer-id ... --audiences '[...]'
生成 argv 能让防护措施成为不可绕过的必经步骤,而不只是可用选项;这也是该技能中唯一不可逆的命令,因此绝不能在最后一步信任手动重输:
- 新 placement 上的每个受众都必须是
flow。 混合 paywall+flow 受众会导致后端返回 400,而build_create_command会抛出异常,不会生成命令 — 即使受众数组全是 paywall 也一样,这是从另一侧确保同一项约束。手写 argv 则不会触发这种拒绝。 - 每个条目都必须包含
content_type;如果缺少,CLI 会以 2 退出,且不会发送请求。来自合并前环境的读取结果不包含该字段;这条路径上的normalize_audience每次都会注入它。 to_flow_audience会原样保留segment_ids和priority— 它们决定定向对象,改动会悄悄改变谁能看到什么。重新输入时,数字最容易漏掉。- paywall 未包含在
flows.json中的行不会带有command,只有missing_flows和原因。不要手动补上缺失内容:完成该 paywall 的阶段 5,然后重新运行plan。 - 带有
multi_segment_audiences的行也不会带有command,而且无法通过重新运行来解决。 其受众包含多个segment_id,API 在读取时会返回这些 ID,但会拒绝写入。请报告此情况并指出 placement 名称,留给用户处理 — 强行写入会失败,而编辑这些分段会改变定向对象。 placements create没有提示,也没有预览 — 与flows publish不同。发送什么 argv,就会发送什么内容。阶段 6 是唯一的把关步骤 (api-surface.md)。
Placement 账本 — placements.json
这是第二个也是最后一个账本,必须遵守与 flows.json 相同的规范: 一个 JSON 对象,将新 developer_id 映射到已创建 placement 的 id,并在每次成功创建后重写。
{ "main-flow": "1f8e...", "onboarding-flow": "77b2..." }
账本中只记录成功项。 一个键代表对应 placement 已存在,绝不能再次创建 — 如果尝试使用已被占用的拟定 ID 再次创建,要么会被拒绝,要么更糟,会生成第二条永久记录。因此,重新进入时要读取 placements.json,跳过所有 developer_id 已是键的已批准条目。故意不记录失败项:失败条目必须重试,否则账本跳过它后,就会悄无声息地搁置下来。
一次失败不会中止运行 — 在你自己的工作笔记中记录失败,继续处理下一项,并在阶段 8 中说明每次失败及其原因。
只有一种错误例外:audiences.0.paywall_id: Field required 会中止所有操作。 这不是当前条目的问题 — 这是阶段 1 探测的结果延迟返回,因此所有剩余条目都会以相同方式失败。请停止并如实报告当前状态:没有创建任何 placement;阶段 5 中的 flows 已存在并已发布,而且无法通过 CLI 删除。
阶段 8 — 报告与交接
已创建、已跳过(包括 A/B 测试)以及失败的项目和原因,然后是用户要执行的两项操作:
旧付费墙展示位 新流程展示位 <old developer_id><new developer_id>
以及调用方式的变更:应用在当前获取旧展示位的位置,改为通过 getFlow("<new developer_id>") 获取新展示位。流程仅能在 Adapty SDK v4.0 或更高版本中渲染,因此,如果 SDK 版本较旧,必须先升级版本才能更改调用;getFlow 可以读取流程和付费墙展示位,所以应用无论哪种情况都只需调用一个方法(迁移到流程)。将实现交给 adapty-integration,由它负责各平台的调用位置和渲染——此处不要编写应用代码。
说明旧展示位会继续生效,并解释原因。 已发布版本中的用户,应用内编译的是旧展示位 ID,在更新前无法访问流程,因此过早停用付费墙展示位会让所有尚未更新的用户都看不到付费墙。两个展示位会并行运行,各自单独衡量指标,直到 v4 的采用率足够高。
不需要回滚文件,并说明原因: 没有修改任何现有内容,因此未动过的付费墙展示位就是回滚方案。 在应用发布调用方式变更之前,用户仍会看到旧付费墙;如果新流程有问题,就先不要发布,并在构建器中修正流程 —— https://adapty.io/docs/adapty-flow-builder.md。**回滚针对展示位,而非流程:** 本次运行创建的所有流程都无法通过 CLI 删除,因此请注明数量以及删除位置: https://app.adapty.io/flows。
最后简要说明仍待处理的事项,一次说完:仍包含占位内容的流程、失败的展示位,以及需要清理的流程。整个报告中只需说明一次,应用发布前用户不会看到这些内容——就在上面的调用方式变更处说明,不要在此处重复。
输出内容
用户看的是你发出的消息,不是这个文件。内容要简短。
**只有两个固定内容块,其余内容均不固定:**审批关卡,以及——当内容是占位内容时——阶段 6 中的占位内容确认。填写相应位置,不要添加冗余内容。阶段 8 中的旧→新表格与审批关卡中间列的数据相同,因此只需输出表格,不要再用文字复述。
**其他内容都用一行或省略。**阶段 3 的计数用一行说明。阶段 4 的问题一次性放在同一条消息中提出。阶段 5 的每个流程进度只报告计数,不要持续播报。能力受限时,用一行说明并附上控制面板链接。每项内容只说一次:如果审批关卡已经说明了展示位会继续生效,阶段 8 就不要重复。