Dify 跨环境应用迁移实战:用迁移包在工作区之间搬运 Workflow、Chatflow 与自定义工具
当团队把 Dify 从测试环境推向生产环境(或从一套自托管集群迁移到另一套),Workflow 应用、Chatflow 应用及其依赖的自定义工具往往无法仅靠 DSL 文件简单复制。本文基于 Dify 仓库的跨环境迁移指南与配套源码,完整讲解如何用“迁移包(migration package)”把应用与工具依赖从一个环境搬到另一个:包括推荐的向导式导出 + 导入流程、可选的脚本化自动导出、全部命令行参数、导出配置字段、引用工具自动发现机制、密钥(secrets)处理策略与导入冲突策略的取舍。读完本文,你可以独立执行一次完整的跨环境迁移,看懂导入报告中的每一项状态,并从源码层面理解 ID 映射与引用重写的实现原理。
迁移包能带走什么
迁移包(JSON 文件)可以包含以下资源(参见 docs/cross-env-app-migration/README.md 的 “What Gets Migrated” 一节):
- Workflow 应用与 advanced-chat(Chatflow)应用:以 DSL 内容形式写入包的
workflows分区; - 自定义 API 工具提供者(Custom API tool providers):写入
tools分区; - Workflow 工具提供者(Workflow tool providers):即把某个 Workflow 应用发布为工具,写入
workflow_tools分区; - MCP 工具提供者:仅在显式开启
include_secrets时以完整提供者数据导出,否则仅记录为依赖元数据,写入mcp_tools或dependencies分区; - 依赖元数据:内置工具(built-in tools)与插件工具(plugin tools)不会被序列化为迁移数据,而是记录为“必须已在目标环境中存在或配置好”的依赖项,写入
dependencies分区。
两条硬性约束:
- 仅支持单源工作区(single source workspace)导出。包的
metadata.source_scope必须为single,源码中MigrationMetadata.from_mapping遇到其他取值会直接抛出MigrationDataError(entities.py)。 - 源与目标工作区名称不需要一致。
--target-tenant允许在导入时指向与源不同的工作区,这正是该机制支持“staging → production 镜像迁移”的关键。
从源码结构看,迁移包的完整形状由 MigrationPackage 定义,包含 metadata(版本、源工作区、可选目标选择器、include_secrets、import_options)以及 workflows、tools、workflow_tools、mcp_tools、dependencies 五个列表分区。包版本常量固定在 package_service.py 的 PACKAGE_VERSION = "1",导入时 load_package 会校验版本,遇到不支持的版本直接报错,因此不同大版本之间不保证兼容。
推荐流程:向导导出 + 导入
官方推荐的迁移路径是(来自文档 “Recommended Flow” 一节):
- 在源环境运行导出向导;
- 在目标环境导入生成的迁移包。
向导方式之所以被推荐,是因为它会列出可用应用与工具、自动发现应用依赖、在写盘前打印摘要(summary),并且与脚本化导出共用同一个导出服务 MigrationExportService,两条路径产出的包结构一致。
第一步:运行导出向导(源环境)
cd api
source .venv/bin/activate
uv run flask app-migration-wizard
该命令没有 CLI 参数,全部通过交互式输入完成。对应源码是 data_migration.py 中 @click.command("app-migration-wizard") 装饰的 migration_data_wizard 函数。按源码实现,向导依次询问以下内容:
| 交互项 | 说明 | 默认值 |
|---|---|---|
Source tenant |
选择源工作区,从按名称排序的租户列表中按编号选择一个 | 1 |
App selection |
选择要导出的应用;仅列出 workflow 与 advanced-chat 两种模式的应用(SUPPORTED_WIZARD_APP_MODES)。输入 all、单个编号或逗号分隔的编号 |
all |
Automatically export tools referenced by selected apps? |
自动发现所选应用引用到的工具,推荐选择 yes。向导会导出每个应用的 DSL(AppDslService.export_dsl)并扫描其中的工具节点与 agent 工具配置,自动纳入引用的自定义 API 工具、workflow 工具与 MCP 工具引用 |
y |
Export additional tools manually? |
手动追加未被所选应用引用的工具(三类分别列出,已自动发现的条目会带 [auto] 标记) |
n |
Include secrets in output JSON? |
是否输出密钥。no 时:workflow/app DSL 密钥被省略或脱敏、API 工具凭据被剔除、MCP 提供者仅以依赖元数据记录;yes 时:输出 JSON 应视为敏感文件 |
n |
Create or reuse app API tokens during import? |
导入时为没有 API token 的已导入应用创建 token,或复用已有 token | n |
Import ID strategy |
preserve-id 或 generate-new-id |
preserve-id |
Import conflict strategy |
fail、skip 或 update(注意:向导默认是 update,而包与脚本模板默认是 fail) |
update |
Output path |
输出路径,默认 migration-data-YYYYMMDD-HHMMSS.json |
时间戳文件名 |
Overwrite existing output |
仅当输出文件已存在时才询问 | n |
Write migration package? |
打印摘要后的最终确认,选择 n 会以 click.Abort 中止且不写盘 |
y |
向导最后一步在写盘前调用 _confirm_wizard_summary 打印源租户、选中应用清单、工具选择([auto]/[manual] 分类)、include secrets、ID/冲突策略与输出路径,确认后才真正调用导出服务并写文件,然后渲染导出报告。
第二步:导入迁移包(目标环境)
把生成的 JSON 包复制到目标环境后执行:
cd api
source .venv/bin/activate
uv run flask import-app-migration \
--input migration-data-20260528-120000.json \
--target-tenant "production Workspace"
完整选项(对应源码 import_migration_data):
--input(必填):迁移包 JSON 路径,可以是向导或脚本化导出生成的包;--target-tenant:目标工作区名称或 UUID。仅当包元数据已包含target_tenant时可省略。CLI 值覆盖包元数据,官方建议对“可复用的包”始终显式传入;--operator-email:目标工作区内作为导入操作者的账号邮箱。省略时,导入服务自动选择目标租户中最早创建的 owner 账号(ImportTargetResolver.resolve);--id-strategy:可选覆盖包内import_options.id_strategy,取值:preserve-id:在目标服务支持时保留源应用/工具 ID,是跨环境迁移的推荐默认,便于保持 workflow 引用的稳定;generate-new-id:由目标环境生成新 ID,并通过迁移 ID 映射(ID mapping)重写引用;
--conflict-strategy:可选覆盖包内冲突策略,取值:fail:遇到第一个已存在的目标资源冲突即停止,已提交的资源不会回滚;skip:保留目标已有资源,跳过该资源的导入;update:就地更新目标已有资源;
--create-app-api-token-on-import/--no-create-app-api-token-on-import:可选覆盖导入时的应用 API token 创建行为。
这些覆盖选项的合并逻辑见 _build_options_override:只有显式传入的项才覆盖包内 import_options,未传入的项沿用包内默认值。
导入完成后会打印一份报告(由 MigrationReportService 渲染),内容包括:解析出的目标租户与操作者、各资源的 created / updated / skipped / unresolved / dependency-only 状态、未解析(unresolved)依赖、应用 API token 的创建/复用数量,以及用于重写引用的 ID 映射表。报告中凡是标记为 dependency-only、skipped、unresolved 的条目,通常都需要人工跟进。
可选流程:脚本化导出(用于可重复自动化)
当你需要可重复、可纳入 CI 或发布流水线的导出时,官方给出四步脚本化路径(来自文档 “Optional Flow” 一节):
- 生成导出配置模板;
- 编辑模板;
- 运行脚本化导出;
- 导入生成的包。
模板命令
cd api
source .venv/bin/activate
uv run flask app-migration-template --output export-config.json
选项(对应 export_migration_data_template):
--output(可选):写出模板 JSON 的路径;省略时模板打印到 stdout;--overwrite(可选):允许替换已存在的输出文件。不带该选项且--output已存在时命令直接失败。
脚本化导出命令
cd api
source .venv/bin/activate
uv run flask export-app-migration \
--input export-config.json \
--output migration-package.json
选项:
--input(必填):导出配置 JSON 路径;--output(必填):迁移包 JSON 输出路径;--overwrite(可选):允许替换已存在的输出包,否则已存在即失败。
随后用与前面完全相同的 import-app-migration 命令导入该包。
导出配置字段详解
app-migration-template 生成的模板形状如下(模板构造函数见 _scripted_export_template):
{
"source_tenant": {
"mode": "single",
"id": "",
"name": "admin's Workspace"
},
"apps": {
"modes": ["workflow", "advanced-chat"],
"ids": [],
"all": true
},
"include_referenced_tools": true,
"additional_tools": {
"api_tools": [],
"workflow_tools": [],
"mcp_tools": []
},
"include_secrets": false,
"import_options": {
"create_app_api_token_on_import": false,
"id_strategy": "preserve-id",
"conflict_strategy": "fail"
}
}
各字段含义(解析逻辑见 ExportConfigParser):
| 字段 | 说明 |
|---|---|
source_tenant.mode |
必须为 single,其他取值报错 |
source_tenant.id |
可选的源工作区 UUID。当存在同名工作区、或希望做严格源选择时填写。若设置了 id,则 ID 与 name 必须匹配,否则导出失败(MigrationExportService._get_tenant 会做一致性校验) |
source_tenant.name |
必填的源工作区名称。仅按名称查询且命中多个同名工作区时,导出会报 “name is ambiguous”,提示改用 source_tenant.id |
apps.modes |
可选的应用模式校验列表,仅支持 workflow 与 advanced-chat,出现其他值报错 |
apps.ids |
当 apps.all 为 false 时,指定要导出的应用 ID 列表。列出的 ID 若不存在或不属于受支持模式,会明确报出缺失的 ID |
apps.all |
true 时导出所选源工作区内的全部受支持应用;false 时仅导出 apps.ids |
include_referenced_tools |
推荐 true。自动发现所选 workflow/chatflow 应用引用的工具 |
additional_tools.api_tools |
追加导出的自定义 API 工具提供者名称列表 |
additional_tools.workflow_tools |
追加导出的 workflow 工具提供者 ID 列表 |
additional_tools.mcp_tools |
追加导出的 MCP 提供者 ID 或 server identifier 列表 |
include_secrets |
默认 false。为 false 时凭据被剔除、MCP 提供者仅记为依赖元数据;为 true 时自定义 API 凭据、workflow/app DSL 密钥值、完整 MCP 连接数据会写入包 |
import_options.create_app_api_token_on_import |
默认 false,包级别的导入 token 创建默认值 |
import_options.id_strategy |
包级别默认 ID 策略:preserve-id / generate-new-id |
import_options.conflict_strategy |
包级别默认冲突策略:fail / skip / update |
从源码结构看,ExportConfigParser.parse 还兼容若干旧字段名:apps.ids 缺省时回退读顶层 workflows、export_all_apps 回退读 export_all_workflows、各类工具列表回退读顶层 tools / workflow_tools / mcp_tools(export_service.py),这属于对早期包结构的向后兼容路径,新配置建议统一使用模板字段。
引用工具自动发现:向导与脚本共用同一机制
文档 “Referenced Tools” 一节的要点是:尽量使用自动引用工具发现。开启后,Dify 会扫描所选 workflow/chatflow 的图(graph)与 agent 工具配置,对发现的自定义 API 工具、workflow 工具、MCP 工具引用去重后再导出,从而降低“导入的应用引用了不存在的 provider”的概率。
对应实现是 DependencyDiscoveryService:
- 它遍历 DSL 中
graph.nodes与workflow.graph.nodes两类节点布局; - 对
tool类型节点读取节点data中的工具配置,对agent类型节点读取tools列表或agent_parameters.tools.value中的每个工具配置(dependency_discovery_service.py); - 按
provider_type归类:api/custom/api_tool→ API 工具;workflow/workflow_tool→ workflow 工具;mcp→ MCP 工具;其余一律归为builtin_or_plugin_tool( _kind_from_provider_type),并按(kind, provider_id)去重。
两个重要的边界行为:
- 内置工具与插件工具永不被序列化为迁移数据。它们只作为依赖元数据写入包的
dependencies分区,报告状态为dependency-only,提示词来自 _dependency_message:“Ensure the built-in or plugin tool exists in the target environment.” 你必须确保目标环境已安装并配置好这些工具。 - MCP 提供者同样是 dependency-only(除非
include_secrets=true)。若未导出 MCP 密钥,需要在目标工作区手动配置对应 MCP 提供者,再运行迁移后的 workflow。导入阶段还会做前置检查: _preflight_dependency_only_mcp 会在目标租户中查找每个依赖型 MCP 提供者,找到则报available,找不到则报skipped并列出引用它的具体 workflow 与工具节点,明确提示 “configure it manually before running the workflow.”
另外,向导在发现工具后还会做名称解析:_discover_auto_tools 用 include_secret=False 的 DSL 导出触发发现,随后 _resolve_auto_tool_names 按租户内 provider 表把发现结果解析为“名称 + ID”,MCP 工具则支持 server_identifier 匹配,保证摘要打印出的都是可读的 provider 名称。
密钥(Secrets)处理
安全默认值是 include_secrets: false。只有在你拥有受控的 JSON 包传输通道时才应开启密钥导出。从源码看,include_secrets=false 的具体行为包括:
- 自定义 API 工具: _export_api_tools 以
mask=True调用ToolManager.user_get_api_provider并显式pop("credentials"),即只导出 provider schema(参数结构、认证类型等),不含任何凭据值; - Workflow/app DSL:
AppDslService.export_dsl(include_secret=selection.include_secrets),false时 DSL 中的密钥类值被省略或脱敏; - MCP 提供者:走 _record_dependency_metadata 分支,只记依赖;开启
include_secrets=true时则通过 _serialize_mcp_provider 写出解密后的server_url、headers、authentication(client_id/client_secret)、超时配置与缓存的工具列表。
因此,启用 include_secrets: true 后,包中可能包含 API 工具凭据、workflow/app DSL 密钥值、MCP server URL、MCP headers、认证数据与缓存的 MCP 工具列表——该 JSON 应按机密文件对待。
FAQ(继承自官方文档)
问:include_secrets=false 时会发生什么?
这是推荐默认值,但它意味着迁移包对敏感运行时配置是故意不完整的:
- MCP 工具不会作为完整工具提供者导出,仅记录为依赖元数据;
- 自定义 API 工具凭据不导出。Provider schema 可以导出,但凭据必须在目标工作区重新配置;
- Workflow/app DSL 密钥被省略或脱敏。导入后需要检查目标工作区中的每个应用变量、凭据与密钥相关配置;
- 内置与插件工具永远不作为自定义迁移数据序列化,仅记为依赖。
处理办法:
- 导入前,先在目标环境安装/启用所需的内置或插件工具;
- 对 MCP 工具,先在目标工作区手动创建或配置对应 MCP 提供者,再运行迁移后的 workflow;
- 对自定义 API 工具,在目标工作区打开导入后的 provider 重新填写凭据;
- 导入后、投产前逐个复查每个迁移的 workflow/chatflow,重点关注工具节点、agent 工具配置、环境变量、应用变量与凭据类设置;
- 使用导入报告:标记为
dependency-only、skipped、unresolved的条目通常需要人工跟进。
如果你确实需要包携带完整 MCP 配置与凭据,把 include_secrets 设为 true 重新导出,并将生成的 JSON 作为敏感数据传输。
问:include_secrets=true 时需要考虑什么?
包中可能包含上述全部敏感运行时配置。处理建议:
- 把 JSON 包按机密存储与传输;
- 若安全策略要求,导入完成后删除包文件;
- 优先只用于“受控的一次性迁移”场景——即目标侧手工配置凭据的成本高于保护包成本的情形。
问:应该用 preserve-id 还是 generate-new-id?
ID 策略控制导入的资源是保留源 ID 还是在目标环境获得新 ID,适用于目标服务支持显式 ID 的 workflow 应用与自定义工具资源(策略枚举见 entities.py)。
默认用 preserve-id,推荐用于跨环境应用迁移:导入的应用与工具尽量保留源 ID,workflow 引用更容易保持原样。适用场景:
- 两个环境本应互为镜像(如 staging → production);
- 你希望 workflow 工具引用、provider 引用尽可能稳定;
- 目标环境中不存在与源 ID 相同但无关的资源。
需要注意:目标已存在同 ID 资源时,fail 会停止导入、skip 保留目标资源、update 就地更新;如果该 ID 在目标环境被复用于不同资源,使用 update 前务必仔细核对。
用 generate-new-id 的场景:目标环境应保留自己的本地 ID;例如目标已有可能与源 ID 冲突的资源、你是把包作为“副本”而非“镜像”导入、或不想在目标环境保留源数据库 ID。需要注意:
- 导入会记录源到目标的 ID 映射,务必复查导入报告中的 ID mappings;
- Workflow DSL 中的 provider 引用会尽可能通过该映射被重写(实现见 _rewrite_workflow_dsl_provider_ids,它会遍历
graph.nodes与workflow.graph.nodes的data.provider_id以及 agent 工具配置); - 依赖元数据型资源(如
include_secrets=false导出的 MCP 提供者)仍需在目标侧手工配置; - 内置/插件工具不会被重映射为迁移的自定义资源,目标环境必须自行提供。
导入执行顺序与源码级细节
理解以下执行细节,能帮助你预测导入报告的内容与失败点。核心实现在 MigrationImportService.import_package,其顺序是刻意为之的依赖拓扑序:
- 解析导入目标(先于任何写入):
ImportTargetResolver按 “CLI--target-tenant> 包元数据target_tenant” 的顺序解析目标工作区,支持 UUID 或名称;名称存在歧义(多个同名工作区)时直接报错(import_service.py)。操作者账号未指定时取最早创建的 owner。 - 导入 API 工具(
package.tools):按 provider 名称查目标租户中的现有提供者;不存在则经ApiToolManageService.create_api_tool_provider创建,存在且conflict_strategy=update则调用update_api_tool_provider就地更新。注意 API 工具的“已存在”判定按名称而非 ID,preserve-id并不直接写入源 UUID,目标侧记录映射。 - 导入 MCP 工具(
package.mcp_tools,仅含密钥时非空):按server_identifier(或 UUID)查现有提供者,创建或更新;更新时会保留目标已存储的 identity_mode,避免静默重置转发行为(import_service.py),并把包中缓存的工具列表写回 provider 且标记authed=True(_restore_mcp_provider_tools)。 - MCP 依赖前置检查:对依赖型 MCP 条目输出
available/skipped报告。 - 先导入 workflow 工具引用的源应用:如果包的
workflow_tools引用了尚未导入的应用,会先把这些“被引用的 workflow 应用”单独导入(only_app_ids分支),且导入 workflow 工具前会确保其宿主应用已发布——必要时自动调用WorkflowService.publish_workflow以 “Migration import” 名义发布( _ensure_workflow_app_is_published)。 - 导入 workflow 工具(
WorkflowToolManageService.create_workflow_tool/update_workflow_tool)。 - 导入其余应用(跳过第 5 步已导入的),每个应用通过
AppDslService.import_app以yaml-content模式导入 DSL;preserve-id且应用不存在时以源app_id作为import_app_id建应用,存在且策略为update时以现有应用 ID 覆盖导入;fail策略下遇到已存在应用会抛出MigrationDataError(import_service.py)。 - 应用 API token:开启时经 _create_or_reuse_app_api_token,已有
APP类型 token 则复用,否则用ApiToken.generate_api_key("app", 24)新建。
再次强调 conflict_strategy=fail 的语义:在第一个冲突处停止,之前已提交(committed)的资源不会回滚。因此对不确定的目标环境,建议先用 fail 试跑看报告,再决定用 skip 或 update 正式执行;也可以先用 fail 探测、通过 --conflict-strategy 覆盖包内默认值复跑。
相关源码与测试入口
- 命令实现(向导、模板、导出、导入四个 flask 命令):api/commands/data_migration.py
- 命令注册:api/extensions/ext_commands.py(
export_migration_data、export_migration_data_template、import_migration_data、migration_data_wizard) - 包结构实体与校验:api/services/data_migration/entities.py
- 导出服务与配置解析:api/services/data_migration/export_service.py
- 导入服务、目标解析与 ID 重写:api/services/data_migration/import_service.py
- 引用工具发现:api/services/data_migration/dependency_discovery_service.py
- 包读写与版本常量:api/services/data_migration/package_service.py
- 单元测试:api/tests/unit_tests/commands/test_data_migration_commands.py、api/tests/unit_tests/commands/test_data_migration_wizard.py
- 官方指南原文:docs/cross-env-app-migration/README.md
快速参考
| 场景 | 命令 |
|---|---|
| 交互式迁移(推荐) | 源环境 uv run flask app-migration-wizard → 目标环境 uv run flask import-app-migration --input <包> --target-tenant "<工作区>" |
| 生成脚本化导出配置 | uv run flask app-migration-template --output export-config.json |
| 可重复自动化导出 | 编辑配置后 uv run flask export-app-migration --input export-config.json --output migration-package.json |
| 覆盖包内导入策略 | 导入时追加 --id-strategy、--conflict-strategy、--create-app-api-token-on-import |
| 携带密钥迁移(受控通道) | 配置/向导中 include_secrets: true,按机密文件传输并在导入后按需删除包 |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00