ToonFlow最新版本 配置 豆包相关 接口完整教程

前言

ToonFlow 是一款强大的 AI 工作流工具,支持通过自定义适配器接入各种 AI 服务。本文将详细介绍如何在 ToonFlow 中配置 豆包 AI 接口,实现:

准备工作

1. 获取 API 密钥

访问 32ai 平台 注册并获取 API Key。

2. 确认 ToonFlow 版本

确保 ToonFlow 已更新到支持自定义适配器的版本。为最新版本哦

配置文件

下载地址:点击

//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)
// ==================== 类型定义 ====================
// 文本模型
interface TextModel {
  name: string; // 显示名称
  modelName: string;
  type: "text";
  think: boolean; // 前端显示用
}

// 图像模型
interface ImageModel {
  name: string; // 显示名称
  modelName: string;
  type: "image";
  mode: ("text" | "singleImage" | "multiReference")[];
  associationSkills?: string; // 关联技能,多个技能用逗号分隔
}
// 视频模型
interface VideoModel {
  name: string; // 显示名称
  modelName: string; //全局唯一
  type: "video";
  mode: (
    | "singleImage" // 单图
    | "startEndRequired" // 首尾帧(两张都得有)
    | "endFrameOptional" // 首尾帧(尾帧可选)
    | "startFrameOptional" // 首尾帧(首帧可选)
    | "text" // 文本生视频
    | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]
  )[]; // 混合参考
  associationSkills?: string; // 关联技能,多个技能用逗号分隔
  audio: "optional" | false | true; // 音频配置
  durationResolutionMap: { duration: number[]; resolution: string[] }[];
}

interface TTSModel {
  name: string; // 显示名称
  modelName: string;
  type: "tts";
  voices: {
    title: string; //显示名称
    voice: string; //说话人
  }[];
}
// 供应商配置
interface VendorConfig {
  id: string; //供应商唯一标识,必须全局唯一
  author: string;
  description?: string; //md5格式
  name: string;
  icon?: string; //仅支持base64格式
  inputs: {
    key: string;
    label: string;
    type: "text" | "password" | "url";
    required: boolean;
    placeholder?: string;
  }[];
  inputValues: Record<string, string>;
  models: (TextModel | ImageModel | VideoModel)[];
}
// ==================== 全局工具函数 ====================
//Axios实例
//压缩图片大小(1MB = 1 * 1024 * 1024)
declare const zipImage: (completeBase64: string, size: number) => Promise<string>;
//压缩图片分辨率
declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;
//多图拼接乘单图 maxSize  最大输出大小,默认为 10mb
declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;
//Url转Base64
declare const urlToBase64: (url: string) => Promise<string>;
//轮询函数
declare const pollTask: (
  fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,
  interval?: number,
  timeout?: number,
) => Promise<{ completed: boolean; data?: string; error?: string }>;
declare const axios: any;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const logger: (logstring: string) => void;
declare const jsonwebtoken: any;
// ==================== 供应商数据 ====================
const SUCCESS_TASK_STATUS = ["video_generation_completed", "succeeded", "completed", "success"];
const FAILED_TASK_STATUS = ["failed", "failure", "error", "canceled", "cancelled"];

const vendor: VendorConfig = {
  id: "32ai-volcengine",
  author: "32ai",
  description: "32ai volcengine AI接口",
  name: "32ai-volcengine",
  inputs: [
    { key: "apiKey", label: "ARK API Key", type: "password", required: true },
  ],
  inputValues: {
    apiKey: "",
    baseUrl: "https://ai.32zi.com"
  },
  models: [
    {
      name: "doubao-seed-2-0-pro-260215",
      type: "text",
      modelName: "doubao-seed-2-0-pro-260215",
      think: false,
    },
    {
      name: "doubao-seed-2-0-lite-260215",
      type: "text",
      modelName: "doubao-seed-2-0-lite-260215",
      think: false,
    },
    {
      name: "doubao-seed-2-0-mini-260215",
      type: "text",
      modelName: "doubao-seed-2-0-mini-260215",
      think: false,
    },
    {
      name: "doubao-seed-2-0-code-preview-260215",
      type: "text",
      modelName: "doubao-seed-2-0-code-preview-260215",
      think: false,
    },
    {
      name: "doubao-seedream-5-0-260128",
      type: "image",
      modelName: "doubao-seedream-5-0-260128",
      mode: ["text", "singleImage", "multiReference"],
    },
    {
      name: "doubao-seedream-4-5-251128",
      type: "image",
      modelName: "doubao-seedream-4-5-251128",
      mode: ["text", "singleImage", "multiReference"],
    },
    {
      name: "doubao-seedream-4-0-250828",
      type: "image",
      modelName: "doubao-seedream-4-0-250828",
      mode: ["text", "singleImage", "multiReference"],
    },
    {
      name: "doubao-seedream-3-0-t2i-250415",
      type: "image",
      modelName: "doubao-seedream-3-0-t2i-250415",
      mode: ["text"],
    },
    {
      name: "doubao-seedance-1-5-pro-251215",
      type: "video",
      modelName: "doubao-seedance-1-5-pro-251215",
      mode: ["text", "startEndRequired", "endFrameOptional"],
      audio: true,
      durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
    },
    {
      name: "doubao-seedance-1-0-pro-250528",
      type: "video",
      modelName: "doubao-seedance-1-0-pro-250528",
      mode: ["text", "startEndRequired", "endFrameOptional"],
      audio: false,
      durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
    },
    {
      name: "doubao-seedance-1-0-lite-i2v-250428",
      type: "video",
      modelName: "doubao-seedance-1-0-lite-i2v-250428",
      mode: ["startEndRequired", "endFrameOptional", ["imageReference"]],
      audio: false,
      durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
    },
    {
      name: "doubao-seedance-1-0-lite-t2v-250428",
      type: "video",
      modelName: "doubao-seedance-1-0-lite-t2v-250428",
      mode: ["text"],
      audio: false,
      durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
    },
    {
      name: "doubao-seedance-1-0-pro-fast-251015",
      type: "video",
      modelName: "doubao-seedance-1-0-pro-fast-251015",
      mode: ["text", "endFrameOptional"],
      audio: false,
      durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
    }
  ],
};
exports.vendor = vendor;

// ==================== 适配器函数 ====================
const getApiKey = () => {
  if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
  return vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
};

const getBaseUrl = () => vendor.inputValues.baseUrl;
const buildUrl = (overrideUrl: string | undefined, fallbackPath: string) => overrideUrl || `${getBaseUrl().replace(/\/$/, "")}${fallbackPath}`;

const getHeaders = () => ({
  Authorization: `Bearer ${getApiKey()}`,
  "Content-Type": "application/json",
});

const readJson = async (response: Response, action: string) => {
  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`${action}失败,状态码: ${response.status}, 错误信息: ${errorText}`);
  }

  return response.json();
};

//补齐图片 data url 前缀
const normalizeImageInput = (value: string) => {
  if (!value) return value;
  if (/^(https?:\/\/|data:|volc:)/i.test(value)) return value;
  return `data:image/png;base64,${value}`;
};

const normalizeImageList = (imageBase64?: string[]) => (imageBase64 || []).filter(Boolean).map(normalizeImageInput);

const extractImageResult = (data: any) => {
  const first = data?.data?.[0] ?? data?.images?.[0] ?? data?.output?.[0];
  return first?.url ?? first?.image_url ?? first?.b64_json ?? first?.base64;
};

const extractTaskId = (data: any) => data?.id ?? data?.task_id ?? data?.taskId ?? data?.data?.id ?? data?.data?.task_id ?? data?.data;

const extractVideoUrl = (data: any) =>
  data?.content?.video_url ??
  data?.content?.video_urls?.[0] ??
  data?.data?.content?.video_url ??
  data?.data?.content?.video_urls?.[0] ??
  data?.data?.video_url ??
  data?.result?.video_url ??
  data?.result_url ??
  data?.data?.result_url;

const getTaskStatus = (data: any) => (data?.status ?? data?.data?.status ?? "").toString().toLowerCase();

const getTaskError = (data: any) =>
  data?.error?.message ?? data?.message ?? data?.data?.message ?? data?.data?.fail_reason ?? data?.fail_reason ?? "任务执行失败";

// 文本请求函数
const textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {
  return createOpenAI({
    baseURL: vendor.inputValues.baseUrl + "/v1",
    apiKey: getApiKey(),
  }).chat(textModel.modelName);
};
exports.textRequest = textRequest;

//图片请求函数
interface ImageConfig {
  prompt: string; //图片提示词
  imageBase64: string[]; //输入的图片提示词
  size: "1K" | "2K" | "4K"; // 图片尺寸
  aspectRatio: `${number}:${number}`; // 长宽比
}

const normalizeImageSize = (imageConfig: ImageConfig) => {
  const normalizedAspectRatio = imageConfig.aspectRatio;
  const size = imageConfig.size === "1K" ? "2K" : imageConfig.size;

  const sizeMap: Record<string, Record<string, string>> = {
    "16:9": {
      "2K": "2848x1600",
      "4K": "4096x2304",
    },
    "9:16": {
      "2K": "1600x2848",
      "4K": "2304x4096",
    },
  };
  return sizeMap[normalizedAspectRatio][size];
};

const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {
  const images = normalizeImageList(imageConfig.imageBase64);
  const body = {
    model: imageModel.modelName,
    prompt: imageConfig.prompt,
    size: normalizeImageSize(imageConfig),
    response_format: "url",
    sequential_image_generation: "disabled",
    stream: false,
    watermark: false,
    image: images
  };

  
  try {
    const response = await axios.post(vendor.inputValues.baseUrl + "/v1/images/generations", body, {
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + getApiKey()
      },
      timeout: 600000 // 10分钟超时
    });

    const result = extractImageResult(response.data);
    if (!result) throw new Error(`图片生成返回格式异常: ${JSON.stringify(response.data)}`);
    return result;

  } catch (error: any) {
    if (error.response) {
      throw new Error(`Volcengine API 错误: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    }
    throw error;
  }

};
exports.imageRequest = imageRequest;

interface VideoConfig {
  duration: number;
  resolution: string;
  aspectRatio: "16:9" | "9:16";
  prompt: string;
  imageBase64?: string[];
  audio?: boolean;
  mode:
    | "singleImage" // 单图
    | "multiImage" // 多图模式
    | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)
    | "startEndRequired" // 首尾帧(两张都得有)
    | "endFrameOptional" // 首尾帧(尾帧可选)
    | "startFrameOptional" // 首尾帧(首帧可选)
    | "text" // 文本生视频
    | ("video" | "image" | "audio" | "text")[]; // 混合参考
}

const isSeedanceModel = (modelName: string) => /^doubao-seedance-/i.test(modelName);
const isSeedance15ProModel = (modelName: string) => /^doubao-seedance-1-5-pro/i.test(modelName);

const appendPromptFlag = (prompt: string, flag: string, value: string | number | boolean | undefined) => {
  if (value === undefined || value === null || value === "") return prompt;
  const normalizedPrompt = prompt.trim();
  const flagPattern = new RegExp(`(^|\\s)--${flag}(?:\\s+\\S+)?(?=\\s|$)`, "i");
  if (flagPattern.test(normalizedPrompt)) return normalizedPrompt;
  return `${normalizedPrompt} --${flag} ${value}`.trim();
};

const buildVideoPrompt = (videoConfig: VideoConfig, videoModel: VideoModel) => {
  if (!isSeedanceModel(videoModel.modelName)) return videoConfig.prompt || "";

  let prompt = videoConfig.prompt || "";
  prompt = appendPromptFlag(prompt, "resolution", videoConfig.resolution);
  prompt = appendPromptFlag(prompt, "duration", videoConfig.duration);
  prompt = appendPromptFlag(prompt, "camerafixed", false);
  prompt = appendPromptFlag(prompt, "watermark", false);
  return prompt;
};

//判断base64前缀 适配多参
function getBase64Type(base64: string) {
  const match = base64.match(/^data:([-\w]+)\/([-\w]+);base64,/);
  if (!match) return "unknown";
  const mainType = match[1];
  if (mainType === "image") return "image";
  if (mainType === "audio") return "audio";
  if (mainType === "video") return "video";
  return "unknown";
}

const buildVideoContent = (videoConfig: VideoConfig, videoModel: VideoModel) => {
  const images = videoConfig?.imageBase64 ?? [];
  const content: any[] = [{ type: "text", text: buildVideoPrompt(videoConfig, videoModel) }];
  if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional") {
    images[0] && content.push({ type: "image_url", image_url: { url: images[0] }, role: "first_frame" });
    images[1] && content.push({ type: "image_url", image_url: { url: images[1] }, role: "last_frame" });
  }
  if (Array.isArray(videoConfig.mode)) {
    images.forEach((item) => {
      const type = getBase64Type(item);
      if (type == "audio") {
        content.push({
          type: "audio_url",
          audio_url: { url: item },
          role: "reference_audio",
        });
      } else if (type == "video") {
        content.push({
          type: "video_url",
          video_url: { url: item },
          role: "reference_video",
        });
      } else {
        content.push({
          type: "image_url",
          image_url: { url: item },
          role: "reference_image",
        });
      }
    });
  }
  if (videoConfig.mode == "singleImage") {
    images[0] && content.push({ type: "image_url", image_url: { url: images[0] } });
  }
  return content;
};

const buildVideoBody = (videoConfig: VideoConfig, videoModel: VideoModel) => {
  return {
    model: videoModel.modelName,
    content: buildVideoContent(videoConfig, videoModel),
    duration: videoConfig.duration,
    resolution: videoConfig.resolution,
    watermark: false, 
    ratio: videoConfig.aspectRatio,
    // }
    // : {}),
    ...(videoModel.audio === true && typeof videoConfig.audio === "boolean" ? { generate_audio: videoConfig.audio } : {}),
  };
};

const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {
  try {
    videoConfig.mode = JSON.parse(videoConfig.mode);
  } catch (e) {}
  try {
    const response = await axios.post(vendor.inputValues.baseUrl + "/volc/v1/contents/generations/tasks", buildVideoBody(videoConfig, videoModel), {
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + getApiKey()
      },
      timeout: 600000 // 10分钟超时
    });

    const taskId = extractTaskId(response);
    if (!taskId) throw new Error(`视频任务创建返回格式异常: ${JSON.stringify(response.data)}`);
    const result = await pollTask(async () => {
      const queryResponse = await axios.get(vendor.inputValues.baseUrl + "/api/v3/contents/generations/tasks/{id}".replace("{id}", taskId), {
        headers: {
          "Authorization": "Bearer " + getApiKey()
        },
        timeout: 600000 // 10分钟超时
      });
      const queryData = queryResponse.data;

      const status = getTaskStatus(queryData);
      if (SUCCESS_TASK_STATUS.includes(status)) {
        const videoUrl = extractVideoUrl(queryData);
        if (!videoUrl) return { completed: true, error: `视频任务成功但未返回结果: ${JSON.stringify(queryData)}` };
        return { completed: true, data: videoUrl };
      }

      if (FAILED_TASK_STATUS.includes(status)) {
        return { completed: true, error: getTaskError(queryData) };
      }

      // 仍在处理中
      return { completed: false };
    }, 5000, 600000); // 每5秒轮询一次,最长10分钟
    
    if (result.error) {
      throw new Error(result.error);
    }

    return result.data;
  } catch (error: any) {
    if (error.response) {
      throw new Error(`Volcengine API 错误: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    }
    throw error;
  }
};
exports.videoRequest = videoRequest;

interface TTSConfig {
  text: string;
  voice: string;
  speechRate: number;
  pitchRate: number;
  volume: number;
}

const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {
  return null;
};
exports.ttsRequest = ttsRequest;




原创声明:本文为原创教程,转载请注明出处。

欢迎在评论区交流讨论!

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐