旧平台上已经跑通了漫剧和小说两条完整的创作工作流。新平台已具备图片生成、异步任务、用户鉴权和资源计费的基础设施。现在需要把这两条链路搬过来,但不是照搬代码,而是复用领域逻辑重新适配。
迁移看似是一个代码挪动的问题,实际上是一个架构解耦的问题。两个平台在三个维度上产生了紧密耦合,直接挪动代码会导致新平台继承旧平台的设计包袱。
三处不兼容的耦合
任务队列与调度模型
旧平台的工作流采用进程内任务 Map 和命令式 API 网关。漫剧工作流从剧本拆分镜头、生成资产图、生成视频、拼接整集——这一系列操作都是通过 __api/comic_* 这样的命令端点来驱动的,任务状态存在内存 Map 里,重启进程就丢了。
新平台设计了一套资源计费底座,任务需要经历预留(reserve)→ 执行 → 结算(settle)→ 失败时退款(refund)的流程。以小说工作流为例,每个阶段(设定、世界观、角色、分卷、大纲、章节)都是独立任务,生成前要冻结估算的算力点,生成后按实际输出字符数结算,多退少补。这套计费流程嵌入到新平台的 Billing 服务里,不能绕过。
如果我直接把旧平台的任务调度逻辑搬过来,新平台的任务会跳过 reserve 这一步,造成"已生成但余额不足无法扣费"的问题。同样,旧平台的视频任务如果失败只是标记 failed,没有退款逻辑,新平台则需要原子地调用 RefundCharge。
两边的任务模型在原语层面就不兼容。
存储路径与对象约定
旧平台用本地 JSON 文件存储小说项目数据。小说作品、设定、世界观、角色这些结构化阶段的原始输出被序列化到磁盘上,依赖一套约定好的目录结构。漫剧项目也类似,源项目中通过 featureDir(userId, 'comic') 这样的函数来确定资产文件的存储根路径。
新平台统一使用 S3 / OOS 对象存储加 Prisma 数据库。项目的元数据(作品标题、阶段状态、版本历史)存 Prisma 表,生成的内容(图片、视频、结构化文本)存对象存储并记录 key。例如小说的 NovelSection 表存储化后的 JSON 结构和展示文本,章节正文存在 NovelChapterVersion 表的 content 字段。
如果我从旧平台直接挪过来一套"读本地 JSON"的逻辑,新平台就要维护两套存储系统。更重要的是,新平台的计费系统依赖持久化的结构化数据——只有把规范化后的展示文本存进表里,后台才能追溯某次扣费对应的具体内容。
计费模型与资源定价
旧平台对小说的计费可能是按生成 token 次数、按模型调用、或者干脆不计费。新平台设计了一套通用资源计费框架:每个资源有一个 key(例如 image_generation、novel_text_output),配套一个定价模型(PER_CALL 或 PER_UNIT),后台可以随时调价。
对小说工作流,新平台的选择是按可见字符数计费,不按 token。这意味着:
- 用户最终看到的文本去掉空白后的字符数才会被计入。
- 模型原始输出如果是 JSON 格式,那么
{}括号、字段名、引号、逗号、缩进都不算——只计算最终展示的内容。 - 每个阶段的计费单位是
算力点 / 千字,后台可配置,用户无感知。
从旧平台的某种计费方式切换到这套新模型,需要重新梳理每个环节的计费触发点。如果只是把旧的生成函数搬过来调用一遍,新平台的 Billing 根本没有机会介入。
迁移方案的抽象层次
面对这三处耦合,如果采取"完全抽象"的思路——把工作流定义成一个通用的步骤编排引擎,每个平台只需要实现 execute_step、store_result、charge_resource 这样的抽象接口——理论上很优雅,但代价是引入了一层不必要的复杂性。通用引擎要支持各种平台的差异,势必要留下很多可配置项,导致理解和维护难度上升。
实际采取的方案是有选择的复用:
复用稳定部分:工作流的步骤编排和领域逻辑。小说工作流的七个阶段顺序(设定 → 宏观 → 世界观 → 角色 → 分卷 → 拆章 → 正文)是领域知识,与平台无关。这套提示词组装、JSON 解析、规范化、展示文本提取的逻辑,从旧平台完整迁移到新平台,TypeScript 化但不改核心算法。漫剧工作流的四阶段(剧本 → 资产 → 分镜 → 成片)也是如此。
重新实现易变部分:任务调度、存储、计费。小说模块新增 NovelProject、NovelSection、NovelChapter、NovelTask 等 Prisma 模型,完全按新平台的设计来。生成任务在开始前调用 billing.reserveResource(),成功后提取展示文本、调用 billing.settleResource(),失败时退款。这套接口与 Image Generation 任务的流程完全一致,在平台已有的基础上建造。
具体的模型调用、LLM 的参数、生成的超时策略这些"具体模型调用"的细节,留在各自的适配层。例如小说模块的 novel-generation.ts 只负责组装提示词、调用 LLM、解析结果,不涉及数据库操作;数据库操作交给 novel-service.ts。这样领域逻辑与平台逻辑的边界清晰,后续不同平台可以并行维护。
关键的取舍决定
完全抽象的诱惑在于"一套代码支持多平台"的承诺。但这需要引入足够的可配置性和接口设计,而代价是灵活性反而下降——当某个平台需要特殊处理某个步骤时,通用引擎不得不打补丁。这在两个平台差异较大的情况下尤其成立。
选择有针对性的复用,意味着承认重复:数据模型要各写一份,任务调度逻辑要各写一份。但这个重复是可控的,因为它们在各自平台内部是一致的。小说模块的任务预留/结算逻辑完全复用 Image Generation 已有的那套,不需要新增抽象层。
另一个关键的取舍是用户模型的配置。旧平台可能让用户选择小说生成用哪个模型,新平台则完全隐藏模型选择,由后台配置或环境变量决定。这简化了前端和 API 的设计——用户请求体里根本不接受 model 字段,Billing 也不需要按模型定价。代价是失去了"用户自行选择成本与质量的平衡"的灵活性,但换来了计费模型的清晰和后台的可控。
这类决定需要在迁移前明确:哪些能力是目标平台"必须有"的,哪些是"很有但可以先不做"的。漫剧工作流迁移时明确排除了白模(3D 白模式)视频相关的所有功能,不复制 Blender 依赖、不暴露白模提示词输入、成片阶段只处理普通视频。这个决定减少了迁移的复杂性,也避免了在新平台上重新部署 Blender 和白模渲染的基础设施。
稳定部分与易变部分的边界
这个分法的关键在于,稳定部分要确实稳定。领域逻辑(各阶段的提示词、章节的上下文组装、长篇的记忆管理)在两个平台上是一样的,因为它反映的是小说创作或漫剧创作的规律。但是,一旦涉及"这个阶段的输入从哪里读、输出存到哪里、失败后怎么处理",就已经是平台相关的。
以小说的世界观生成为例:
稳定部分是 worldPrompt(previousSections, projectSettings) 这个函数,它组装 LLM 提示词,描述"根据设定和前序阶段的输出,生成世界观"。这个函数与平台无关。
易变部分是:
- 生成前如何预留算力点(新平台调
billing.reserveResource,旧平台可能不调) - 生成结果是一个 JSON 对象,如何从中提取展示文本(两个平台的数据模型可能不同,但提取逻辑应该是一样的——属于稳定部分)
- 提取后的展示文本存到哪里(新平台的
NovelSection.displayText字段,旧平台可能是本地文件sections/world.json) - 失败时如何处理(新平台调
billing.refundResource,旧平台可能是清除临时文件)
这样划分后,新平台只需要在适配层(novel-routes.ts、novel-service.ts)重新实现存储和计费部分,核心生成逻辑(novel-generation.ts、novel-prompts.ts)从旧平台迁移过来。
实际的迁移清单
从这个分析,迁移的具体工作清单变成:
- 评估可复用的代码 — 在旧平台找出真正与平台无关的部分。对小说工作流,这包括提示词、JSON 解析、规范化逻辑。对漫剧工作流,这包括分镜拆分的逻辑、资产提取的规则。
- 定义新平台的合同 — 每个工作流步骤的输入输出是什么,资源消耗如何计量。小说阶段的输出是规范化 JSON 加展示文本,消耗的资源是可见字符数。漫剧镜头的输出是镜头配置和首帧图片,消耗的资源是视频秒数(按模型和分辨率估算)。
- 实现平台适配层 — 数据模型(Prisma 表)、任务调度(与 Billing 集成)、错误处理和重试。这部分代码是新平台特有的,不能也不应该复用。
- 测试边界 — 在适配层写测试,验证计费逻辑、任务状态机、错误恢复。在稳定部分写测试,验证生成质量、规范化正确性。
不采用这样的分层,而是直接把旧平台的 Next.js 路由、本地文件操作、命令式 API 端点搬到新平台,结果是新平台沦为旧平台代码的容器。后续需要升级某个依赖、调整计费模型、或对接新的生成模型时,都会发现改一个地方影响到另一个地方。
设置清晰的边界,允许有选择的重复,是避免这个问题的方法。
■