跳到內容

Plugin SDK + Runtime 重構計畫:打造更穩定的插件架構

每次更新核心代碼,最怕的就是插件跟著一起掛掉。特別是當插件直接引用核心內部的代碼(src/**)時,任何微小的改動都可能引發連鎖反應,讓你的開發工作陷入無止盡的修復循環。

這種緊密耦合不僅讓升級變得脆弱,也讓開發外部插件變得很困難。為了徹底解決這個問題,我們正在推動一套全新的重構計畫,讓每個 messaging connector 都成為基於穩定 API 的插件。

  • openclaw/plugin-sdk 庫
  • OpenClawPluginApi.runtime 存取權限

如果你準備開始將現有的 connector 遷移到新的架構,可以參考這 4 個步驟:

  1. 更換依賴來源:停止從 src/** 直接 import 任何內容。
  2. 接入 SDK:使用 openclaw/plugin-sdk 提供的 ChannelPlugin 類型與配置工具(如 buildChannelConfigSchema)。
  3. 改用 Runtime 呼叫:將原本直接調用的核心邏輯,改為透過 api.runtime 呼叫(例如 api.runtime.channel.text.chunkMarkdownText)。
  4. 聲明版本範圍:在插件配置中註明你需要的 openclawRuntime 版本範圍,例如 ">=2026.2.0"。

我們將架構拆分為兩個層級,確保插件與核心之間的邊界清晰:

這是一個穩定且可發布的包,主要包含類型定義、Helper 函數與配置工具。它不包含任何運行時狀態。

  • 類型: ChannelPlugin, ChannelMeta, ChannelCapabilities
  • 配置輔助: setAccountEnabledInConfigSection, deleteAccountFromConfigSection
  • 引導與工具: promptChannelAccessConfig, readStringParam, formatDocsLink

插件透過 OpenClawPluginApi.runtime 存取核心行為,這也是插件觸碰核心邏輯的唯一途徑。

export type PluginRuntime = {
channel: {
text: {
chunkMarkdownText(text: string, limit: number): string[];
resolveTextChunkLimit(cfg: OpenClawConfig, channel: string, accountId?: string): number;
hasControlCommand(text: string, cfg: OpenClawConfig): boolean;
};
reply: {
dispatchReplyWithBufferedBlockDispatcher(params: {
ctx: unknown;
cfg: unknown;
dispatcherOptions: {
deliver: (payload: {
text?: string;
mediaUrls?: string[];
mediaUrl?: string;
}) => void | Promise<void>;
onError?: (err: unknown, info: { kind: string }) => void;
};
}): Promise<void>;
createReplyDispatcherWithTyping?: unknown; // adapter for Teams-style flows
};
routing: {
resolveAgentRoute(params: {
cfg: unknown;
channel: string;
accountId: string;
peer: { kind: RoutePeerKind; id: string };
}): { sessionKey: string; accountId: string };
};
pairing: {
buildPairingReply(params: { channel: string; idLine: string; code: string }): string;
readAllowFromStore(channel: string): Promise<string[]>;
upsertPairingRequest(params: {
channel: string;
id: string;
meta?: { name?: string };
}): Promise<{ code: string; created: boolean }>;
};
media: {
fetchRemoteMedia(params: { url: string }): Promise<{ buffer: Buffer; contentType?: string }>;
saveMediaBuffer(
buffer: Uint8Array,
contentType: string | undefined,
direction: "inbound" | "outbound",
maxBytes: number,
): Promise<{ path: string; contentType?: string }>;
};
mentions: {
buildMentionRegexes(cfg: OpenClawConfig, agentId?: string): RegExp[];
matchesMentionPatterns(text: string, regexes: RegExp[]): boolean;
};
groups: {
resolveGroupPolicy(
cfg: OpenClawConfig,
channel: string,
accountId: string,
groupId: string,
): {
allowlistEnabled: boolean;
allowed: boolean;
groupConfig?: unknown;
defaultConfig?: unknown;
};
resolveRequireMention(
cfg: OpenClawConfig,
channel: string,
accountId: string,
groupId: string,
override?: boolean,
): boolean;
};
debounce: {
createInboundDebouncer<T>(opts: {
debounceMs: number;
buildKey: (v: T) => string | null;
shouldDebounce: (v: T) => boolean;
onFlush: (entries: T[]) => Promise<void>;
onError?: (err: unknown) => void;
}): { push: (v: T) => void; flush: () => Promise<void> };
resolveInboundDebounceMs(cfg: OpenClawConfig, channel: string): number;
};
commands: {
resolveCommandAuthorizedFromAuthorizers(params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
}): boolean;
};
};
logging: {
shouldLogVerbose(): boolean;
getChildLogger(name: string): PluginLogger;
};
state: {
resolveStateDir(cfg: OpenClawConfig): string;
};
};

如果你在遷移過程中遇到問題,請參考以下常見情境:

  • 無法引用核心內部的 Helper:請檢查該 Helper 是否已遷移至 openclaw/plugin-sdk。如果沒有,代表該功能應透過 api.runtime 存取。
  • 核心更新後插件失效:請檢查你的 openclawRuntime 版本聲明,確保它涵蓋了當前核心的版本號。

我們預期在重構完成後達成以下目標:

  • 所有 channel connector 均透過 SDK + Runtime 運作。
  • 禁止從 src/** 導入任何 extensions/** 的代碼。
  • 新的連接器模板僅依賴 SDK 與 Runtime。
  • 外部開發者無需存取核心原始碼即可開發與更新插件。

如果你在設定過程中遇到困難,可以隨時諮詢 AI Setup Assistant。

OpenClaw

OpenClaw Expert

還是卡住了?

如果這篇文件沒有解決你的情境,直接問 OpenClaw Expert,拿到可執行步驟。