插件 SDK 重构
插件 SDK + 运行时重构计划
Section titled “插件 SDK + 运行时重构计划”目标:每个消息连接器都是一个插件(内置或外部),使用统一稳定的 API。
插件不直接从 src/** 导入任何内容。所有依赖项均通过 SDK 或运行时获取。
为什么现在做
Section titled “为什么现在做”- 当前连接器混用多种模式:直接导入核心模块、仅 dist 的桥接方式以及自定义辅助函数。
- 这使得升级变得脆弱,并阻碍了干净的外部插件接口。
目标架构(两层)
Section titled “目标架构(两层)”1)插件 SDK(编译时,稳定,可发布)
Section titled “1)插件 SDK(编译时,稳定,可发布)”范围:类型、辅助函数和配置工具。无运行时状态,无副作用。
内容(示例):
- 类型:
ChannelPlugin、适配器、ChannelMeta、ChannelCapabilities、ChannelDirectoryEntry。 - 配置辅助函数:
buildChannelConfigSchema、setAccountEnabledInConfigSection、deleteAccountFromConfigSection、applyAccountNameToChannelSection。 - 配对辅助函数:
PAIRING_APPROVED_MESSAGE、formatPairingApproveHint。 - 新手引导辅助函数:
promptChannelAccessConfig、addWildcardAllowFrom、新手引导类型。 - 工具参数辅助函数:
createActionGate、readStringParam、readNumberParam、readReactionParams、jsonResult。 - 文档链接辅助函数:
formatDocsLink。
交付方式:
- 以
coderclaw/plugin-sdk发布(或从核心以coderclaw/plugin-sdk导出)。 - 使用语义化版本控制,提供明确的稳定性保证。
2)插件运行时(执行层,注入式)
Section titled “2)插件运行时(执行层,注入式)”范围:所有涉及核心运行时行为的内容。
通过 CoderClawPluginApi.runtime 访问,确保插件永远不会导入 src/**。
建议的接口(最小但完整):
export type PluginRuntime = { channel: { text: { chunkMarkdownText(text: string, limit: number): string[]; resolveTextChunkLimit(cfg: CoderClawConfig, channel: string, accountId?: string): number; hasControlCommand(text: string, cfg: CoderClawConfig): 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: CoderClawConfig, agentId?: string): RegExp[]; matchesMentionPatterns(text: string, regexes: RegExp[]): boolean; }; groups: { resolveGroupPolicy( cfg: CoderClawConfig, channel: string, accountId: string, groupId: string, ): { allowlistEnabled: boolean; allowed: boolean; groupConfig?: unknown; defaultConfig?: unknown; }; resolveRequireMention( cfg: CoderClawConfig, 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: CoderClawConfig, 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: CoderClawConfig): string; };};备注:
- 运行时是访问核心行为的唯一方式。
- SDK 故意保持小巧和稳定。
- 每个运行时方法都映射到现有的核心实现(无重复代码)。
迁移计划(分阶段,安全)
Section titled “迁移计划(分阶段,安全)”阶段 0:基础搭建
Section titled “阶段 0:基础搭建”- 引入
coderclaw/plugin-sdk。 - 在
CoderClawPluginApi中添加带有上述接口的api.runtime。 - 在过渡期内保留现有导入方式(添加弃用警告)。
阶段 1:桥接清理(低风险)
Section titled “阶段 1:桥接清理(低风险)”- 用
api.runtime替换每个扩展中的core-bridge.ts。 - 优先迁移 BlueBubbles、Zalo、Zalo Personal(已经接近完成)。
- 移除重复的桥接代码。
阶段 2:轻度直接导入的插件
Section titled “阶段 2:轻度直接导入的插件”- 将 Matrix 迁移到 SDK + 运行时。
- 验证新手引导、目录、群组提及逻辑。
阶段 3:重度直接导入的插件
Section titled “阶段 3:重度直接导入的插件”- 迁移 Microsoft Teams(使用运行时辅助函数最多的插件)。
- 确保回复/正在输入的语义与当前行为一致。
阶段 4:iMessage 插件化
Section titled “阶段 4:iMessage 插件化”- 将 iMessage 移入
extensions/imessage。 - 用
api.runtime替换直接的核心调用。 - 保持配置键、CLI 行为和文档不变。
阶段 5:强制执行
Section titled “阶段 5:强制执行”- 添加 lint 规则 / CI 检查:禁止
extensions/**从src/**导入。 - 添加插件 SDK/版本兼容性检查(运行时 + SDK 语义化版本)。
兼容性与版本控制
Section titled “兼容性与版本控制”- SDK:语义化版本控制,已发布,变更有文档记录。
- 运行时:按核心版本进行版本控制。添加
api.runtime.version。 - 插件声明所需的运行时版本范围(例如
coderclawRuntime: ">=2026.2.0")。
- 适配器级单元测试(使用真实核心实现验证运行时函数)。
- 每个插件的黄金测试:确保行为无偏差(路由、配对、允许列表、提及过滤)。
- CI 中使用单个端到端插件示例(安装 + 运行 + 冒烟测试)。
- SDK 类型托管在哪里:独立包还是核心导出?
- 运行时类型分发:在 SDK 中(仅类型)还是在核心中?
- 如何为内置插件与外部插件暴露文档链接?
- 过渡期间是否允许仓库内插件有限地直接导入核心模块?
- 所有渠道连接器都是使用 SDK + 运行时的插件。
extensions/**不再从src/**导入。- 新连接器模板仅依赖 SDK + 运行时。
- 外部插件可以在无需访问核心源码的情况下进行开发和更新。