关于 meshy-3d-agent
meshy-3d-agent 是使用 Meshy AI API 生成、处理并准备打印 3D 资产的技能。它指导创建任务、轮询状态、下载文件及执行多步骤模型工作流,并要求先确认费用且遵循文件管理规则。
主要功能
- 01文本和图像生成 3D 模型
- 02重新贴图、重构网格和格式转换
- 03模型缩放与 UV 展开
- 04骨骼绑定和动画任务指导
- 05可打印性分析与修复
- 06本地文件分类下载及任务历史
适用场景
- 01根据提示词或图像生成 3D 模型
- 02将模型转换为其他格式
- 03为角色绑定骨骼并制作动画
- 04检查或修复模型的可打印性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meshy-dev/ meshy-3d-agent --skill meshy-3d-agent安装后在 Claude Code 中输入/meshy-3d-agent确认已加载。
附带文件reference.md
详细介绍
Meshy 3D — 生成与打印
直接与 Meshy AI API 通信,以生成和打印 3D 资产。涵盖完整生命周期:API 密钥设置、创建任务、指数退避轮询、下载、多步骤流水线,以及通过切片软件集成来准备 3D 打印。
安全清单
访问的环境变量:
MESHY_API_KEY— API 身份验证令牌,仅通过 HTTPAuthorization: Bearer标头发送。绝不记录,也不会写入任何文件;只有在用户明确请求时,才会写入当前工作目录中的.env。
外部网络端点:
https://api.meshy.ai— Meshy AI API(创建任务、轮询状态、下载模型和图像)
文件系统访问:
- 读取:仅当前工作目录中的
.env(用于查找 API 密钥) - 写入:仅当前工作目录中的
.env(用于存储 API 密钥,且仅在用户请求时) - 写入:当前工作目录中的
./meshy_output/(下载的模型文件、元数据) - 读取:用户明确提供的文件(例如,用于图像转 3D 转换的本地图像);仅在用户指定的确切路径访问
- 无权访问主目录、Shell 配置文件或上述范围以外的任何路径
离开此设备的数据:
- 向
api.meshy.ai发送的 API 请求会在 Authorization 标头中包含MESHY_API_KEY,以及用户提供的文本提示词或图像 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 Key Detection ==="
# 1. Check current env var
if [ -n "$MESHY_API_KEY" ]; then
echo "ENV_VAR: FOUND (${MESHY_API_KEY:0:8}...)"
else
echo "ENV_VAR: NOT_FOUND"
fi
# 2. Check .env in current working directory only
if [ -f ".env" ] && grep -q "MESHY_API_KEY" ".env" 2>/dev/null; then
echo "DOTENV(.env): FOUND"
export MESHY_API_KEY=$(grep "^MESHY_API_KEY=" ".env" | head -1 | cut -d'=' -f2- | tr -d '"'"'" )
fi
# 3. Final status
if [ -n "$MESHY_API_KEY" ]; then
echo "READY: key=${MESHY_API_KEY:0:8}..."
else
echo "READY: NO_KEY_FOUND"
fi
# 4. Python requests check
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"
echo "=== Detection Complete ==="
检测后的处理
- 找到密钥 → 继续步骤 1。
- 未找到密钥 → 前往步骤 0a。
- 缺少 Python requests → 运行
pip install requests。
步骤 0a:设置 API 密钥(仅在未找到密钥时)
告知用户:
要使用 Meshy API,你需要一个 API 密钥:
- 前往 https://www.meshy.ai/settings/api
- 点击 "Create API Key",为密钥命名并复制密钥(以
msy_开头)- 密钥只会显示一次——请妥善保存
注意: API 访问要求使用 Pro 计划或更高等级。免费账户无法创建 API 密钥。
用户提供密钥后,将其设置到当前会话中,并可选择持久化到 .env:
# Set for current session only
export MESHY_API_KEY="msy_PASTE_KEY_HERE"
# Verify the key
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 "Key valid. $BALANCE"
else
echo "Key invalid (HTTP $STATUS). Please check the key and try again."
fi
要持久化密钥(仅限当前项目):
# Write to .env in current working directory
echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env
echo "Saved to .env"
# IMPORTANT: add .env to .gitignore to avoid leaking the key
grep -q "^\.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore
echo ".env added to .gitignore"
安全提醒: 密钥仅存储在当前项目目录中的
.env文件里。切勿将此文件提交到版本控制中。.env已自动添加到.gitignore。
步骤 1:先与用户确认计划,再消耗点数
重要:创建任何任务之前,先向用户展示费用摘要并等待确认:
I'll generate a 3D model of "<prompt>" using the following plan:
1. Preview (mesh generation) — 20 credits
2. Refine (texturing with PBR) — 10 credits
3. Download as .glb
Total cost: 30 credits
Current balance: <N> credits
Shall I proceed?
对于多步骤流水线(文本转 3D → 绑定骨骼 → 动画),应提前展示完整流水线的费用。
注意: 自动绑定骨骼会包含行走和跑步动画,不额外收费。只有在需要这些动画之外的自定义动画时,才添加
Animate(3 点)。
意图 → API 映射
| 用户想要…… | API | 端点 | 点数 |
|---|---|---|---|
| 从文本生成 3D 模型 | Text to 3D | POST /openapi/v2/text-to-3d | 5–20(预览)+ 10(精修) |
| 从单张图片生成 3D 模型 | Image to 3D | POST /openapi/v1/image-to-3d | 5–30 |
| 从多张图片生成 3D 模型 | Multi-Image to 3D | POST /openapi/v1/multi-image-to-3d | 5–30 |
| 为现有模型添加新纹理 | Retexture | POST /openapi/v1/retexture | 10 |
| 更改网格格式/拓扑 | Remesh | POST /openapi/v1/remesh | 5 |
| 将模型转换为其他格式(不重新网格化) | Convert | POST /openapi/v1/convert | 1 |
| 将模型缩放至真实尺寸 | Resize | POST /openapi/v1/resize | 1 |
| 在外部纹理处理前生成全新的 UV(GLB,≤40k 个面) | UV Unwrap | POST /openapi/v1/uv-unwrap | 5 |
| 为角色添加骨架 | Auto-Rigging | POST /openapi/v1/rigging | 5 |
| 为已绑定骨骼的角色添加动画 | Animation | POST /openapi/v1/animations | 3 |
| 根据文本生成 2D 图像(建议作为图像转 3D 的前置步骤) | Text to Image | POST /openapi/v1/text-to-image | 3 / 6 / 9 / 9 |
| 优化/编辑 2D 图像(建议作为图像转 3D 的前置步骤) | Image to Image | POST /openapi/v1/image-to-image | 3 / 6 / 9 / 12 |
| 照片 → 风格化实体产品(手办/台灯/钥匙扣/冰箱贴) | Creative Lab | POST /openapi/creative-lab/{product}/v1/prototype 然后 .../build | 6 + 30 |
| 检查 FDM 打印适用性 | Analyze Printability | POST /openapi/v1/print/analyze | 0(免费) |
| 修复非流形/退化面/孔洞拓扑 | Repair Printability | POST /openapi/v1/print/repair | 10 |
| 多色 3D 打印 | Multi-Color Print | POST /openapi/v1/print/multi-color | 10(+ 生成费用) |
| 3D 打印模型(白色) | → 请参阅打印流水线部分 | — | 20 |
| 查看点数余额 | Balance | GET /openapi/v1/balance | 0 |
步骤 2:执行工作流
可复用的脚本模板
将此模板用作所有工作流的基础。它仅从环境变量或当前目录中的 .env 安全读取 API 密钥:
#!/usr/bin/env python3
"""Meshy API task runner. Handles create → poll → download."""
import requests, time, os, sys, re, json
from datetime import datetime
# --- Secure API key loading ---
def load_api_key():
"""Load MESHY_API_KEY from environment, then .env in cwd only."""
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("ERROR: MESHY_API_KEY not set. Run Step 0a to configure it.")
# Never log the full key — only first 8 chars for traceability
print(f"API key loaded: {API_KEY[:8]}...")
BASE = "https://api.meshy.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
SESSION = requests.Session()
SESSION.trust_env = False # bypass any system proxy settings
def create_task(endpoint, payload):
resp = SESSION.post(f"{BASE}{endpoint}", headers=HEADERS, json=payload, timeout=30)
if resp.status_code == 401:
sys.exit("ERROR: Invalid API key (401). Re-run Step 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"ERROR: Insufficient credits (402). Balance: {balance}. Top up at https://www.meshy.ai/pricing")
except Exception:
sys.exit("ERROR: Insufficient credits (402). Check balance at https://www.meshy.ai/pricing")
if resp.status_code == 429:
sys.exit("ERROR: Rate limited (429). Wait and retry.")
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):
"""Poll with exponential backoff (5s→30s, fixed 15s at 95%+)."""
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 after {timeout}s ({poll_count} polls)")
def download(url, filepath):
"""Download a file into a project directory (within cwd/meshy_output/)."""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
print(f"Downloading {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"DOWNLOADED: {filepath} ({os.path.getsize(filepath)/1024/1024:.1f} MB)")
# --- File organization helpers ---
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.raise_for_status()
open(path, "wb").write(r.content)
except Exception: pass
文本转 3D(预览 + 精修)
将以下内容附加到上方模板:
PROMPT = "USER_PROMPT"
# Preview
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview",
"prompt": PROMPT,
"ai_model": "latest",
# "pose_mode": "t-pose", # Use "t-pose" if rigging/animating later
# "hd_texture": True, # 4K base color on refine (meshy-6/latest only)
# "target_formats": ["glb", "3mf"], # 3mf must be explicitly requested
# NOTE: symmetry_mode / art_style / is_a_t_pose are deprecated (symmetry_mode & art_style ignored; use pose_mode)
})
task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
save_thumbnail(project_dir, task["thumbnail_url"])
print(f"\nPREVIEW COMPLETE — Task: {preview_id} | Project: {project_dir}")
# Refine
refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine",
"preview_task_id": preview_id,
"enable_pbr": True,
"ai_model": "latest",
})
task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])
print(f"\nREFINE COMPLETE — Task: {refine_id} | Formats: {', '.join(task['model_urls'].keys())}")
精修兼容性: 精修支持
meshy-5、meshy-6或latest(即 Meshy 6)——为保持一致,请选择与预览相同的系列。无论使用哪种模型,精修均需 10 点。(meshy-4已停用,调用会返回 400。)
###(可选但强烈建议)2D 优化预处理
优先选择图像转 3D,而非直接文本转 3D——这种方式质量更高,也更可控。因此,对于纯文本请求,请先制作设计图,再转换为 3D。
图像质量直接决定 3D 模型质量。在调用 /openapi/v1/image-to-3d 或 /openapi/v1/multi-image-to-3d 之前,请先评估用户输入,并主动建议进行 2D 处理:
| 用户输入 | 推荐的预处理步骤 |
|---|---|
| 只有文字描述,没有参考图 | 使用 nano-banana-pro 调用 /openapi/v1/text-to-image。对于角色,添加 generate_multi_view: True 和 pose_mode: "a-pose" 或 "t-pose",以生成便于绑定骨骼的输出。 |
| 参考图分辨率低 / 背景杂乱 / 主体不清晰 / 光线不佳 | 使用 nano-banana-pro 调用 /openapi/v1/image-to-image 进行清理。 |
| 用户想调整风格 / 颜色 / 细节 | 调用 /openapi/v1/image-to-image 进行风格迁移,然后转换为 3D。 |
额外花费 3-9 个点数通常能显著提升质量。如果用户已经提供干净的影棚风格图片,则跳过此步骤。对于 Creative Lab 产品(摆件 / 台灯 / 钥匙扣 / 冰箱贴)也跳过此步骤:它们有自己的内置风格化处理——将原始照片(或台灯产品的文本)直接输入 Creative Lab,而不要先经过 text-to-image / image-to-image。
图像转 3D
import base64
# For local files: convert to data URI
# with open("photo.jpg", "rb") as f:
# image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
task_id = create_task("/openapi/v1/image-to-3d", {
"image_url": "IMAGE_URL_OR_DATA_URI",
"should_texture": True,
"enable_pbr": True,
"ai_model": "latest",
})
task = poll_task("/openapi/v1/image-to-3d", task_id)
project_dir = get_project_dir(task_id, task_type="image-to-3d")
download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb"))
record_task(project_dir, task_id, "image-to-3d", "complete", files=["model.glb"])
多图转 3D
task_id = create_task("/openapi/v1/multi-image-to-3d", {
"image_urls": ["URL_1", "URL_2", "URL_3"], # 1–4 images
"should_texture": True,
"enable_pbr": True,
"ai_model": "latest",
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
project_dir = get_project_dir(task_id, task_type="multi-image-to-3d")
download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb"))
重新贴图
重要:先询问用户的纹理风格——text_style_prompt 或 image_style_url(必须提供其中一个;如果两者都提供,则优先使用图像)。
# REQUIRED: ask user for text_style_prompt OR image_style_url
task_id = create_task("/openapi/v1/retexture", {
"input_task_id": "PREVIOUS_TASK_ID",
"text_style_prompt": "wooden texture", # REQUIRED if no image_style_url
# "image_style_url": "URL", # REQUIRED if no prompt (takes precedence)
"enable_pbr": True,
# "target_formats": ["glb", "3mf"], # 3mf must be explicitly requested
})
task = poll_task("/openapi/v1/retexture", task_id)
project_dir = get_project_dir(task_id, task_type="retexture")
download(task["model_urls"]["glb"], os.path.join(project_dir, "retextured.glb"))
网格重建 / 格式转换
task_id = create_task("/openapi/v1/remesh", {
"input_task_id": "TASK_ID",
"target_formats": ["glb", "fbx", "obj"],
"topology": "quad",
"target_polycount": 10000,
})
task = poll_task("/openapi/v1/remesh", task_id)
project_dir = get_project_dir(task_id, task_type="remesh")
for fmt, url in task["model_urls"].items():
download(url, os.path.join(project_dir, f"remeshed.{fmt}"))
网格实用工具(转换 / 调整尺寸 / UV 展开)
对已完成的模型进行轻量级后处理(通过 input_task_id 或 model_url):
# Convert to other formats without remeshing (1 credit). Cheapest way to get 3MF/STL.
conv_id = create_task("/openapi/v1/convert", {
"input_task_id": "TASK_ID", # or "model_url": "URL"
"target_formats": ["stl", "3mf"], # required: glb/fbx/obj/usdz/blend/stl/3mf
})
poll_task("/openapi/v1/convert", conv_id)
# Resize to a real-world size (1 credit). Give EXACTLY ONE resize mode.
resize_id = create_task("/openapi/v1/resize", {
"input_task_id": "TASK_ID", # or "model_url": "URL"
"resize_height": 0.15, # meters — OR "resize_longest_side": 0.2 OR "auto_size": True
# "origin_at": "bottom", # "bottom" | "center"
})
poll_task("/openapi/v1/resize", resize_id)
# UV Unwrap a GLB (5 credits). GLB only, ≤ 40,000 faces (else 400 → remesh down first).
# Output: a GLB "UV white model" (fresh UVs + placeholder grey material) for external texturing.
uv_id = create_task("/openapi/v1/uv-unwrap", {
"input_task_id": "TASK_ID", # or "model_url": "GLB_URL"
})
poll_task("/openapi/v1/uv-unwrap", uv_id)
Creative Lab 消费级产品
将照片转换为风格化、可打印的实体产品。产品包括:摆件、台灯、钥匙扣、冰箱贴。分为两个阶段(将 {product} 替换为其中一种产品):
- 原型(6 个点数):照片 → 风格化概念图。
- 构建(30 个点数):对状态为 SUCCEEDED 的原型运行 image-to-3d → 带纹理的 GLB / OBJ+MTL。
PRODUCT = "figure" # figure | lamp | keychain | fridge-magnet
# Stage 1 — prototype (6 credits)
proto_id = create_task(f"/openapi/creative-lab/{PRODUCT}/v1/prototype", {
"image_url": "PHOTO_URL_OR_DATA_URI", # jpg/jpeg/png/webp
# "name": "My figure", # optional, ≤ 100 chars
})
poll_task(f"/openapi/creative-lab/{PRODUCT}/v1/prototype", proto_id)
# Stage 2 — build (30 credits). Must reference a SUCCEEDED prototype of the SAME product.
# Web-app prototypes are rejected with 404 — only API-created prototypes are accepted.
build_id = create_task(f"/openapi/creative-lab/{PRODUCT}/v1/build", {
"input_task_id": proto_id,
})
build = poll_task(f"/openapi/creative-lab/{PRODUCT}/v1/build", build_id)
project_dir = get_project_dir(build_id, task_type=f"creative-lab-{PRODUCT}")
download(build["model_urls"]["glb"], os.path.join(project_dir, "creative-lab.glb"))
# To MULTICOLOR a Creative Lab result: a Creative Lab model can only be sent to multi-color
# as model_url — pass the build's GLB URL (or a data URI of the downloaded GLB):
# create_task("/openapi/v1/print/multi-color", {"model_url": build["model_urls"]["glb"], "max_colors": 4})
自动绑定骨骼 + 动画
用户要求绑定骨骼或制作动画时,生成步骤必须使用 pose_mode: "t-pose"。
# Pre-rig check: polycount must be ≤ 300,000
source_endpoint = "/openapi/v2/text-to-3d" # adjust to match source task endpoint
source_task_id = "TASK_ID"
check = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check.raise_for_status()
face_count = check.json().get("face_count", 0)
if face_count > 300000:
sys.exit(f"ERROR: {face_count:,} faces exceeds 300,000 limit. Remesh first.")
# Rig
rig_id = create_task("/openapi/v1/rigging", {
"input_task_id": source_task_id,
"height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
project_dir = get_project_dir(rig_id, task_type="rigging")
download(rig_task["result"]["rigged_character_glb_url"], os.path.join(project_dir, "rigged.glb"))
download(rig_task["result"]["basic_animations"]["walking_glb_url"], os.path.join(project_dir, "walking.glb"))
download(rig_task["result"]["basic_animations"]["running_glb_url"], os.path.join(project_dir, "running.glb"))
# Custom animation (optional, 3 credits — only if user needs beyond walking/running)
# anim_id = create_task("/openapi/v1/animations", {"rig_task_id": rig_id, "action_id": 1})
# anim_task = poll_task("/openapi/v1/animations", anim_id)
# download(anim_task["result"]["animation_glb_url"], os.path.join(project_dir, "animated.glb"))
文本转图像 / 图像转图像
# Text to Image
task_id = create_task("/openapi/v1/text-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "a futuristic spaceship",
})
task = poll_task("/openapi/v1/text-to-image", task_id)
# Result URL: task["image_url"]
# Image to Image
task_id = create_task("/openapi/v1/image-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "make it look cyberpunk",
"reference_image_urls": ["URL"],
})
task = poll_task("/openapi/v1/image-to-image", task_id)
3D 打印工作流
重要:当用户的请求涉及 3D 打印时,整个工作流(包括模型生成)都必须使用本节。不要先运行上述生成工作流,再切换到此处。本节从一开始就会控制 target_formats 和其他打印专用参数。
当用户提到以下内容时触发:打印、3D 打印、切片软件、切片、bambu、orca、prusa、cura、多色、multicolor、multi-color、3mf、手办、微缩模型、雕像、实体模型、桌面玩具、手机支架。
决策:白色模型还是多色模型
- 先检测已安装的切片软件(见下方脚本)
- 询问用户:“白色模型(单色)还是多色模型?”
- 如果选择多色模型:检查是否有支持多色打印的切片软件(OrcaSlicer、Bambu Studio、Creality Print、Elegoo Slicer、Anycubic Slicer Next),询问 max_colors(1-16,默认值为 4)和 max_depth(3-6,默认值为 4),并确认费用:40 个点数(如果需要修复,则额外加 10 个点数)
- (推荐)生成后运行可打印性分析(
POST /openapi/v1/print/analyze,免费)。仅当状态为 error 时才运行**POST /openapi/v1/print/repair**(10 个点数)。
可打印性分析与修复
# After the textured/final mesh is ready:
INPUT_TASK_ID = refine_id # or whatever produced the print-ready mesh
# input_task_id MUST refer to a Meshy 6 / Preview task. For Meshy 4/5 outputs,
# pass `model_url` (the GLB download URL) instead.
analyze_id = create_task("/openapi/v1/print/analyze", {
"input_task_id": INPUT_TASK_ID,
})
analyze_task = poll_task("/openapi/v1/print/analyze", analyze_id)
p = analyze_task.get("printability") or {}
metrics = p.get("metrics", {})
print(f"Printability: {p.get('status')}: {metrics}")
if p.get("status") == "error":
repair_id = create_task("/openapi/v1/print/repair", {
"input_task_id": INPUT_TASK_ID, # output is GLB
})
repair_task = poll_task("/openapi/v1/print/repair", repair_id)
repaired_url = next((u for u in repair_task["model_urls"].values() if u), None)
# Use repaired_url for the next step (download / multicolor / slicer)
状态含义:healthy(可直接打印)| warning(存在退化面 / 孔洞——可选择修复)| error(非水密 / 非流形——建议修复)| unknown(分析失败)。修复只会保留几何形状,不会保留纹理——如果需要多色打印,请重新贴图。
切片软件检测与打开
import subprocess, shutil, platform, os, glob as glob_mod
SLICER_MAP = {
"OrcaSlicer": {"mac_app": "OrcaSlicer", "win_exe": "orca-slicer.exe", "win_dir": "OrcaSlicer", "linux_exe": "orca-slicer"},
"Bambu Studio": {"mac_app": "BambuStudio", "win_exe": "bambu-studio.exe", "win_dir": "BambuStudio", "linux_exe": "bambu-studio"},
"Creality Print": {"mac_app": "Creality Print", "win_exe": "CrealityPrint.exe", "win_dir": "Creality Print*", "linux_exe": None},
"Elegoo Slicer": {"mac_app": "ElegooSlicer", "win_exe": "elegoo-slicer.exe", "win_dir": "ElegooSlicer", "linux_exe": None},
"Anycubic Slicer Next": {"mac_app": "AnycubicSlicerNext", "win_exe": "AnycubicSlicerNext.exe", "win_dir": "AnycubicSlicerNext", "linux_exe": None},
"PrusaSlicer": {"mac_app": "PrusaSlicer", "win_exe": "prusa-slicer.exe", "win_dir": "PrusaSlicer", "linux_exe": "prusa-slicer"},
"UltiMaker Cura": {"mac_app": "UltiMaker Cura", "win_exe": "UltiMaker-Cura.exe", "win_dir": "UltiMaker Cura*", "linux_exe": None},
}
MULTICOLOR_SLICERS = {"OrcaSlicer", "Bambu Studio", "Creality Print", "Elegoo Slicer", "Anycubic Slicer Next"}
def detect_slicers():
found = []
system = platform.system()
for name, info in SLICER_MAP.items():
path = None
if system == "Darwin":
app = info.get("mac_app")
if app and os.path.exists(f"/Applications/{app}.app"):
path = f"/Applications/{app}.app"
elif system == "Windows":
win_dir, win_exe = info.get("win_dir", ""), info.get("win_exe", "")
for base in [os.environ.get("ProgramFiles", r"C:\Program Files"),
os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")]:
if "*" in win_dir:
matches = glob_mod.glob(os.path.join(base, win_dir, win_exe))
if matches: path = matches[0]; break
else:
candidate = os.path.join(base, win_dir, win_exe)
if os.path.exists(candidate): path = candidate; break
else:
exe = info.get("linux_exe")
if exe: path = shutil.which(exe)
if path:
found.append({"name": name, "path": path, "multicolor": name in MULTICOLOR_SLICERS})
return found
def open_in_slicer(file_path, slicer_name):
info = SLICER_MAP.get(slicer_name, {})
system, abs_path = platform.system(), os.path.abspath(file_path)
if system == "Darwin":
subprocess.run(["open", "-a", info.get("mac_app", slicer_name), abs_path])
elif system == "Windows":
exe_path = shutil.which(info.get("win_exe", ""))
(subprocess.Popen([exe_path, abs_path]) if exe_path else os.startfile(abs_path))
else:
exe_path = shutil.which(info.get("linux_exe", ""))
(subprocess.Popen([exe_path, abs_path]) if exe_path else subprocess.run(["xdg-open", abs_path]))
print(f"Opened {abs_path} in {slicer_name}")
# Several SEPARATE/unrelated models (e.g. results from different tasks)? Call this once per
# model with a short gap — Bambu Studio especially may respond to only one if fired
# back-to-back. (Parts of ONE model belong in a single project — not spaced.)
# import time
# for f in [model_a, model_b, model_c]:
# open_in_slicer(f, slicer_name); time.sleep(2)
slicers = detect_slicers()
for s in slicers:
mc = " [multicolor]" if s["multicolor"] else ""
print(f" - {s['name']}{mc}: {s['path']}")
白色模型工作流
| 步骤 | 操作 | 点数 |
|---|---|---|
| 1 | 生成无纹理模型 | 20 |
| 2 | 下载 OBJ | 0 |
| 3 | 修复 OBJ(fix_obj_for_printing) | 0 |
| 4 | 在切片软件中打开 | 0 |
生成时在 target_formats 中包含 "obj",然后修复模型以便打印:
# --- Generate for white model printing ---
# Text to 3D:
task_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest",
"target_formats": ["obj"], # Only OBJ for white model printing
})
# OR Image to 3D:
# task_id = create_task("/openapi/v1/image-to-3d", {
# "image_url": "URL", "should_texture": False,
# "target_formats": ["glb", "obj"],
# })
task = poll_task("/openapi/v2/text-to-3d", task_id)
project_dir = get_project_dir(task_id, "print")
obj_url = task["model_urls"].get("obj") or task["model_urls"].get("glb")
obj_path = os.path.join(project_dir, "model.obj")
download(obj_url, obj_path)
def fix_obj_for_printing(input_path, output_path=None, target_height_mm=75.0):
if output_path is None: output_path = input_path
lines = open(input_path, "r").readlines()
rotated, min_x, max_x, min_y, max_y, min_z, max_z = [], float("inf"), float("-inf"), float("inf"), float("-inf"), float("inf"), float("-inf")
for line in lines:
if line.startswith("v "):
parts = line.split()
x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
rx, ry, rz = x, -z, y
min_x, max_x = min(min_x, rx), max(max_x, rx)
min_y, max_y = min(min_y, ry), max(max_y, ry)
min_z, max_z = min(min_z, rz), max(max_z, rz)
rotated.append(("v", rx, ry, rz, parts[4:]))
elif line.startswith("vn "):
parts = line.split()
rotated.append(("vn", float(parts[1]), -float(parts[3]), float(parts[2]), []))
else:
rotated.append(("line", line))
h = max_z - min_z
s = target_height_mm / h if h > 1e-6 else 1.0
xo, yo, zo = -(min_x+max_x)/2*s, -(min_y+max_y)/2*s, -(min_z*s)
with open(output_path, "w") as f:
for item in rotated:
if item[0] == "v":
_, rx, ry, rz, extra = item
e = " "+" ".join(extra) if extra else ""
f.write(f"v {rx*s+xo:.6f} {ry*s+yo:.6f} {rz*s+zo:.6f}{e}\n")
elif item[0] == "vn":
f.write(f"vn {item[1]:.6f} {item[2]:.6f} {item[3]:.6f}\n")
else:
f.write(item[1])
print(f"OBJ fixed: scaled to {target_height_mm:.0f}mm, Z-up, centered")
fix_obj_for_printing(obj_path, target_height_mm=75.0)
if slicers: open_in_slicer(obj_path, slicers[0]["name"])
多色工作流
| 步骤 | 操作 | 点数 |
|---|---|---|
| 1 | 生成并添加纹理 | 30 |
| 2 | 多色处理 | 10 |
| 3 | 下载 3MF | 0 |
| 4 | 在多色切片软件中打开 | 0 |
mc_slicers = [s for s in slicers if s["multicolor"]]
if not mc_slicers:
print("WARNING: No multicolor slicer detected. Install: OrcaSlicer, Bambu Studio, etc.")
# --- Generate + texture with target_formats including 3mf ---
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest",
# No target_formats needed — 3MF comes from multi-color API
})
poll_task("/openapi/v2/text-to-3d", preview_id)
refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine", "preview_task_id": preview_id, "enable_pbr": True,
})
poll_task("/openapi/v2/text-to-3d", refine_id)
project_dir = get_project_dir(preview_id, "multicolor-print")
# --- Multi-color processing ---
mc_task_id = create_task("/openapi/v1/print/multi-color", {
"input_task_id": refine_id,
"max_colors": 4, # 1-16, ask user
"max_depth": 4, # 3-6, ask user
})
task = poll_task("/openapi/v1/print/multi-color", mc_task_id)
threemf_path = os.path.join(project_dir, "multicolor.3mf")
download(task["model_urls"]["3mf"], threemf_path)
if mc_slicers: open_in_slicer(threemf_path, mc_slicers[0]["name"])
可打印性检查清单
| 检查项 | 建议 |
|---|---|
| 壁厚 | FDM 至少 1.2mm,树脂至少 0.8mm |
| 悬垂 | 控制在 45° 以下,或添加支撑 |
| 流形网格 | 水密、无孔洞 |
| 最小细节 | FDM 为 0.4mm,树脂为 0.05mm |
| 底座稳定性 | 使用平底,或在切片软件中添加裙边/筏层 |
| 悬空部件 | 所有部件都应连接,或分开打印 |
步骤 3:报告结果
任务成功后:
- 已下载文件的路径和大小
- 任务 ID(用于后续操作:细化、绑定、重新贴图)
- 可用格式(列出
model_urls的键) - 已消耗点数及当前余额
- 建议的后续操作:
- 预览完成 → “要细化模型(添加纹理)吗?”
- 模型完成 → “要为这个角色绑定骨骼吗?”
- 已绑定骨骼 → “要应用自定义动画吗?”
- 任何带纹理的模型 → “要 3D 打印这个模型吗?支持多色打印!”
- 任何模型 → “要 3D 打印这个模型吗?”
错误恢复
| HTTP 状态 | 含义 | 操作 |
|---|---|---|
| 401 | API 密钥无效 | 重新执行步骤 0;请用户检查密钥 |
| 402 | 点数不足 | 显示余额,并提供链接 https://www.meshy.ai/pricing |
| 422 | 无法处理 | 说明原因(例如,用于绑定的模型不是人形模型) |
| 429 | 请求频率受限 | 5s 后自动重试(最多 3 次) |
| 5xx | 服务端错误 | 10s 后自动重试(仅 1 次) |
任务 FAILED 消息:
"The server is busy..."→ 采用退避策略重试(5s、10s、20s)"Internal server error."→ 简化提示词,然后重试一次
已知行为与限制
- 99% 卡住:这是正常的最终处理过程(30–120s)。请勿中断。
- 资源保留期限:文件将在 3 天后删除(非 Enterprise)。请立即下载。
- PBR 贴图:必须显式设置
enable_pbr: true。 - 细化:支持
meshy-5、meshy-6或latest——为保持一致,请选择与预览相同的系列。无论模型为何,均消耗 10 点。(meshy-4已退役 → 400。) - 已弃用参数:
symmetry_mode不再影响输出;Meshy-6 会忽略art_style;请使用pose_mode替代旧的is_a_t_pose标志。 consumed_credits:每个任务 GET 响应都包含consumed_credits——读取此值以报告实际消耗的点数,而非估算。- 绑定骨骼:仅支持双足人形模型,面数 ≤ 300,000。
- 打印格式:白色模型 → 使用
fix_obj_for_printing()处理 OBJ。多色模型 → 使用多色打印 API 生成 3MF。请务必先检测切片软件。 - 下载格式:下载前询问用户需要哪种格式。GLB(查看)、OBJ(打印)、3MF(多色打印)、FBX(游戏)、USDZ(AR)。请勿下载所有格式。
- 多色打印的 3MF:多色打印 API 会直接输出 3MF——无需在生成/细化时请求 3MF。对于需要 3MF 的非打印用途,请在
target_formats中传入"3mf"。 - 时间戳:所有 API 时间戳均为 Unix 纪元毫秒。
执行检查清单
- 已运行 API 密钥检测(步骤 0)——仅检查环境变量和
.env - 已验证 API 密钥(从未完整打印)
- 已展示费用摘要并获得用户确认
- 已将完整工作流写为单个 Python 脚本
- 已使用
python3 -u运行,以启用无缓冲输出 - 已报告文件路径、格式、任务 ID 和余额
- 已建议后续操作
其他资源
如需查看包含所有参数、响应架构和错误代码的完整 API 端点参考,请阅读 reference.md。