用 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 文章
- 如果你想先了解這個系列的整體定位與插件在 OpenClaw 裡扮演的角色,可以先讀 OpenClaw 總覽。
- 如果你想看另一個偏向排程與任務派遣的插件案例,可以讀 為 OpenClaw 打造自然語言提醒與排程任務系統。
- 如果你還需要先把 Azure VM、Key Vault 與 Azure AI Foundry 環境搭起來,可以回頭看 基礎設施篇。
- 如果你也想讓同一套聊天通道支援語音訊息,可以接著讀 讓 OpenClaw 聽懂語音:整合 MAI-Transcribe-1 語音轉文字完整教學。
第一步:部署 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.bicepparam 的 openaiModelDeployments 陣列中加入:
{
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.json 的 plugins 區段加入:
{
"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 | 直接收到圖片。 |
| 收到一則含圖片 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 呼叫都失敗 |
registerTool 的 execute 必須回傳 { 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