跳转至

用 MAI-Image-2 讓 OpenClaw 畫圖

想讓你的 OpenClaw AI 助手根據文字描述直接畫圖嗎?OpenClaw 是一個開源的多通道聊天 Agent 閘道,這篇教學會帶你把微軟的 MAI-Image-2 圖片生成模型接到它,並讓圖片能在 Telegram、LINE、WhatsApp 三個平台上穩定送達。

你會完成什麼

整套接好之後,你的 OpenClaw 可以這樣用:

  • 使用者在任何聊天頻道說「幫我畫一張太空貓」,AI 會自動生成圖片並回傳。
  • 圖片生成走 MAI-Image-2,不需要自建 GPU,費用按產圖量計價。
  • Telegram 可以直接收到圖片。
  • LINE 和 WhatsApp 可以透過公開 URL 讀取圖片。
  • 生成圖片可在 7 天後自動清理。

前置條件

  • 你已經有一台執行 OpenClaw 的 Azure VM,可以先參考基礎設施篇
  • Azure AI Foundry 已經部署完成,且使用 kind: AIServices
  • 你的 Bicep IaC 目前可以正常部署。

相關 OpenClaw 文章

第一步:部署 MAI-Image-2 模型

1.1 讓 Bicep 支援 Microsoft 格式模型

MAI-Image-2 的模型格式是 Microsoft,不是 GPT 系列常見的 OpenAI。先修改 openai.bicep 裡的部署迴圈,讓它可以讀取格式參數:

properties: {
  model: {
    format: deployment.?modelFormat ?? 'OpenAI'
    name: deployment.modelName
    version: deployment.modelVersion
  }
}

這個改動可以向後相容。既有的 GPT 部署如果沒有 modelFormat 欄位,仍然會自動回退到 OpenAI

1.2 在參數檔加入 MAI-Image-2

prod.bicepparamopenaiModelDeployments 陣列中加入:

{
  name: 'mai-image-2'
  modelName: 'MAI-Image-2'
  modelVersion: '2026-02-20'
  modelFormat: 'Microsoft'
  skuName: 'GlobalStandard'
  skuCapacity: 3
}

[!IMPORTANT] modelName 必須寫成 MAI-Image-2。如果你改成小寫 mai-image-2,Azure 很可能會回傳具有誤導性的 SpecialFeatureOrQuotaIdRequired 錯誤。

1.3 部署與驗證

az deployment group create \
  --resource-group oc-family-rg \
  --template-file infra/bicep/main.bicep \
  --parameters infra/bicep/params/prod.bicepparam

接著驗證部署結果:

az cognitiveservices account deployment list \
  --name <你的 Foundry 資源名稱> \
  --resource-group oc-family-rg \
  --query "[].{name:name, model:properties.model.name, format:properties.model.format}" \
  -o table

你應該會看到 mai-image-2 | MAI-Image-2 | Microsoft

第二步:建立 Azure Blob Storage 供圖片公開存取

這一步的目的很直接。Telegram 可以處理直接上傳的圖片,但 LINE 與 WhatsApp 更適合接收公開的 HTTPS URL。把生成圖片統一上傳到 Blob Storage,可以讓三個通道的投遞策略保持一致。

2.1 新增 media-storage.bicep

建一個獨立的 Storage Account,不要與 AI Foundry 共用:

resource mediaStorage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    allowBlobPublicAccess: true
    minimumTlsVersion: 'TLS1_2'
  }
}

resource imagesContainer '...' = {
  name: 'images'
  properties: {
    publicAccess: 'Blob'
  }
}

再加上一條 7 天自動刪除的 lifecycle policy:

resource lifecyclePolicy '...' = {
  properties: {
    policy: {
      rules: [
        {
          name: 'auto-delete-7d'
          type: 'Lifecycle'
          definition: {
            filters: {
              blobTypes: ['blockBlob']
              prefixMatch: ['images/']
            }
            actions: {
              baseBlob: {
                delete: {
                  daysAfterCreationGreaterThan: 7
                }
              }
            }
          }
        }
      ]
    }
  }
}

2.2 接到 main.bicep

param enableMediaStorage bool = true

module mediaStorage './modules/media-storage.bicep' = if (enableMediaStorage) {
  name: 'mediaStorage'
  params: {
    location: location
    prefix: prefix
  }
}

output mediaStorageEndpoint string = enableMediaStorage
  ? mediaStorage.outputs.storageEndpoint
  : ''

2.3 部署並把 Key 存進 Key Vault

Bash 指令,可以在 Linux VM、WSL 或 Azure Cloud Shell 執行:

az deployment group create \
  --resource-group oc-family-rg \
  --template-file infra/bicep/main.bicep \
  --parameters infra/bicep/params/prod.bicepparam

STORAGE_NAME=$(az storage account list -g oc-family-rg \
  --query "[?contains(name,'media')].name" -o tsv)

STORAGE_KEY=$(az storage account keys list -g oc-family-rg \
  -n "$STORAGE_NAME" --query "[0].value" -o tsv)

az keyvault secret set \
  --vault-name <你的 Key Vault 名稱> \
  --name media-storage-key \
  --value "$STORAGE_KEY"

第三步:建立 OpenClaw Plugin

Plugin 做四件事:接收畫圖請求、呼叫 MAI-Image-2、把圖片上傳到 Blob Storage,最後回傳圖片和公開 URL。

3.1 建議的專案結構

extensions/mai-image/
├── index.js
├── lib/
│   ├── api.js
│   └── blob.js
├── openclaw.plugin.json
├── package.json
└── test/
    ├── tool.test.js
    └── blob.test.js

3.2 核心註冊邏輯

const crypto = require("crypto");
const { generateImage } = require("./lib/api");
const { uploadToBlob } = require("./lib/blob");

function register(api) {
  const cfg = Object.assign({
    endpoint: "",
    deploymentName: "mai-image-2",
    defaultWidth: 1024,
    defaultHeight: 1024,
    mediaStorageAccount: "",
    mediaStorageKey: "",
    mediaStorageContainer: "images",
  }, api.pluginConfig || {});

  api.registerTool({
    name: "mai_image_generate",
    label: "mai_image_generate",
    description: "Generate an image from a text prompt using MAI-Image-2.",
    parameters: {
      type: "object",
      required: ["prompt"],
      properties: {
        prompt: { type: "string" },
        width: { type: "integer" },
        height: { type: "integer" },
      },
    },
    execute: async (_toolCallId, params) => {
      const result = await generateImage({ ...cfg, prompt: params.prompt });
      const buffer = Buffer.from(result.b64_json, "base64");

      const blobName = `${Date.now()}-${crypto.randomUUID()}.png`;
      const publicUrl = await uploadToBlob({
        accountName: cfg.mediaStorageAccount,
        accountKey: cfg.mediaStorageKey,
        containerName: cfg.mediaStorageContainer,
        blobName,
        buffer,
        contentType: "image/png",
      });

      return {
        content: [
          { type: "image", data: result.b64_json, mimeType: "image/png" },
          { type: "text", text: `Image generated: ${publicUrl}` },
        ],
        details: { status: "ok", publicUrl },
      };
    },
  });

  api.on(
    "before_prompt_build",
    () => ({
      appendSystemContext:
        "You have a mai_image_generate tool. When the user asks to draw or generate an image, use it. After calling the tool, include the returned URL in your reply.",
    }),
    { priority: 20 }
  );
}

module.exports = register;

3.3 api.js 的角色

lib/api.js 的工作很單純,就是把請求轉成 MAI-Image-2 的 HTTP API 呼叫:

// POST https://<resource>.cognitiveservices.azure.com/mai/v1/images/generations
// Headers: api-key, Content-Type: application/json
// Body: { model: "mai-image-2", prompt, width, height }
// Response: { data: [{ b64_json: "<base64 PNG>" }] }

如果你只是想把依賴壓到最少,這裡用 Node.js 內建的 https 模組就夠了,不一定要額外安裝 SDK。

3.4 blob.js 的角色

lib/blob.js 負責透過 Azure Blob Storage REST API 上傳檔案:

// PUT https://<account>.blob.core.windows.net/<container>/<blob>
// Authorization: SharedKey <account>:<HMAC-SHA256 signature>
// Public URL:
// https://<account>.blob.core.windows.net/images/<blob>.png

同樣地,如果你想避免再加一個大型依賴,也可以直接用 REST API 與 Shared Key 完成這件事。

第四步:把 Plugin 部署到 VM

4.1 複製檔案

scp -r extensions/mai-image/ weijen@family-claw.multiagentai.co:~/.openclaw/extensions/mai-image/

[!NOTE] 如果你是在 Codespaces 內操作,且 22 port 無法直接連線,可以改用 az vm run-command invoke 搭配 base64 傳檔。

4.2 更新 OpenClaw 設定

~/.openclaw/openclaw.jsonplugins 區段加入:

{
  "plugins": {
    "allow": ["...", "mai-image"],
    "entries": {
      "mai-image": {
        "enabled": true,
        "config": {
          "endpoint": "https://<你的 Foundry>.cognitiveservices.azure.com",
          "deploymentName": "mai-image-2",
          "defaultWidth": 1024,
          "defaultHeight": 1024,
          "mediaStorageAccount": "<Storage Account 名稱>",
          "mediaStorageKey": "<從 Key Vault 取得的 key>",
          "mediaStorageContainer": "images"
        }
      }
    },
    "load": {
      "paths": ["...", "/home/weijen/.openclaw/extensions/mai-image"]
    }
  }
}

4.3 重啟 Gateway 並確認 Plugin 載入

openclaw gateway restart
journalctl --user -u openclaw-gateway.service --since "30 seconds ago" | grep mai-image

正常情況下,你會看到類似 mai-image plugin ready 的訊息。

第五步:測試

CLI 測試

openclaw agent \
  --message "Use mai_image_generate to draw a cute cat" \
  --session-id test \
  --json \
  --timeout 120

通道測試

在 Telegram、LINE、WhatsApp 中發送:

幫我畫一隻在月球上看書的貓

預期行為如下:

頻道 預期結果
Telegram 直接收到圖片。
WhatsApp 收到一則含圖片 URL 的訊息,點擊即可查看。
LINE 行為與 WhatsApp 類似,收到可點擊的圖片 URL。

成本估算

項目 說明
MAI-Image-2 按圖片生成量計費。
Blob Storage(Standard LRS) 如果每天只有少量圖片,通常非常便宜。
7 天 lifecycle 清理 沒有額外功能費用。

對大多數家庭或個人實驗場景來說,圖片託管的額外成本通常可以忽略,真正需要觀察的是模型調用量;想要在 Azure AI Foundry 追蹤 MAI-Image-2 呼叫與每張圖片成本,可以搭配 OpenTelemetry 設定讓每次請求的用量可見。

踩坑紀錄

坑 1:模型名稱大小寫

症狀 解法
Bicep 部署回傳 SpecialFeatureOrQuotaIdRequired 確認 modelName 寫的是 MAI-Image-2,不是 mai-image-2

建議用 az cognitiveservices account list-models 再確認一次模型名稱拼法。

坑 2:Plugin 回傳格式錯誤

症狀 解法
TypeError: Cannot read properties of undefined (reading 'trim'),導致後續 LLM 呼叫都失敗 registerToolexecute 必須回傳 { content: [...], details: {...} },不能只回傳 { media, text }

坑 3:API Key 在註冊階段還沒準備好

症狀 解法
401 Access denied 不要在 plugin 註冊階段就依賴 api.resolveSecret(),改成在每次 execute 時再從 provider 設定讀取。

坑 4:registerImageGenerationProvider 無法完整覆蓋所有通道

症狀 解法
Telegram 正常,但 WhatsApp 與 LINE 只收到文字 目前可改用 registerTool 搭配 Blob Storage URL,避開 OpenClaw 的媒體投遞限制。

registerImageGenerationProvider 會把圖片存到本地磁碟。Telegram Bot API 可以直接處理本地檔案,但 WhatsApp 與 LINE 更適合拿到公開的 HTTPS URL。

坑 5:Session 記憶污染

症狀 解法
你明明修好 plugin 了,LLM 還是一直說「我不能畫圖」 清掉舊 session 與 workspace memory,再重啟 gateway。
find ~/.openclaw/agents/main/sessions/ -name "*.jsonl" \
  -exec grep -l "<對方ID>" {} \; -delete

rm -f ~/.openclaw/workspace/memory/$(date +%Y-%m-%d)*.md
openclaw gateway restart

參考資料