import { app } from "../../../scripts/app.js"; const LEGACY_NETWORK_ROUTES = new Set(["全球加速", "CF加速", "美国直连"]); const GPT_IMAGE_ROUTE_ALIASES = new Map([ ["gpt-image-2-特价", "畅速"], ["gpt-image-2-官方", "专线"], ["gpt-image-2-次卡", "畅速"], ["gpt-image-2-按量", "专线"], ["gpt-image-2-c", "畅速"], ["gpt-image-2-c-sp", "畅速"], ["gpt-image-2-c-sd", "直连"], ["gpt-image-2", "专线"], ]); const GPT_IMAGE_NODE_TYPES = new Set(["O1keyGPTImage", "O1keyGPTImageBatch"]); const GPT_IMAGE_MODELS = new Set([ "gpt-image-2", "gpt-image-2.5-sunburst", "gpt-image-2.5-flare", ]); const GPT_IMAGE_ROUTES = new Set(["畅速", "直连", "专线"]); const GPT_IMAGE_RESIZE_MODES = new Set(["不缩放", "智能缩放"]); const GPT_IMAGE_COLOR_CORRECTION_MODES = new Set(["不纠正", "智能纠正"]); const UNIFIED_COLOR_CORRECTION_MODES = new Set(["不纠正", "智能纠正"]); const GPT_IMAGE_BACKGROUNDS = new Set(["auto", "transparent", "opaque"]); const GPT_IMAGE_MODERATION_MODES = new Set(["自动", "低"]); const GPT_IMAGE_BATCH_OUTPUT_FORMATS = new Set(["原始", "JPEG", "PNG", "WebP"]); const GPT_IMAGE_BATCH_NAMING_RULES = new Set(["和原始图片名保持一致", "自然数字"]); function findLastIndex(values, predicate) { for (let index = values.length - 1; index >= 0; index--) { if (predicate(values[index], index, values)) return index; } return -1; } function batchGPTImageRandomSelectionIndex(wv) { const match = /^([1-5])个路径$/.exec(String(wv[6] ?? "")); if (!match) return -1; const pathCount = Number(match[1]); const pairingWidgetCount = pathCount >= 2 ? 1 : 0; return 7 + pathCount + pairingWidgetCount; } function batchGPTImageConcurrencyIndex(wv) { const randomSelectionIndex = batchGPTImageRandomSelectionIndex(wv); return randomSelectionIndex < 0 ? -1 : randomSelectionIndex + 1; } const SEEDANCE_AUTO_PASS_MODELS = new Set([ "seedance 2.0", "seedance 2.0 fast", "seedance 2.0 mini", "seedance 2.5", ]); const SEEDANCE_AUTO_PASS_ROUTES = new Set(["国内", "海外", "海外HC", "海外破限高并发", "海外破限", "海外破限标准", "海外标准"]); const SEEDANCE_AUTO_PASS_NODE_TYPES = new Set(["SeedanceAutoPass", "SeedanceAutoPassBatch"]); const SEEDANCE_ASSET_CREATE_TOGGLES = new Set(["关闭", "打开"]); const SEEDANCE_ASSET_CREATE_MODES = new Set(["标准", "高并发", "HC"]); const SEEDANCE_AUTO_PASS_DEFAULT_MODE = "多模态"; const SEEDANCE_AUTO_PASS_MODE_ALIASES = new Map([ ["多模态参考生视频", "多模态"], ["文生视频", "多模态"], ["图生视频-首帧", "首尾帧"], ["图生视频-首尾帧", "首尾帧"], ]); const LEGACY_BATCH_MODELS = new Map([ ["seedance 2.0 海外版(高并发)", ["seedance 2.0", "海外"]], ["seedance 2.0 fast 海外版(高并发)", ["seedance 2.0 fast", "海外"]], ["seedance 2.0 mini 海外版(高并发)", ["seedance 2.0 mini", "海外"]], ["seedance 2.0 海外版", ["seedance 2.0", "海外"]], ["seedance 2.0 海外版(破限)", ["seedance 2.0", "海外"]], ["seedance 2.0 fast 海外版", ["seedance 2.0 fast", "海外"]], ["seedance 2.0 fast 海外版(破限)", ["seedance 2.0 fast", "海外"]], ["seedance 2.0 mini 海外版", ["seedance 2.0 mini", "海外"]], ["seedance 2.0 mini 海外版(破限)", ["seedance 2.0 mini", "海外"]], ]); const GLOBAL_ROUTE_NODE_TYPES = new Set([ "NanoBanana", "BatchNanoBananaPro", "MiniMaxH3Video", "K3Video", "K3MotionControl", "Google31Video", "O1keyGPTImage", "O1keyGPTImageBatch", "O1keyGrokImage", "O1keyGrokVideo", "O1keyGrokVideoEdit", "SeedanceMultiModal", "SeedanceElementCreate", "SeedanceAutoPass", "SeedanceAutoPassBatch", "UniversalLLMChat", ]); const MINIMAX_H3_MODELS = new Set(["MiniMax-H3", "MiniMax-H3-MAX"]); const OMNI_FLASH_MODELS = new Set(["omni_flash_8s", "omni_flash_10s"]); const GROK_VIDEO_MODES = new Set(["文生视频", "图生视频", "参考生视频"]); const GROK_VIDEO_EDIT_MODES = new Set(["编辑视频", "续写视频"]); const LEGACY_GROK_VIDEO_MODELS = new Map([ ["grok-imagine-video-1.5-preview", "grok-imagine-video-1.5"], ["grok-imagine-1.0-video", "grok-imagine-video"], ]); function findLegacySeedanceAssetWidgets(wv) { const modelIndex = wv.findIndex(value => SEEDANCE_AUTO_PASS_MODELS.has(value)); if (modelIndex < 0) return -1; const autoCreateIndex = modelIndex + 6; return SEEDANCE_ASSET_CREATE_TOGGLES.has(wv[autoCreateIndex]) && SEEDANCE_ASSET_CREATE_MODES.has(wv[autoCreateIndex + 1]) ? autoCreateIndex : -1; } const NANO_BANANA_MODELS = new Set([ "Nano Banana Pro", "Nano Banana 2 Lite", "Nano Banana 2", "Nano Banana", ]); const NANO_BANANA_ROUTE_ALIASES = new Map([ ["特价", "畅速"], ["官方", "专线"], ]); const NANO_BANANA_ROUTES = new Set(["畅速", "直连", "专线"]); const NANO_BANANA_RESOLUTIONS = new Set(["512", "1K", "2K", "4K"]); const NANO_BANANA_NODE_TYPES = new Set(["NanoBanana", "BatchNanoBananaPro"]); const REMOVED_NANO_BANANA_RESOLUTIONS = new Set(["512", "512px"]); const NANO_BANANA_ASPECT_RATIOS = new Set([ "智能", "1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9", ]); const BATCH_NANO_PAIRING_ALIASES = new Map([ ["按相同图片命名", "相同文件名"], ["1*N", "全匹配"], ]); const BATCH_NANO_IMAGE_FORMATS = new Set(["原始", "JPEG", "PNG", "WebP"]); const BATCH_NANO_NAMING_RULES = new Set(["和原始图片名保持一致", "自然数字"]); const NANO_BANANA_RESIZE_MODES = new Set(["不缩放", "智能缩放"]); const NANO_BANANA_COLOR_CORRECTION_MODES = new Set(["不纠正", "智能纠正"]); function isCurrentGPTImageBatchWidgetLayout(wv) { const namingIndex = findLastIndex(wv, value => GPT_IMAGE_BATCH_NAMING_RULES.has(value)); if (namingIndex < 1) return false; const formatIndex = namingIndex - 2; const backgroundIndex = namingIndex - 1; const savePathIndex = namingIndex + 1; const resizeIndex = namingIndex + 2; const seedIndex = namingIndex + 3; const randomSelectionIndex = batchGPTImageRandomSelectionIndex(wv); return GPT_IMAGE_BATCH_OUTPUT_FORMATS.has(wv[formatIndex]) && GPT_IMAGE_BACKGROUNDS.has(wv[backgroundIndex]) && typeof wv[savePathIndex] === "string" && GPT_IMAGE_RESIZE_MODES.has(wv[resizeIndex]) && Number.isInteger(wv[seedIndex]) && formatIndex === randomSelectionIndex && seedIndex === wv.length - 1; } function isGPTImageBatchWidgetLayoutWithRandomSelection(wv) { const namingIndex = findLastIndex(wv, value => GPT_IMAGE_BATCH_NAMING_RULES.has(value)); if (namingIndex < 1) return false; const formatIndex = namingIndex - 2; const backgroundIndex = namingIndex - 1; const savePathIndex = namingIndex + 1; const resizeIndex = namingIndex + 2; const seedIndex = namingIndex + 3; const randomSelectionIndex = batchGPTImageRandomSelectionIndex(wv); const concurrencyIndex = batchGPTImageConcurrencyIndex(wv); const hasLegacyConcurrency = Number.isInteger(wv[concurrencyIndex]) && formatIndex === concurrencyIndex + 1; const hasRandomSelection = wv[randomSelectionIndex] === "关闭" || (typeof wv[randomSelectionIndex] === "string" && /^[1-5](?:[,,、 ]+[1-5])*$/.test(wv[randomSelectionIndex])); return GPT_IMAGE_BATCH_OUTPUT_FORMATS.has(wv[formatIndex]) && GPT_IMAGE_BACKGROUNDS.has(wv[backgroundIndex]) && typeof wv[savePathIndex] === "string" && GPT_IMAGE_RESIZE_MODES.has(wv[resizeIndex]) && Number.isInteger(wv[seedIndex]) && hasRandomSelection && (formatIndex === concurrencyIndex || hasLegacyConcurrency) && seedIndex === wv.length - 1; } function isLegacyGPTImageBatchWidgetLayout(wv) { const namingIndex = findLastIndex(wv, value => GPT_IMAGE_BATCH_NAMING_RULES.has(value)); if (namingIndex < 2) return false; const formatIndex = namingIndex - 1; const savePathIndex = namingIndex + 1; const resizeIndex = namingIndex + 2; const seedIndex = formatIndex - 1; const concurrencyIndex = batchGPTImageConcurrencyIndex(wv); if (!GPT_IMAGE_BATCH_OUTPUT_FORMATS.has(wv[formatIndex]) || !Number.isInteger(wv[seedIndex]) || !Number.isInteger(wv[concurrencyIndex]) || typeof wv[savePathIndex] !== "string" || !GPT_IMAGE_RESIZE_MODES.has(wv[resizeIndex]) || formatIndex !== concurrencyIndex + 2) { return false; } let backgroundIndex = resizeIndex + 1; if (GPT_IMAGE_COLOR_CORRECTION_MODES.has(wv[backgroundIndex])) backgroundIndex++; if (!GPT_IMAGE_BACKGROUNDS.has(wv[backgroundIndex])) return false; let endIndex = backgroundIndex + 1; if (GPT_IMAGE_MODERATION_MODES.has(wv[endIndex])) endIndex++; return endIndex === wv.length; } function removeLegacyNetworkRoute(wv) { const index = wv.findIndex(value => LEGACY_NETWORK_ROUTES.has(value)); if (index < 0) return false; wv.splice(index, 1); return true; } function batchNanoPairingWidgetIndex(wv) { const match = /^([1-5])个(?:路径|文件夹)$/.exec(String(wv[4])); if (!match) return -1; const pathCount = Number(match[1]); return pathCount >= 2 ? 5 + pathCount : -1; } function batchNanoRandomWidgetIndex(wv) { const match = /^([1-5])个路径$/.exec(String(wv[6] ?? "")); if (!match) return -1; const count = Number(match[1]); return 7 + count + (count >= 2 ? 1 : 0); } // ComfyUI 把每个节点的控件值按「位置」存进 widgets_values 数组,而不是按名字。 // 一旦某个节点的 schema 删掉中间的控件,旧工作流里该位置之后的所有值都会整体错位一格。 // 下面的规则在 ComfyUI 把数组映射到控件之前,把废弃的值从原始数组里剔除,使后续值重新对齐。 // // 新增规则时:type 填节点的注册名(与保存文件里的 "type" 一致), // when 判断旧结构特征(务必保证幂等,避免对已迁移的数据重复处理),apply 做数组修正。 const MIGRATIONS = [ { type: "O1keyAutoRedCast", note: "为旧版自动红偏校正补齐末尾 seed 参数", when: (wv) => wv.length === 6 && typeof wv[0] === "number" && typeof wv[1] === "number" && typeof wv[2] === "number" && typeof wv[3] === "string" && typeof wv[4] === "number" && typeof wv[5] === "number", apply: (wv) => wv.push(0), }, { type: "O1keyAutoRedCast", note: "将自动红偏校正的 seed 移到末尾", when: (wv) => wv.length === 7 && Number.isInteger(wv[4]) && wv[4] >= 0 && typeof wv[5] === "number" && wv[5] >= 20 && wv[5] <= 95 && typeof wv[6] === "number" && wv[6] >= 3 && wv[6] <= 40 && (wv[4] < 20 || wv[4] > 95 || wv[5] > 40), apply: (wv) => wv.push(wv.splice(4, 1)[0]), }, { type: "O1keyOmniFlashVideo", note: "移除 Omni Flash 节点的模型控件", when: (wv) => OMNI_FLASH_MODELS.has(wv[2]), apply: (wv) => wv.splice(2, 1), }, { type: "O1keyPromptMultiFunction", note: "为提示词(多功能)节点补齐多选参数", when: (wv) => wv.length >= 2 && wv.length < 4, apply: (wv) => { const appendedDefaults = [3, "1,2,3"]; while (wv.length < 4) wv.push(appendedDefaults[wv.length - 2]); }, }, { type: "O1keyPromptMultiFunction", note: "合并提示词节点的旧随机抽取模式", when: (wv) => wv[1] === "随机抽取1套" || wv[1] === "随机抽取多套", apply: (wv) => { const wasRandomOne = wv[1] === "随机抽取1套"; wv[1] = "随机抽取n套"; if (wasRandomOne) wv[2] = 1; }, }, { type: "MiniMaxH3Video", note: "为 MiniMax H3 节点补齐模型参数", when: (wv) => wv.length >= 4 && !MINIMAX_H3_MODELS.has(wv[wv.length - 1]) && !MINIMAX_H3_MODELS.has(wv[wv.length - 2]), apply: (wv) => wv.push("MiniMax-H3"), }, { type: "MiniMaxH3Video", note: "为 MiniMax H3 节点补齐原生 seed 参数", when: (wv) => wv.length >= 5 && MINIMAX_H3_MODELS.has(wv[wv.length - 1]), apply: (wv) => wv.push(0), }, { type: "O1keyImageGenerator", note: "移除统一图片生成节点已停用的色彩纠正参数", when: (wv) => UNIFIED_COLOR_CORRECTION_MODES.has(wv[13]), apply: (wv) => wv.splice(13, 1), }, { type: "O1keyImageGenerator", note: "为统一图片生成节点补齐 GPT 输出、缩放、背景及内容审查强度字段", when: (wv) => wv.length >= 9 && wv.length < 15, apply: (wv) => { const appendedDefaults = ["自动", "png", "{}", "不缩放", "auto", "自动"]; while (wv.length < 15) wv.push(appendedDefaults[wv.length - 9]); }, }, { type: "O1keyImageGenerator", note: "为统一图片生成节点补齐可选批量出图字段", when: (wv) => wv.length >= 15 && wv.length < 18, apply: (wv) => { const appendedDefaults = [false, "一组搭配+多模特", "[]"]; while (wv.length < 18) wv.push(appendedDefaults[wv.length - 15]); }, }, { type: "NanoBanana", note: "移除 Nano Banana 2 的「思考深度」(idx 4)", when: (wv) => wv[1] === "Nano Banana 2" && (wv[4] === "高" || wv[4] === "低"), apply: (wv) => wv.splice(4, 1), }, { type: "BatchNanoBananaPro", note: "移除 Nano Banana 2 的「思考深度」(idx 5)", when: (wv) => wv[1] === "Nano Banana 2" && (wv[5] === "高" || wv[5] === "低"), apply: (wv) => wv.splice(5, 1), }, { type: "SeedanceAutoPass", note: "移除 Seedance 自动过审的「固定镜头」(idx 6)", when: (wv) => wv.length >= 13 && (wv[6] === "关闭" || wv[6] === "打开") && (wv[7] === "关闭" || wv[7] === "打开") && (wv[8] === "关闭" || wv[8] === "打开") && (wv[9] === "关闭" || wv[9] === "打开") && SEEDANCE_ASSET_CREATE_MODES.has(wv[10]), apply: (wv) => wv.splice(6, 1), }, { type: "*", note: "移除已迁移到全局设置的节点网络线路", when: (wv, node) => GLOBAL_ROUTE_NODE_TYPES.has(node.type) && wv.some(value => LEGACY_NETWORK_ROUTES.has(value)), apply: (wv) => removeLegacyNetworkRoute(wv), }, { type: "O1keyGrokVideo", note: "迁移旧版 Grok Video 模型、模式、时长与分辨率参数", when: (wv) => wv.length === 5 && LEGACY_GROK_VIDEO_MODELS.has(wv[1]), apply: (wv, node) => { const [prompt, legacyModel, ratio, rawDuration] = wv; const connectedReference = Array.isArray(node?.inputs) && node.inputs.some(input => /^图片[1-3]$/.test(input?.name) && input?.link != null); const parsedDuration = Number(rawDuration); const duration = Number.isFinite(parsedDuration) ? Math.max(1, Math.min(15, Math.trunc(parsedDuration))) : 8; wv.splice( 0, wv.length, connectedReference ? "参考生视频" : "文生视频", prompt, LEGACY_GROK_VIDEO_MODELS.get(legacyModel), duration, ratio, "720p", "", ); }, }, { type: "O1keyGrokVideo", note: "为新版 Grok Video 节点补齐参考音色 ID 参数", when: (wv) => wv.length === 6 && GROK_VIDEO_MODES.has(wv[0]), apply: (wv) => wv.push(""), }, { type: "O1keyGrokVideoEdit", note: "为 Grok Video 编辑/续写节点补齐模型参数", when: (wv) => wv.length === 3 && GROK_VIDEO_EDIT_MODES.has(wv[0]), apply: (wv) => wv.push("grok-imagine-video-1.5"), }, { type: "BatchNanoBananaPro", note: "迁移早期批量 Nano Banana 的动态模型布局", when: (wv) => wv.length === 15 && NANO_BANANA_MODELS.has(wv[1]) && NANO_BANANA_ASPECT_RATIOS.has(wv[2]) && (NANO_BANANA_RESOLUTIONS.has(wv[3]) || wv[3] === "512px") && (wv[4] === "关闭" || wv[4] === "打开") && (wv[5] === "原始" || wv[5] === "JPEG" || wv[5] === "PNG" || wv[5] === "WebP") && (wv[6] === "特价" || wv[6] === "官方"), apply: (wv) => { const [prompt, model, ratio, rawResolution, , outputFormat, billing, seed, path1, path2, path3, path4, path5, savePath, pairing] = wv; const paths = [path1, path2, path3, path4, path5]; const highestFilledPath = paths.reduce( (highest, value, index) => typeof value === "string" && value.trim() ? index + 1 : highest, 0, ); const folderCount = Math.min( 5, Math.max(1, highestFilledPath, pairing !== "不配对" ? 2 : 1), ); const migrated = [ prompt, model, rawResolution === "512px" ? "512" : rawResolution, ratio, `${folderCount}个路径`, ...paths.slice(0, folderCount), ]; if (folderCount >= 2) migrated.push(pairing); migrated.push( "关闭", billing, seed, outputFormat, "和原始图片名保持一致", savePath, ); wv.splice(0, wv.length, ...migrated); }, }, { type: "BatchNanoBananaPro", note: "将批量 Nano Banana 迁移为动态图片路径和高级参数布局", when: (wv) => wv.length === 16 && NANO_BANANA_MODELS.has(wv[1]) && NANO_BANANA_ASPECT_RATIOS.has(wv[2]) && NANO_BANANA_RESOLUTIONS.has(wv[3]) && (wv[4] === "特价" || wv[4] === "官方"), apply: (wv) => { const [prompt, model, ratio, resolution, billing, seed, path1, path2, path3, path4, path5, pairing, randomPath, outputFormat, namingRule, savePath] = wv; const paths = [path1, path2, path3, path4, path5]; const highestFilledPath = paths.reduce( (highest, value, index) => typeof value === "string" && value.trim() ? index + 1 : highest, 0, ); const randomIndex = /^\d+$/.test(String(randomPath)) ? Number(randomPath) : 0; const folderCount = Math.min( 5, Math.max(1, highestFilledPath, randomIndex, pairing !== "不配对" ? 2 : 1), ); const migrated = [ prompt, model, resolution, ratio, `${folderCount}个路径`, ...paths.slice(0, folderCount), ]; if (folderCount >= 2) migrated.push(pairing); migrated.push( randomPath, billing, seed, outputFormat, namingRule, savePath, ); wv.splice(0, wv.length, ...migrated); }, }, { type: "BatchNanoBananaPro", note: "将图片文件夹数量重命名为图片路径数量", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && /^([1-5])个文件夹$/.test(String(wv[4])), apply: (wv) => { wv[4] = String(wv[4]).replace("个文件夹", "个路径"); }, }, { type: "BatchNanoBananaPro", note: "更新批量 Nano Banana 图片配对模式名称", when: (wv) => { const index = batchNanoPairingWidgetIndex(wv); return index >= 0 && BATCH_NANO_PAIRING_ALIASES.has(wv[index]); }, apply: (wv) => { const index = batchNanoPairingWidgetIndex(wv); wv[index] = BATCH_NANO_PAIRING_ALIASES.get(wv[index]); }, }, { type: "BatchNanoBananaPro", note: "为批量节点新增「思考等级」(idx 2)", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && NANO_BANANA_RESOLUTIONS.has(wv[2]), apply: (wv) => wv.splice(2, 0, "高"), }, { type: "NanoBanana", note: "将 Nano Banana 的计费参数迁移为模型线路", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && NANO_BANANA_ROUTE_ALIASES.has(wv[6]), apply: (wv) => { wv[6] = NANO_BANANA_ROUTE_ALIASES.get(wv[6]); }, }, { type: "BatchNanoBananaPro", note: "将批量 Nano Banana 的计费参数迁移为模型线路", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && wv.some(value => NANO_BANANA_ROUTE_ALIASES.has(value)), apply: (wv) => { const index = wv.findIndex(value => NANO_BANANA_ROUTE_ALIASES.has(value)); if (index >= 0) wv[index] = NANO_BANANA_ROUTE_ALIASES.get(wv[index]); }, }, { type: "NanoBanana", note: "将 Nano Banana 模型线路移动到模型下方", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && !NANO_BANANA_ROUTES.has(wv[2]) && NANO_BANANA_ROUTES.has(wv[6]), apply: (wv) => { const [route] = wv.splice(6, 1); wv.splice(2, 0, route); }, }, { type: "BatchNanoBananaPro", note: "将批量 Nano Banana 模型线路移动到模型下方", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && !NANO_BANANA_ROUTES.has(wv[2]) && wv.some((value, index) => index > 2 && NANO_BANANA_ROUTES.has(value)), apply: (wv) => { const index = wv.findIndex((value, valueIndex) => ( valueIndex > 2 && NANO_BANANA_ROUTES.has(value) )); if (index < 0) return; const [route] = wv.splice(index, 1); wv.splice(2, 0, route); }, }, { type: "BatchNanoBananaPro", note: "为批量 Nano Banana 的有损图片输出新增「图片质量」", when: (wv) => { const formatIndex = wv.findIndex((value, index) => ( index > 0 && BATCH_NANO_IMAGE_FORMATS.has(value) && BATCH_NANO_NAMING_RULES.has(wv[index + 1]) )); return formatIndex >= 0; }, apply: (wv) => { const formatIndex = wv.findIndex((value, index) => ( index > 0 && BATCH_NANO_IMAGE_FORMATS.has(value) && BATCH_NANO_NAMING_RULES.has(wv[index + 1]) )); if (formatIndex >= 0) wv.splice(formatIndex + 1, 0, 95); }, }, { type: "BatchNanoBananaPro", note: "将批量 Nano Banana 的字符串图片质量恢复为整数", when: (wv) => { const formatIndex = wv.findIndex((value, index) => ( index > 0 && BATCH_NANO_IMAGE_FORMATS.has(value) )); if (formatIndex < 0 || typeof wv[formatIndex + 1] !== "string") { return false; } const quality = Number(wv[formatIndex + 1]); return Number.isInteger(quality) && quality >= 1 && quality <= 100; }, apply: (wv) => { const formatIndex = wv.findIndex((value, index) => ( index > 0 && BATCH_NANO_IMAGE_FORMATS.has(value) )); if (formatIndex >= 0) wv[formatIndex + 1] = Number(wv[formatIndex + 1]); }, }, { type: "O1keyGPTImageBatch", note: "将旧 GPT Image 批量节点迁移为动态路径和并发布局", when: (wv) => wv.length >= 14 && GPT_IMAGE_ROUTE_ALIASES.has(wv[1]) && typeof wv[2] === "string" && Number.isInteger(wv[3]) && (wv[4] === "高" || wv[4] === "中" || wv[4] === "低" || wv[4] === "自动") && (wv[6] === "原始" || wv[6] === "JPEG" || wv[6] === "PNG" || wv[6] === "WebP"), apply: (wv) => { const [prompt, model, resolution, imageCount, quality, seed, imageFormat, path1, path2, path3, path4, path5, savePath, rawPairing] = wv; const paths = [path1, path2, path3, path4, path5]; const pairing = BATCH_NANO_PAIRING_ALIASES.get(rawPairing) || rawPairing || "不配对"; const highestFilled = paths.reduce( (highest, value, index) => typeof value === "string" && value.trim() ? index + 1 : highest, 0, ); const pathCount = Math.min(5, Math.max(1, highestFilled, pairing !== "不配对" ? 2 : 1)); const migrated = [ prompt, model, resolution, imageCount, quality, `${pathCount}个路径`, ...paths.slice(0, pathCount), ]; if (pathCount >= 2) migrated.push(pairing); migrated.push("关闭", 3, seed, imageFormat, "和原始图片名保持一致", savePath); wv.splice(0, wv.length, ...migrated); }, }, { type: "*", note: "修复被旧迁移规则错位的 GPT Image 参数(wv[2] 为误插入的线路)", when: (wv, node) => GPT_IMAGE_NODE_TYPES.has(node.type) && wv[1] === "gpt-image-2" && GPT_IMAGE_ROUTES.has(wv[2]) && GPT_IMAGE_ROUTES.has(wv[3]), apply: (wv) => wv.splice(2, 1), }, { type: "*", note: "将 GPT Image 模型名称迁移为模型线路", // 新格式的 wv[1] 恰好也是 "gpt-image-2"(主模型),须以 wv[2] 是否已是线路来区分, // 否则会把新格式误判为旧格式并导致后续参数整体错位。 when: (wv, node) => GPT_IMAGE_NODE_TYPES.has(node.type) && GPT_IMAGE_ROUTE_ALIASES.has(wv[1]) && !GPT_IMAGE_ROUTES.has(wv[2]), apply: (wv) => { wv[1] = GPT_IMAGE_ROUTE_ALIASES.get(wv[1]); }, }, { type: "*", note: "为 GPT Image 节点补充主模型参数", when: (wv, node) => GPT_IMAGE_NODE_TYPES.has(node.type) && GPT_IMAGE_ROUTES.has(wv[1]) && !GPT_IMAGE_ROUTES.has(wv[2]), apply: (wv) => wv.splice(1, 0, "gpt-image-2"), }, { type: "SeedanceAutoPass", note: "移除 Seedance 自动过审的「图片链接」(idx 11)", when: (wv) => wv.length >= 12 && (wv[6] === "关闭" || wv[6] === "打开") && (wv[7] === "关闭" || wv[7] === "打开") && (wv[8] === "关闭" || wv[8] === "打开") && SEEDANCE_ASSET_CREATE_MODES.has(wv[9]) && typeof wv[11] === "string", apply: (wv) => wv.splice(11, 1), }, { type: "SeedanceAutoPass", note: "新增 Seedance 自动过审的「生成模式」(idx 1)", when: (wv) => wv.length === 11 && SEEDANCE_AUTO_PASS_MODELS.has(wv[1]) && SEEDANCE_AUTO_PASS_ROUTES.has(wv[2]), apply: (wv) => wv.splice(1, 0, SEEDANCE_AUTO_PASS_DEFAULT_MODE), }, { type: "SeedanceAutoPass", note: "将 Seedance 自动过审的「生成模式」移动到提示词之后", when: (wv) => wv.length >= 12 && _isSeedanceAutoPassMode(wv[0]) && typeof wv[1] === "string" && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]), apply: (wv) => wv.splice(0, 2, wv[1], wv[0]), }, { type: "SeedanceAutoPass", note: "将 Seedance 自动过审的「联网搜索」迁入生成模式", when: (wv) => wv.length >= 12 && wv.length !== 62 && _isSeedanceAutoPassMode(wv[1]) && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]) && (wv[8] === "关闭" || wv[8] === "打开") && (wv[9] === "关闭" || wv[9] === "打开") && SEEDANCE_ASSET_CREATE_MODES.has(wv[10]), apply: (wv) => { const [webSearch] = wv.splice(8, 1); wv.splice(2, 0, webSearch); }, }, { type: "SeedanceAutoPassBatch", note: "将旧批量节点迁移为 Seedance 全能生成视频(批量)", when: (wv) => wv.length === 12 && LEGACY_BATCH_MODELS.has(wv[3]) && typeof wv[1] === "string" && typeof wv[2] === "string", apply: (wv) => { const [baseModel, modelRoute] = LEGACY_BATCH_MODELS.get(wv[3]); const [prompt, imageFolder, videoFolder, , resolution, ratio, duration, generateAudio, webSearch, concurrency, outputSubdir, seed] = wv; wv.splice(0, wv.length, prompt, SEEDANCE_AUTO_PASS_DEFAULT_MODE, webSearch, imageFolder, videoFolder, "", baseModel, modelRoute, resolution, ratio, duration, generateAudio, "打开", "HC", seed, concurrency, outputSubdir, ); }, }, { type: "*", note: "移除 Seedance 全能生成视频的素材创建参数(改为按模型线路自动匹配)", when: (wv, node) => SEEDANCE_AUTO_PASS_NODE_TYPES.has(node.type) && !(node.type === "SeedanceAutoPass" && wv.length === 62) && findLegacySeedanceAssetWidgets(wv) >= 0, apply: (wv) => wv.splice(findLegacySeedanceAssetWidgets(wv), 2), }, { type: "SeedanceElementCreate", note: "移除 Seedance 真人素材的图片链接并迁移到 HC 请求模式", when: (wv) => wv.length >= 4 && SEEDANCE_ASSET_CREATE_MODES.has(wv[1]), apply: (wv) => { wv.splice(2, 1); wv[1] = "HC"; }, }, { type: "SeedanceElementCreate", note: "将 Seedance 真人素材请求模式迁移为 HC", when: (wv) => wv[1] === "标准" || wv[1] === "高并发", apply: (wv) => { wv[1] = "HC"; }, }, { type: "SeedanceAutoPass", note: "为 Seedance 全能生成视频补齐返回末帧参数", when: (wv) => wv.length === 10 && _isSeedanceAutoPassMode(wv[1]) && SEEDANCE_AUTO_PASS_MODELS.has(wv[3]) && SEEDANCE_AUTO_PASS_ROUTES.has(wv[4]) && typeof wv[9] === "number", apply: (wv) => wv.push("关闭"), }, { type: "SeedanceAutoPass", note: "将 Seedance 全能生成视频简化为两种模式并下移原高级参数", when: (wv) => wv.length === 11 && SEEDANCE_AUTO_PASS_MODE_ALIASES.has(wv[1]) && (wv[2] === "关闭" || wv[2] === "打开") && SEEDANCE_AUTO_PASS_MODELS.has(wv[3]) && SEEDANCE_AUTO_PASS_ROUTES.has(wv[4]), apply: (wv) => { const [prompt, legacyMode, webSearch, model, route, resolution, ratio, duration, generateAudio, seed, returnLastFrame] = wv; wv.splice( 0, wv.length, prompt, SEEDANCE_AUTO_PASS_MODE_ALIASES.get(legacyMode), model, route === "国内" ? "国内" : "海外", resolution, ratio, duration, generateAudio, "自动创建", webSearch, seed, returnLastFrame, ); }, }, { type: "SeedanceAutoPass", note: "将 Seedance 全能生成视频的海外HC线路重命名为海外", when: (wv) => wv.length >= 12 && (wv[1] === "多模态" || wv[1] === "首尾帧") && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]) && wv[3] === "海外HC", apply: (wv) => { wv[3] = "海外"; }, }, { type: "SeedanceAutoPass", note: "为稳定版素材创建下拉补齐三个手动素材 ID 参数", when: (wv) => wv.length === 12 && (wv[1] === "多模态" || wv[1] === "首尾帧") && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]) && SEEDANCE_AUTO_PASS_ROUTES.has(wv[3]) && (wv[8] === "自动创建" || wv[8] === "手动") && (wv[9] === "关闭" || wv[9] === "打开") && typeof wv[10] === "number", apply: (wv) => wv.splice(9, 0, "", "", ""), }, { type: "SeedanceAutoPass", note: "将三类聚合素材 ID 迁移为渐进显示的编号单行输入", when: (wv) => wv.length === 15 && (wv[1] === "多模态" || wv[1] === "首尾帧") && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]) && (wv[8] === "自动创建" || wv[8] === "手动") && typeof wv[13] === "number", apply: (wv) => { const rows = []; for (const [index, maximum] of [[9, 30], [10, 10], [11, 10]]) { const ids = String(wv[index] ?? "").split(/[\s,,;;]+/).filter(Boolean); for (let row = 0; row < maximum; row++) { // 保留超限旧值供后端校验,不能在迁移时静默丢弃。 rows.push(row === maximum - 1 ? ids.slice(row).join(",") : (ids[row] ?? "")); } } wv.splice(9, 3, ...rows); }, }, { type: "SeedanceAutoPass", note: "将素材创建模式恢复为关闭/打开开关", when: (wv) => wv.length === 62 && SEEDANCE_AUTO_PASS_MODELS.has(wv[2]) && (wv[8] === "自动创建" || wv[8] === "手动"), apply: (wv) => { wv[8] = wv[8] === "手动" ? "打开" : "关闭"; }, }, { type: "SeedanceAutoPassBatch", note: "将 Seedance 全能生成视频批量节点的海外HC线路重命名为海外", when: (wv) => { const modelIndex = wv.findIndex(value => SEEDANCE_AUTO_PASS_MODELS.has(value)); return modelIndex >= 0 && wv[modelIndex + 1] === "海外HC"; }, apply: (wv) => { const modelIndex = wv.findIndex(value => SEEDANCE_AUTO_PASS_MODELS.has(value)); wv[modelIndex + 1] = "海外"; }, }, { type: "O1keyVideoGenerator", note: "为 o1key 视频生成补齐素材创建模式", when: (wv) => wv.length === 16, apply: (wv) => wv.push("auto"), }, { type: "SeedanceMultiModal", note: "将 Seedance 多模态模型线路的海外HC重命名为海外", when: (wv) => SEEDANCE_AUTO_PASS_MODELS.has(wv[1]) && wv[2] === "海外HC", apply: (wv) => { wv[2] = "海外"; }, }, { type: "NanoBanana", note: "移除 Nano Banana 已停用的色彩纠正参数", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && NANO_BANANA_COLOR_CORRECTION_MODES.has(wv[wv.length - 1]), apply: (wv) => wv.pop(), }, { type: "NanoBanana", note: "为 Nano Banana 补齐缩放图片参数", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && wv.length >= 8 && !NANO_BANANA_RESIZE_MODES.has(wv[wv.length - 1]), apply: (wv) => wv.push("不缩放"), }, { type: "BatchNanoBananaPro", note: "移除批量 Nano Banana 已停用的色彩纠正参数", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && wv.some(value => BATCH_NANO_IMAGE_FORMATS.has(value)) && wv.some(value => BATCH_NANO_NAMING_RULES.has(value)) && NANO_BANANA_COLOR_CORRECTION_MODES.has(wv[wv.length - 1]), apply: (wv) => wv.pop(), }, { type: "BatchNanoBananaPro", note: "为批量 Nano Banana 补齐缩放图片参数", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && wv.some(value => BATCH_NANO_IMAGE_FORMATS.has(value)) && wv.some(value => BATCH_NANO_NAMING_RULES.has(value)) && !NANO_BANANA_RESIZE_MODES.has(wv[wv.length - 1]) && !NANO_BANANA_RESIZE_MODES.has(wv[wv.length - 6]), apply: (wv) => wv.push("不缩放"), }, { type: "BatchNanoBananaPro", note: "将批量 Nano Banana 的保存参数置底并将 seed 移到末端", when: (wv) => NANO_BANANA_MODELS.has(wv[1]) && Number.isInteger(wv[wv.length - 6]) && BATCH_NANO_IMAGE_FORMATS.has(wv[wv.length - 5]) && Number.isInteger(wv[wv.length - 4]) && BATCH_NANO_NAMING_RULES.has(wv[wv.length - 3]) && typeof wv[wv.length - 2] === "string" && NANO_BANANA_RESIZE_MODES.has(wv[wv.length - 1]), apply: (wv) => { const start = wv.length - 6; const [seed, format, quality, naming, savePath, resize] = wv.slice(start); wv.splice(start, 6, resize, format, quality, naming, savePath, seed); }, }, { type: "BatchNanoBananaPro", note: "移除批量 Nano Banana 已停用的图片随机抽取参数", when: (wv) => { const index = batchNanoRandomWidgetIndex(wv); return NANO_BANANA_MODELS.has(wv[1]) && index >= 0 && NANO_BANANA_RESIZE_MODES.has(wv[wv.length - 6]) && Number.isInteger(wv[wv.length - 1]) && (wv[index] === "关闭" || (typeof wv[index] === "string" && /^[1-5](?:[,,、 ]+[1-5])*$/.test(wv[index]))); }, apply: (wv) => wv.splice(batchNanoRandomWidgetIndex(wv), 1), }, { type: "O1keyGPTImage", note: "移除独立 GPT Image 节点已停用的色彩纠正参数", when: (wv) => GPT_IMAGE_MODELS.has(wv[1]) && GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 4]) && GPT_IMAGE_COLOR_CORRECTION_MODES.has(wv[wv.length - 3]) && GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 2]) && GPT_IMAGE_MODERATION_MODES.has(wv[wv.length - 1]), apply: (wv) => wv.splice(wv.length - 3, 1), }, { type: "O1keyGPTImage", note: "为独立 GPT Image 节点补齐缩放和背景参数", when: (wv) => GPT_IMAGE_MODELS.has(wv[1]) && !( (Number.isInteger(wv[wv.length - 3]) && GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 2]) && GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 1])) || (GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 3]) && GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 2]) && GPT_IMAGE_MODERATION_MODES.has(wv[wv.length - 1])) || (GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 3]) && GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 2]) && Number.isInteger(wv[wv.length - 1])) ), apply: (wv) => wv.push("智能缩放", "auto", "自动"), }, { type: "O1keyGPTImage", note: "移除独立 GPT Image 节点已删除的内容审查强度参数", when: (wv) => GPT_IMAGE_MODELS.has(wv[1]) && GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 3]) && GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 2]) && GPT_IMAGE_MODERATION_MODES.has(wv[wv.length - 1]), apply: (wv) => wv.pop(), }, { type: "O1keyGPTImage", note: "调整独立 GPT Image 节点的背景、缩放和 seed 顺序", when: (wv) => GPT_IMAGE_MODELS.has(wv[1]) && Number.isInteger(wv[wv.length - 3]) && GPT_IMAGE_RESIZE_MODES.has(wv[wv.length - 2]) && GPT_IMAGE_BACKGROUNDS.has(wv[wv.length - 1]), apply: (wv) => { const start = wv.length - 3; const [seed, resize, background] = wv.slice(start); wv.splice(start, 3, background, resize, seed); }, }, { type: "*", note: "为批量 GPT Image 节点补齐缩放和背景参数", when: (wv, node) => node.type === "O1keyGPTImageBatch" && GPT_IMAGE_MODELS.has(wv[1]) && !isLegacyGPTImageBatchWidgetLayout(wv) && !isGPTImageBatchWidgetLayoutWithRandomSelection(wv) && !isCurrentGPTImageBatchWidgetLayout(wv), apply: (wv) => wv.push("智能缩放", "不纠正", "auto", "自动"), }, { type: "O1keyGPTImageBatch", note: "同步 GPT Image 批量节点的参数顺序并移除旧控件和随机抽取", when: (wv) => GPT_IMAGE_MODELS.has(wv[1]) && (isLegacyGPTImageBatchWidgetLayout(wv) || isGPTImageBatchWidgetLayoutWithRandomSelection(wv)), apply: (wv) => { if (isGPTImageBatchWidgetLayoutWithRandomSelection(wv)) { const randomSelectionIndex = batchGPTImageRandomSelectionIndex(wv); const concurrencyIndex = batchGPTImageConcurrencyIndex(wv); const namingIndex = findLastIndex(wv, value => GPT_IMAGE_BATCH_NAMING_RULES.has(value)); const formatIndex = namingIndex - 2; const hasLegacyConcurrency = Number.isInteger(wv[concurrencyIndex]) && formatIndex === concurrencyIndex + 1; wv.splice(randomSelectionIndex, hasLegacyConcurrency ? 2 : 1); return; } const namingIndex = findLastIndex(wv, value => GPT_IMAGE_BATCH_NAMING_RULES.has(value)); const formatIndex = namingIndex - 1; const seedIndex = formatIndex - 1; const randomSelectionIndex = batchGPTImageRandomSelectionIndex(wv); const savePathIndex = namingIndex + 1; const resizeIndex = namingIndex + 2; let backgroundIndex = resizeIndex + 1; if (GPT_IMAGE_COLOR_CORRECTION_MODES.has(wv[backgroundIndex])) backgroundIndex++; const [format, background, naming, savePath, resize, seed] = [ wv[formatIndex], wv[backgroundIndex], wv[namingIndex], wv[savePathIndex], wv[resizeIndex], wv[seedIndex], ]; wv.splice(0, wv.length, ...wv.slice(0, randomSelectionIndex), format, background, naming, savePath, resize, seed); }, }, { type: "*", note: "将独立 Nano Banana 节点已移除的 512 分辨率迁移为 1K", when: (wv, node) => NANO_BANANA_NODE_TYPES.has(node.type) && NANO_BANANA_MODELS.has(wv[1]) && REMOVED_NANO_BANANA_RESOLUTIONS.has(wv[4]), apply: (wv) => { wv[4] = "1K"; }, }, ]; function _isSeedanceAutoPassMode(value) { return value === "多模态" || value === "首尾帧" || value === "多模态参考生视频" || value === "图生视频-首帧" || value === "图生视频-首尾帧" || value === "文生视频"; } function migrateSeedanceAutoPassInputPaths(node) { if (node?.type !== "SeedanceAutoPass" || !Array.isArray(node.inputs)) return 0; const groups = ["参考图片", "参考视频", "参考音频"]; const directInputs = new Set(["首帧图片", "尾帧图片", "图片素材ID", "视频素材ID", "音频素材ID"]); let migrated = 0; for (const input of node.inputs) { const name = input?.name; if (typeof name !== "string") continue; if (name === "素材创建") { input.name = "素材创建模式"; migrated++; continue; } if (name === "生成模式.提示词") { input.name = "提示词"; migrated++; continue; } if (name === "生成模式.联网搜索") { input.name = "联网搜索"; migrated++; continue; } if (name.startsWith("生成模式.")) { input.name = name.slice("生成模式.".length); migrated++; continue; } if (name.startsWith("素材创建.")) { const childName = name.slice("素材创建.".length); if (directInputs.has(childName)) { input.name = childName.endsWith("素材ID") ? `${childName}1` : childName; migrated++; } continue; } if (name.endsWith("素材ID") && directInputs.has(name)) { input.name = `${name}1`; migrated++; continue; } for (const group of groups) { if (name.startsWith(`${group}.`)) break; if (new RegExp(`^${group}\\d+$`).test(name)) { input.name = `${group}.${name}`; migrated++; break; } } } return migrated; } function migrateSeedanceMultiModalInputPaths(node) { if (node?.type !== "SeedanceMultiModal" || !Array.isArray(node.inputs)) return 0; const groups = ["参考图片", "参考视频", "参考音频"]; let migrated = 0; for (const input of node.inputs) { const name = input?.name; if (typeof name !== "string") continue; const legacyImageAsset = /^真人素材ID([1-9]|[12]\d|30)$/.exec(name); if (legacyImageAsset) { input.name = `图片素材ID${legacyImageAsset[1]}`; migrated++; continue; } for (const group of groups) { if (name.startsWith(`${group}.`)) break; if (new RegExp(`^${group}\\d+$`).test(name)) { input.name = `${group}.${name}`; migrated++; break; } } } return migrated; } const SEEDANCE_ELEMENT_INPUT_RENAMES = new Map([ ["真人照片", "照片"], ["真人视频", "视频"], ["真人音频", "音频"], ]); function migrateSeedanceElementInputNames(node) { if (node?.type !== "SeedanceElementCreate" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (const input of node.inputs) { const replacement = SEEDANCE_ELEMENT_INPUT_RENAMES.get(input?.name); if (!replacement) continue; input.name = replacement; migrated++; } return migrated; } function migrateBatchNanoBananaInputPaths(node) { if (node?.type !== "BatchNanoBananaPro" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (const input of node.inputs) { const name = input?.name; if (typeof name !== "string" || name.startsWith("参考图组.")) continue; const pathMatch = /^(?:图片文件夹数量\.|图片路径数量\.)?(?:图片路径|文件夹)([1-5])$/.exec(name); if (pathMatch) { const index = Number(pathMatch[1]); input.name = `图片路径数量.${index === 1 ? "参考图1(主图)" : `参考图${index}`}`; migrated++; continue; } if (name === "图片配对模式" || name === "图片随机抽取") { input.name = `图片路径数量.${name}`; migrated++; continue; } if (name.startsWith("图片文件夹数量.")) { input.name = `图片路径数量.${name.slice("图片文件夹数量.".length)}`; migrated++; continue; } if (/^参考图[1-9]$/.test(name)) { input.name = `参考图组.${name}`; migrated++; } } return migrated; } function migrateUniversalLLMInputPaths(node) { if (node?.type !== "UniversalLLMChat" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (const input of node.inputs) { const name = input?.name; if (typeof name !== "string" || name.startsWith("图片组.")) continue; if (name === "图片") { input.name = "图片组.图片1"; migrated++; continue; } if (/^图片[1-9]$/.test(name)) { input.name = `图片组.${name}`; migrated++; } } return migrated; } function migrateGPTImageInputPaths(node) { if (node?.type !== "O1keyGPTImage" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (const input of node.inputs) { const name = input?.name; if (typeof name !== "string" || name.startsWith("参考图组.")) continue; if (/^参考图[1-9]$/.test(name)) { input.name = `参考图组.${name}`; migrated++; } } return migrated; } function migrateGPTImageBatchInputPaths(node) { if (node?.type !== "O1keyGPTImageBatch" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (let index = node.inputs.length - 1; index >= 0; index--) { const input = node.inputs[index]; const name = input?.name; if (typeof name !== "string") continue; if (/^文件夹([1-5])$/.test(name)) { const index = Number(name.match(/^文件夹([1-5])$/)[1]); input.name = `图片路径数量.${index === 1 ? "参考图1(主图)" : `参考图${index}`}`; migrated++; } else if (name === "图片配对模式") { input.name = "图片路径数量.图片配对模式"; migrated++; } else if (name === "图片随机抽取") { node.inputs.splice(index, 1); migrated++; } else if (/^参考图[1-9]$/.test(name)) { input.name = `参考图组.${name}`; migrated++; } } return migrated; } function migrateGrokVideoInputNames(node) { if (node?.type !== "O1keyGrokVideo" || !Array.isArray(node.inputs)) return 0; let migrated = 0; for (const input of node.inputs) { const match = /^参考图([1-7])$/.exec(input?.name); if (!match) continue; input.name = `图片${match[1]}`; migrated++; } return migrated; } function collectNodes(graphData) { const out = []; if (!graphData || typeof graphData !== "object") return out; if (Array.isArray(graphData.nodes)) out.push(...graphData.nodes); const subgraphs = graphData.definitions?.subgraphs; if (Array.isArray(subgraphs)) { for (const sub of subgraphs) { if (Array.isArray(sub?.nodes)) out.push(...sub.nodes); } } return out; } function collectGraphContainers(graphData) { const out = []; if (!graphData || typeof graphData !== "object") return out; if (Array.isArray(graphData.nodes)) out.push(graphData); const subgraphs = graphData.definitions?.subgraphs; if (Array.isArray(subgraphs)) { for (const subgraph of subgraphs) { if (Array.isArray(subgraph?.nodes)) out.push(subgraph); } } return out; } function linkedUnifiedGenerator(container, saveNode, nodesById) { const propertyId = saveNode?.properties?.o1keyGeneratorNodeId; const propertyNode = nodesById.get(String(propertyId)); if (propertyNode?.type === "O1keyImageGenerator") return propertyNode; const linkId = saveNode?.inputs?.[0]?.link; if (linkId == null || !Array.isArray(container?.links)) return null; const link = container.links.find((item) => String( Array.isArray(item) ? item[0] : item?.id, ) === String(linkId)); const originId = Array.isArray(link) ? link[1] : link?.origin_id; const originNode = nodesById.get(String(originId)); return originNode?.type === "O1keyImageGenerator" ? originNode : null; } function migrateUnifiedExternalPromptInput(container) { const nodes = Array.isArray(container?.nodes) ? container.nodes : []; const removedLinks = new Set(); let migrated = 0; for (const node of nodes.filter((item) => item?.type === "O1keyImageGenerator")) { if (!Array.isArray(node.inputs)) continue; for (let index = node.inputs.length - 1; index >= 0; index--) { const input = node.inputs[index]; if (input?.name !== "external_prompt") continue; if (input.link != null) removedLinks.add(String(input.link)); node.inputs.splice(index, 1); migrated++; } } if (!removedLinks.size) return migrated; if (Array.isArray(container.links)) { container.links = container.links.filter((link) => !removedLinks.has(String( Array.isArray(link) ? link[0] : link?.id, ))); } for (const node of nodes) { for (const output of Array.isArray(node?.outputs) ? node.outputs : []) { if (Array.isArray(output?.links)) { output.links = output.links.filter((id) => !removedLinks.has(String(id))); } } } return migrated; } function migrateBatchNanoRandomInput(container) { const nodes = Array.isArray(container?.nodes) ? container.nodes : []; const removedLinks = new Set(); let migrated = 0; for (const node of nodes.filter((item) => item?.type === "BatchNanoBananaPro")) { if (!Array.isArray(node.inputs)) continue; for (let index = node.inputs.length - 1; index >= 0; index--) { const input = node.inputs[index]; if (!/(?:^|\.)图片随机抽取$/.test(String(input?.name ?? ""))) continue; if (input.link != null) removedLinks.add(String(input.link)); node.inputs.splice(index, 1); for (const link of Array.isArray(container.links) ? container.links : []) { const targetId = Array.isArray(link) ? link[3] : link?.target_id; const targetSlot = Array.isArray(link) ? link[4] : link?.target_slot; if (String(targetId) !== String(node.id) || !Number.isInteger(targetSlot) || targetSlot <= index) continue; if (Array.isArray(link)) link[4]--; else link.target_slot--; } migrated++; } } if (!removedLinks.size) return migrated; if (Array.isArray(container.links)) { container.links = container.links.filter((link) => !removedLinks.has(String( Array.isArray(link) ? link[0] : link?.id, ))); } for (const node of nodes) { for (const output of Array.isArray(node?.outputs) ? node.outputs : []) { if (Array.isArray(output?.links)) { output.links = output.links.filter((id) => !removedLinks.has(String(id))); } } } return migrated; } function migrateUnifiedSaveSettings(container) { const nodes = Array.isArray(container?.nodes) ? container.nodes : []; const nodesById = new Map(nodes.map((node) => [String(node?.id), node])); const defaults = ["自定义前缀", "o1key", "原始", ""]; let migrated = 0; for (const saveNode of nodes.filter((node) => node?.type === "O1keyImageSave")) { const values = saveNode?.widgets_values; if (!Array.isArray(values) || values.length === 0) continue; const legacy = [...values]; if (legacy.length === 1) legacy.push("原始"); if (legacy.length === 2) legacy.push(""); if (legacy.length === 3) legacy.push("自定义前缀"); const generator = linkedUnifiedGenerator(container, saveNode, nodesById); const generatorValues = generator?.widgets_values; if (!Array.isArray(generatorValues) || generatorValues.length < 18) continue; if (generatorValues.length === 18) { generatorValues.push(legacy[3], legacy[0], legacy[1], legacy[2]); } else { while (generatorValues.length < 22) { generatorValues.push(defaults[generatorValues.length - 18]); } } values.splice(0, values.length); migrated++; } for (const generator of nodes.filter((node) => node?.type === "O1keyImageGenerator")) { const values = generator?.widgets_values; if (!Array.isArray(values) || values.length < 18) continue; const before = values.length; while (values.length < 22) values.push(defaults[values.length - 18]); if (values.length !== before) migrated++; } return migrated; } function migrateUnifiedOnlineSearch(container) { const nodes = Array.isArray(container?.nodes) ? container.nodes : []; let migrated = 0; for (const node of nodes.filter((item) => item?.type === "O1keyImageGenerator")) { const values = node?.widgets_values; if (!Array.isArray(values)) continue; const expectedLength = GPT_IMAGE_MODERATION_MODES.has(values[14]) ? 22 : 21; if (values.length !== expectedLength) continue; values.push("关闭"); migrated++; } return migrated; } function migrateUnifiedLayerDecomposition(container) { const nodes = Array.isArray(container?.nodes) ? container.nodes : []; let migrated = 0; for (const node of nodes.filter((item) => item?.type === "O1keyImageGenerator")) { const values = node?.widgets_values; if (!Array.isArray(values)) continue; const expectedLength = GPT_IMAGE_MODERATION_MODES.has(values[14]) ? 23 : 22; if (values.length !== expectedLength) continue; values.push(false); migrated++; } return migrated; } function migrateUnifiedModeration(container) { let migrated = 0; for (const node of (container?.nodes || []).filter((item) => item?.type === "O1keyImageGenerator")) { const values = node?.widgets_values; // The retired widget occupied index 14, before batch and save settings. if (!Array.isArray(values) || !GPT_IMAGE_MODERATION_MODES.has(values[14])) continue; values.splice(14, 1); migrated++; } return migrated; } app.registerExtension({ name: "o1key.workflowMigration", beforeConfigureGraph(graphData) { let migrated = 0; for (const node of collectNodes(graphData)) { migrated += migrateSeedanceAutoPassInputPaths(node); migrated += migrateSeedanceMultiModalInputPaths(node); migrated += migrateSeedanceElementInputNames(node); migrated += migrateBatchNanoBananaInputPaths(node); migrated += migrateUniversalLLMInputPaths(node); migrated += migrateGPTImageInputPaths(node); migrated += migrateGPTImageBatchInputPaths(node); migrated += migrateGrokVideoInputNames(node); const wv = node?.widgets_values; if (!Array.isArray(wv)) continue; for (const rule of MIGRATIONS) { if (rule.type !== "*" && node.type !== rule.type) continue; try { if (rule.when(wv, node)) { rule.apply(wv, node); migrated++; } } catch (e) { console.warn(`[o1key] 工作流迁移失败: ${rule.note}`, e); } } } for (const container of collectGraphContainers(graphData)) { migrated += migrateBatchNanoRandomInput(container); migrated += migrateUnifiedExternalPromptInput(container); migrated += migrateUnifiedSaveSettings(container); migrated += migrateUnifiedOnlineSearch(container); migrated += migrateUnifiedLayerDecomposition(container); migrated += migrateUnifiedModeration(container); } if (migrated > 0) { console.log(`[o1key] 工作流参数迁移:已修复 ${migrated} 处参数或端口`); } }, });