mcpField
S
stream-feeds-migration

stream-feeds-migration

github.com/getstream数据与数据库2072 次安装今天更新
代码仓库

关于 stream-feeds-migration

用于推导 Stream v2 到 v3 同步映射的技能,以应用实际存储的数据为依据;也可检查代码中的写入路径并规划迁移。它输出映射对象及证据说明,不会修改应用代码。

主要功能

  1. 01抽样读取已存储的活动和反应
  2. 02检查 Feed 分组与代表性数据
  3. 03对照样本检查 v2 写入代码
  4. 04生成 v3 同步映射
  5. 05编写代码迁移计划

适用场景

  1. 01准备 v2 到 v3 的同步配置
  2. 02核对已存数据中的字段与反应结构
  3. 03规划应用读写路径的迁移

安装

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

$npx skills add https://github.com/getstream/agent-skills --skill stream-feeds-migration

安装后在 Claude Code 中输入/stream-feeds-migration确认已加载。

包含的文件

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

code-inventory.md
fetch_sample.py
mapping-keys.md
porting-plan.md
scan_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:

  1. 运行 <skill-dir>/scan_v2_calls.py --root <repo> --out v2-code-scan.json,列出所有 v2 写入调用位置 (只读;片段中的机密会被遮盖——切勿自行引用代码中的机密)。
  2. 阅读这些调用位置并建立清单:每条写入路径一行,列出其动态分组、目标、字段、foreign_id 的构造方式、反应类型和数据形状、子反应及用户字段。
  3. 检查字段变更历史(git log -S'<field>'):旧动态内容仍可能保留旧格式。
  4. 将清单与示例交叉核对。两者一致即可确认某个键;仅代码中有的证据表示存在罕见或新增路径(请映射该路径,并使用 --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 不受影响,因此样例中混合两种情况也没问题。

  • 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。

评论

支持 Markdown