# 更新与迁移

插件通过 configurationVersion 标记宿主管理的配置格式，通过 configurationMigrations 提供相邻版本转换。宿主收集当前用户配置中的全部相关记录，执行迁移、校验目标格式，再统一保存。插件只处理自己的字段，设备布局、流程结构和文件存储由宿主负责。

## 声明配置版本

以下字段放在 manifest 顶层，与 apiVersion 和插件包 version 分开管理：

```json
{
  "configurationVersion": 3,
  "configurationMigrations": [2, 3]
}
```

目标版本 2 表示从配置格式 1 升到 2，目标版本 3 表示从 2 升到 3。未声明 configurationVersion 时初始格式为 1；尚未建立版本记录的原始配置也属于格式 1。这里转换的是保存数据，宿主与 SDK 使用同一份当前协议，不为旧 SDK 消息提供兼容处理。

宿主按当前用户配置和插件 ID 保存版本记录。已有配置必须有完整迁移链，缺少任一步骤会停止迁移；拒绝配置版本降级。首次安装且没有该插件的已保存配置时，直接建立当前版本记录，不对默认值执行历史转换。修改 SDK 或插件包版本本身不触发配置迁移。

## 编写转换

在 definePlugin 中注册与清单一致的目标版本。回调不接收宿主执行上下文，只返回字段操作。

```js
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);
definePlugin({
  // actions、widgetActions 等处理器仍须按完整清单注册。
  configurationMigrations: {
    2(request) {
      const has = name => Object.hasOwn(request.values, name) || request.boundFields.includes(name);
      if (request.kind === "settings") {
        return has("token") ? { rename: { token: "apiKey" } } : {};
      }
      if (request.kind === "widget" && request.contributionId === "card") {
        return { contributionId: "panel", ...(has("label") ? { rename: { label: "caption" } } : {}) };
      }
      if (request.kind === "action" && request.contributionId === "send") {
        return has("text") ? { rename: { text: "content" } } : {};
      }
      return {};
    },
    3(request) {
      if (request.kind === "settings" && typeof request.values.retries === "string") {
        const retries = Number(request.values.retries);
        if (!Number.isSafeInteger(retries)) throw new Error("无法转换重试次数");
        return { set: { retries } };
      }
      return {};
    }
  }
});
```

rename 只移动字段，不改变其值或绑定；多个改名同时应用，目标字段已有值时必须显式移除或一并改名，防止静默覆盖。不存在的原字段不能改名，应像示例一样检查可选字段。remove 删除原字段，set 最后写入字面量。转换引用字段时应使用 rename；不能取得其运行时值并进行类型转换，需要改变绑定含义时应明确拒绝迁移，保留配置等待处理。

contributionId 可改为本插件的新动作或超级块 ID；widgetAction 可同时修改 widgetId。宿主不允许迁移其他插件的标识，也不会解释普通插件参数中恰好叫 __workflow 的业务对象。

## 覆盖范围

迁移覆盖全局设置（含私密字段）、所有已保存设备的超级块设置、按钮手势、多状态按钮、预设参数、数值控件固定参数，以及流程库、按钮内嵌流程和自动触发内嵌流程中的动作参数。流程中的状态、输入和步骤结果引用保持宿主管理，插件只看到 boundFields 中的字段名。数值控件的数值参数也作为绑定字段，可随动作参数一起改名。

历史流程版本、运行历史和已打开窗口中的未保存草稿不会改写。迁移后递增设置、设备布局、流程和触发绑定的修订标识，旧窗口无法覆盖新的已保存配置；用户可保留草稿内容，重新加载后再应用。派生的 Scratch 画布缓存会失效并由迁移后的流程重建。

插件自己保存在 dataDir 的文件格式、状态来源和动作输出引用的重构由插件与调用方另行管理，不属于本接口。跨产品配置导入不在本功能范围内。

## 提交与恢复

迁移在插件普通 onStart 之前、独立的短生命周期进程中运行；不调用普通 onStart/onStop，不提供宿主 SDK 能力。迁移代码应只转换传入数据，避免外部副作用。私密字段仅提供给拥有它们的插件后台；宿主不将迁移请求、返回值或插件回调错误细节送入页面和日志。

所有记录完成转换并通过当前清单契约后，宿主提交一次配置事务。提交前检查文件是否被其他编辑修改，发生冲突时停止并保留较新的编辑。字段不合规、处理器失败或超时都不会写入部分迁移结果。应用内安装更新会保留旧包直到迁移完成；迁移失败时恢复旧包及原运行状态。开发文件夹的源文件仍由开发者或 CLI 管理。

事务日志和最近一次成功迁移前的原始文件备份位于当前用户数据目录的 plugin-configuration 下，采用受限文件权限；不要将其当作可公开的诊断附件。应用启动时先恢复未完成事务，再加载设备与流程存储。如果检测到事务文件被外部修改，保留日志并停止恢复，避免覆盖外部编辑。正常提交后不会重复执行同一配置版本，失败时可修复迁移函数后重新启动或重新安装重试。

单个迁移请求或响应上限为 128 KiB，最多处理 10000 个配置记录、128 个相邻版本步骤、2000 份存储文档，单份文档上限 16 MiB，事务日志上限 64 MiB，迁移处理总预算为 60 秒并受单次 IPC 超时约束。插件普通启动失败与配置迁移失败分别处理：已成功提交的配置不会因后续联网等启动故障而倒退。
