콘텐츠로 이동

Plugin SDK와 Runtime 리팩토링: 더 안정적인 확장 생태계 만들기

프로젝트가 커지면서 여기저기 직접 import를 하다 보면 나중에 코드를 수정하기가 정말 힘들어지죠. 특히 플러그인을 만들 때 코어 내부 구조를 너무 많이 알게 되면, 코어가 업데이트될 때마다 플러그인이 고장 나기 일쑤입니다.

지금까지는 커넥터마다 코어를 직접 가져다 쓰거나 별도의 브릿지를 만드는 등 방식이 제각각이었어요. 이런 복잡함을 해결하고 누구나 안정적으로 외부 플러그인을 만들 수 있도록, OpenClaw의 아키텍처를 두 개의 레이어로 깔끔하게 정리하려고 합니다.

  • openclaw/plugin-sdk 패키지
  • OpenClaw 코어 환경

모든 메시징 커넥터를 하나의 안정적인 API를 사용하는 플러그인으로 전환하는 것이 목표입니다. 이제 src/**에서 직접 코드를 가져오는 대신, SDK와 Runtime을 거치게 됩니다.

SDK는 컴파일 타임에 사용하며, 타입 정의와 설정 헬퍼를 포함합니다. openclaw/plugin-sdk에서 필요한 기능을 가져오세요.

  • Types: ChannelPlugin, ChannelMeta, ChannelCapabilities 등
  • Config Helpers: buildChannelConfigSchema, deleteAccountFromConfigSection 등
  • Tool Param Helpers: createActionGate, readStringParam, jsonResult 등

실행 시점에 코어의 동작이 필요한 기능은 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;
};
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;
};
};
  • Phase 0 & 1: api.runtime을 도입하고 기존의 개별 core-bridge.ts를 교체합니다.
  • Phase 2 & 3: Matrix, MS Teams 같은 플러그인들을 새로운 SDK와 Runtime으로 옮깁니다.
  • Phase 4 & 5: iMessage를 플러그인화하고, src/** 임포트를 금지하는 CI 규칙을 적용합니다.
  • 업데이트 시 플러그인 작동 중지: 기존 커넥터들이 코어 내부 코드를 직접 참조(direct core import)하고 있기 때문입니다. api.runtime을 사용하도록 전환하면 코어 내부가 바뀌어도 플러그인은 안전하게 유지됩니다.
  • 중복된 브릿지 코드: 각 확장 프로그램마다 별도의 브릿지를 만들면 유지보수가 어렵습니다. Phase 1 단계에서 api.runtime으로 통합하여 중복을 제거하세요.

궁금한 점이 있다면 AI Setup Assistant에게 물어보세요!

  • Plugins: 플러그인 시스템 기초 알아보기
  • Channels: 지원되는 채널 목록 확인하기
  • Configuration: 게이트웨이 설정 가이드
OpenClaw

OpenClaw Expert

아직 막혀 있나요?

이 문서에서 답을 못 찾았다면 OpenClaw Expert에게 바로 물어보세요.