关于 stream-feeds-migration
用于推导 Stream v2 到 v3 同步映射的技能,以应用实际存储的数据为依据;也可检查代码中的写入路径并规划迁移。它输出映射对象及证据说明,不会修改应用代码。
主要功能
- 01抽样读取已存储的活动和反应
- 02检查 Feed 分组与代表性数据
- 03对照样本检查 v2 写入代码
- 04生成 v3 同步映射
- 05编写代码迁移计划
适用场景
- 01准备 v2 到 v3 的同步配置
- 02核对已存数据中的字段与反应结构
- 03规划应用读写路径的迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ getstream/ agent-skills --skill stream-feeds-migration安装后在 Claude Code 中输入/stream-feeds-migration确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
code-inventory.mdfetch_sample.pymapping-keys.mdporting-plan.mdscan_v2_calls.py详细介绍
Stream Feeds - v2 到 v3 同步映射
对于 CLI 命令或转到另一个 Stream 技能,请阅读
../stream/SKILL.md。
请根据应用的 v2 数据实际包含的内容,而不是任何人的主观判断,生成应用 v2 -> v3 同步配置中的 mapping 对象。
两种迁移路径共用同一份映射——实时同步器和批量导出器读取相同的配置。一次配置正确,两者都会正常工作;配置错误,两者也会以相同的方式出错。
工作流程
1. 将凭据载入环境
使用 getstream env 将 v2 应用的凭据写入环境文件。阅读
getstream env -h,了解应用选择和输出选项。使用 --target web,确保文件中同时包含 STREAM_API_KEY 和 STREAM_API_SECRET,这是采样器的要求。
运行下方的采样器时,加载该文件。不要打印文件内容,也不要在聊天或工具命令中填写凭据值。
2. 选择目标环境
默认使用生产环境——https://api.stream-io-api.com。除非用户指定了其他环境(EU、专用环境、staging),否则使用该地址;若用户指定了其他环境,则传入 --base-url <that URL>。
不要主动提出其他环境;默认目标是生产环境,如果用户指的是其他环境,他们会说明。
3. 获取样本
set -a
. "<env-file>"
set +a
python3 <skill-dir>/fetch_sample.py --out v3sync-sample.json
<env-file> 是 getstream env 写入的文件的绝对路径。请在一次 shell 调用中运行整个代码块,以便 Python 能继承凭据。
<skill-dir> 是存放此 SKILL.md 的目录。运行前请将其解析为绝对路径——技能会从用户当前所在的目录运行,因此相对路径只是碰巧可用。getstream skills 会将技能包链接到以下两个位置之一:
.agents/skills/stream-feeds-migration(Cursor、Codex).claude/skills/stream-feeds-migration(Claude Code)
使用 Glob {.agents,.claude}/skills/stream-feeds-migration/SKILL.md 确定具体位置。
密钥和密钥值分别从 STREAM_API_KEY 和 STREAM_API_SECRET 读取。脚本有意不提供 --secret 标志——将密钥作为参数传入会出现在进程列表和 shell 历史记录中,而环境变量可以避免这一点。(由于 API key 不敏感,因此提供了 --api-key。)样本会写入工作目录,也就是你需要的位置。
仅使用标准库——无需安装 pip 包。脚本会调用两个仅限服务端使用的端点:
GET /api/v1.0/migration/sample/activities- 最多 100 条活动,取自多个时间段(最新记录,以及 7 天、30 天、180 天、1 年和 2 年前或更早的记录)GET /api/v1.0/migration/sample/reactions- 每种不同类型取 5 条 reaction
两者都会返回原始存储载荷:未扁平化的 extra_context、未经修改的 reaction data。这正是重点——你要查找真实的字段名称和嵌套结构。
401 或 403 表示密钥错误,或令牌是作为用户令牌而非服务端令牌构建的。404 表示应用的部署尚未提供这些端点;Stream 支持团队可以确认。
如果响应为 200,但 activities 数组为空,应明确指出这一点,而不是尝试绕过:应用确实为空,或者其活动存储无法提供此次扫描所需的数据。reaction 携带 activity_id,因此样本中的 reaction 可以证明活动存在,并指向第二种解释。
阅读活动样本遗漏的内容
近期部署还会返回 reaction_reference_count、
reaction_references 和 slices,脚本会将它们保存在样本文件中。请先阅读这些内容,再查看活动,因为它们说明了活动的代表性:
reaction_reference_count- 样本遗漏的目标动态流中的反应副本数量。v2 会为带有target_feeds的反应写入这类副本。你可以通过foreign_id为reaction:<id>,且extra_context.reaction中包含SR:引用来识别它们。它们不包含帖子字段,因此无法说明映射情况;reaction_references中最多会提供 3 条供参考。slices- 每个时段对应一条记录,包含scanned、posts和reaction_references。before是该时段的截止时间;最新时段没有此字段。extended表示由于反应引用占用了名额,该时段已用更大的扫描范围重新读取。- 如果某个时段的
posts相较于scanned较少,说明该时段名额被挤占:来自该时段的帖子代表性不足。请说明这一点。 scanned: 0表示该时间窗口内没有插入任何活动。时段按插入时间划分,而非活动的time,因此最近导入 v2 的数据都会落入最新时段。
- 如果某个时段的
如果某个已运行一段时间的应用,其较早的时段为空,那么样本无法展示较早帖子的样貌。请询问帖子格式是否曾发生变化(例如文本、媒体或 ID 使用了不同的字段名),并说明映射仅涵盖样本所展示的格式。
如果缺少这些键,说明该部署版本早于这些改动:活动是最新的 100 条,且可能包含反应引用。请在分析前自行按上述格式将它们排除。
覆盖每个动态流组
样本涵盖整个应用:它会获取无论写入到哪个动态流中的最新活动,但不会说明每条活动来自哪个动态流组。流量较少或较早的动态流组可能完全缺失。整个应用共用一个映射,因此,如果某个组以不同方式写入活动(例如由后端任务、通知动态流或旧版客户端写入),映射就会依据其他组的数据来拟合。
近期部署会在 feed_groups 中列出应用的动态流组(名称和类型);对于较旧的部署,请询问用户应用使用了哪些组。无论哪种情况,样本都不会说明每条活动来自哪个组,因此请询问是否有任何组通过不同的代码路径写入。对于样本可能遗漏的每个组,请使用 --feed 直接读取其中一个或两个动态流(只读,可重复执行):
python3 <skill-dir>/fetch_sample.py --out v3sync-sample.json \
--feed notification:<user-id> --feed timeline:<user-id>
这些活动会保存在同一文件的 feed_samples 下,并规范化为样本的格式(自定义字段位于 extra_context 下,另加 feed 字段),因此请用相同的方法进行分析。聚合动态流和通知动态流会展开为其中的活动。它们也包含目标动态流中的反应副本;请按上述方式将其排除。读取失败的动态流(例如未知组)会连同错误一起记录,不会导致运行中止。
4. 阅读集成代码(如果有)
示例展示了存储的内容;应用的 v2 集成代码则展示了所有写入方式,包括示例最不完整的路径:低流量动态分组、很少使用的反应类型、子反应、用户的创建方式,以及 foreign_id 是否唯一。如果用户的代码仓库位于工作目录中(或用户提供了仓库路径),请阅读代码。如果没有,请询问一次仓库是否可用;即使没有代码,这项技能仍然可以使用。
请遵循 code-inventory.md:
- 运行
<skill-dir>/scan_v2_calls.py --root <repo> --out v2-code-scan.json,列出所有 v2 写入调用位置 (只读;片段中的机密会被遮盖——切勿自行引用代码中的机密)。 - 阅读这些调用位置并建立清单:每条写入路径一行,列出其动态分组、目标、字段、
foreign_id的构造方式、反应类型和数据形状、子反应及用户字段。 - 检查字段变更历史(
git log -S'<field>'):旧动态内容仍可能保留旧格式。 - 将清单与示例交叉核对。两者一致即可确认某个键;仅代码中有的证据表示存在罕见或新增路径(请映射该路径,并使用
--feed读取其动态内容);仅示例中有的证据则表示存在其他写入者或较旧版本(请询问)。示例仍是事实依据——代码很少是唯一的写入来源。
5. 分析
阅读 v3sync-sample.json,并按下面的清单逐项检查。请阅读 mapping-keys.md,了解完整的键目录、默认值、精确的解析规则,以及常见数据形状的示例——不要凭记忆猜测键名或默认值。
最常见的错误是配置过多。每个键都有一个适用于常规 v2 应用的默认值。只有当示例显示应用偏离该默认值时,才输出相应的键。空映射也是有效答案。
6. 输出映射
只输出 mapping 对象,供直接粘贴到应用现有的同步配置中。不要输出外层配置(enabled、base_url、api_key、secret、sample_rates、feed_groups_synced);这些属于部署配置,无法从示例推导出来,而且把它们与占位凭据一并重述,可能导致有人部署仍含有 <v3 app key> 的配置。
为设置的每个键说明其依据,指出对应的证据:示例中的行;如果阅读了代码,还要指出调用位置(file:line)。说明哪些键仅依据代码设置。
明确指出所有无法从示例中确定的内容——请参阅下文的示例的局限。如果对此保持沉默,就会被理解为“已验证”,但事实并非如此。
7. 规划代码迁移(如果阅读了代码)
如果你在第 4 步阅读了集成代码,还要按照 porting-plan.md 编写迁移计划:每条写入和读取路径一行,列出其 v2 调用、v3 调用、所属的切换步骤(先迁移读取,最后统一迁移所有写入)以及行为变化。v3 调用应参考《V2 to V3 Code Migration》指南(通过 getstream docs 获取),不要凭记忆填写。
迁移计划是一份文档:不要编辑应用代码。可以提出代为迁移;如果用户提出要求,请遵循该参考文档中的迁移代码部分(v3 读取路径放在读取标记后,所有写入路径统一放在一个写入标记后,绝不同时写入两个版本)。
分析清单
活动
查看整个样本中的 activities[].extra_context。
- 自定义字段名称——任何名称与 v3 保留键冲突,或应显示在
custom顶层的字段,都需要添加重命名条目。名称已符合惯例的字段无需处理。 - 活动 ID——是否有字段保存稳定的外部 ID(
external_id、post_id、uuid)?如果有,id应指向该字段。如果没有,但foreign_id已填充,且在整个样本中有意义,那么使用foreign_id: "id"更简单。如果两者都没有,则都省略,让 v2 UUID 沿用。在建议使用foreign_id: "id"之前,请确认样本中没有两条活动共享同一个foreign_id。v2 允许多条活动拥有相同的 foreign_id(它们的time不同),但作为 v3 ID 时会发生冲突,最终只会保留一条。如果有重复,或样本太小而无法判断,请询问应用中的 foreign_id 是否唯一;如果不唯一,则保留 v2 UUID。 - 转发 / 回复——保存
SA:<uuid>引用的字段(通常为shared_origin_post)就是parent_id字段。请注意,SA:引用也会自动检测,因此只有当样本显示该引用嵌套在自动检测不会优先查找的位置时,才需要配置此项。 - 文本——帖子正文是否位于
text(默认,无需配置),还是位于body/message/content等其他字段?只有字段不同时才设置text。 - 附件——位于自定义路径下的媒体对象列表需要配置
activity_attachments_field。 - 参与者格式——检查样本中的
actor。SU:<id>可原生处理。其他前缀(User:<id>)需要添加replaces条目。整个 JSON 数据块形式的参与者属于已知的旧版格式,无需映射。 - extra_context 嵌套——确定该字段包应继续嵌套在
custom.extra_context下(默认)、重命名,还是展平("")。这是偏好选择,无法仅凭样本判断:请询问。
Reactions
先查看 kinds——该列表包含样例中表示的 reaction kind。
-
对每种 kind 分类,且只能归入以下一种:评论、书签、评论书签或普通 reaction。然后相应设置
comments、bookmarks、comments_bookmarks。未列入这三项中的 kind 就是普通 reaction,这通常是正确的。 -
记住默认值:
comments已涵盖["comment", "reply"]。kind 匹配不区分大小写,因此COMMENT和REPLY也属于这两种 kind。如果它们是应用中仅有的评论 kind,无需重复声明。 -
评论文本——检查每个评论 kind reaction 的
data。默认字段是data.message。如果该字段缺失,且文本位于text或body下,则将comment_field设为对应的键("text",而不是"data.text")。如果评论 kind 在已配置的字段中没有文本,该项会被丢弃,不会作为 reaction 保留。 -
回复——父评论 ID 位于顶层的
parent(默认值,无需配置),还是嵌套在data中?如果是嵌套字段,则将parent_id_field设为该路径。还要用一条非回复进行确认:顶层评论必须使该路径为空,否则每条评论都会被视为回复。 -
评论上的 reaction——指向评论的非评论 reaction 需要设置
reaction_comment_id_field。常见的结构有两种:- 评论 ID 位于其
data中(例如data.comment_id) - v2 的原生子 reaction,通过
reactions.addChild创建:这是一种非评论 kind(通常为like),其顶层parent已设置。这种情况需要设置"reaction_comment_id_field": "parent"。
如果不设置该字段,v3 中评论上的 like 会变成对 activity 的 like。没有 parent 且没有评论 ID 的 reaction 不受影响,因此样例中混合两种情况也没问题。
- 评论 ID 位于其
-
kind 重命名——仅当应用在 v3 中应使用不同类型名称时才设置。
-
附件——如果评论附件不在
data.attachments下,则设置attachments_field。
留意同一种 kind 携带两种不兼容的结构的情况——例如,某些 like 行在一个路径下存放评论 ID,而另一些行则存放在不同路径下。单路径键(parent_id_field、reaction_comment_id_field)无法同时处理两者。请说明映射无法覆盖哪些行,而不要默默地只选其中一种。
有些应用带有旧版布局标志——参阅 mapping-keys.md 中的 Legacy layout flags。切勿设置这些标志。
用户
样例端点返回的是 activity 和 reaction,而不是用户,因此无法据此推断用户字段映射。默认值已涵盖常见的 v2 约定:
- 名称:
name,然后是username - 图片:
image,然后是profileImage,再然后是avatar
因此,只有显示名称或头像可能存储在其他键下时才需要询问。此时设置 name / image,并记住这两个键的读取方向是从 v3 键到 v2 来源,与其他所有字符串键的方向相反。列表("image": ["avatar_url", "profileImage"])会按顺序尝试每个来源。
输出格式
仅输出映射对象:
{
"bookmarks": ["bookmark"],
"parent_id_field": "data.comment.parent.id",
"attachments_field": "media.files"
}
每个键都必须有样本依据。对于 v2 数据已符合 v3 约定的应用,空映射({})也是有效且正确的答案——请直言说明,不要为了填充内容而添加默认值。
在对象旁边,列出你有意省略的键,以及省略原因(哪些默认值已覆盖该应用的结构)。这能让读者知道,省略是经过考虑的决定,而非疏漏。
省略列表仅包含通用键。 切勿在该列表、映射或回复的任何其他位置包含旧版布局标记(参见 mapping-keys.md)——不能将其作为一行、顺带一提,甚至不能说考虑后已排除。它们与当前应用无关,无需说明:直接不提即可。
建议在映射中设置 "debug": true,用于初次验证运行——它会将当前映射写入每条已同步活动的 custom._v3sync_debug 字段。并提醒用户在确认映射后将其移除。
样本的局限性
生成映射时,请一并说明以下适用的限制。这些限制决定了这是“映射正确”,还是“映射仅适用于该样本”:
- 活动数量最多为 100 条,按插入时间抽取。 样本最多回溯 2 年,但只有较早活动才携带的字段仍可能缺失:较早时段可能没有数据或数据过多(参见
slices),也可能是最近才批量导入数据。一个映射仅适用于一种帖子格式。如果较早的帖子将文本或媒体存储在其他字段中,这些内容在 v3 中会被放入custom。数据结构随时间变化的应用尤其容易遇到这个问题。 - 反应取自有限的近期时间窗口。 最近未使用过的类型可能完全不会出现在
kinds中,因此长期运行的应用可能存在词汇覆盖不全的问题。 - 用户数据完全未抽样。 默认值覆盖
name/username和image/profileImage/avatar;任何其他键都必须另行确认。 - 写入方不只有代码。 如果映射依据的是集成代码,请说明哪些键仅有代码作为依据,并指出其他服务、使用用户令牌写入的移动客户端、脚本和旧版应用可能会写入代码中未体现的数据结构。
- Feed 分组的覆盖并不均衡。 样本无法说明活动来自哪个分组。请列出使用
--feed读取的分组,以及样本和--feed均未覆盖的分组。 - 罕见结构可能藏在尾部。 在 100 条记录中,出现率仅为 1% 的字段可能不会出现。如果映射决策取决于某个字段是否存在,请说明哪些抽样行包含该字段。
- 样本会变化。 后续重新运行时,受每种类型最多 5 条的限制,旧记录可能被替换,新结构也可能出现。因此,如果某项判断仅依据单条记录,再运行一次就能轻松验证该判断。
定稿前,请根据 mapping-keys.md 中的示例逐一检查输出的每个键。与文档示例的结构匹配,是路径和嵌套关系正确的有力依据;如果不匹配,值得再检查一次样本。
后续交接
- 关于 v3 Feeds API 的 SDK 问题(例如,某个 SDK 中的 v3 活动、评论或书签是什么样子)→
getstream docs。 - 迁移应用现有的 v2 代码 → 迁移计划(第 7 步);仅在用户提出要求时才编辑代码,并遵循该计划。
- 映射确定后,基于 v3 构建或更新应用 → 该应用对应的平台包(
stream-react、stream-swift、stream-android、stream-react-native、stream-flutter)。
支持
如果用户询问支持或如何联系相关人员,请引导他们访问 getstream.io/contact。