Communitygithub.com

Meshy 3D — 生成 + 打印

通过与 Meshy AI API 直接通信,生成并打印 3D 资源。覆盖完整生命周期:API 密钥设置、任务创建、指数退避轮询、下载、多步骤管线以及集成切片软件的 3D 打印准备。

Meshy 3D — 生成 + 打印 是什么?

该技能让 AI 智能体(如 Claude Code、Cursor 等)能够直接与 Meshy AI API 交互,实现从文本或图像创建 3D 模型、重新纹理、重新网格化、自动绑定骨骼、动画、FDM 打印准备等完整工作流。它支持 Meshy API 的所有核心功能,包括文本转 3D、图像转 3D、多图像转 3D、重新纹理、重新网格化、格式转换、缩放、UV 展开、自动绑定、动画、打印分析、修复和多色打印。 该技能专为开发者设计,在首次使用时会在当前工作目录中创建 `meshy_output/` 文件夹,并自动组织所有下载的文件:每个项目都有一个以时间戳、提示词和任务 ID 命名的子文件夹。它会持续跟踪 `metadata.json`(按项目)和全局 `history.json`(`meshy_output/history.json`)中的历史记录。 该技能安全地加载 `MESHY_API_KEY`(仅从环境变量或当前工作目录中的 `.env` 文件中加载),并且永远不会扫描主目录或 shell 配置文件。它使用指数退避轮询机制(5 秒到 30 秒,进度 >= 95% 时固定为 15 秒),优雅地处理 API 错误(401、402、429),并包含一个清晰的一次性会话通知,让用户了解文件结构和工作流成本。它还会在消耗任何信用点数之前提示用户确认,显示每个多步骤管线的完整成本明细。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add meshy-dev/meshy-3d-agent

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Meshy 3D — 生成 + 打印 是做什么的?

直接与 Meshy AI API 通信,以生成和打印 3D 资源。涵盖完整生命周期:API 密钥设置、任务创建、指数退避轮询、下载、多步骤管线以及集成切片软件的 3D 打印准备。


安全清单

访问的环境变量:

  • MESHY_API_KEY — 仅在 HTTP Authorization: Bearer 头中发送的 API 认证令牌。永不记录,永不写入除当前工作目录中 .env 文件以外的任何文件(仅在用户明确要求时)。

外部网络端点:

  • https://api.meshy.ai — Meshy AI API(任务创建、状态轮询、模型/图像下载)

文件系统访问:

  • 读取:仅当前工作目录中的 .env 文件(API 密钥查找)
  • 写入:仅当前工作目录中的 .env 文件(API 密钥存储,仅根据用户请求)
  • 写入:当前工作目录中的 ./meshy_output/(下载的模型文件、元数据)
  • 读取:用户显式提供的文件(例如,为图像转 3D 传递的本地图像),仅以用户指定的确切路径访问
  • 不访问主目录、shell 配置文件或上述路径之外的任何路径

离开此机器的数据:

  • 发送到 api.meshy.ai 的 API 请求包括 MESHY_API_KEY(在 Authorization 头中)以及用户提供的文本提示或图像 URL。不传输其他本地数据。下载的模型文件仅保存在本地。

重要提示:首次使用会话通知

当此技能在会话中首次激活时,请通知用户:

所有生成的文件都将保存到当前工作目录中的 meshy_output/ 中。每个项目都有自己的文件夹({YYYYMMDD_HHmmss}_{prompt}_{id}/),其中包含模型文件、纹理、缩略图和元数据。历史记录保存在 meshy_output/history.json 中。

此通知每个会话只需说一次。


重要提示:文件组织

所有下载的文件必须放入当前工作目录中结构化的 meshy_output/ 目录中。不要随意散放文件。

  • 每个项目:meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • 链式任务(预览 → 精细处理 → 绑定)重用相同的 project_dir
  • 按项目在 metadata.json 中跟踪任务,并在全局 history.json 中跟踪
  • 在下载模型的同时自动下载缩略图

重要提示:Shell 命令规则

仅使用标准的 POSIX 工具。不要使用 rg、fd、bat、exa/eza。


重要提示:正确运行长时间任务

Meshy 生成需要 1–5 分钟。将整个创建 → 轮询 → 下载流程编写为一个 Python 脚本,并在单个 Bash 调用中执行。使用 python3 -u script.py 实现无缓冲输出。任务在 99% 处停留 30–120 秒是正常的最终处理阶段 — 请勿中断。


步骤 0:API 密钥检测(始终首先运行)

仅检查当前会话环境和当前工作目录中的 .env 文件。不要扫描主目录或 shell 配置文件。

echo "=== Meshy API 密钥检测 ==="

# 1. 检查当前环境变量
if [ -n "$MESHY_API_KEY" ]; then
  echo "ENV_VAR: 已找到 (${MESHY_API_KEY:0:8}...)"
else
  echo "ENV_VAR: 未找到"
fi

# 2. 仅检查当前工作目录中的 .env
if [ -f ".env" ] && grep -q "MESHY_API_KEY" ".env" 2>/dev/null; then
  echo "DOTENV(.env): 已找到"
  export MESHY_API_KEY=$(grep "^MESHY_API_KEY=" ".env" | head -1 | cut -d'=' -f2- | tr -d '"\'')
fi

# 3. 最终状态
if [ -n "$MESHY_API_KEY" ]; then
  echo "就绪: key=${MESHY_API_KEY:0:8}..."
else
  echo "就绪: NO_KEY_FOUND"
fi

# 4. Python requests 检查
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: 缺失 (运行: pip install requests)"

echo "=== 检测完成 ==="

检测后的决策

  • 找到密钥 → 继续步骤 1。
  • 未找到密钥 → 转到步骤 0a。
  • 缺少 Python requests → 运行 pip install requests。

步骤 0a:API 密钥设置(仅在未找到密钥时)

告知用户:

要使用 Meshy API,您需要一个 API 密钥:

  1. 前往 https://www.meshy.ai/settings/api
  2. 点击 "Create API Key",为其命名,然后复制密钥(以 msy_ 开头)
  3. 该密钥仅显示一次 — 请将其保存在安全的地方

注意: API 访问需要 Pro 计划或以上。免费套餐账户无法创建 API 密钥。

一旦用户提供密钥,为当前会话设置密钥,并可选择持久化到 .env:

# 仅为当前会话设置
export MESHY_API_KEY="msy_PASTE_KEY_HERE"

# 验证密钥
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $MESHY_API_KEY" \
  https://api.meshy.ai/openapi/v1/balance)

if [ "$STATUS" = "200" ]; then
  BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
  echo "密钥有效。$BALANCE"
else
  echo "密钥无效 (HTTP $STATUS)。请检查密钥并重试。"
fi

要持久化密钥(仅限当前项目):

# 写入当前工作目录的 .env 文件
echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env
echo "已保存到 .env"

# 重要:添加 .env 到 .gitignore 以避免泄漏密钥
grep -q "^\.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore
echo ".env 已添加到 .gitignore"

安全提醒: 密钥仅存储在您当前项目目录中的 .env 文件中。切勿将此文件提交到版本控制。.env 已自动添加到 .gitignore。


步骤 1:在消耗信用点数之前与用户确认计划

关键:在创建任何任务之前,向用户显示成本摘要并等待确认:

我将使用以下方案生成 "<prompt>" 的 3D 模型:

  1. 预览(网格生成)— 20 信用点数
  2. 精细处理(基于 PBR 的纹理)— 10 信用点数
  3. 下载为 .glb

  总成本:30 信用点数
  当前余额:<N> 信用点数

  是否继续?

对于多步骤管线(文本转 3d → 绑定 → 动画),请预先显示完整的管线成本。

注意: 绑定自动包括行走和奔跑动画,无需额外费用。对于超出这些的自定义动画,仅添加 Animate(3 信用点数)。

意图 → API 映射

用户想要...API端点信用点数
从文本生成 3D 模型文本转 3DPOST /openapi/v2/text-to-3d5–20(预览)+ 10(精细处理)
从单张图像生成 3D 模型图像转 3DPOST /openapi/v1/image-to-3d5–30
从多张图像生成 3D 模型多图像转 3DPOST /openapi/v1/multi-image-to-3d5–30
为现有模型添加新纹理重新纹理POST /openapi/v1/retexture10
更改网格格式/拓扑重新网格化POST /openapi/v1/remesh5
将模型转换为其他格式(无重新网格化)转换POST /openapi/v1/convert1
将模型缩放到真实世界尺寸调整大小POST /openapi/v1/resize1
在外部纹理处理前生成新 UV(GLB,≤4 万面)UV 展开POST /openapi/v1/uv-unwrap5
为角色添加骨架自动绑定POST /openapi/v1/rigging5
为已绑定角色添加动画动画POST /openapi/v1/animations3
从文本生成 2D 图像(推荐作为图像转 3d 的前置步骤)文本转图像POST /openapi/v1/text-to-image3 / 6 / 9 / 9
优化/编辑 2D 图像(推荐作为图像转 3d 的前置步骤)图像转图像POST /openapi/v1/image-to-image3 / 6 / 9 / 12
照片 → 定制实体产品(人偶/灯/钥匙扣/冰箱贴)创意工作室POST /openapi/creative-lab/{product}/v1/prototype 然后 .../build6 + 30
检查 FDM 可打印性分析可打印性POST /openapi/v1/print/analyze0(免费)
修复非流形/退化面/孔洞拓扑修复可打印性POST /openapi/v1/print/repair10
多色 3D 打印多色打印POST /openapi/v1/print/multi-color10(+ 生成成本)
3D 打印模型(白色)→ 参见打印管线部分—20
检查信用点数余额余额GET /openapi/v1/balance0

步骤 2:执行工作流

可重用脚本模板

将此作为所有工作流的基础。它从环境或当前目录中的 .env 文件安全地加载 API 密钥:

#!/usr/bin/env python3
"""Meshy API 任务运行器。处理创建 → 轮询 → 下载。"""
import requests, time, os, sys, re, json
from datetime import datetime

# --- 安全 API 密钥加载 ---
def load_api_key():
    """从环境变量加载 MESHY_API_KEY,然后从 cwd 中的 .env 加载。"""
    key = os.environ.get("MESHY_API_KEY", "").strip()
    if key:
        return key
    env_path = os.path.join(os.getcwd(), ".env")
    if os.path.exists(env_path):
        with open(env_path) as f:
            for line in f:
                line = line.strip()
                if line.startswith("MESHY_API_KEY=") and not line.startswith("#"):
                    val = line.split("=", 1)[1].strip().strip('"').strip("'")
                    if val:
                        return val
    return ""

API_KEY = load_api_key()
if not API_KEY:
    sys.exit("错误:MESHY_API_KEY 未设置。运行步骤 0a 进行配置。")

# 永不记录完整密钥 — 仅记录前 8 个字符用于可追溯性
print(f"API 密钥已加载: {API_KEY[:8]}...")

BASE = "https://api.meshy.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
SESSION = requests.Session()
SESSION.trust_env = False  # 绕过系统代理设置

def create_task(endpoint, payload):
    resp = SESSION.post(f"{BASE}{endpoint}", headers=HEADERS, json=payload, timeout=30)
    if resp.status_code == 401:
        sys.exit("错误:API 密钥无效 (401)。重新运行步骤 0a。")
    if resp.status_code == 402:
        try:
            bal = SESSION.get(f"{BASE}/openapi/v1/balance", headers=HEADERS, timeout=10)
            balance = bal.json().get("balance", "unknown")
            sys.exit(f"错误:信用点数不足 (402)。余额:{balance}。请在 https://www.meshy.ai/pricing 充值")
        except Exception:
            sys.exit("错误:信用点数不足 (402)。请在 https://www.meshy.ai/pricing 检查余额")
    if resp.status_code == 429:
        sys.exit("错误:已达到速率限制 (429)。请等待并重试。")
    resp.raise_for_status()
    task_id = resp.json()["result"]
    print(f"TASK_CREATED: {task_id}")
    return task_id

def poll_task(endpoint, task_id, timeout=300):
    """使用指数退避进行轮询(5秒→30秒,进度 >= 95% 时固定为 15 秒)。"""
    elapsed, delay, max_delay, backoff, finalize_delay, poll_count = 0, 5, 30, 1.5, 15, 0
    while elapsed < timeout:
        poll_count += 1
        resp = SESSION.get(f"{BASE}{endpoint}/{task_id}", headers=HEADERS, timeout=30)
        resp.raise_for_status()
        task = resp.json()
        status = task["status"]
        progress = task.get("progress", 0)
        bar = f"[{'█' * int(progress/5)}{'░' * (20 - int(progress/5))}] {progress}%"
        print(f"  {bar} — {status} ({elapsed}s, poll #{poll_count})", flush=True)
        if status == "SUCCEEDED":
            return task
        if status in ("FAILED", "CANCELED"):
            msg = task.get("task_error", {}).get("message", "Unknown")
            sys.exit(f"TASK_{status}: {msg}")
        current_delay = finalize_delay if progress >= 95 else delay
        time.sleep(current_delay)
        elapsed += current_delay
        if progress < 95:
            delay = min(delay * backoff, max_delay)
    sys.exit(f"超时,已等待 {timeout}s({poll_count} 次轮询)")

def download(url, filepath):
    """将文件下载到项目目录(在 cwd/meshy_output/ 内)。"""
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    print(f"正在下载 {filepath}...", flush=True)
    resp = SESSION.get(url, timeout=300, stream=True)
    resp.raise_for_status()
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
    print(f"已下载: {filepath} ({os.path.getsize(filepath)/1024/1024:.1f} MB)")

# --- 文件组织辅助函数 ---
OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")

def get_project_dir(task_id, prompt="", task_type="model"):
    slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
    folder = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{slug}_{task_id[:8]}"
    project_dir = os.path.join(OUTPUT_ROOT, folder)
    os.makedirs(project_dir, exist_ok=True)
    return project_dir

def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
    meta_path = os.path.join(project_dir, "metadata.json")
    meta = json.load(open(meta_path)) if os.path.exists(meta_path) else {
        "project_name": prompt or task_type, "folder": os.path.basename(project_dir),
        "root_task_id": task_id, "created_at": datetime.now().isoformat(), "tasks": []
    }
    meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
                          "files": files or [], "created_at": datetime.now().isoformat()})
    meta["updated_at"] = datetime.now().isoformat()
    json.dump(meta, open(meta_path, "w"), indent=2)
    history = json.load(open(HISTORY_FILE)) if os.path.exists(HISTORY_FILE) else {"version": 1, "projects": []}
    folder = os.path.basename(project_dir)
    entry = next((p for p in history["projects"] if p["folder"] == folder), None)
    if entry:
        entry.update({"task_count": len(meta["tasks"]), "updated_at": meta["updated_at"]})
    else:
        history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
            "root_task_id": task_id, "created_at": meta["created_at"],
            "updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
    json.dump(history, open(HISTORY_FILE, "w"), indent=2)

def save_thumbnail(project_dir, url):
    path = os.path.join(project_dir, "thumbnail.png")
    if os.path.exists(path): return
    try:
        r = SESSION.get(url, timeout=15); r

(脚本模板的其余部分遵循相同的模式。)

相关技能