Skill: ruiww/grsai-nano-banana (version 1.0.0)

--- name: grsai-nano-banana description: 使用 grsai 平台的 nano-banana 模型生成图片（支持文生图、图生图） homepage: https://grsai.ai/ metadata: { "openclaw": { "emoji": "🎨", "requires": { "bins": ["uv", "curl"] }, "primaryEnv": "G RSAI_API_KEY", } } --- # 🎨 grsai nano-banana 生图技能 使用 grsai 平台的 nano-banana 系列模型生成高质量图片，支持文生图和图生图。 --- ## 📋 使用前准备 ### 1. 获取 grsai API Key 1. 访问 https://grsai.ai/zh/dashboard 2. 注册/登录账户 3. 进入 **API Key 管理** 页面 4. 创建新的 API Key 5. **充值** - 确保账户有足够 credits（生图消耗积分） ### 2. 配置 API Key **方式一：openclaw.json 配置（推荐）** ```json { "skills": { "entries": { "grsai-nano-banana": { "apiKey": "sk-your-api-key-here" } } } } ``` **方式二：环境变量** ```bash export G RSAI_API_KEY="sk-your-api-key-here" ``` --- ## 🚀 使用方法 ### 方式一：自然语言对话（推荐） 直接告诉助理你的生图需求，助理会引导你确认方案： ``` 用户：帮我生成一张可爱柴犬的头像 助理：好的，使用 nano-banana 生图需要确认以下信息： 【必填】模型、提示词 【选填】尺寸、比例、参考图 ... ``` ### 方式二：命令行调用 **文生图：** ```bash uv run ~/.openclaw/workspace/skills/grsai-nano-banana/generate.py \ --prompt "手绘版可爱柴犬头像" \ --model "nano-banana-pro" \ --resolution "1K" \ --aspect-ratio "1:1" \ --api-key "sk-xxx" ``` **图生图：** ```bash uv run ~/.openclaw/workspace/skills/grsai-nano-banana/generate.py \ --prompt "把这张图变成油画风格" \ --input-image "https://example.com/photo.png" \ --model "nano-banana-pro" \ --api-key "sk-xxx" ``` --- ## 📐 参数说明 ### 必填参数 | 参数 | 说明 | 示例 | |------|------|------| | `--prompt` | 提示词，清晰描述想要的内容 | `"一只可爱的猫咪在草地上"` | | `--api-key` | grsai API Key | `"sk-xxx"` | ### 选填参数 | 参数 | 默认值 | 选项 | 说明 | |------|--------|------|------| | `--model` | `nano-banana-pro` | `nano-banana-pro`<br>`nano-banana-fast`<br>`nano-banana-2`<br>`nano-banana-pro-vip` | 不同模型支持不同分辨率 | | `--resolution` | `1K` | `1K` / `2K` / `4K` | 分辨率越高，生成时间越长 | | `--aspect-ratio` | `auto` | `auto` / `1:1` / `16:9` / `9:16`<br>`4:3` / `3:4` / `3:2` / `2:3`<br>`5:4` / `4:5` / `21:9` | 输出图片比例 | | `--input-image` | - | 图片 URL | 参考图（图生图时使用） | | `--filename` | 自动生成 | 自定义文件名 | 输出文件名 | | `--output-dir` | `./generated` | 目录路径 | 输出目录（默认相对于当前目录） | --- ## ⏱️ 生成时间 | 模型 | 1K | 2K | 4K | |------|-----|-----|-----| | nano-banana-fast | ~2 分钟 | ~3 分钟 | - | | nano-banana-pro | ~5-8 分钟 | ~8-12 分钟 | ~15 分钟 | **注意：** - 实际时间取决于 grsai 服务器负载 - 高峰时段可能需要更长时间 - 脚本会自动轮询，无需手动等待 --- ## 📁 输出文件 **保存路径：** `./generated/`（可自定义） **命名规则：** `yyyymmdd_模型_描述.png` **示例：** - `20260304_pro_手绘版可爱柴犬头像.png` - `20260304_fast_sunset_beach.png` **自定义输出目录：** ```bash uv run generate.py --prompt "xxx" --output-dir "/your/custom/path" --api-key "sk-xxx" ``` --- ## ⚠️ 常见问题与踩坑记录 ### 1. API 返回 "apikey credits not enough" **原因：** 账户余额不足 **解决：** 1. 登录 grsai 后台 2. 检查当前 API Key 对应的账户 3. 充值 credits --- ### 2. 生成超时 "gemini timeout... Please try again later" **原因：** grsai 后端生成时间较长 **解决：** - 使用异步轮询模式（脚本默认） - 等待 5 分钟后开始轮询 - 最多轮询 3 次，间隔 1 分钟 - 如仍失败，稍后重试 --- ### 3. 内容违规 **原因：** 提示词包含敏感内容 **解决：** - 调整提示词，避免敏感词汇 - 使用更委婉的描述方式 --- ### 4. 模型不支持的分辨率 **原因：** 某些模型有分辨率限制 **限制说明：** - `nano-banana-pro-vip`：仅支持 1K、2K - `nano-banana-pro-4k-vip`：仅支持 4K **解决：** 选择正确的模型或调整分辨率 --- ### 5. 接口返回空响应 **原因：** 请求格式不正确 **正确格式：** ```json { "model": "nano-banana-pro", "prompt": "xxx", "imageSize": "1K", "aspectRatio": "auto", "webHook": "-1", "shutProgress": false } ``` **注意：** `webHook: "-1"` 是关键，让接口立即返回 task_id --- ### 6. 查询结果时参数错误 **错误：** 使用 `task_id` 参数 **正确：** 使用 `id` 参数 ```json {"id": "1-xxxxx"} ``` --- ## 🔧 技术细节 ### API 端点 | 接口 | URL | 方法 | |------|-----|------| | 提交任务 | `/v1/draw/nano-banana` | POST | | 查询结果 | `/v1/draw/result` | POST | ### 请求格式 **提交任务：** ```bash curl -X POST "https://grsai.dakka.com.cn/v1/draw/nano-banana" \ -H "Authorization: Bearer sk-xxx" \ -H "Content-Type: application/json" \ -d '{ "model": "nano-banana-pro", "prompt": "手绘版可爱柴犬头像", "imageSize": "1K", "aspectRatio": "1:1", "webHook": "-1", "shutProgress": false }' ``` **查询结果：** ```bash curl -X POST "https://grsai.dakka.com.cn/v1/draw/result" \ -H "Authorization: Bearer sk-xxx" \ -H "Content-Type: application/json" \ -d '{"id": "1-b834c0d4-dda7-4ab2-9ce6-378ea325ab3a"}' ``` ### 响应格式 **提交成功：** ```json { "code": 0, "data": { "id": "1-b834c0d4-dda7-4ab2-9ce6-378ea325ab3a" }, "msg": "success" } ``` **查询成功：** ```json { "code": 0, "data": { "id": "1-xxxxx", "status": "succeeded", "progress": 100, "results": [{ "url": "https://file3.aitohumanize.com/file/xxx.png" }] }, "msg": "success" } ``` --- ## 📝 更新日志 - **2026-03-04** - 初始版本 - 支持文生图、图生图 - 异步轮询模式 - 自动保存至 `./generated/` 目录 - 智能文件命名 --- ## 🔗 相关链接 - grsai 官网：https://grsai.ai/ - grsai 后台：https://grsai.ai/zh/dashboard - 文档：https://grsai.ai/zh/dashboard/documents/nano-banana

path: README.md
content: # 🎨 grsai nano-banana 生图技能

> 使用 grsai 平台的 nano-banana 系列模型生成高质量图片

---

## ⚡ 快速开始

### 1. 获取 API Key

1. 访问 https://grsai.ai/zh/dashboard
2. 注册/登录 → API Key 管理 → 创建 Key
3. **充值**（重要！）

### 2. 使用方式

**对话模式（推荐）：**
```
直接告诉助理你的需求，助理会引导确认方案
```

**命令行：**
```bash
cd ~/.openclaw/workspace/skills/grsai-nano-banana

# 文生图
uv run generate.py --prompt "可爱柴犬头像" --aspect-ratio "1:1" --api-key "sk-xxx"

# 图生图
uv run generate.py --prompt "油画风格" --input-image "https://example.com/img.png" --api-key "sk-xxx"
```

### 3. 查看结果

图片保存在：`./generated/` 目录（或自定义目录）

---

## 📖 详细文档

查看 [SKILL.md](./SKILL.md) 了解：
- 完整参数说明
- API 获取方法
- 常见问题与踩坑记录
- 技术细节

---

## 🆘 遇到问题？

查看 SKILL.md 中的 **常见问题与踩坑记录** 章节，或：
1. 检查 grsai 账户余额
2. 确认 API Key 正确
3. 调整提示词（避免敏感内容）


path: generate.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.28.0",
# ]
# ///
"""
grsai nano-banana 图片生成脚本

使用 grsai 平台的 nano-banana 系列模型生成图片。
支持文生图、图生图，异步轮询模式。

Usage:
    uv run generate.py --prompt "描述" --api-key "sk-xxx" [选项]

示例:
    # 文生图 - 头像
    uv run generate.py --prompt "可爱柴犬头像" --model "nano-banana-pro" --aspect-ratio "1:1" --api-key "sk-xxx"
    
    # 图生图
    uv run generate.py --prompt "油画风格" --input-image "https://example.com/photo.png" --api-key "sk-xxx"
"""

import argparse
import base64
import io
import re
import sys
import time
from pathlib import Path

try:
    import requests
except ImportError:
    print("Error: requests 库未安装", file=sys.stderr)
    print("Run: uv run pip install requests", file=sys.stderr)
    sys.exit(1)


def parse_args():
    parser = argparse.ArgumentParser(
        description="grsai nano-banana 图片生成",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 文生图 - 头像
  uv run generate.py --prompt "可爱柴犬头像" --model "nano-banana-pro" --aspect-ratio "1:1" --api-key "sk-xxx"
  
  # 图生图
  uv run generate.py --prompt "油画风格" --input-image "https://example.com/photo.png" --api-key "sk-xxx"
  
  # 自定义输出
  uv run generate.py --prompt "风景" --filename "my-image.png" --output-dir "/path/to/dir" --api-key "sk-xxx"
        """
    )
    
    # 必填参数
    parser.add_argument("--prompt", "-p", required=True, help="提示词，描述想要生成的内容")
    parser.add_argument("--api-key", "-k", required=True, help="grsai API Key")
    
    # 选填参数
    parser.add_argument("--filename", "-f", default=None, help="输出文件名（默认自动生成：yyyymmdd_模型_描述.png）")
    parser.add_argument("--output-dir", default="./generated", help="输出目录（默认：./generated，相对于当前目录）")
    parser.add_argument("--input-image", "-i", action="append", dest="input_images", metavar="URL",
                        help="参考图 URL（图生图时使用，可多个）")
    parser.add_argument("--resolution", "-r", choices=["1K", "2K", "4K"], default="1K",
                        help="输出分辨率：1K（默认）、2K、4K")
    parser.add_argument("--aspect-ratio", "-a", default="auto",
                        help="宽高比：auto（默认）、1:1、16:9、9:16、4:3、3:4、3:2、2:3、5:4、4:5、21:9")
    parser.add_argument("--model", "-m", default="nano-banana-pro",
                        help="模型：nano-banana-pro（默认）、nano-banana-fast、nano-banana-2 等")
    
    # 轮询参数
    parser.add_argument("--initial-wait", type=int, default=300,
                        help="首次轮询前等待时间（秒），默认 300 = 5 分钟")
    parser.add_argument("--poll-interval", type=int, default=60,
                        help="轮询间隔（秒），默认 60 = 1 分钟")
    parser.add_argument("--max-retries", type=int, default=3,
                        help="最大轮询次数，默认 3 次")
    
    # API 参数
    parser.add_argument("--base-url", default="https://grsai.dakka.com.cn",
                        help="grsai API 基础 URL（默认：https://grsai.dakka.com.cn）")
    
    return parser.parse_args()


def generate_filename(args):
    """生成输出文件名"""
    if args.filename:
        return args.filename
    
    import datetime
    import os
    
    date_str = datetime.datetime.now().strftime("%Y%m%d")
    
    # 简化模型名
    model_short = args.model
    if "pro-4k-vip" in args.model:
        model_short = "pro-4k-vip"
    elif "pro-vip" in args.model:
        model_short = "pro-vip"
    elif "pro-vt" in args.model:
        model_short = "pro-vt"
    elif "pro-cl" in args.model:
        model_short = "pro-cl"
    elif "pro" in args.model:
        model_short = "pro"
    elif "fast" in args.model:
        model_short = "fast"
    elif "nano-banana-2" in args.model:
        model_short = "banana-2"
    
    # 清理提示词（保留中文、英文、数字，其他替换为下划线）
    prompt_clean = re.sub(r'[^\w\u4e00-\u9fff]', '_', args.prompt)[:20].strip('_')
    
    return f"{date_str}_{model_short}_{prompt_clean}.png"


def submit_task(args):
    """提交生图任务"""
    url = f"{args.base_url}/v1/draw/nano-banana"
    
    payload = {
        "model": args.model,
        "prompt": args.prompt,
        "imageSize": args.resolution,
        "aspectRatio": args.aspect_ratio,
        "webHook": "-1",  # 立即返回 task_id
        "shutProgress": False
    }
    
    # 添加参考图
    if args.input_images:
        payload["urls"] = args.input_images
    
    headers = {
        "Authorization": f"Bearer {args.api_key}",
        "Content-Type": "application/json"
    }
    
    try:
        response = requests.post(url, json=payload, headers=headers, timeout=30)
        response.raise_for_status()
        result = response.json()
        
        if result.get("code") != 0:
            print(f"❌ 提交失败：{result.get('msg', 'Unknown error')}", file=sys.stderr)
            sys.exit(1)
        
        task_id = result.get("data", {}).get("id")
        if not task_id:
            print(f"❌ 未返回 task_id", file=sys.stderr)
            print(f"响应：{result}", file=sys.stderr)
            sys.exit(1)
        
        return task_id
        
    except requests.exceptions.RequestException as e:
        print(f"❌ 网络错误：{e}", file=sys.stderr)
        if hasattr(e, 'response') and e.response is not None:
            print(f"响应：{e.response.text}", file=sys.stderr)
        sys.exit(1)


def poll_result(args, task_id):
    """轮询生图结果"""
    url = f"{args.base_url}/v1/draw/result"
    headers = {
        "Authorization": f"Bearer {args.api_key}",
        "Content-Type": "application/json"
    }
    
    for attempt in range(1, args.max_retries + 1):
        print(f"🔄 第 {attempt}/{args.max_retries} 次轮询...")
        
        try:
            response = requests.post(url, json={"id": task_id}, headers=headers, timeout=30)
            response.raise_for_status()
            result = response.json()
            
            if result.get("code") != 0:
                print(f"⚠️  查询失败：{result.get('msg', 'Unknown error')}")
                if attempt < args.max_retries:
                    time.sleep(args.poll_interval)
                    continue
                return None
            
            data = result.get("data", {})
            status = data.get("status")
            results = data.get("results", [])
            image_url = results[0].get("url") if results else None
            
            if status == "succeeded" and image_url:
                print(f"✅ 生成成功！")
                return image_url
            elif status == "failed" or status == "error":
                error_msg = data.get("error") or data.get("failure_reason", "Unknown error")
                print(f"❌ 任务失败：{error_msg}", file=sys.stderr)
                sys.exit(1)
            else:
                print(f"⏳ 状态：{status} 或 {data.get('progress', '?')}%")
                if attempt < args.max_retries:
                    print(f"   等待 {args.poll_interval}秒后重试...")
                    time.sleep(args.poll_interval)
                    
        except requests.exceptions.RequestException as e:
            print(f"⚠️  轮询错误：{e}")
            if attempt < args.max_retries:
                time.sleep(args.poll_interval)
        except Exception as e:
            print(f"⚠️  处理错误：{e}")
            if attempt < args.max_retries:
                time.sleep(args.poll_interval)
    
    print(f"❌ 超过最大轮询次数，任务未完成", file=sys.stderr)
    return None


def download_image(image_url, output_path):
    """下载图片"""
    try:
        response = requests.get(image_url, timeout=120)
        response.raise_for_status()
        
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, 'wb') as f:
            f.write(response.content)
        
        return True
        
    except Exception as e:
        print(f"❌ 下载失败：{e}", file=sys.stderr)
        return False


def main():
    args = parse_args()
    
    # 生成输出路径
    filename = generate_filename(args)
    output_path = Path(args.output_dir) / filename
    
    print("=" * 60)
    print("🎨 grsai nano-banana 图片生成")
    print("=" * 60)
    print(f"模型：{args.model}")
    print(f"提示词：{args.prompt}")
    print(f"分辨率：{args.resolution}")
    print(f"比例：{args.aspect_ratio}")
    if args.input_images:
        print(f"参考图：{len(args.input_images)} 张")
    print(f"输出：{output_path}")
    print("=" * 60)
    
    # 步骤 1: 提交任务
    print("📤 提交任务...")
    task_id = submit_task(args)
    print(f"✅ 任务 ID: {task_id}")
    
    # 步骤 2: 等待并轮询
    print(f"⏳ 等待 {args.initial_wait}秒（约{args.initial_wait//60}分钟）后开始轮询...")
    print("   （grsai 生图需要时间，请耐心等待）")
    time.sleep(args.initial_wait)
    
    print("🔍 开始轮询结果...")
    image_url = poll_result(args, task_id)
    
    if not image_url:
        sys.exit(1)
    
    # 步骤 3: 下载图片
    print(f"📥 下载图片：{image_url}")
    if download_image(image_url, output_path):
        print("=" * 60)
        print(f"✅ 完成！图片已保存：{output_path}")
        print(f"MEDIA: {output_path}")
        print("=" * 60)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()


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