Plugin maintainer reference
訊息呈現
訊息呈現是 OpenClaw 用於豐富外送聊天 UI 的共用契約。 它讓代理程式、命令列介面命令、核准流程和外掛只需描述一次訊息 意圖,而各個頻道外掛則以其可提供的最佳原生形式進行轉譯。
使用呈現功能建立可攜式訊息 UI:文字區段、小型情境/頁尾 文字、分隔線、圖表、表格、按鈕、選取選單,以及卡片標題/語氣。
請勿將 Discord components、Slack
blocks、Telegram buttons、Teams card 或 Feishu card 等新的供應商原生欄位新增至共用
訊息工具。這些是由頻道外掛擁有的轉譯器輸出。
契約
外掛作者從以下位置匯入公開契約:
MessagePresentation, ReplyPayloadDelivery,} from "openclaw/plugin-sdk/interactive-runtime";結構:
type MessagePresentation = { title?: string; tone?: "neutral" | "info" | "success" | "warning" | "danger"; blocks: MessagePresentationBlock[];}; type MessagePresentationBlock = | { type: "text"; text: string } | { type: "context"; text: string } | { type: "divider" } | { type: "buttons"; buttons: MessagePresentationButton[] } | { type: "select"; placeholder?: string; options: MessagePresentationOption[] } | { type: "chart"; chartType: "pie"; title: string; segments: Array<{ label: string; value: number }>; } | { type: "chart"; chartType: "bar" | "area" | "line"; title: string; categories: string[]; series: Array<{ name: string; values: number[] }>; xLabel?: string; yLabel?: string; } | { type: "table"; caption: string; headers: string[]; rows: Array<Array<string | number>>; rowHeaderColumnIndex?: number; }; type MessagePresentationAction = | { type: "command"; command: string } | { type: "callback"; value: string } | { type: "approval"; approvalId: string; approvalKind: "exec" | "plugin"; decision: "allow-once" | "allow-always" | "deny"; } | { type: "question"; questionId: string; optionValue: string; } | { type: "url"; url: string } | { type: "web-app"; url: string; widgetId?: string; } | { type: "web-app"; url?: string; widgetId: string; }; type MessagePresentationButton = { label: string; action?: MessagePresentationAction; /** 舊版回呼值。新控制項應優先使用 action。 */ value?: string; /** @deprecated 請使用 type 為 "url" 的 action。 */ url?: string; /** @deprecated 請使用 type 為 "web-app" 的 action。 */ webApp?: { url: string }; /** @deprecated 請使用 type 為 "web-app" 的 action。 */ web_app?: { url: string }; priority?: number; disabled?: boolean; reusable?: boolean; style?: "primary" | "secondary" | "success" | "danger";}; type MessagePresentationOption = { label: string; action?: Extract<MessagePresentationAction, { type: "command" | "callback" }>; /** 舊版回呼值。新控制項應優先使用 action。 */ value?: string;}; type ReplyPayloadDelivery = { pin?: | boolean | { enabled: boolean; notify?: boolean; required?: boolean; };};按鈕語意:
action.type: "command"透過核心的命令路徑執行原生斜線命令。 內建命令按鈕和選單應使用此類型。action.type: "callback"透過頻道的互動路徑攜帶不透明的外掛資料。 頻道外掛不得將回呼資料重新解讀為斜線 命令。action.type: "approval"識別一項持久的操作者核准、其明確的exec或plugin類型,以及要求的決定。頻道外掛 會將該動作編碼為傳輸層私有的回呼,並透過 核准服務解析;它們不得剖析/approve命令文字或根據 ID 推斷 類型。action.type: "question"識別即時、由執行階段撰寫的ask_user問題中的一個選項。與approval相同,這是 OpenClaw 執行階段動作; 代理程式和外掛不得自行產生問題 ID。Telegram、Discord 和 Slack 會將其對應至傳輸層私有的原生回呼,並透過 閘道解析選項。當問題已回答、過期或 取消時,這些頻道會編輯已傳送的訊息、移除其動作, 並附加最終狀態。WhatsApp、Signal 和 iMessage 會將最多 四個單選選項轉譯為1️⃣至4️⃣回應。其他問題 形式會降級為標籤文字,而使用者可以用純文字 回覆作答。action.type: "url"會開啟一般連結。action.type: "web-app"會啟動頻道原生的網頁應用程式。請為 URL 支援的應用程式設定url,或為啟動 機制由頻道擁有、託管於 OpenClaw 的小工具設定widgetId;至少必須提供其中一項。當兩者 都存在時,頻道可以優先使用其原生託管小工具啟動機制,並在該機制 不可用時使用 URL。value是舊版不透明回呼值。新控制項應使用action, 讓頻道外掛無須根據文字猜測,即可對應命令和回呼。url、webApp和web_app仍可作為已棄用的邊界輸入。 正規化器會保留這些欄位,讓轉譯器能區分已發布的舊版 語意與明確的型別化動作。新的產生端應使用action。label為必填,也會用於文字備援。style僅供提示。轉譯器應將不支援的樣式對應至安全的 預設值,而非使傳送失敗。priority為選填。當頻道宣告動作限制且必須捨棄部分控制項時, 核心會優先保留優先順序較高的按鈕,並在優先順序相同的按鈕之間維持 原始順序。當所有控制項都容納得下時,會保留撰寫時的 順序。disabled為選填。頻道必須透過supportsDisabled明確選擇加入;否則 核心會將停用的控制項降級為非互動式備援文字。 即使停用的按鈕帶有command動作,在備援文字中也一律只轉譯 標籤。reusable為選填。支援可重複使用原生回呼的頻道,可以在 互動成功後繼續提供該動作。它適用於 重新整理、檢查或更多詳細資料等可重複或具冪等性的動作; 一般的一次性核准和破壞性動作則不要設定。
選取語意:
options[].action僅接受command或callback;核准和連結動作僅能用於按鈕。options[].value是舊版選取的應用程式值。placeholder僅供提示,不支援原生選取功能的頻道可以忽略。- 如果頻道不支援選取功能,備援文字會列出標籤。
圖表語意:
pie要求區段值必須為正數。bar、area和line使用一個有序的categories陣列。每個數列 都必須依相同順序,為每個類別提供恰好一個有限值。- 類別標籤和數列名稱必須是唯一的。無效或不完整的圖表 區塊會在正規化期間遭到捨棄,而不會默默變更資料。
- 必須透過
presentationCapabilities.charts明確選擇加入原生圖表轉譯。 其他頻道會以確定性的文字接收圖表標題、座標軸、類別、數列和值。 這也是無障礙備援。
表格語意:
-
caption是必填的簡短標題。headers必須包含至少一個 唯一且非空白的欄標籤。 -
rows必須包含至少一列。每列必須恰好為每個 表頭提供一個儲存格,而每個儲存格都必須是非空白字串或有限數值。 -
rowHeaderColumnIndex是選填的零起始索引,用於識別原生轉譯器應將其 儲存格公開為列標題的欄。 -
表格正規化具原子性。無效的說明文字、表頭、列寬、儲存格 或列標題索引會使表格區塊遭到捨棄,而不是截斷或修復 其資料。
-
必須透過
presentationCapabilities.tables明確選擇加入原生表格轉譯。 其他頻道會以確定性的線性文字接收說明文字和每一列, 並摺疊內部空白:text 開放中的銷售管線(表格)- 帳戶:Acme;階段:已贏得;ARR:125000- 帳戶:Globex;階段:審查;ARR:82000
沒有獨立的 report 判別欄位。請使用 title、
tone、text、context、chart、table 和動作區塊組成報告。這能讓每個
區塊都可獨立轉譯,並讓完整報告使用相同的
確定性文字備援。
產生端範例
簡易卡片:
{ "title": "部署核准", "tone": "warning", "blocks": [ { "type": "text", "text": "Canary 已準備好升級。" }, { "type": "context", "text": "組建 1234,預備環境已通過。" }, { "type": "buttons", "buttons": [ { "label": "核准", "action": { "type": "callback", "value": "deploy:approve" }, "style": "success" }, { "label": "拒絕", "action": { "type": "callback", "value": "deploy:decline" }, "style": "danger" } ] } ]}僅 URL 的連結按鈕:
{ "blocks": [ { "type": "text", "text": "版本資訊已準備就緒。" }, { "type": "buttons", "buttons": [ { "label": "開啟版本資訊", "action": { "type": "url", "url": "https://example.com/release" } } ] } ]}Telegram Mini App 按鈕:
{ "blocks": [ { "type": "buttons", "buttons": [ { "label": "啟動", "action": { "type": "web-app", "url": "https://example.com/app" } } ] } ]}選取選單:
{ "title": "選擇環境", "blocks": [ { "type": "select", "placeholder": "環境", "options": [ { "label": "Canary", "value": "env:canary" }, { "label": "正式環境", "value": "env:prod" } ] } ]}圖表:
{ "blocks": [ { "type": "chart", "chartType": "line", "title": "季度營收", "categories": ["第 1 季", "第 2 季", "第 3 季"], "series": [ { "name": "產品", "values": [120, 145, 138] }, { "name": "服務", "values": [80, 95, 104] } ], "xLabel": "季度", "yLabel": "營收" } ]}表格報告:
{ "title": "銷售管線報告", "tone": "info", "blocks": [ { "type": "text", "text": "依階段顯示目前的商機。" }, { "type": "table", "caption": "開放中的銷售管線", "headers": ["帳戶", "階段", "ARR"], "rows": [ ["Acme", "已贏得", 125000], ["Globex", "審查", 82000] ], "rowHeaderColumnIndex": 0 }, { "type": "context", "text": "已從 CRM 快照更新。" } ]}命令列介面傳送:
openclaw message send --channel slack \ --target channel:C123 \ --message "部署核准" \ --presentation '{"title":"部署核准","tone":"warning","blocks":[{"type":"text","text":"Canary 已準備就緒。"},{"type":"buttons","buttons":[{"label":"核准","value":"deploy:approve","style":"success"},{"label":"拒絕","value":"deploy:decline","style":"danger"}]}]}'釘選傳送:
openclaw message send --channel telegram \ --target -1001234567890 \ --message "已開啟主題" \ --pin使用明確 JSON 的釘選傳送:
{ "pin": { "enabled": true, "notify": true, "required": false }}轉譯器合約
頻道外掛會在其輸出配接器上宣告轉譯支援:
const adapter: ChannelOutboundAdapter = { deliveryMode: "direct", presentationCapabilities: { supported: true, buttons: true, selects: true, context: true, divider: true, charts: false, tables: false, limits: { actions: { maxActions: 25, maxActionsPerRow: 5, maxRows: 5, maxLabelLength: 80, maxValueBytes: 100, supportsStyles: true, supportsDisabled: false, }, selects: { maxOptions: 25, maxLabelLength: 100, maxValueBytes: 100, }, text: { maxLength: 2000, encoding: "characters", markdownDialect: "discord-markdown", }, }, }, deliveryCapabilities: { pin: true, }, renderPresentation({ payload, presentation, ctx }) { return renderNativePayload(payload, presentation, ctx); }, async pinDeliveredMessage({ target, messageId, pin }) { await pinNativeMessage(target, messageId, { notify: pin.notify === true }); },};能力布林值描述轉譯器能讓哪些項目具備互動功能。選用的
limits 描述核心在呼叫轉譯器前可調整的通用封套:
type ChannelPresentationCapabilities = { supported?: boolean; buttons?: boolean; selects?: boolean; context?: boolean; divider?: boolean; charts?: boolean; tables?: boolean; limits?: { actions?: { maxActions?: number; maxActionsPerRow?: number; maxRows?: number; maxLabelLength?: number; maxValueBytes?: number; supportsStyles?: boolean; supportsDisabled?: boolean; supportsLayoutHints?: boolean; }; selects?: { maxOptions?: number; maxLabelLength?: number; maxValueBytes?: number; }; text?: { maxLength?: number; encoding?: "characters" | "utf8-bytes" | "utf16-units"; markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown"; supportsEdit?: boolean; }; };};核心會在轉譯前對語意控制項套用通用限制。轉譯器仍負責最終的供應商特定驗證與截斷,包括原生區塊數量、卡片大小、URL 限制,以及無法在通用合約中表達的供應商特殊行為。如果限制移除了區塊中的所有控制項,核心會將標籤保留為非互動式情境文字,使傳送的訊息仍有可見的備援內容。
核心轉譯流程
在命令列介面與標準訊息動作所使用的標準輸出路徑上,核心會:
- 正規化呈現內容承載。
- 解析目標頻道的輸出配接器。
- 讀取
presentationCapabilities。 - 當配接器宣告相關限制時,套用動作數量、標籤長度與選取選項數量等通用能力限制。除非配接器分別明確宣告
charts: true或tables: true,否則圖表與表格區塊會轉為具確定性的文字。 - 當配接器能轉譯內容承載時,呼叫
renderPresentation。 - 當配接器不存在或無法轉譯時,退回保守的文字格式。
- 透過一般頻道傳送路徑傳送產生的內容承載。
- 在第一則訊息成功送出後,套用
delivery.pin等傳送中繼資料。
直接使用 ReplyPayload 的頻道本地回覆或預覽匯流流程,必須進入該標準路徑,或在將內容承載投射為純文字/媒體前,具體化相同的呈現備援內容。
核心負責備援行為,讓產生端可維持與頻道無關。頻道外掛負責原生轉譯與互動處理。
降級規則
呈現內容必須能安全地傳送至能力受限的頻道。
備援文字包括:
- 第一行的
title - 以一般段落呈現的
text區塊 - 以精簡情境行呈現的
context區塊 - 以視覺分隔線呈現的
divider區塊 - 按鈕標籤,包括連結按鈕的 URL
- 選取選項標籤
- 圖表標題、類型、座標軸、類別、數列和值
- 表格標題、標頭及每一列的值
按鈕值的備援可見性
當頻道無法轉譯互動式控制項時,按鈕與選取值會退回純文字。此備援行為會維持可用性,同時將不透明的回呼資料保留為私密資訊:
command類型的動作會轉譯為label: `command`,讓使用者能複製命令,並在頻道輸入欄中手動執行。callback類型的動作與舊版value欄位只會轉譯標籤。不透明的回呼值不會公開於備援文字中。approval類型的動作只會轉譯標籤。核准 ID 與決策屬於傳輸資料,不會透過通用純量輔助函式或備援文字公開。url動作、由 URL 支援的web-app動作,以及已棄用的url/webApp/web_app輸入,會在按鈕標籤旁轉譯 URL 文字,因為 URL 是面向使用者的內容。僅限託管小工具的動作,在沒有原生小工具啟動功能的頻道上只會轉譯標籤。- 選取選項只會轉譯標籤。底層選項值不會公開於備援文字中。
在備援 UI 中新增手動命令指引的頻道配接器(例如 Feishu 文件留言指示),必須根據備援轉譯器使用的相同呈現區塊,判斷是否存在命令,使指引文字只在確實顯示手動命令時出現。
不受支援的原生控制項應降級,而不是讓整次傳送失敗。例如:
- 停用行內按鈕的 Telegram 會傳送文字備援內容。
- 不支援選取功能的頻道會以文字列出選取選項。
- 不支援原生圖表的頻道會以文字列出圖表資料。
- 不支援原生表格的頻道會以文字列出每一個表格列。
- 僅含 URL 的按鈕會變成原生連結按鈕或備援 URL 行。
- 選用的釘選失敗不會導致已傳送訊息失敗。
主要例外是 delivery.pin.required: true;如果要求必須釘選,而頻道無法釘選已傳送的訊息,傳送作業會回報失敗。
供應商對應
目前內建的轉譯器:
| 頻道 | 原生轉譯目標 | 備註 |
|---|---|---|
| Discord | 元件與元件容器 | 為現有供應商原生內容承載產生端保留舊版 channelData.discord.components,但新的共用傳送應使用 presentation。 |
| Feishu | 互動式卡片 | 卡片標頭可使用 title;本文會避免重複該標題。 |
| Matrix | 文字備援加結構化事件欄位 | 按鈕/選取功能會宣告為受支援,但目前每個區塊都會轉譯為包含在 com.openclaw.presentation 事件欄位中的 renderMessagePresentationFallbackText 輸出,而非原生互動式小工具。 |
| Mattermost | 文字加互動式屬性 | 不支援選取功能與分隔線;這些區塊會降級為文字。 |
| Microsoft Teams | Adaptive Cards | 同時提供卡片和純 message 文字時,卡片中會包含該文字。不支援選取功能、樣式與停用狀態。 |
| Slack | Block Kit | 將 chart 轉譯為原生 data_visualization,並將 table 轉譯為原生 data_table;保留舊版 channelData.slack.blocks,但新的共用傳送應使用 presentation。 |
| Telegram | 文字加行內鍵盤 | 按鈕/選取功能要求目標介面具備行內按鈕能力;否則會使用文字備援內容。 |
| 純文字頻道 | 文字備援 | 沒有轉譯器的頻道仍會獲得可讀的輸出。 |
供應商原生內容承載相容性是為現有回覆產生端提供的過渡機制。這不構成新增共用原生欄位的理由。
Presentation 與 InteractiveReply
InteractiveReply 是核准與互動輔助函式使用的較舊內部子集。它支援:
- 文字
- 按鈕
- 選取功能
MessagePresentation 是標準的共用傳送合約。它新增:
- 標題
- 語調
- 情境
- 分隔線
- 圖表
- 表格
- 僅含 URL 的按鈕
- 透過
ReplyPayload.delivery提供的通用傳送中繼資料
橋接舊版程式碼時,請使用 openclaw/plugin-sdk/interactive-runtime 中的輔助函式:
adaptMessagePresentationForChannel, applyPresentationActionLimits, hasMessagePresentationBlocks, interactiveReplyToPresentation, isMessagePresentationInteractiveBlock, normalizeMessagePresentation, presentationPageSize, presentationToInteractiveControlsReply, presentationToInteractiveReply, renderMessagePresentationChartFallbackText, renderMessagePresentationFallbackText, renderMessagePresentationTableFallbackText, resolveMessagePresentationActionValue, resolveMessagePresentationButtonAction, resolveMessagePresentationControlValue, resolveMessagePresentationOptionAction,} from "openclaw/plugin-sdk/interactive-runtime";新程式碼應直接接受或產生 MessagePresentation。現有的
interactive 內容承載是 presentation 的已棄用子集;仍保留執行階段支援供舊版產生端使用。
值得了解的未棄用輔助函式:
normalizeMessagePresentation(raw)/hasMessagePresentationBlocks(value)驗證未指定型別的承載資料(例如來自命令列介面--presentation旗標的 JSON)並進行型別強制轉換,以產生MessagePresentation。isMessagePresentationInteractiveBlock(block)將區塊的型別縮限為buttons|select聯集。resolveMessagePresentationButtonAction(button)和resolveMessagePresentationOptionAction(option)會傳回標準型別化動作, 同時接受已淘汰的邊界欄位。明確的action一律優先。resolveMessagePresentationActionValue(action)/resolveMessagePresentationControlValue(control)僅讀取命令/回呼的 純量值。非純量的標準動作絕不會遞降至舊版影子value,因此核准 ID 和連結目標會維持其型別。renderMessagePresentationChartFallbackText(block)/renderMessagePresentationTableFallbackText(block)會將單一結構化 資料區塊呈現為確定性文字,供頻道專屬的後援路徑使用。
舊版 InteractiveReply* 型別和轉換輔助函式在 SDK 中標記為
@deprecated:
InteractiveReply、InteractiveReplyBlock、InteractiveReplyButton和InteractiveReplyOptionnormalizeInteractiveReply(...)hasInteractiveReplyBlocks(...)interactiveReplyToPresentation(...)presentationToInteractiveReply(...)presentationToInteractiveControlsReply(...)resolveInteractiveTextFallback(...)reduceInteractiveReply(...)
presentationToInteractiveReply(...) 和
presentationToInteractiveControlsReply(...) 仍可作為舊版頻道實作的呈現器
橋接。新的產生端程式碼不應呼叫它們;請傳送 presentation,並讓核心/頻道調適機制處理呈現。
核准輔助函式也有以呈現為優先的替代項目:
- 使用
buildApprovalPresentation(...),而非buildApprovalInteractiveReply(...) - 使用
buildExecApprovalPresentation(...),而非buildExecApprovalInteractiveReply(...)
為了維持外掛相容性,這些已發布的建構器仍以命令為基礎。擁有持久核准種類的閘道
和內建頻道程式碼應使用
buildTypedApprovalPresentation(...)、
buildTypedExecApprovalPendingReplyPayload(...) 或
buildTypedPluginApprovalPendingReplyPayload(...),讓傳輸層收到
明確的 approval 動作,而不是從 /approve 文字推斷語意。
對於沒有文字後援的呈現區塊(例如僅含分隔線的
呈現),renderMessagePresentationFallbackText(...) 會傳回空字串。
需要非空白傳送本文的傳輸層可以傳入
emptyFallback,選擇使用最小本文,而不變更預設後援
合約。
傳遞釘選
釘選是傳遞行為,而非呈現。請使用 delivery.pin,而不是
channelData.telegram.pin 等供應商原生欄位。
語意:
pin: true會釘選第一則成功傳遞的訊息。pin.notify預設為false。pin.required預設為false。- 選用的釘選失敗會降級處理,並保留已傳送的訊息。
- 必要的釘選失敗會使傳遞失敗。
- 分塊訊息會釘選第一個已傳遞的區塊,而非末尾區塊。
對於供應商支援這些操作的現有
訊息,手動 pin、unpin 和 pins 訊息動作仍然存在。
外掛作者檢查清單
- 當頻道可呈現語意式呈現內容,或可安全地將其降級時,請從
describeMessageTool(...)宣告presentation。 - 將
presentationCapabilities新增至執行階段輸出配接器。 - 在執行階段程式碼中實作
renderPresentation,而非在控制平面的外掛 設定程式碼中實作。 - 不要讓原生 UI 程式庫進入高頻設定/目錄路徑。
- 已知通用能力限制時,請在
presentationCapabilities.limits上 宣告這些限制。 - 在呈現器和測試中保留最終平台限制。
- 為不支援的圖表、表格、按鈕、選取項、URL
按鈕、標題/文字重複,以及混合
message與presentation傳送新增後援測試。 - 僅當供應商能釘選已傳送的訊息 ID 時,才透過
deliveryCapabilities.pin和pinDeliveredMessage新增傳遞釘選支援。 - 不要透過共用訊息動作結構描述公開新的供應商原生資訊卡/區塊/元件/按鈕 欄位。