Skill: shengy983/sudocode-gpt-image-2-gen (version 1.0.0)

--- name: sudocode-gpt-image-2-gen description: 图片生成技能，当用户需要生成图片、视觉信息图、创建图像、编辑/修改/调整已有图片时使用此技能。基于 SudoCode 平台(https://sudocode.us)的 GPT Image 2 模型图片生成服务。该模型支持精确的尺寸/画质控制（含4K），按token计费。关键点：使用 /v1/images/generations 和 /v1/images/edits 端点；有显式 size 参数；有 quality 参数；使用 multipart/form-data 上传参考图；b64_json 为纯 base64 无前缀。 --- # 图片生成与编辑（GPT Image 2 官方正式版） 基于基于 SudoCode 平台的 GPT Image 2 模型实现图片生成技能，可以通过自然语言帮助用户生成图片，支持 Node.js 和 Python 两种运行环境。该模型支持精确的尺寸/画质控制（含 4K）。 ## 使用指引 遵循以下步骤： ### 第1步：分析需求与参数提取 1. **明确意图**：区分用户是需要【文生图】（生成新图片）还是【图生图】（编辑/修改现有图片）或【多图融合】。 2. **提示词（Prompt）分析**： - **使用用户原始完整输入**：把用户输入的原始完整问题需求描述（原文）直接作为 `-p` 提示词的主体，避免自行改写、总结或二次创作，防止细节丢失。 - **需要补充时先确认**：如果信息不足（例如缺少风格、主体数量、镜头语言、场景细节、文字内容、禁止元素等），先向用户提问确认；用户确认后，再把补充内容**以"追加"的方式**拼接到原始提示词后。 - 样例： - 用户输入："帮我生成一张猫的图片，风格要可爱一点。" - 正例说明：直接使用用户输入作为提示词：`-p "帮我生成一张猫的图片，风格要可爱一点。"` - 反例说明：擅自改写为"生成一张可爱风格的猫的图片"会丢失用户原始输入的细节和语气。 - 如果需要补充细节（例如颜色、背景等），先提问确认："你希望猫是什么颜色的？背景有什么要求吗？"用户回答后，再追加到提示词中：`-p "帮我生成一张猫的图片，风格要可爱一点。猫是橘色的，背景是草地。"` 3. **关键参数整理**： - **Prompt（必需）**：提示词分析后的最终提示词（默认=用户原始完整且一致的输入；仅在用户确认后才追加补充信息）。 - **Filename（可选）**：输出图片文件名/路径(需包含文件随机标识，避免重复)。不传则脚本会自动生成带时间戳的文件名。建议根据内容生成合理文件名（例如 `cat_in_garden.png`），避免使用通用名。 - **Size（可选）**：输出尺寸。 - 预设值：`1024x1024`、`1536x1024`、`1024x1536`、`2048x2048`、`2048x1152`、`3840x2160`、`2160x3840` - 也可使用自定义尺寸（满足：最大边≤3840、两边16倍数、比例≤3:1、总像素0.65–8.3MP） - 默认由模型自适应（auto） - **Quality（可选）**：画质档位。`low`（草图/批量）、`medium`（日常）、`high`（终稿/精细文字）、`auto`（默认） - **Output Format（可选）**：`png`（默认）、`jpeg`、`webp` - **Output Compression（可选）**：输出压缩率（0-100），仅jpeg/webp生效 - **注意**：该模型使用官方正式版端点，与官逆版gpt-image-2-all不同。 ### 第2步：环境检查与命令执行 1. **检查环境**：确认 `SUDOCODE_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}" [-s {size}] [-q {quality}] [-o {output_format}] ``` **图生图命令模板（优先 Node.js）：** ```bash node scripts/generate_image.js -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-s {size}] [-q {quality}] ``` **多图融合命令模板（优先 Node.js）：** ```bash node scripts/generate_image.js -p "融合图1和图2的风格" -i ref1.png ref2.png -f "merged.png" [-s {size}] [-q {quality}] ``` **（可选）Python 版本命令模板（Node 不可用时）**： ```bash python scripts/generate_image.py -p "{prompt}" -f "{filename}" [-s {size}] [-q {quality}] [-o {output_format}] python scripts/generate_image.py -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-s {size}] [-q {quality}] ``` ## ⏱️ 长时间任务处理策略 ### 1. 任务前提示 **执行前必须告知用户**： - "图片生成已启动，预计需要120-150秒，请耐心等待" ### 2. 🎨 最佳实践示例 > "图片生成中，预计120-150秒完成...\n⏳ 正在生成...\n（high + 2K/4K 复杂场景可能需要更长时间，请耐心等待）" ### 第3步：结果反馈 1. **执行反馈**：等待终端命令执行完毕。 2. **成功**：告知用户图片已生成，并指出保存路径。 3. **失败**： - 若提示 API Key 缺失，请指导用户设置环境变量。 - 若提示网络错误，建议用户检查网络或稍后重试。 ## 命令行使用样例 ### 生成新图片 ```bash python scripts/generate_image.py -p "图片描述文本" -f "output.png" [-s {size}] [-q {quality}] [-o {output_format}] ``` **示例：** ```bash # 基础生成 python scripts/generate_image.py -p "一只可爱的橘猫在草地上玩耍" -f "cat.png" # 指定尺寸和画质 python scripts/generate_image.py -p "日落山脉风景" -f "sunset.png" -s "2048x1152" -q "high" # 竖版高清图片（适合手机壁纸） python scripts/generate_image.py -p "城市夜景" -f "city.png" -s "2160x3840" -q "high" # 输出为JPEG python scripts/generate_image.py -p "风景照片" -f "landscape.jpg" -s "3840x2160" -q "high" -o "jpeg" ``` **（可选）Node.js 版本示例：** ```bash # 基础生成 node scripts/generate_image.js -p "一只可爱的橘猫在草地上玩耍" -f "cat.png" # 指定尺寸和画质 node scripts/generate_image.js -p "日落山脉风景" -f "sunset.png" -s "2048x1152" -q "high" ``` ### 编辑已有图片 ```bash python scripts/generate_image.py -p "编辑指令" -f "output.png" -i "path/to/input.png" [-s {size}] [-q {quality}] ``` **示例：** ```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" -q "high" # 替换背景 python scripts/generate_image.py -p "将背景换成海滩" -f "beach-bg.png" -i "portrait.png" -s "2048x2048" ``` **（可选）Node.js 版本示例：** ```bash # 修改风格 node scripts/generate_image.js -p "将图片转换成水彩画风格" -f "watercolor.png" -i "original.png" # 多��参考图融合（最多5张） node scripts/generate_image.js -p "把图1的人物放进图2的场景" -i ref1.png ref2.png -f "merged.png" ``` ## 附加资源 - 尺寸与比例控制文档：references/size-guide.md ## 命令行参数说明 > Python 与 Node.js 版本参数保持一致（短参数与长参数等价）。 | 参数 | 必填 | 说明 | |------|------|------| | `-p` / `--prompt` | 是 | 图片描述（文生图）或编辑指令（图生图）。保留用户原始完整输入。 | | `-f` / `--filename` | 否 | 输出图片路径/文件名；不传则自动生成带时间戳的文件名。 | | `-s` / `--size` | 否 | 输出尺寸：1024x1024 / 1536x1024 / 1024x1536 / 2048x2048 / 2048x1152 / 3840x2160 / 2160x3840 或自定义尺寸。 | | `-q` / `--quality` | 否 | 画质档位：low / medium / high / auto（默认auto）。 | | `-o` / `--output-format` | 否 | 输出格式：png（默认）/ jpeg / webp。 | | `-c` / `--output-compression` | 否 | 输出压缩率（0-100），仅jpeg/webp生效。 | | `-i` / `--input-image` | 否 | 图生图输入图片路径；可传多张（最多5张）。传入该参数即进入编辑模式。 | ## 尺寸说明 ### 预设尺寸 | 尺寸 | 比例 | 适用场景 | |------|------|----------| | 1024x1024 | 1:1 | 头像、Instagram帖子 | | 1536x1024 | 3:2 | 标准横版 | | 1024x1536 | 2:3 | 标准竖版 | | 2048x2048 | 1:1 | 高清方图 | | 2048x1152 | 16:9 | 横版视频封面、桌面壁纸 | | 3840x2160 | 16:9 | 4K超高清 | | 2160x3840 | 9:16 | 竖版4K | ### 自定义尺寸 可使用任意合法自定义尺寸，需满足： - 最大边 ≤ 3840 - 两边都能被16整除 - 比例 ≤ 3:1 - 总像素 0.65–8.3MP ## 画质说明 | 画质 | 说明 | 适用场景 | |------|------|----------| | low | 草图/批量生成 | 快速预览、多次迭代 | | medium | 日常 | 普通使用 | | high | 终稿/精细文字 | 最终输出、包含文字的图像 | | auto | 默认 | 由模型决定 | ## 输出格式说明 | 格式 | 说明 | 适用场景 | |------|------|----------| | png | 无压缩，透明背景 | 需要透明背景、保留最佳画质 | | jpeg | 有压缩 | 照片、存储空间敏感 | | webp | 现代格式 | Web使用、平衡画质与大小 | **注意**：b64_json字段是纯base64，不含 `data:image/...;base64,` 前缀。客户端需要： - 写文件：`base64.b64decode(b64_str)` → 写入磁盘 - 浏览器渲染：自行拼前缀 `data:image/png;base64,` + b64 ## 注意事项 - API密钥必须设置，可通过环境变量或命令行参数提供 - 图片生成时间：约120-150秒，high + 2K/4K 复杂场景可能需要更长时间 - 编辑图片时，使用multipart/form-data上传参考图 - 确保输出目录有写入权限 - 按token计费（非按张） ### API Key设置与获取 #### 如何获取API Key 如果你还没有 API 密钥，请前往 **https://sudocode.us** 获取或管理 API Key。 获取步骤： 1. 访问 https://sudocode.us 2. 注册/登录你的账号 3. 在控制台中创建API密钥 4. 复制密钥并设置环境变量或在命令行中使用 #### 设置API Key 脚本按以下顺序查找API密钥： 1. `--api-key` 命令行参数（临时使用） 2. `SUDOCODE_API_KEY` 环境变量（推荐） **设置环境变量（推荐）：** ```bash # Linux/Mac export SUDOCODE_API_KEY="your-api-key-here" # Windows CMD 我的电脑高级设置中设置环境变量或者执行set SUDOCODE_API_KEY=your-api-key-here # Windows PowerShell 在我的电脑中设置环境变量:$env:SUDOCODE_API_KEY="your-api-key-here" ``` **命令行参数方式（临时）：** ```bash python scripts/generate_image.py -p "一只猫" -k "your-api-key-here" ``` ## API端点说明 ### 文生图端点：POST /v1/images/generations 文生图端点，使用JSON格式请求。 ### 图生图端点：POST /v1/images/edits 图生图端点，使用multipart/form-data格式请求。上传参考图（最多5张）+ 指令进行单图改图、多图融合。 参考图顺序有意义，prompt中可用"图1/图2/图3"指代。 ## 模型信息 - 模型名：gpt-image-2 - 出图速度：约 120-150秒（4K复杂场景可能需要更长时间） - 输出分辨率：1024x1024 / 1536x1024 / 1024x1536 / 2048x2048 / 2048x1152 / 3840x2160 / 2160x3840 或自定义 - 默认响应格式：b64_json（纯base64，无前缀） - 画质档位：low / medium / high / auto - 输出格式：png / jpeg / webp - 支持能力：文生图、单图编辑、多图融合 - 计费方式：按token计费 ## gpt-image-2（官转）vs gpt-image-2-all（官逆）对比 | 特性 | gpt-image-2 | gpt-image-2-all | |------|-------------|-----------------| | 性质 | 官方正式版 | 官方逆向版 | | 计费 | 按token | 统一$0.03/张 | | 端点 | /v1/images/generations, /v1/images/edits | /v1/chat/completions | | 上传参考图 | multipart form-data | base64 data URL | | 下载图片 | b64_json（纯base64） | url或b64_json（带前缀） | | 多图融合 | image[]数组最多5张 | chat多个image_url | | 尺寸控制 | 显式size参数 | prompt描述 | | 速度 | 约120-150秒 | 约60-300秒 | ## 作者介绍

path: _meta.json
content: ownerId: kn70agzrbzdzn85zv9h1me7mwx80x19p
slug: sudocode-gpt-image-2-gen
version: 1.0.0
publishedAt: 1777046859950

path: scripts/generate_image.js
content: #!/usr/bin/env node
/*
基于GPT Image 2官方正式版的图片生成与编辑脚本（Node.js版）
使用 SudoCode 服务

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

参数说明：
- -p, --prompt             图片描述或编辑指令文本（必需）
- -f, --filename          输出图片路径（可选，默认自动生成时间戳文件名）
- -s, --size              输出尺寸（可选）
- -q, --quality           画质档位（可选：low/medium/high/auto，默认auto）
- -o, --output-format      输出格式（可选：png/jpeg/webp，默认png）
- -c, --output-compression 输出压缩率（可选：0-100，默认85）
- -i, --input-image       输入图片路径（可选，可多张，最多5张）
- -k, --api-key           API密钥（可选，覆盖环境变量 SUDOCODE_API_KEY）

使用示例：
【生成新图片】
  node generate_image.js -p "一只可爱的橘猫"
  node generate_image.js -p "日落山脉" -s "2048x1152" -q "high" -f sunset.png
  node generate_image.js -p "城市夜景" -s "2160x3840" -q "high" -f city.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 -f beach-bg.png

【多图融合】
  node generate_image.js -p "融合图1和图2的风格" -i ref1.png ref2.png -f merged.png

【环境变量】
  export SUDOCODE_API_KEY="your-api-key"
  export OPENAI_IMAGE_BASE_URL="https://sudocode.us"
*/

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

const SUPPORTED_SIZES = [
  '1024x1024',
  '1536x1024',
  '1024x1536',
  '2048x2048',
  '2048x1152',
  '3840x2160',
  '2160x3840',
];

const SUPPORTED_QUALITIES = ['auto', 'low', 'medium', 'high'];
const SUPPORTED_OUTPUT_FORMATS = ['png', 'jpeg', 'webp'];

function printHelpAndExit(exitCode = 0) {
  const help = `usage: generate_image.js [-h] --prompt PROMPT [--filename FILENAME]
                        [--size SIZE]
                        [--quality auto|low|medium|high]
                        [--output-format png|jpeg|webp]
                        [--output-compression 0-100]
                        [--input-image INPUT_IMAGE [INPUT_IMAGE ...]]
                        [--api-key API_KEY]

基于GPT Image 2官方正式版的图片生成与编辑工具（Node.js版）

options:
  -h, --help                  show this help message and exit
  -p, --prompt PROMPT         图片描述或编辑指令文本（必需）
  -f, --filename FILE        输出图片路径 (默认: 自动生成时间戳文件名)
  -s, --size               输出尺寸 (可选: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160, 2160x3840)
  -q, --quality            画质档位 (可选: auto, low, medium, high)
  -o, --output-format     输出格式 (可选: png, jpeg, webp)
  -c, --output-compression 输出压缩率 (0-100，仅jpeg/webp生效)
  -i, --input-image      输入图片路径（编辑模式，可传多张，最多5张）
  -k, --api-key        API密钥（覆盖环境变量）

尺寸说明：
  - 预设值: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160, 2160x3840
  - 也支持自定义尺寸（最大边≤3840，两边16倍数，比例≤3:1）

运行示例:
  node scripts/generate_image.js -p "一只可爱的橘猫"
  node scripts/generate_image.js -p "日落山脉" -s "2048x1152" -q "high" -f sunset.png
  node scripts/generate_image.js -p "转换成油画风格" -i original.png
  node scripts/generate_image.js -p "融合图1和图2的风格" -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] || process.env.APIYI_API_KEY;
  if (!apiKey) {
    exitWithError(
      '错误: 未设置 SUDOCODE_API_KEY 或 APIYI_API_KEY 环境变量\n' +
        'SudoCode 请前往 https://sudocode.us 获取；如需兼容旧配置也可设置 APIYI_API_KEY\n' +
        '或使用 -k/--api-key 参数临时指定'
    );
  }
  return apiKey;
}

function getBaseUrl() {
  const raw = process.env.OPENAI_IMAGE_BASE_URL || 'https://sudocode.us';
  return raw.replace(/\/$/, '');
}

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

function parseArgs(argv) {
  const args = {
    prompt: null,
    filename: null,
    size: null,
    quality: null,
    outputFormat: null,
    outputCompression: null,
    inputImages: null,
    apiKey: null,
  };

  const knownFlags = new Set([
    '-h', '--help',
    '-p', '--prompt',
    '-f', '--filename',
    '-s', '--size',
    '-q', '--quality',
    '-o', '--output-format',
    '-c', '--output-compression',
    '-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 === '-s' || a === '--size') {
      args.size = requireValue(i, a);
      i++;
      continue;
    }

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

    if (a === '-o' || a === '--output-format') {
      args.outputFormat = requireValue(i, a);
      i++;
      continue;
    }

    if (a === '-c' || a === '--output-compression') {
      args.outputCompression = 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;
}

function buildBoundary() {
  let s = '';
  const chars = [REDACTED_SECRET];
  for (let i = 0; i < 24; i++) {
    s += chars[Math.floor(Math.random() * chars.length)];
  }
  return s;
}

function postMultipart(urlString, headers, boundary, parts) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlString);

    const bodyParts = [];
    for (const part of parts) {
      bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
      bodyParts.push(Buffer.from(`${part.header}\r\n`, 'utf8'));
      bodyParts.push(Buffer.from('\r\n', 'utf8'));
      if (Buffer.isBuffer(part.body)) {
        bodyParts.push(part.body);
      } else {
        bodyParts.push(Buffer.from(part.body, 'utf8'));
      }
      bodyParts.push(Buffer.from('\r\n', 'utf8'));
    }
    bodyParts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));

    const body = Buffer.concat(bodyParts);

    const req = https.request(
      {
        protocol: url.protocol,
        hostname: url.hostname,
        port: url.port || 443,
        path: url.pathname + url.search,
        method: 'POST',
        headers: {
          ...headers,
          'Content-Type': `multipart/form-data; boundary=${boundary}`,
          '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(500_000, () => {
      req.destroy(new Error('timeout'));
    });

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

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-Type': 'application/json',
          '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();
  });
}

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.size != null && !SUPPORTED_SIZES.includes(args.size)) {
    const sizePattern = /^\d+x\d+$/;
    if (!sizePattern.test(args.size)) {
      exitWithError(
        `错误: 不支持的尺寸 '${args.size}'\n支持的预设尺寸: ${SUPPORTED_SIZES.join(', ')}\n或自定义尺寸（如 1920x1080）`
      );
    }
    const [w, h] = args.size.split('x').map(Number);
    if (w > 3840 || h > 3840) {
      exitWithError(`错误: 尺寸最大边不能超过3840`);
    }
    if (w % 16 !== 0 || h % 16 !== 0) {
      exitWithError(`错误: 尺寸两边必须能被16整除`);
    }
    if (Math.max(w / h, h / w) > 3) {
      exitWithError(`错误: 尺寸比例不能超过3:1`);
    }
    const mp = (w * h) / 1000000;
    if (mp < 0.65 || mp > 8.3) {
      exitWithError(`错误: 总像素必须在0.65-8.3MP之间，当前${mp.toFixed(2)}MP`);
    }
  }

  if (args.quality != null && !SUPPORTED_QUALITIES.includes(args.quality)) {
    exitWithError(
      `错误: 不支持的画质 '${args.quality}'\n支持的画质: ${SUPPORTED_QUALITIES.join(', ')}`
    );
  }

  if (args.outputFormat != null && !SUPPORTED_OUTPUT_FORMATS.includes(args.outputFormat)) {
    exitWithError(
      `错误: 不支持的输出格式 '${args.outputFormat}'\n支持的格式: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}`
    );
  }

  if (args.outputCompression != null) {
    const comp = parseInt(args.outputCompression);
    if (isNaN(comp) || comp < 0 || comp > 100) {
      exitWithError(`错误: 输出压缩率必须在0-100之间`);
    }
  }

  if (!args.filename) {
    const ext = args.outputFormat === 'jpeg' ? 'jpg' : args.outputFormat === 'webp' ? 'webp' : 'png';
    args.filename = generateFilename(args.prompt).replace(/\.png$/, `.${ext}`);
  } 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 baseUrl = getBaseUrl();
  const headers = {
    Authorization: `Bearer ${apiKey}`,
  };

  let modeStr = '生成图片';
  let data;

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

    for (const imgPath of args.inputImages) {
      if (!fs.existsSync(imgPath)) {
        exitWithError(`错误: 输入图片不存在: ${imgPath}`);
      }
    }

    modeStr = args.inputImages.length === 1 ? '编辑图片' : '多图融合';

    const boundary = buildBoundary();
    const parts = [];

    parts.push({
      header: 'Content-Disposition: form-data; name="model"',
      body: 'gpt-image-2',
    });

    parts.push({
      header: `Content-Disposition: form-data; name="prompt"`,
      body: args.prompt,
    });

    if (args.size != null) {
      parts.push({
        header: 'Content-Disposition: form-data; name="size"',
        body: args.size,
      });
    }

    if (args.quality != null) {
      parts.push({
        header: 'Content-Disposition: form-data; name="quality"',
        body: args.quality,
      });
    }

    if (args.outputFormat != null) {
      parts.push({
        header: 'Content-Disposition: form-data; name="output_format"',
        body: args.outputFormat,
      });
    }

    if (args.outputCompression != null) {
      parts.push({
        header: 'Content-Disposition: form-data; name="output_compression"',
        body: args.outputCompression,
      });
    }

    for (let i = 0; i < args.inputImages.length; i++) {
      const imgPath = args.inputImages[i];
      const imageData = fs.readFileSync(imgPath);
      const fileName = path.basename(imgPath);
      const mimeType = `image/${path.extname(imgPath).slice(1)}`;
      parts.push({
        header: `Content-Disposition: form-data; name="image[]"; filename="${fileName}"\r\nContent-Type: ${mimeType}`,
        body: imageData,
      });
    }

    const url = `${baseUrl}/v1/images/edits`;

    process.stdout.write('🎨 图片生成已启动！\n');
    process.stdout.write(`⏱️ 预计时间: 约120-150秒，请耐心等待\n`);
    process.stdout.write(`正在${modeStr}...\n`);
    process.stdout.write(`提示词: ${args.prompt}\n`);

    if (args.size) {
      process.stdout.write(`尺寸: ${args.size}\n`);
    }
    if (args.quality) {
      process.stdout.write(`画质: ${args.quality}\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);

    try {
      data = await postMultipart(url, headers, boundary, parts);
    } 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)}`);
    }
  } else {
    const payload = {
      model: 'gpt-image-2',
      prompt: args.prompt,
    };

    if (args.size != null) payload.size = args.size;
    if (args.quality != null) payload.quality = args.quality;
    if (args.outputFormat != null) payload.output_format = args.outputFormat;
    if (args.outputCompression != null) payload.output_compression = parseInt(args.outputCompression);

    const url = `${baseUrl}/v1/images/generations`;

    process.stdout.write('🎨 图片生成已启动！\n');
    process.stdout.write(`⏱️ 预计时间: 约120-150秒，请耐心等待\n`);
    process.stdout.write(`正在${modeStr}...\n`);
    process.stdout.write(`提示词: ${args.prompt}\n`);

    if (args.size) {
      process.stdout.write(`尺寸: ${args.size}\n`);
    }
    if (args.quality) {
      process.stdout.write(`画质: ${args.quality}\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);

    try {
      data = await postJson(url, headers, payload, 500_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 b64Json =
    data &&
    data.data &&
    Array.isArray(data.data) &&
    data.data[0] &&
    data.data[0].b64_json;

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

  const imageBytes = Buffer.from(b64Json, '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)}`);
});

path: scripts/generate_image.py
content: #!/usr/bin/env python3
"""
基于GPT Image 2官方正式版的图片生成与编辑脚本（Python版）
使用 SudoCode 服务

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

参数说明：
- -p, --prompt             图片描述或编辑指令文本（必需）
- -f, --filename           输出图片路径（可选，默认自动生成时间戳文件名）
- -s, --size              输出尺寸（可选）
- -q, --quality           画质档位（可选：low/medium/high/auto，默认auto）
- -o, --output-format     输出格式（可选：png/jpeg/webp，默认png）
- -c, --output-compression 输出压缩率（可选：0-100，默认85）
- -i, --input-image       输入图片路径（可选，可多张，最多5张）
- -k, --api-key           API密钥（可选，覆盖环境变量 SUDOCODE_API_KEY）

使用示例：
【生成新图片】
  python generate_image.py -p "一只可爱的橘猫"
  python generate_image.py -p "日落山脉" -s "2048x1152" -q "high" -f sunset.png
  python generate_image.py -p "城市夜景" -s "2160x3840" -q "high" -f city.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 -f beach-bg.png

【多图融合】
  python generate_image.py -p "融合图1和图2的风格" -i ref1.png ref2.png -f merged.png

【环境变量】
  export SUDOCODE_API_KEY="your-api-key"
  export OPENAI_IMAGE_BASE_URL="https://sudocode.us"
"""

import os
import sys
import re
import json
import base64
import argparse
import datetime
from pathlib import Path
from typing import Optional

try:
    import requests
except ImportError:
    print("错误: 需要安装 requests 库，请运行: pip install requests")
    sys.exit(1)


SUPPORTED_SIZES = ['1024x1024', '1536x1024', '1024x1536', '2048x2048', '2048x1152', '3840x2160', '2160x3840']
SUPPORTED_QUALITIES = ['auto', 'low', 'medium', 'high']
SUPPORTED_OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
DEFAULT_TIMEOUT = 500


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description='基于GPT Image 2官方正式版的图片生成与编辑工具（Python版）',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
尺寸说明：
  - 预设值: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160, 2160x3840
  - 也支持自定义尺寸（最大边≤3840，两边16倍数，比例≤3:1）

运行示例:
  python scripts/generate_image.py -p "一只可爱的橘猫"
  python scripts/generate_image.py -p "日落山脉" -s "2048x1152" -q "high" -f sunset.png
  python scripts/generate_image.py -p "转换成油画风格" -i original.png
  python scripts/generate_image.py -p "融合图1和图2的风格" -i ref1.png ref2.png -f merged.png
'''
    )
    parser.add_argument('-p', '--prompt', required=True, help='图片描述或编辑指令文本（必需）')
    parser.add_argument('-f', '--filename', default=None, help='输出图片路径 (默认: 自动生成时间戳文件名)')
    parser.add_argument('-s', '--size', default=None, help='输出尺寸 (可选)')
    parser.add_argument('-q', '--quality', default='auto', choices=SUPPORTED_QUALITIES, help='画质档位 (默认: auto)')
    parser.add_argument('-o', '--output-format', default='png', choices=SUPPORTED_OUTPUT_FORMATS, help='输出格式 (默认: png)')
    parser.add_argument('-c', '--output-compression', type=int, default=None, help='输出压缩率 (0-100，仅jpeg/webp生效)')
    parser.add_argument('-i', '--input-image', nargs='+', default=None, help='输入图片路径（编辑模式，可传多张，最多5张）')
    parser.add_argument('-k', '--api-key', default=None, help='API密钥（覆盖环境变量）')

    return parser.parse_args()


def get_api_key(args_key: Optional[str]) -> str:
    if args_key:
        return args_key
    [REDACTED_SECRET]'SUDOCODE_API_KEY') or os.environ.get('APIYI_API_KEY')
    if not [REDACTED_SECRET]'错误: 未设置 SUDOCODE_API_KEY 或 APIYI_API_KEY 环境变量', file=sys.stderr)
        print('SudoCode 请前往 https://sudocode.us 获取；如需兼容旧配置也可设置 APIYI_API_KEY', file=sys.stderr)
        print('或使用 -k/--api-key 参数临时指定', file=sys.stderr)
        sys.exit(1)
    return api_key


def get_base_url() -> str:
    return (os.environ.get('OPENAI_IMAGE_BASE_URL') or 'https://sudocode.us').rstrip('/')


def encode_image_to_base64(image_path: str) -> bytes:
    try:
        with open(image_path, 'rb') as f:
            return f.read()
    except Exception as e:
        print(f'错误: 无法读取图片文件 {image_path} - {e}', file=sys.stderr)
        sys.exit(1)


def generate_filename(prompt: str, output_format: str = 'png') -> str:
    now = datetime.datetime.now()
    timestamp = now.strftime('%Y-%m-%d-%H-%M-%S')

    keywords = str(prompt).split()[:3]
    keyword_str = '-'.join(keywords) if keywords else 'image'

    keyword_str = ''.join(c if c.isalnum() or c in '-_.' else '-' for c in keyword_str)
    keyword_str = keyword_str.lower()[:30]

    ext = output_format if output_format != 'jpeg' else 'jpg'
    return f'{timestamp}-{keyword_str}.{ext}'


def add_timestamp_to_filename(file_path: str, timestamp: str) -> str:
    path = Path(file_path)
    name = path.stem
    ext = path.suffix
    new_name = f'{name}-{timestamp}{ext}'
    return str(path.parent / new_name)


def main():
    args = parse_args()

    timestamp = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')

    if args.size and args.size not in SUPPORTED_SIZES:
        size_pattern = re.match(r'^(\d+)x(\d+)$', args.size)
        if not size_pattern:
            print(f"错误: 无效的尺寸格式 '{args.size}'", file=sys.stderr)
            print(f"支持的预设尺寸: {', '.join(SUPPORTED_SIZES)} 或自定义尺寸 (如 1920x1080)", file=sys.stderr)
            sys.exit(1)
        w = int(size_pattern.group(1))
        h = int(size_pattern.group(2))
        if w > 3840 or h > 3840:
            print('错误: 尺寸最大边不能超过3840', file=sys.stderr)
            sys.exit(1)
        if w % 16 != 0 or h % 16 != 0:
            print('错误: 尺寸两边必须能被16整除', file=sys.stderr)
            sys.exit(1)
        if max(w / h, h / w) > 3:
            print('错误: 尺寸比例不能超过3:1', file=sys.stderr)
            sys.exit(1)
        mp = (w * h) / 1000000
        if mp < 0.65 or mp > 8.3:
            print(f'错误: 总像素必须在0.65-8.3MP之间 (当前{mp:.2f}MP)', file=sys.stderr)
            sys.exit(1)

    if args.quality not in SUPPORTED_QUALITIES:
        print(f"错误: 不支持的画质 '{args.quality}'", file=sys.stderr)
        print(f"支持的画质: {', '.join(SUPPORTED_QUALITIES)}", file=sys.stderr)
        sys.exit(1)

    if args.output_format not in SUPPORTED_OUTPUT_FORMATS:
        print(f"错误: 不支持的输出格式 '{args.output_format}'", file=sys.stderr)
        print(f"支持的格式: {', '.join(SUPPORTED_OUTPUT_FORMATS)}", file=sys.stderr)
        sys.exit(1)

    if args.output_compression is not None:
        if args.output_compression < 0 or args.output_compression > 100:
            print('错误: 输出压缩率必须在0-100之间', file=sys.stderr)
            sys.exit(1)

    if not args.filename:
        args.filename = generate_filename(args.prompt, args.output_format)
    else:
        resolved = Path(args.filename).resolve()
        if resolved.exists():
            adjusted = add_timestamp_to_filename(args.filename, timestamp)
            print(f'⚠️ 输出文件已存在，将避免覆盖并改为: {adjusted}')
            args.filename = adjusted

    [REDACTED_SECRET])
    base_url = get_base_url()
    headers = {
        'Authorization': f'Bearer {api_key}',
    }

    mode_str = '生成图片'
    start_time = datetime.datetime.now()

    if args.input_image and len(args.input_image) > 0:
        if len(args.input_image) > 5:
            print(f'错误: 输入图片最多支持5张，当前为 {len(args.input_image)} 张', file=sys.stderr)
            sys.exit(1)

        for img_path in args.input_image:
            if not Path(img_path).exists():
                print(f'错误: 输入图片不存在: {img_path}', file=sys.stderr)
                sys.exit(1)

        mode_str = '编辑图片' if len(args.input_image) == 1 else '多图融合'

        url = f'{base_url}/v1/images/edits'

        files_list = [
            ('model', (None, 'gpt-image-2')),
            ('prompt', (None, args.prompt)),
        ]

        if args.size:
            files_list.append(('size', (None, args.size)))
        if args.quality:
            files_list.append(('quality', (None, args.quality)))
        if args.output_format:
            files_list.append(('output_format', (None, args.output_format)))
        if args.output_compression is not None:
            files_list.append(('output_compression', (None, str(args.output_compression))))

        for img_path in args.input_image:
            img_data = encode_image_to_base64(img_path)
            file_name = Path(img_path).name
            suffix = Path(img_path).suffix[1:].lower()
            mime_type = f'image/{suffix}' if suffix in ['png', 'jpg', 'jpeg', 'webp'] else 'image/png'
            files_list.append(('image[]', (file_name, img_data, mime_type)))

        print('🎨 图片生成已启动！')
        print(f'⏱️ 预计时间: 约120-150秒，请耐心等待')
        print(f'正在{mode_str}...')
        print(f'提示词: {args.prompt}')

        if args.size:
            print(f'尺寸: {args.size}')
        if args.quality:
            print(f'画质: {args.quality}')

        print('image generation in progress...')

        try:
            response = requests.post(url, headers=headers, files=files_list, timeout=DEFAULT_TIMEOUT)
            response.raise_for_status()
            data = response.json()
        except requests.exceptions.Timeout:
            print('错误: 请求超时，请稍后重试', file=sys.stderr)
            sys.exit(1)
        except requests.exceptions.HTTPError as e:
            print(f'错误: 请求失败 - {e}', file=sys.stderr)
            try:
                error_detail = e.response.json()
                print(f'错误详情: {json.dumps(error_detail, indent=2, ensure_ascii=False)}', file=sys.stderr)
            except:
                print(f'响应内容: {e.response.text}', file=sys.stderr)
            sys.exit(1)
        except requests.exceptions.RequestException as e:
            print(f'错误: 请求失败 - {e}', file=sys.stderr)
            sys.exit(1)
    else:
        url = f'{base_url}/v1/images/generations'

        payload = {
            'model': 'gpt-image-2',
            'prompt': args.prompt,
        }

        if args.size:
            payload['size'] = args.size
        if args.quality:
            payload['quality'] = args.quality
        if args.output_format:
            payload['output_format'] = args.output_format
        if args.output_compression is not None:
            payload['output_compression'] = args.output_compression

        print('🎨 图片生成已启动！')
        print(f'⏱️ 预计时间: 约120-150秒，请耐心等待')
        print(f'正在{mode_str}...')
        print(f'提示词: {args.prompt}')

        if args.size:
            print(f'尺��: {args.size}')
        if args.quality:
            print(f'画质: {args.quality}')

        print('image generation in progress...')

        try:
            response = requests.post(url, headers=headers, json=payload, timeout=DEFAULT_TIMEOUT)
            response.raise_for_status()
            data = response.json()
        except requests.exceptions.Timeout:
            print('错误: 请求超时，请稍后重试', file=sys.stderr)
            sys.exit(1)
        except requests.exceptions.HTTPError as e:
            print(f'错误: 请求失败 - {e}', file=sys.stderr)
            try:
                error_detail = e.response.json()
                print(f'错误详情: {json.dumps(error_detail, indent=2, ensure_ascii=False)}', file=sys.stderr)
            except:
                print(f'响应内容: {e.response.text}', file=sys.stderr)
            sys.exit(1)
        except requests.exceptions.RequestException as e:
            print(f'错误: 请求失败 - {e}', file=sys.stderr)
            sys.exit(1)

    elapsed = (datetime.datetime.now() - start_time).total_seconds()
    print(f'⏱️ 生成完成，耗时 {elapsed:.1f}秒')

    b64_json = None
    if data and data.get('data') and len(data['data']) > 0:
        b64_json = data['data'][0].get('b64_json')

    if not b64_json:
        print('错误: 响应中未找到图片数据', file=sys.stderr)
        print(f'完整响应: {json.dumps(data, indent=2, ensure_ascii=False)}', file=sys.stderr)
        sys.exit(1)

    try:
        image_bytes = base64.b64decode(b64_json)
    except Exception as e:
        print(f'错误: 图片数据解码失败 - {e}', file=sys.stderr)
        sys.exit(1)

    output_file = Path(args.filename).resolve()
    output_dir = output_file.parent
    output_dir.mkdir(parents=True, exist_ok=True)
    output_file.write_bytes(image_bytes)

    print(f'✓ 图片已成功{mode_str}并保存到: {args.filename}')
    print('✅ 生成完成！')


if __name__ == '__main__':
    main()

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