Skill: wuchubuzai2018/nano-banana-2-image-gen (version 0.1.1)

--- name: nano-banana-2-image-gen description: 图片生成技能，当用户需要生成图片、视觉信息图、创建图像、编辑/修改/调整已有图片时使用此技能。基于中国的API易代理站(https://api.apiyi.com/)的NanoBanana2模型的图片生成服务，无需访问外网。支持14种宽高比的图片比例（`1:1`、`16:9`、`9:16`、`4:3`、`3:4`、`3:2`、`2:3`、`5:4`、`4:5`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9`等）和3种分辨率（1K、2K、4K），支持文生图和图生图编辑。基于谷歌的NanoBanana2模型(快速模型)，使用API易国内代理服务访问。 --- # 图片生成与编辑 基于谷歌的Gemini 3.1 Flash模型实现图片生成技能，可以通过自然语言帮助用户生成图片，通过API易国内代理服务访问，支持Node.js和Python两种运行环境。Nano Banana 2（代号）是谷歌于 2026 年 2 月 26 日发布的最新图像生成模型，模型 ID 为 gemini-3.1-flash-image-preview。它以 Pro 级画质 + Flash 级速度和成本 重新定义了图像生成的性价比，是 Nano Banana 系列的最新旗舰。 ## 使用指引 遵循以下步骤： ### 第1步：分析需求与参数提取 1. **明确意图**：区分用户是需要【文生图】（生成新图片）还是【图生图】（编辑/修改现有图片）。 2. **提示词（Prompt）分析**： - **使用用户原始完整输入**：把用户输入的原始完整问题需求描述（原文）直接作为 `-p` 提示词的主体，避免自行改写、总结或二次创作，防止细节丢失。 - **需要补充时先确认**：如果信息不足（例如缺少风格、主体数量、镜头语言、场景细节、文字内容、禁止元素等），先向用户提问确认；用户确认后，再把补充内容**以“追加”的方式**拼接到原始提示词后。 - 样例： - 用户输入：“帮我生成一张猫的图片，风格要可爱一点。” - 正例说明：直接使用用户输入作为提示词：`-p "帮我生成一张猫的图片，风格要可爱一点。"` - 反例说明：擅自改写为“生成一张可爱风格的猫的图片”会丢失用户原始输入的细节和语气。 - 如果需要补充细节（例如颜色、背景等），先提问确认：“你希望猫是什么颜色的？背景有什么要求吗？”用户回答后，再追加到提示词中：`-p "帮我生成一张猫的图片，风格要可爱一点。猫是橘色的，背景是草地。"` 3. **关键参数整理**： - **Prompt（必需）**：提示词分析后的最终提示词（默认=用户原始完整且一致的输入；仅在用户确认后才追加补充信息）。 - **Filename（可选）**：输出图片文件名/路径(需包含文件随机标识，避免重复)。不传则脚本会自动生成带时间戳的文件名。建议根据内容生成合理文件名（例如 `cat_in_garden.png`），避免使用通用名。 - **Aspect Ratio（可选）**：根据用户描述推断比例。例如： - "手机壁纸" -> `9:16` - "电脑壁纸/视频封面" -> `16:9` - "头像" -> `1:1` - 默认若用户未明确不指定图片比例，保持图片比例为空。 - **Resolution（可选）**： - 默认图片比例使用 `2K`。 - 仅在极端高清需求或用户指定时使用 `4K`，并通过友好性提示，提示用户生成较慢，耐心等待。 - **注意**：参数值必须大写（`1K`, `2K`, `4K`）。 ### 第2步：环境检查与命令执行 1. **检查环境**：确认 `APIYI_API_KEY` 环境变量是否已设置（通常假定已设置，若运行失败再提示用户）。 2. **构建并运行命令**： - **优先尝试 Node.js 版本**：如果环境有 Node（`node` 命令可用），优先使用 `scripts/generate_image.js`（零依赖，参数与 Python 保持一致）。 - **Node 不可用再用 Python 版本**：使用 `scripts/generate_image.py`。 **文生图命令模板（优先 Node.js）：** ```bash node scripts/generate_image.js -p "{prompt}" -f "{filename}" [-a {ratio}] [-r {res}] ``` **图生图命令模板（优先 Node.js）：** ```bash node scripts/generate_image.js -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-r {res}] ``` **（可选）Python 版本命令模板（Node 不可用时）**： ```bash python scripts/generate_image.py -p "{prompt}" -f "{filename}" [-a {ratio}] [-r {res}] python scripts/generate_image.py -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-r {res}] ``` ## ⏱️ 长时间任务处理策略 ### 1. 任务前提示 **执行前必须告知用户**： - "图片生成已启动，预计需要25秒到5分钟" ### 2. 🎨 最佳实践示例 1. 快速生成场景（1K分辨率） > "快速模式：1K分辨率生成，预计30秒内完成" 2. 高质量生成场景（2K/4K分辨率） > "高质量模式：2K分辨率生成，预计1-4分钟\n⏳ 开始生成... 🔄" ### 第3步：结果反馈 1. **执行反馈**：等待终端命令执行完毕。 2. **成功**：告知用户图片已生成，并指出保存路径。 3. **失败**： - 若提示 API Key 缺失，请指导用户设置环境变量。 - 若提示网络错误，建议用户检查网络或稍后重试。 ## 命令行使用样例 ### 生成新图片 ```bash python scripts/generate_image.py -p "图片描述文本" -f "output.png" [-a 1:1] [-r 1K] ``` **示例：** ```bash # 基础生成 python scripts/generate_image.py -p "一只可爱的橘猫在草地上玩耍" -f "cat.png" # 指定比例和分辨率 python scripts/generate_image.py -p "日落山脉风景" -f "sunset.png" -a 16:9 -r 4K # 竖版高清图片（适合手机壁纸） python scripts/generate_image.py -p "城市夜景" -f "city.png" -a 9:16 -r 2K ``` **（可选）Node.js 版本示例：** ```bash # 基础生成 node scripts/generate_image.js -p "一只可爱的橘猫在草地上玩耍" -f "cat.png" # 指定比例和分辨率 node scripts/generate_image.js -p "日落山脉风景" -f "sunset.png" -a 16:9 -r 4K ``` ### 编辑已有图片 ```bash python scripts/generate_image.py -p "编辑指令" -f "output.png" -i "path/to/input.png" [-a 1:1] [-r 1K] ``` **示例：** ```bash # 修改风格 python scripts/generate_image.py -p "将图片转换成水彩画风格" -f "watercolor.png" -i "original.png" # 添加元素 python scripts/generate_image.py -p "在天空添加彩虹" -f "rainbow.png" -i "landscape.png" -r 2K # 替换背景 python scripts/generate_image.py -p "将背景换成海滩" -f "beach-bg.png" -i "portrait.png" -a 3:4 ``` **（可选）Node.js 版本示例：** ```bash # 修改风格 node scripts/generate_image.js -p "将图片转换成水彩画风格" -f "watercolor.png" -i "original.png" # 多张参考图（最多14张） node scripts/generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f "merged.png" ``` ## 附加资源 - 常见使用场景文档：references/scene.md ## 命令行参数说明 > Python 与 Node.js 版本参数保持一致（短参数与长参数等价）。 | 参数 | 必填 | 说明 | |------|------|------| | `-p` / `--prompt` | 是 | 图片描述（文生图）或编辑指令（图生图）。保留用户原始完整输入。 | | `-f` / `--filename` | 否 | 输出图片路径/文件名；不传则自动生成带时间戳的 PNG 文件名，并写入当前目录。 | | `-a` / `--aspect-ratio` | 否 | 图片比例：`1:1`、`16:9`、`9:16`、`4:3`、`3:4`、`3:2`、`2:3`、`5:4`、`4:5`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9`。 | | `-r` / `--resolution` | 否 | 图片分辨率：`1K` / `2K` / `4K`（必须大写）。不传则不在请求中指定，由 API 侧决定。 | | `-i` / `--input-image` | 否 | 图生图输入图片路径；可传多张（最多 14 张）。传入该参数即进入编辑模式。 | ## 图片参数说明 ### aspect_ratio - 图片比例 支持以下14种比例： | 比例 | 方向 | 适用场景 | |------|------|----------| | 1:1 | 正方形 | 头像、Instagram帖子 | | 16:9 | 横版 | YouTube缩略图、桌面壁纸、演示文稿 | | 9:16 | 竖版 | 抖音/TikTok、Instagram Stories、手机壁纸 | | 4:3 | 横版 | 经典照片、演示文稿 | | 3:4 | 竖版 | Pinterest、人像摄影 | | 3:2 | 横版 | 单反相机标准、印刷媒体 | | 2:3 | 竖版 | 人像海报 | | 5:4 | 横版 | 大幅面打印、艺术印刷 | | 4:5 | 竖版 | Instagram帖子、社交媒体 | | 21:9 | 超宽 | 电影感、横幅、全景 | | 1:4 | 超竖 | 手机长图、漫画 | | 4:1 | 超横 | 横幅、网站头图 | | 1:8 | 超竖 | 手机长图、漫画 | | 8:1 | 超横 | 横幅、网站头图 | ### resolution - 图片分辨率 1K、2K、4K三种分辨率选项 **注意：** 分辨率值必须大写（1K、2K、4K） **默认：** 2K ## 注意事项 - API密钥必须设置，可通过环境变量或命令行参数提供 - 分辨率参数必须大写（1K/2K/4K），小写会默认使用1K - 图片生成时间：25秒到5分钟不等，取决于分辨率和服务器负载 - 编辑图片时，输入图片会自动转换为base64编码 - 确保输出目录有写入权限 ### API Key设置与获取 #### 如何获取API Key 如果你还没有API密钥，请前往 **https://api.apiyi.com** 注册账号并申请API Key。 获取步骤： 1. 访问 https://api.apiyi.com 2. 注册/登录你的账号 3. 在控制台中创建API密钥 4. 复制密钥并设置环境变量或在命令行中使用 #### 设置API Key 脚本按以下顺序查找API密钥： 1. `--api-key` 命令行参数（临时使用） 2. `APIYI_API_KEY` 环境变量（推荐） **设置环境变量（推荐）：** ```bash # Linux/Mac export APIYI_API_KEY="your-api-key-here" # Windows CMD 我的电脑高级设置中设置环境变量或者执行set APIYI_API_KEY=your-api-key-here # Windows PowerShell 在我的电脑中设置环境变量:$env:APIYI_API_KEY="your-api-key-here" ``` **命令行参数方式（临时）：** ```bash python scripts/generate_image.py -p "一只猫" -k "your-api-key-here" ``` ## 作者介绍 - 爱海贼的无处不在 - 我的微信公众号：无处不在的技术

path: references/scene.md
content: ## 常见使用场景

### 社交媒体内容
- **微信朋友圈/公众号**: `-a 1:1` 或 `-a 3:4`， 或 `-a 9:16`，配图文案图、封面图
- **小红书笔记**: `-a 3:4` 或 `-a 4:5`，竖版配图更受欢迎
- **抖音/视频号**: `-a 9:16`，短视频封面、竖版内容图
- **B站/知乎**: `-a 16:9 -r 2K`，视频封面、专栏头图
- **微博**: `-a 1:1` 或 `-a 3:4`，九宫格配图、话题图

### 电商与营销
- **淘宝/京东主图**: `-a 1:1`，白底产品图、场景图
- **电商详情页**: `-a 3:4`，产品展示、卖点图、对比图
- **活动海报**: `-a 2:3` 或 `-a 9:16`，促销海报、节日海报
- **PPT配图**: `-a 16:9`，演讲背景、数据可视化插图

### 个人创作
- **艺术创作**: 视觉信息图、笔记图、插画图等等
- **头像/壁纸**: `-a 1:1`（头像）、`-a 9:16`（手机壁纸）
- **表情包/梗图**: `-a 1:1`，搞笑图片、表情包素材
- **节日贺卡**: `-a 3:4` 或 `-a 4:5`，春节、中秋祝福图
- **LOGO/图标**: `-a 1:1`，品牌标识、应用图标设计

### 编辑任务
- **风格转换**: "转换成国潮/水墨/二次元/像素风格"
- **元素添加**: "添加春节元素/红包/灯笼/烟花"
- **背景替换**: "将背景换成故宫/西湖/长城/现代都市"
- **色彩调整**: "调整为莫兰迪色系/中国传统色"
- **对象移除**: "去除水印/路人/杂物"
- **画质修复**: "修复老照片/提升清晰度/去噪点"

path: scripts/generate_image.py
content: #!/usr/bin/env python3
"""
基于NanoBanana2的图片生成与编辑脚本
使用API易国内代理服务

支持功能：
- 文生图：根据提示词生成图片
- 图生图：根据编辑指令修改已有图片

参数说明：
- aspect_ratio: 图片比例 (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)
- resolution: 图片分辨率 (1K, 2K, 4K)，必须大写
"""

import os
import sys
import json
import base64
import argparse
from unittest import result
import requests
from pathlib import Path
from datetime import datetime


# 支持的比例列表
SUPPORTED_ASPECT_RATIOS = [
    "1:1",
    "16:9",
    "9:16",
    "4:3",
    "3:4",
    "3:2",
    "2:3",
    "5:4",
    "4:5",
    "1:4",
    "4:1",
    "1:8",
    "8:1",
    "21:9",
]
SUPPORTED_RESOLUTIONS = ["1K", "2K", "4K"]


def get_api_key(args_key=None):
    """获取API密钥，优先使用命令行参数"""
    if args_key:
        return args_key

    [REDACTED_SECRET]"APIYI_API_KEY")
    if not [REDACTED_SECRET]"错误: 未设置 APIYI_API_KEY 环境变量")
        print("请前往 https://api.apiyi.com 注册申请API Key")
        print("或使用 --api-key 参数临时指定")
        sys.exit(1)

    return api_key


def generate_filename(prompt):
    """根据提示词生成带时间戳的文件名"""
    timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

    # 从提示词提取关键词（简化处理）
    keywords = prompt.split()[:3]  # 取前3个词
    keyword_str = "-".join(keywords)

    # 清理文件名中的特殊字符
    keyword_str = "".join(c if c.isalnum() or c in "-_." else "-" for c in keyword_str)
    keyword_str = keyword_str.lower()[:30]  # 限制长度

    return f"{timestamp}-{keyword_str}.png"


def add_timestamp_to_filename(filename: str, timestamp: str) -> str:
    p = Path(filename)
    stem = p.stem or "image"
    suffix = p.suffix
    # If suffix is empty, keep it empty (caller may intentionally omit extension)
    new_name = f"{stem}-{timestamp}{suffix}"
    return str(p.with_name(new_name))


def encode_image_to_base64(image_path):
    """将图片文件转换为base64编码"""
    try:
        with open(image_path, "rb") as f:
            return base64.b64encode(f.read()).decode("utf-8")
    except Exception as e:
        print(f"错误: 无法读取图片文件 {image_path} - {e}")
        sys.exit(1)


def generate_image(
    prompt,
    filename,
    aspect_ratio=None,
    resolution=None,
    input_image=None,
    api_key=None,
):
    """
    生成或编辑图片

    Args:
        prompt: 图片描述或编辑指令文本
        filename: 输出图片路径
        aspect_ratio: 图片比例 (可选，默认由API决定)
        resolution: 图片分辨率 (可选，默认由API决定)
        input_image: 输入图片路径（编辑模式时使用）
        api_key: API密钥
    """
    # 验证参数（仅在提供了参数时验证）
    if aspect_ratio is not None and aspect_ratio not in SUPPORTED_ASPECT_RATIOS:
        print(f"错误: 不支持的比例 '{aspect_ratio}'")
        print(f"支持的比例: {', '.join(SUPPORTED_ASPECT_RATIOS)}")
        sys.exit(1)

    if resolution is not None and resolution not in SUPPORTED_RESOLUTIONS:
        print(f"错误: 不支持的分辨率 '{resolution}'")
        print(f"支持的分辨率: {', '.join(SUPPORTED_RESOLUTIONS)} (必须大写)")
        sys.exit(1)

    [REDACTED_SECRET])
    url = (
        "https://api.apiyi.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent"
    )

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

    # 构建content部分
    parts = [{"text": prompt}]

    # 如果提供了输入图片，添加图片数据（图生图模式）
    # NanoBanana2最多支持14张参考图片
    if input_image:
        if isinstance(input_image, (list, tuple)):
            input_images = list(input_image)
        else:
            input_images = [input_image]

        if len(input_images) > 14:
            print(f"错误: 输入图片最多支持14张，当前为 {len(input_images)} 张")
            sys.exit(1)

        for image_path in input_images:
            if not os.path.exists(image_path):
                print(f"错误: 输入图片不存在: {image_path}")
                sys.exit(1)

            image_base64 = encode_image_to_base64(image_path)
            parts.append({"inlineData": {"mimeType": "image/png", "data": image_base64}})

        mode_str = "编辑图片"
    else:
        mode_str = "生成图片"

    # 构建payload，只添加用户指定的参数
    generation_config = {
        "responseModalities": ["IMAGE"],
    }
    image_config = {}
    if aspect_ratio is not None:
        image_config["aspectRatio"] = aspect_ratio
    if resolution is not None:
        image_config["imageSize"] = resolution
    if image_config:
        generation_config["imageConfig"] = image_config

    payload = {
        "contents": [{"parts": parts}],
        "generationConfig": generation_config,
    }

    print(f"正在{mode_str}...")
    print(f"提示词: {prompt}")

    if generation_config.get("imageConfig", {}).get("aspectRatio"):
        print(f"比例: {generation_config['imageConfig']['aspectRatio']}")
    if generation_config.get("imageConfig", {}).get("imageSize"):
        print(f"分辨率: {generation_config['imageConfig']['imageSize']}")

    # 输出请求参数（脱敏：不直接输出base64图片数据，避免刷屏）
    payload_log = {
        "generationConfig": generation_config,
        "contents": [],
    }
    for content in payload.get("contents", []):
        parts_log = []
        for part in content.get("parts", []):
            if isinstance(part, dict) and "inlineData" in part and isinstance(part["inlineData"], dict):
                inline_data = dict(part["inlineData"])
                data_value = inline_data.get("data")
                if isinstance(data_value, str):
                    inline_data["data"] = f"<omitted base64: {len(data_value)} chars>"
                parts_log.append({"inlineData": inline_data})
            else:
                parts_log.append(part)
        payload_log["contents"].append({"parts": parts_log})

    print(f"输出请求参数: {json.dumps(payload_log, indent=2, ensure_ascii=False)}")
    print(f"image generation in progress...")
    try:
        response = requests.post(url, headers=headers, json=payload, timeout=400)
        response.raise_for_status()

        data = response.json()

        # 解析响应，查找图片数据
        image_data = None
        text_response = ""

        if "candidates" in data and len(data["candidates"]) > 0:
            candidate = data["candidates"][0]
            image_data = candidate["content"]["parts"][0]["inlineData"]["data"]

        if image_data:
            # 解码base64图片数据
            image_bytes = base64.b64decode(image_data)

            # 确保输出目录存在
            output_file = Path(filename)
            output_file.parent.mkdir(parents=True, exist_ok=True)

            # 保存图片
            with open(output_file, "wb") as f:
                f.write(image_bytes)

            print(f"✓ 图片已成功{mode_str}并保存到: {filename}")

            if text_response.strip():
                print(f"模型响应: {text_response.strip()}")

            return filename
        else:
            print("错误: 响应中未找到图片数据")
            print(f"完整响应: {json.dumps(data, indent=2, ensure_ascii=False)}")
            sys.exit(1)

    except requests.exceptions.Timeout:
        print("错误: 请求超时，请稍后重试")
        sys.exit(1)
    except requests.exceptions.RequestException as e:
        print(f"错误: 请求失败 - {e}")
        if hasattr(e, "response") and e.response is not None:
            try:
                error_detail = e.response.json()
                print(
                    f"错误详情: {json.dumps(error_detail, indent=2, ensure_ascii=False)}"
                )
            except:
                print(f"响应状态码: {e.response.status_code}")
                print(f"响应内容: {e.response.text}")
        sys.exit(1)
    except Exception as e:
        print(f"错误: {str(e)}")
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(
        description="基于Gemini 3 Pro的图片生成与编辑工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用示例:

【生成新图片】
    python generate_image.py -p "一只可爱的橘猫"
    python generate_image.py -p "日落山脉" -a 16:9 -r 4K
    python generate_image.py -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png

【编辑已有图片】
    python generate_image.py -p "转换成油画风格" -i original.png
    python generate_image.py -p "添加彩虹到天空" -i photo.jpg -f edited.png
    python generate_image.py -p "将背景换成海滩" -i portrait.png -a 3:4 -r 2K
    python generate_image.py -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f merged.png

【支持的参数值】
  --aspect-ratio: 可选 (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)
  --resolution:   可选 (1K, 2K, 4K，必须大写)

【环境变量】
  export APIYI_API_KEY="your-api-key"
        """,
    )

    parser.add_argument("--prompt", "-p", required=True, help="图片描述或编辑指令文本")
    parser.add_argument(
        "--filename",
        "-f",
        default=None,
        help="输出图片路径 (默认: 自动生成时间戳文件名)",
    )
    parser.add_argument(
        "--aspect-ratio",
        "-a",
        default=None,
        choices=SUPPORTED_ASPECT_RATIOS,
        help="图片比例 (可选: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)",
    )
    parser.add_argument(
        "--resolution",
        "-r",
        default=None,
        choices=SUPPORTED_RESOLUTIONS,
        help="图片分辨率 (可选: 1K, 2K, 4K，必须大写)",
    )
    parser.add_argument(
        "--input-image",
        "-i",
        nargs="+",
        default=None,
        help="输入图片路径（编辑模式，可传多张，最多14张）",
    )
    parser.add_argument("--api-key", "-k", default=None, help="API密钥（覆盖环境变量）")

    args = parser.parse_args()

    run_timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

    # 如果没有指定文件名，自动生成
    if args.filename is None:
        args.filename = generate_filename(args.prompt)
    else:
        out_path = Path(args.filename)
        if out_path.exists():
            adjusted = add_timestamp_to_filename(args.filename, run_timestamp)
            print(f"警告: 输出文件已存在，将避免覆盖并改为: {adjusted}")
            args.filename = adjusted

    generate_image(
        prompt=args.prompt,
        filename=args.filename,
        aspect_ratio=args.aspect_ratio,
        resolution=args.resolution,
        input_image=args.input_image,
        [REDACTED_SECRET],
    )


if __name__ == "__main__":
    main()


path: scripts/generate_image.js
content: #!/usr/bin/env node
/*
基于NanoBanana2的图片生成与编辑脚本（Node.js版）
使用API易国内代理服务

支持功能：
- 文生图：根据提示词生成图片
- 图生图：根据编辑指令修改已有图片

参数说明：
- -p, --prompt        图片描述或编辑指令文本（必需）
- -f, --filename      输出图片路径（可选，默认自动生成时间戳文件名）
- -a, --aspect-ratio  图片比例（可选）
- -r, --resolution    图片分辨率（可选：1K/2K/4K，必须大写）
- -i, --input-image   输入图片路径（可选，可多张，最多14张）
- -k, --api-key       API密钥（可选，覆盖环境变量 APIYI_API_KEY）

使用示例：
【生成新图片】
  node generate_image.js -p "一只可爱的橘猫"
  node generate_image.js -p "日落山脉" -a 16:9 -r 4K
  node generate_image.js -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png

【编辑已有图片】
  node generate_image.js -p "转换成油画风格" -i original.png
  node generate_image.js -p "添加彩虹到天空" -i photo.jpg -f edited.png
  node generate_image.js -p "将背景换成海滩" -i portrait.png -a 3:4 -r 2K
  node generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f merged.png

【环境变量】
  export APIYI_API_KEY="your-api-key"
*/

const fs = require('fs');
const path = require('path');
const https = require('https');

const SUPPORTED_ASPECT_RATIOS = [
  '1:1',
  '16:9',
  '9:16',
  '4:3',
  '3:4',
  '3:2',
  '2:3',
  '5:4',
  '4:5',
  '1:4',
  '4:1',
  '1:8',
  '8:1',
  '21:9',
];

const SUPPORTED_RESOLUTIONS = ['1K', '2K', '4K'];

function printHelpAndExit(exitCode = 0) {
  const help = `usage: generate_image.js [-h] --prompt PROMPT [--filename FILENAME]
                        [--aspect-ratio ${SUPPORTED_ASPECT_RATIOS.join(', ')}]
                        [--resolution ${SUPPORTED_RESOLUTIONS.join(', ')}]
                        [--input-image INPUT_IMAGE [INPUT_IMAGE ...]]
                        [--api-key API_KEY]

基于NanoBanana2的图片生成与编辑工具（Node.js版）

options:
  -h, --help            show this help message and exit
  -p, --prompt PROMPT   图片描述或编辑指令文本（必需）
  -f, --filename FILE   输出图片路径 (默认: 自动生成时间戳文件名)
  -a, --aspect-ratio    图片比例 (可选)
  -r, --resolution      图片分辨率 (可选: 1K, 2K, 4K，必须大写)
  -i, --input-image     输入图片路径（编辑模式，可传多张，最多14张）
  -k, --api-key         API密钥（覆盖环境变量）

运行示例:
  node scripts/generate_image.js -p "一只可爱的橘猫"
  node scripts/generate_image.js -p "日落山脉" -a 16:9 -r 4K
  node scripts/generate_image.js -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png

  node scripts/generate_image.js -p "转换成油画风格" -i original.png
  node scripts/generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png -f merged.png
`;
  process.stdout.write(help);
  process.exit(exitCode);
}

function exitWithError(message) {
  process.stderr.write(`${message}\n`);
  process.exit(1);
}

function pad2(n) {
  return String(n).padStart(2, '0');
}

function formatTimestamp(dateObj) {
  const d = dateObj || new Date();
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}-${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
}

function addTimestampToFilename(filePath, timestamp) {
  const ts = timestamp || formatTimestamp(new Date());
  const parsed = path.parse(filePath);
  const base = parsed.name ? `${parsed.name}-${ts}` : ts;
  return path.join(parsed.dir || '.', `${base}${parsed.ext || ''}`);
}

function generateFilename(prompt) {
  const now = new Date();
  const timestamp = formatTimestamp(now);

  const keywords = String(prompt).split(/\s+/).filter(Boolean).slice(0, 3);
  const keywordStrRaw = keywords.join('-') || 'image';

  const keywordStr = keywordStrRaw
    .split('')
    .map((c) => (/^[a-zA-Z0-9\-_.]$/.test(c) ? c : '-'))
    .join('')
    .toLowerCase()
    .slice(0, 30);

  return `${timestamp}-${keywordStr}.png`;
}

function getApiKey(argsKey) {
  if (argsKey) return argsKey;
  const [REDACTED_SECRET];
  if (!apiKey) {
    exitWithError(
      '错误: 未设置 APIYI_API_KEY 环境变量\n' +
        '请前往 https://api.apiyi.com 注册申请API Key\n' +
        '或使用 -k/--api-key 参数临时指定'
    );
  }
  return apiKey;
}

function encodeImageToBase64(imagePath) {
  try {
    const bytes = fs.readFileSync(imagePath);
    return bytes.toString('base64');
  } catch (e) {
    exitWithError(`错误: 无法读取图片文件 ${imagePath} - ${e.message || String(e)}`);
  }
}

function postJson(urlString, headers, payload, timeoutMs) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlString);

    const body = Buffer.from(JSON.stringify(payload), 'utf8');
    const req = https.request(
      {
        protocol: url.protocol,
        hostname: url.hostname,
        port: url.port || 443,
        path: url.pathname + url.search,
        method: 'POST',
        headers: {
          ...headers,
          'Content-Length': body.length,
        },
      },
      (res) => {
        const chunks = [];
        res.on('data', (d) => chunks.push(d));
        res.on('end', () => {
          const text = Buffer.concat(chunks).toString('utf8');
          const statusCode = res.statusCode || 0;

          if (statusCode < 200 || statusCode >= 300) {
            const err = new Error(`HTTP ${statusCode}`);
            err.statusCode = statusCode;
            err.responseText = text;
            return reject(err);
          }

          try {
            resolve(JSON.parse(text));
          } catch (e) {
            const err = new Error('响应不是有效的JSON');
            err.responseText = text;
            return reject(err);
          }
        });
      }
    );

    req.on('error', reject);

    req.setTimeout(timeoutMs, () => {
      req.destroy(new Error('timeout'));
    });

    req.write(body);
    req.end();
  });
}

function parseArgs(argv) {
  const args = {
    prompt: null,
    filename: null,
    aspectRatio: null,
    resolution: null,
    inputImages: null,
    apiKey: null,
  };

  const knownFlags = new Set([
    '-h',
    '--help',
    '-p',
    '--prompt',
    '-f',
    '--filename',
    '-a',
    '--aspect-ratio',
    '-r',
    '--resolution',
    '-i',
    '--input-image',
    '-k',
    '--api-key',
  ]);

  function requireValue(i, flag) {
    const v = argv[i + 1];
    if (!v || (v.startsWith('-') && knownFlags.has(v))) {
      exitWithError(`错误: 参数 ${flag} 需要一个值`);
    }
    return v;
  }

  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];

    if (a === '-h' || a === '--help') {
      printHelpAndExit(0);
    }

    if (a === '-p' || a === '--prompt') {
      args.prompt = requireValue(i, a);
      i++;
      continue;
    }

    if (a === '-f' || a === '--filename') {
      args.filename = requireValue(i, a);
      i++;
      continue;
    }

    if (a === '-a' || a === '--aspect-ratio') {
      args.aspectRatio = requireValue(i, a);
      i++;
      continue;
    }

    if (a === '-r' || a === '--resolution') {
      args.resolution = requireValue(i, a);
      i++;
      continue;
    }

    if (a === '-k' || a === '--api-key') {
      args.[REDACTED_SECRET], a);
      i++;
      continue;
    }

    if (a === '-i' || a === '--input-image') {
      const images = [];
      let j = i + 1;
      while (j < argv.length) {
        const v = argv[j];
        if (v.startsWith('-') && knownFlags.has(v)) break;
        images.push(v);
        j++;
      }
      if (images.length === 0) {
        exitWithError(`错误: 参数 ${a} 需要至少一个图片路径`);
      }
      args.inputImages = images;
      i = j - 1;
      continue;
    }

    if (a.startsWith('-')) {
      exitWithError(`错误: 未知参数 ${a}，请使用 --help 查看帮助`);
    }
  }

  if (!args.prompt) {
    exitWithError('错误: 缺少必需参数 -p/--prompt');
  }

  return args;
}

async function main() {
  const argv = process.argv.slice(2);
  const args = parseArgs(argv);

  const runTimestamp = formatTimestamp(new Date());

  let checkProgress = null;
  const clearProgressTimer = () => {
    if (checkProgress) {
      clearInterval(checkProgress);
      checkProgress = null;
    }
  };

  if (args.aspectRatio != null && !SUPPORTED_ASPECT_RATIOS.includes(args.aspectRatio)) {
    exitWithError(
      `错误: 不支持的比例 '${args.aspectRatio}'\n支持的比例: ${SUPPORTED_ASPECT_RATIOS.join(', ')}`
    );
  }

  if (args.resolution != null && !SUPPORTED_RESOLUTIONS.includes(args.resolution)) {
    exitWithError(
      `错误: 不支持的分辨率 '${args.resolution}'\n支持的分辨率: ${SUPPORTED_RESOLUTIONS.join(', ')} (必须大写)`
    );
  }

  if (!args.filename) {
    args.filename = generateFilename(args.prompt);
  } else {
    const resolved = path.resolve(args.filename);
    if (fs.existsSync(resolved)) {
      const adjusted = addTimestampToFilename(args.filename, runTimestamp);
      process.stdout.write(`⚠️ 输出文件已存在，将避免覆盖并改为: ${adjusted}\n`);
      args.filename = adjusted;
    }
  }

  const [REDACTED_SECRET]);
  const url =
    'https://api.apiyi.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent';

  const headers = {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  };

  const parts = [{ text: args.prompt }];
  let modeStr = '生成图片';

  if (args.inputImages && args.inputImages.length > 0) {
    if (args.inputImages.length > 14) {
      exitWithError(`错误: 输入图片最多支持14张，当前为 ${args.inputImages.length} 张`);
    }

    for (const imgPath of args.inputImages) {
      if (!fs.existsSync(imgPath)) {
        exitWithError(`错误: 输入图片不存在: ${imgPath}`);
      }
      const imageBase64 = encodeImageToBase64(imgPath);
      parts.push({
        inlineData: {
          mimeType: 'image/png',
          data: imageBase64,
        },
      });
    }
    modeStr = '编辑图片';
  }

  const generationConfig = {
    responseModalities: ['IMAGE'],
  };

  const imageConfig = {};
  if (args.aspectRatio != null) imageConfig.aspectRatio = args.aspectRatio;
  if (args.resolution != null) imageConfig.imageSize = args.resolution;
  if (Object.keys(imageConfig).length > 0) generationConfig.imageConfig = imageConfig;

  const payload = {
    contents: [{ parts }],
    generationConfig,
  };

  // 生成前通知 + 生成中实时日志（避免长时间无输出导致体验不佳）
  const resolutionHint = args.resolution;
  const etaText = resolutionHint === '4K' ? '1-6分钟' : '30-120秒';
  process.stdout.write('🎨 图片生成已启动！\n');
  process.stdout.write(`⏱️ 预计时间: ${etaText}\n`);
  process.stdout.write('📊 我会定期给您发送进度更新\n');

  process.stdout.write(`正在${modeStr}...\n`);
  process.stdout.write(`提示词: ${args.prompt}\n`);

  if (generationConfig.imageConfig && generationConfig.imageConfig.aspectRatio) {
    process.stdout.write(`比例: ${generationConfig.imageConfig.aspectRatio}\n`);
  }
  if (generationConfig.imageConfig && generationConfig.imageConfig.imageSize) {
    process.stdout.write(`分辨率: ${generationConfig.imageConfig.imageSize}\n`);
  }

  // 输出请求参数（脱敏：不直接输出base64图片数据，避免刷屏）
  const payloadLog = {
    generationConfig,
    contents: [],
  };

  for (const content of payload.contents || []) {
    const partsLog = [];
    for (const part of content.parts || []) {
      if (part && typeof part === 'object' && part.inlineData && typeof part.inlineData === 'object') {
        const inlineData = { ...part.inlineData };
        if (typeof inlineData.data === 'string') {
          inlineData.data = `<omitted base64: ${inlineData.data.length} chars>`;
        }
        partsLog.push({ inlineData });
      } else {
        partsLog.push(part);
      }
    }
    payloadLog.contents.push({ parts: partsLog });
  }

  process.stdout.write(`输出请求参数: ${JSON.stringify(payloadLog, null, 2)}\n`);
  process.stdout.write('image generation in progress...\n');

  const startTime = Date.now();
  checkProgress = setInterval(() => {
    const elapsed = Math.floor((Date.now() - startTime) / 1000);
    process.stdout.write(`🔄 已进行 ${elapsed}秒...\n`);
  }, 5000);

  let data;
  try {
    data = await postJson(url, headers, payload, 120_000);
  } catch (e) {
    clearProgressTimer();
    if (e && e.message === 'timeout') {
      exitWithError('错误: 请求超时，请稍后重试');
    }

    if (e && e.statusCode) {
      process.stderr.write(`错误: 请求失败 - HTTP ${e.statusCode}\n`);
      if (e.responseText) {
        try {
          const detail = JSON.parse(e.responseText);
          process.stderr.write(`错误详情: ${JSON.stringify(detail, null, 2)}\n`);
        } catch {
          process.stderr.write(`响应内容: ${e.responseText}\n`);
        }
      }
      process.exit(1);
    }

    exitWithError(`错误: 请求失败 - ${e.message || String(e)}`);
  }

  clearProgressTimer();

  const imageData =
    data &&
    data.candidates &&
    Array.isArray(data.candidates) &&
    data.candidates[0] &&
    data.candidates[0].content &&
    data.candidates[0].content.parts &&
    data.candidates[0].content.parts[0] &&
    data.candidates[0].content.parts[0].inlineData &&
    data.candidates[0].content.parts[0].inlineData.data;

  if (!imageData) {
    process.stderr.write('错误: 响应中未找到图片数据\n');
    process.stderr.write(`完整响应: ${JSON.stringify(data, null, 2)}\n`);
    process.exit(1);
  }

  const imageBytes = Buffer.from(imageData, 'base64');
  const outputFile = path.resolve(args.filename);
  const outputDir = path.dirname(outputFile);

  fs.mkdirSync(outputDir, { recursive: true });
  fs.writeFileSync(outputFile, imageBytes);

  process.stdout.write(`✓ 图片已成功${modeStr}并保存到: ${args.filename}\n`);
  process.stdout.write('✅ 生成完成！\n');
}

main().catch((e) => {
  exitWithError(`错误: ${e && e.message ? e.message : String(e)}`);
});


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)