Communitygithub.com

Meshy 3D — 模型生成與列印

透過 Meshy AI API 直接溝通,生成並列印 3D 模型。涵蓋完整生命週期:API 金鑰設定、任務建立、指數倒退輪詢、下載、多步驟管線,以及與切片軟體整合的 3D 列印準備。

Meshy 3D — 模型生成與列印 是什麼?

此技能為 AI 助手(如 Claude Code、Cursor、Codex)提供與 Meshy AI API 直接互動的完整工具集。使用者可透過文字或圖片提示生成 3D 模型、重新貼圖、重新網格、重新縮放、自動骨架綁定與動畫,最終進行 3D 列印準備。技能內建安全機制,確保 API 金鑰僅從環境變數或工作目錄下的 `.env` 檔案讀取,絕不存取家目錄或 shell 設定檔。所有下載的模型檔案會自動組織在 `meshy_output/` 目錄下,每個任務建立獨立專案資料夾,並記錄詳細中繼資料與歷史記錄。技能採用一次性 Python 腳本執行完整「建立→輪詢→下載」流程,內含指數倒退輪詢演算法,可處理 1–5 分鐘的生成時間,並在接近完成時自動調整輪詢間隔,避免非預期中斷。支援 macOS、Linux 與 Windows (WSL/Cygwin) 環境,僅需標準 POSIX 工具與 Python requests 程式庫。

相容平台~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 — API 認證令牌,僅在 HTTP Authorization: Bearer 標頭中傳送。絕不記錄、絕不寫入任何檔案,除非使用者明確要求在當前工作目錄的 .env 檔案中寫入。

外部網路端點:

  • https://api.meshy.ai — Meshy AI API(任務建立、狀態輪詢、模型/圖片下載)

檔案系統存取:

  • 讀取:僅限當前工作目錄下的 .env 檔案(API 金鑰查找)
  • 寫入:僅限當前工作目錄下的 .env 檔案(API 金鑰儲存,僅在使用者要求時)
  • 寫入:當前工作目錄下的 ./meshy_output/(下載的模型檔案、中繼資料)
  • 讀取:使用者明確提供的檔案(例如用於圖片轉 3D 的本地圖片),僅在使用者指定的確切路徑存取
  • 無法存取家目錄、shell 設定檔或上述範圍之外的任何路徑

離開此機器的資料:

  • 對 api.meshy.ai 的 API 請求包含 MESHY_API_KEY(位於 Authorization 標頭)以及使用者提供的文字提示或圖片網址。不會傳輸其他本地資料。下載的模型檔案僅儲存在本地。

重要:首次使用會話通知

當此技能在會話中首次啟用時,請告知使用者:

所有產生的檔案將儲存至目前工作目錄的 meshy_output/。每個專案都有各自的資料夾({YYYYMMDD_HHmmss}_{提示文字}_{id}/),包含模型檔案、紋理、縮圖及中繼資料。歷史記錄會追蹤在 meshy_output/history.json。

每個會話僅需說明 一次。


重要:檔案組織

所有下載的檔案必須放入目前工作目錄下結構化的 meshy_output/ 目錄。請勿隨意散落檔案。

  • 每個專案:meshy_output/{YYYYMMDD_HHmmss}_{提示文字縮寫}_{任務ID前綴}/
  • 鏈式任務(預覽→精煉→骨架綁定)重複使用相同的 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 "就緒:金鑰=${MESHY_API_KEY:0:8}..."
else
  echo "就緒:未找到金鑰"
fi

# 4. Python requests 檢查
python3 -c "import requests; print('PYTHON_REQUESTS: 正常')" 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. 點選 「建立 API 金鑰」,為其命名,然後複製金鑰(以 msy_ 開頭)
  3. 金鑰 僅顯示一次 — 請將其存放在安全的地方

注意: API 存取需要 Pro 方案或以上。免費帳戶無法建立 API 金鑰。

使用者提供金鑰後,為當前會話設定金鑰,並可選擇性地持久化至 .env:

# 僅為目前會話設定
export MESHY_API_KEY="msy_貼上金鑰"

# 驗證金鑰
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_貼上金鑰' >> .env
echo "已儲存至 .env"

# 重要:將 .env 加入 .gitignore 以避免洩漏金鑰
grep -q "^\.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore
echo ".env 已加入 .gitignore"

安全提醒: 金鑰僅儲存在您目前專案目錄的 .env 中。切勿將此檔案提交到版本控制。.env 已自動加入 .gitignore。


步驟 1:在花費點數前與使用者確認計劃

關鍵: 在建立任何任務之前,向使用者顯示費用摘要並等待確認:

我將使用以下計劃生成 "<提示文字>" 的 3D 模型:

  1. 預覽(網格生成)— 20 點數
  2. 精煉(含 PBR 的紋理)— 10 點數
  3. 下載為 .glb

  總費用:30 點數
  目前餘額:<N> 點數

  是否繼續?

對於多步驟管線(文字轉 3D → 骨架綁定 → 動畫),提前顯示完整的管線費用。

注意: 骨架綁定會自動包含走路 + 跑步動畫,無需額外費用。僅在需要自訂動畫時才加入 Animate(3 點數)。

意圖 → API 對應

使用者想要...API端點點數
從文字生成 3D 模型Text to 3DPOST /openapi/v2/text-to-3d5–20(預覽)+ 10(精煉)
從單張圖片生成 3D 模型Image to 3DPOST /openapi/v1/image-to-3d5–30
從多張圖片生成 3D 模型Multi-Image to 3DPOST /openapi/v1/multi-image-to-3d5–30
在現有模型上產生新紋理RetexturePOST /openapi/v1/retexture10
變更網格格式/拓撲RemeshPOST /openapi/v1/remesh5
將模型轉換為其他格式(不重新網格)ConvertPOST /openapi/v1/convert1
將模型重新縮放至真實世界尺寸ResizePOST /openapi/v1/resize1
在外部紋理前生成全新 UV(GLB,≤40k 面)UV UnwrapPOST /openapi/v1/uv-unwrap5
為角色添加骨架Auto-RiggingPOST /openapi/v1/rigging5
為已骨架綁定的角色製作動畫AnimationPOST /openapi/v1/animations3
從文字生成 2D 圖片(建議在圖片轉 3D 前的步驟)Text to ImagePOST /openapi/v1/text-to-image3 / 6 / 9 / 9
優化/編輯 2D 圖片(建議在圖片轉 3D 前的步驟)Image to ImagePOST /openapi/v1/image-to-image3 / 6 / 9 / 12
照片 → 風格化實體產品(人偶/燈/鑰匙圈/冰箱磁鐵)Creative LabPOST /openapi/creative-lab/{product}/v1/prototype 然後 .../build6 + 30
檢查 FDM 可列印性Analyze PrintabilityPOST /openapi/v1/print/analyze0(免費)
修復非流形/退化面/孔洞拓撲Repair PrintabilityPOST /openapi/v1/print/repair10
多色 3D 列印Multi-Color PrintPOST /openapi/v1/print/multi-color10(+ 生成費用)
3D 列印模型(白色)→ 請參閱列印管線章節—20
檢查點數餘額BalanceGET /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", "未知")
            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_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}秒,第 #{poll_count} 次輪詢)", flush=True)
        if status == "SUCCEEDED":
            return task
        if status in ("FAILED", "CANCELED"):
            msg = task.get("task_error", {}).get("message", "未知")
            sys.exit(f"任務_{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} 秒({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

相關技能