跳转到主要内容

Documentation Index

Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt

Use this file to discover all available pages before exploring further.

概述

这是一个面向 Coze 平台 的自定义 Python 插件,把 API易 的 Nano Banana Pro 模型(gemini-3-pro-image-preview)封装成 Coze 工作流可直接调用的节点。插件内置完整的请求构造、错误码识别、违规内容判定与阿里云 OSS 上传链路,返回的是可直接展示的公网 URL,省去你在 Coze 工作流里再做一次结果转发的工作。
项目信息
  • 📦 形态:代码包形式分享(未公开在 GitHub
  • 👤 作者:Shuaila1996
  • 🎯 适用平台:Coze 国内版 / 海外版自定义插件
  • 🔌 调用模型:gemini-3-pro-image-preview(API易)
  • 📝 完整代码已在下方”插件完整源码”章节提供,可直接复制使用

核心功能

文生图 / 图生图统一入口

根据 fileurls 是否为空,自动切换文生图与改图模式,无需在 Coze 工作流里写两套节点

多张参考图改图

传入图片 URL 列表后自动下载并以 inline_data 方式注入请求,保留原图细节

精细化错误识别

区分 ZERO_CANDIDATES_TOKENFINISH_REASONINLINE_DATA_EMPTYTEXT_RESPONSE 等多种失败原因,便于工作流分支处理

违规内容自动归类

遇到水印移除、换脸、色情、超出知识库年限等任务时,返回明确的拒绝类型而非让用户自行猜测

OSS 直传

生成的 base64 图片直接上传阿里云 OSS,工作流拿到的是可直接外发或入库的 URL

分辨率超时自适应

1K / 2K / 4K 各自配置独立超时(360s / 600s / 1200s),4K 高清场景也不会被中断

支持的 API易 模型

模型名称模型标识用途API 文档
Nano Banana Progemini-3-pro-image-preview文生图、图生图查看文档
插件调用的是 API易 的 https://api.apiyi.com/v1beta/models/gemini-3-pro-image-preview:generateContent 端点(Gemini 原生协议),与官方 Google AI Studio 完全一致,便于复用现成 prompt。

插件架构

Coze 插件配置示意 插件核心调用链:
Coze 工作流入参 (cleantext / fileurls / aspect_ratio / resolution / apikey)

    handler() 入口

generate_image()  — 构造 parts + 调用 API易 端点

   解析候选项 / 兜底错误码

upload_base64_to_oss()  — 上传阿里云 OSS

返回 { analysis, url, error }

输入输出参数

入参(Input

参数类型必填说明
cleantextstring用户文字提示词或编辑指令
fileurlsstring[]参考图 URL 列表,留空走文生图
aspect_ratiostring宽高比,如 1:116:99:16
resolutionstring分辨率,必须大写:1K / 2K / 4K
apikeystringAPI易 密钥(建议在 Coze 工作流上游分发,按用户分配)

出参(Output

字段类型说明
analysisstring状态文案:图片生成成功 / 图片生成失败
urlstring | null成功时返回 OSS 公网链接
errorstring | null失败时返回友好错误描述

部署步骤

1

第一步:准备 API易 与 OSS 凭证

  • API易控制台 申请密钥(以 sk- 开头)
  • 在阿里云开通 OSS Bucket,并创建一个 RAM 子账号,授予该 Bucket 的 oss:PutObject 权限
  • 记录 AccessKey IDAccessKey SecretBucket 名称Endpoint(如 oss-cn-beijing.aliyuncs.com
2

第二步:在 Coze 创建自定义插件

  1. 进入 Coze 工作台 → 资源库 → 创建自定义插件
  2. 运行模式选择「云侧插件 - 在 Coze IDE 中创建」
  3. 工具运行环境选择 Python
  4. 添加依赖包:requestsoss2
3

第三步:复制插件代码

将下方”插件完整源码”章节的 Python 代码完整粘贴到 Coze IDE 中,并把代码顶部的阿里云 OSS 配置改成你自己的:
# 阿里云 OSS 配置
ACCESS_KEY_ID = "你的 AK"
ACCESS_KEY_SECRET = "你的 SK"
BUCKET_NAME = "你的 Bucket 名称"
ENDPOINT = "oss-cn-beijing.aliyuncs.com"
4

第四步:配置元数据与入参出参

按下图配置 Input / Output 字段类型与必填项,与代码中的 args.input 字段保持一致:Coze 插件元数据配置
5

第五步:测试与发布

  • 在 Coze IDE 内填入测试参数(建议先用 1K + 简单 prompt 验证 OSS 链路)
  • 测试通过后点击「发布」即可在工作流中拖拽使用

错误码识别策略

插件不只判断 success=True/False,还会按以下顺序识别失败原因,便于在 Coze 工作流里做差异化处理:
优先级错误类型触发条件推荐处理
1ZERO_CANDIDATES_TOKENusageMetadata.candidatesTokenCount == 0提示词或图片触发审核,建议改写
2NO_CANDIDATEScandidates 为空系统出错,重试
3FINISH_REASONfinishReasonSTOPPROHIBITED_CONTENT / SAFETY 等映射
4NO_PARTScontent.parts 为空重试
5INLINE_DATA_EMPTY检测到 inlineDatadata 为空重试或换 prompt
6TEXT_RESPONSE只返回了文本说明自动归类为水印/换脸/色情/年限超出

插件完整源码

下面是 coze-nanobanana-pro.py 的完整代码,可以直接复制到 Coze IDE。只需修改顶部 4 行 OSS 配置即可投入使用。
coze-nanobanana-pro.py
from runtime import Args
from typings.nanobanana_apiyi.nanobanana_apiyi import Input, Output
import requests
import base64
import io
import oss2
import uuid
import re
from datetime import datetime



# 阿里云 OSS 配置
ACCESS_KEY_ID = ""  #填入自己的阿里云 Access Key ID
ACCESS_KEY_SECRET = "" #填入自己的阿里云 Access Key Secret
BUCKET_NAME = "" #填入自己的阿里云 OSS Bucket 名称
ENDPOINT = "oss-cn-beijing.aliyuncs.com" #填入自己的阿里云 OSS Endpoint,例如 "oss-cn-beijing.aliyuncs.com"

# 分辨率超时时间
TIMEOUT = {
    "1K": 360,  # 快速预览
    "2K": 600,  # 推荐使用
    "4K": 1200,  # 超高清
}

def upload_base64_to_oss(image_base64: str) -> str:
    """
    将 base64 图片上传到阿里云 OSS 并返回链接
    支持带 data:image/...;base64, 前缀 和 纯 base64 两种情况
    """
    # 去掉 data:image/...;base64, 前缀
    base64_str = re.sub(r"^data:image/[^;]+;base64,", "", image_base64)
    image_data = base64.b64decode(base64_str)
    image_io = io.BytesIO(image_data)

    auth = oss2.Auth(ACCESS_KEY_ID, ACCESS_KEY_SECRET)
    bucket = oss2.Bucket(auth, ENDPOINT, BUCKET_NAME)
    object_name = f"coze/generated_{uuid.uuid4().hex}.png"
    bucket.put_object(object_name, image_io)

    return f"https://{BUCKET_NAME}.{ENDPOINT}/{object_name}"

# ==============================
# 工具函数:根据 URL 猜测 MIME 类型
# ==============================

def guess_mime_from_url(url: str) -> str:
    url_lower = url.lower()
    if url_lower.endswith(".png"):
        return "image/png"
    if url_lower.endswith(".jpg") or url_lower.endswith(".jpeg"):
        return "image/jpeg"
    if url_lower.endswith(".webp"):
        return "image/webp"
    if url_lower.endswith(".gif"):
        return "image/gif"
    # 默认
    return "image/png"

# ==============================
# 核心:生成 / 编辑图片
# ==============================

def generate_image(prompt: str, aspect_ratio: str, resolution: str, apikey:str,apiurl:str,image_urls=None):
    """
    生成 / 编辑图片的核心函数

    - 如果 image_urls 为空:纯文生图
    - 如果 image_urls 不为空:把 URL 指向的图片下载下来,按 inline_data 方式传给 API,实现改图
    """

    # 组装 parts
    parts = []

    # 1. 如果有图片 URL,则按 apiyi 改图 demo 的方式构造 inline_data
    if image_urls:
        for url in image_urls:
            try:
                resp = requests.get(url, timeout=180)
                if resp.status_code != 200:
                    return {
                        "success": False,
                        "error": f"图片上传阶段,获取图片失败({url})HTTP {resp.status_code}"
                    }

                image_bytes = resp.content
                image_base64 = base64.b64encode(image_bytes).decode("utf-8")
                mime_type = guess_mime_from_url(url)

                parts.append({
                    "inline_data": {
                        "mime_type": mime_type,
                        "data": image_base64
                    }
                })
            except Exception as e:
                return {
                    "success": False,
                    "error": f"图片上传阶段,获取图片失败({url}): {e}"
                }

    # 2. 文字部分(编辑指令或文生图提示词)
    #    注意:这里不再把图片 URL 塞进 prompt 里,仅用纯文字描述
    if prompt:
        parts.append({"text": prompt})
    else:
        # 没有文字时给一个默认提示(可按需要修改)
        parts.append({"text": "根据图片进行合理的编辑生成。"})

    # 3. 构造请求 payload(和官方改图 demo 一致的结构)
    payload = {
        "contents": [
            {
                "parts": parts
            }
        ],
        "generationConfig": {
            "responseModalities": ["IMAGE"],
            "imageConfig": {
                "aspectRatio": aspect_ratio,
                "image_size": resolution
            }
        }
    }

    headers = {
        "Authorization": f"Bearer {apikey}",
        "Content-Type": "application/json"
    }

    try:
        response = requests.post(apiurl, headers=headers, json=payload, timeout=TIMEOUT[resolution])

        # HTTP 非200
        if response.status_code != 200:
            return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}

        # JSON 解析
        try:
            data = response.json()
        except ValueError:
            return {"success": False, "error": "响应不是有效JSON", "response": (response.text or "")[:500]}

        # 1️⃣ 最高优先级:candidatesTokenCount
        usage = data.get("usageMetadata") or {}
        if usage.get("candidatesTokenCount") == 0:
            return {
                "success": False,
                "errorType": "ZERO_CANDIDATES_TOKEN",
                "error": "❌ 内容审核失败\n您的请求在内容审核阶段被拒绝,请修改提示词或图片",
                "response": data
            }

        # 2️⃣ candidates 检查
        candidates = data.get("candidates")
        if not isinstance(candidates, list) or len(candidates) == 0:
            return {
                "success": False,
                "errorType": "NO_CANDIDATES",
                "error": "系统出错,请稍后重试",
                "response": data
            }

        candidate = candidates[0] if isinstance(candidates[0], dict) else None
        if candidate is None:
            return {
                "success": False,
                "errorType": "NO_CANDIDATES",
                "error": "系统出错,请稍后重试(candidates[0]结构异常)",
                "response": data
            }

        # 3️⃣ finishReason
        finish_reason = candidate.get("finishReason")
        if isinstance(finish_reason, str) and finish_reason != "STOP":
            reason_map = {
                "PROHIBITED_CONTENT": "内容违反安全策略,已被拒绝处理",
                "SAFETY": "内容触发了安全过滤器",
                "RECITATION": "内容可能涉及版权问题",
                "MAX_TOKENS": "内容长度超出限制",
            }
            return {
                "success": False,
                "errorType": "FINISH_REASON",
                "finishReason": finish_reason,
                "error": reason_map.get(finish_reason, f"请求被拒绝:{finish_reason}"),
                "response": data
            }

        # 4️⃣ content.parts
        content = candidate.get("content") or {}
        parts = content.get("parts")
        if not isinstance(parts, list) or len(parts) == 0:
            return {
                "success": False,
                "errorType": "NO_PARTS",
                "error": "生成失败,请重试(content.parts为空)",
                "response": data
            }

        # 5️⃣ 提取图片和文本(更精准:识别 inlineData 存在但 data 为空)
        images = []
        texts = []
        saw_inline_but_empty = False

        for i, part in enumerate(parts):
            if not isinstance(part, dict):
                continue

            # 收集 text(即使有 thoughtSignature,也照收)
            t = part.get("text")
            if isinstance(t, str) and t.strip() and not t.startswith("data:image/"):
                texts.append(t.strip())

            # 兼容 inlineData / inline_data
            inline = None
            if isinstance(part.get("inlineData"), dict):
                inline = part["inlineData"]
            elif isinstance(part.get("inline_data"), dict):
                inline = part["inline_data"]

            if inline is not None:
                b64 = inline.get("data")
                if not isinstance(b64, str) or not b64.strip():
                    saw_inline_but_empty = True
                    continue
                images.append(b64.strip())

        # ✅ 更精准:inlineData 存在但全都没 data
        if not images and saw_inline_but_empty:
            return {
                "success": False,
                "errorType": "INLINE_DATA_EMPTY",
                "error": "生成失败:检测到 inlineData 但图片数据为空(inlineData.data为空)",
                "response": data
            }

        # 6️⃣ 有图片:成功(保持你原来的返回结构)
        if images:
            return {"success": True, "image_data": images[0]}

        # 7️⃣ 无图片:有文本 -> TEXT_RESPONSE
        if texts:
            text_content = "\n".join(texts)

            # —— 可选:不做函数,直接就地识别类型(想更简单可删掉这段 detectedType)——
            low = text_content.lower()
            detected = "general"
            if any(k in low for k in ["watermark", "remove watermark", "去水印", "移除水印", "删除水印"]):
                detected = "拒绝处理水印任务"
            elif any(k in low for k in ["faceswap", "face swap", "换脸", "deepfake"]):
                detected = "拒绝处理换脸任务"
            elif any(k in low for k in ["sexually", "explicit", "porn", "nude", "nsfw", "色情", "不雅", "裸"]):
                detected = "拒绝色情任务"
            elif any(str(y) in low for y in range(2026, 2101)):
                detected = "拒绝超过知识库范围任务"

            return {
                "success": False,
                "errorType": "TEXT_RESPONSE",
                "error": detected,   # 你文档要求:直接展示 API text
                "response": data
            }

        # ✅ 更精准:parts 有结构但既无图也无文本
        return {
            "success": False,
            "error": "生成失败:parts存在但未找到图片数据或文本说明",
            "response": data
        }

    except requests.exceptions.Timeout:
        return {"success": False, "error": f"图片生成请求超时(超过 {TIMEOUT[resolution]} 秒)"}
    except Exception as e:
        return {"success": False, "error": f"图片生成请求失败: {str(e)}"}


# ==============================
# Coze Node 入口
# ==============================

def handler(args: Args[Input]) -> Output:
    """
    Coze / NanobananaPro 节点入口

    - args.input.cleantext: 用户文字提示词
    - args.input.fileurls:  用户上传图片的 URL 列表(用于改图)
    - args.input.aspect_ratio: 宽高比,如 "1:1" / "9:16"
    - args.input.resolution: 分辨率,如 "1K" / "2K" / "4K"
    """
    API_URL = "https://api.apiyi.com/v1beta/models/gemini-3-pro-image-preview:generateContent"
    API_KEY = args.input.apikey
    cleanttext = args.input.cleantext or ""
    fileurls = args.input.fileurls or []
    aspectratio = args.input.aspect_ratio
    resolution = args.input.resolution
    # - 图片通过 image_urls 传入 generate_image,走 inline_data 改图逻辑
    prompt = cleanttext.strip()

    # 调用 Gemini 3 Pro 生成 / 编辑图片
    # 如果 fileurls 不为空,会按"改图"模式调用

    result = generate_image(prompt, aspectratio, resolution, API_KEY,API_URL,image_urls=fileurls if fileurls else None)

    if result["success"]:
        image_base64 = result["image_data"]
        oss_url = upload_base64_to_oss(image_base64)
        return {"analysis": "图片生成成功", "url": oss_url, "error": None}
    else:
        return {"analysis": "图片生成失败", "url": None, "error": result["error"]}

在 Coze 工作流中使用

插件发布后,在 Coze 工作流编辑器里拖入插件节点,按以下方式连线:
开始节点 (用户输入提示词 + 图片)

图片提示词分离 (代码节点)

人员 apikey 分离 (字典查询,按用户分发 API易 密钥)

nanobanana_apiyi 插件节点 (本插件)

成功 / 失败分支

结束节点 (输出 url 或 error)
推荐配合 飞书多维表格 AI 生图方案 使用,整套方案让运营/设计同学在飞书表格里填提示词就能批量出图,无需打开任何代码。

常见问题

Coze 工作流后续节点(特别是飞书字段捷径)大多需要 可访问的 URL 才能转换为图片附件。直接返回 base64 会让数据在工作流里反复传输,不仅性能差,飞书侧还无法直接渲染。OSS 链接还方便长期归档与对外分享。
Coze 自定义插件目前不支持系统环境变量。推荐做法:把 OSS 凭证写在代码顶部常量,并通过 Coze 的「插件加密配置」功能保护。如果你的工作流面向多租户,建议把 OSS 路径前缀也按租户分目录写入。
便于按用户分发不同密钥。在 Coze 工作流中可以前置一个「人员 apikey 分离」字典节点,按调用人姓名匹配对应的 API易 密钥,方便用量核算与权限控制。
Nano Banana Pro 的 4K 出图本身需要较长时间(通常 5-15 分钟)。插件已为 4K 配置了 1200 秒超时,如果还是超时,建议:
  1. 降级到 2K 调试 prompt
  2. 检查 API易 控制台是否有限流
  3. 减少同时调用并发数
这通常意味着模型拒绝了任务(违规、超出知识库等),插件会自动判别类型:水印移除、换脸、色情、年份超出 2025 等。按 error 字段文案展示给用户即可,不要重试——重试结果一致。
可以。本文档”插件完整源码”章节提供了 coze-nanobanana-pro.py 的完整代码(作者 Shuaila1996 贡献),只需修改顶部 4 行 OSS 配置就能直接粘贴到 Coze IDE 投入使用,无需额外索取。如果你还需要:
  • 飞书字段捷径代码 → 见 飞书多维表格 AI 生图方案 中的”飞书字段捷径完整源码”章节
  • 阿里云函数计算代码 → 同上文档中的”阿里云函数计算完整源码”章节

相关资源

飞书多维表格 AI 生图方案

本插件的最佳搭档:把整条 Coze 工作流接入飞书多维表格,运营同学填表即可批量出图

Nano Banana Pro 文档

查看 Nano Banana Pro 完整 API 文档、定价与生图样例

生图失败排查

Nano Banana 生图常见问题排查指南,匹配本插件的错误码体系

API易控制台

管理 API 密钥、查看用量与余额