v0.5 架构演进:为什么我们需要插件机制

在开源 AI CLI 工具迭代到 v0.4 的时候,核心代码库的体积已经开始失控。
最初做这个命令行工具时,目标非常单纯:在终端里通过简单的指令,调用大语言模型完成代码补全、文档生成与日常技术问答。代码结构一目了然,一个调度器模块、一个流式解析器、外加终端输出格式化。
但随着项目在社区中获得更多关注,GitHub Issue 里开始涌入各种各样的功能诉求:有人希望在每次请求后把 Token 消耗统计推送到飞书或钉钉群;有人希望在输出 Markdown 时支持特定语言的语法高亮和折叠;有人希望在生成 Git Commit 时自动读取当前的 Husky 配置;还有人希望针对不同云厂商的模型做私有鉴权和代理转发。
如果把这些长尾需求全部塞进核心仓库,不出三个版本,整个项目就会变成一个无法维护的臃肿怪物:外部依赖项暴增、测试用例难以覆盖、安装包体积翻倍,更致命的是,核心调度逻辑会被大量的 if-else 条件分支割裂得支离破碎。
在 v0.5 版本中,我做了一个决定:停下所有业务功能的迭代,重构核心架构,推行“微内核 + 插件机制”。
核心包的职责收敛
重构的第一步不是写插件系统,而是给核心包画定严格的边界。
内核只做三件事:
所有与具体业务、三方集成、特定平台格式化相关的逻辑,全部从核心代码中剥离,降级为内置插件或外部扩展插件。
这样一来,核心仓库的生产依赖项直接从 14 个骤降到 3 个,冷启动时间从 180 毫秒缩短到 45 毫秒,代码结构也回归到最初的清爽状态。
极简插件接口设计
在设计插件接口时,我坚决放弃了复杂的服务发现和 RPC 方案。对于一个基于 Node.js/TypeScript 构建的 CLI 工具来说,最轻量、最直观的方案就是基于生命周期钩子的事件机制。
一个合法的插件只需要导出一个满足标准的接口对象:
export interface PluginContext {
cwd: string;
config: Record<string, unknown>;
logger: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
}
export interface RequestPayload {
prompt: string;
model: string;
temperature?: number;
messages: Array<{ role: string; content: string }>;
}
export interface ResponseResult {
content: string;
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
export interface CliPlugin {
name: string;
version: string;
setup?: (ctx: PluginContext) => Promise<void> | void;
beforeRequest?: (payload: RequestPayload) => Promise<RequestPayload | void> | RequestPayload | void;
onStreamChunk?: (chunk: string) => Promise<string | void> | string | void;
afterResponse?: (result: ResponseResult) => Promise<void> | void;
cleanup?: () => Promise<void> | void;
}
每个钩子承担明确的单一职责:
- beforeRequest:允许插件在请求发送给模型前修改 Payload,比如注入团队统一的 System Prompt、过滤敏感词、或动态切换模型。
- onStreamChunk:在模型流式返回每一个 Token 片段时触发,用于实时渲染、自定义进度条或者动态敏感字符替换。
- afterResponse:在请求彻底结束并拿到完整响应后触发,用于记录本地审计日志、上报监控指标或发送即时通知。
插件加载器的安全与容错
插件是第三方代码,运行在主进程中,必须保证任何插件的报错都不会导致主命令行崩溃退出。
加载器在执行钩子时,使用简单的顺序执行器包装,并给每个异步调用设置超时和异常兜底:
export class PluginManager {
private plugins: CliPlugin[] = [];
public register(plugin: CliPlugin): void {
if (!plugin.name) {
throw new Error("Plugin must provide a valid name.");
}
this.plugins.push(plugin);
}
public async executeBeforeRequest(payload: RequestPayload): Promise<RequestPayload> {
let currentPayload = { …payload };
for (const plugin of this.plugins) {
if (typeof plugin.beforeRequest === "function") {
try {
const modified = await plugin.beforeRequest(currentPayload);
if (modified && typeof modified === "object") {
currentPayload = modified;
}
} catch (err) {
console.warn(`[Plugin Error] "${plugin.name}" beforeRequest failed:`, err);
}
}
}
return currentPayload;
}
public async executeAfterResponse(result: ResponseResult): Promise<void> {
for (const plugin of this.plugins) {
if (typeof plugin.afterResponse === "function") {
try {
await plugin.afterResponse(result);
} catch (err) {
console.warn(`[Plugin Error] "${plugin.name}" afterResponse failed:`, err);
}
}
}
}
}
对于用户本地配置的插件,支持两种引入方式:
在 CLI 启动时,解析用户目录下的配置文件(如 .aicliconfig.json),动态通过 import() 导入插件实例并注册到 PluginManager 中。
架构演进带来的真实收益
微内核改造完成后,我们在社区治理和开发效率上获得了非常清晰的收益:
做开源工具不是功能堆得越多越好。当核心业务逻辑稳定后,尽早把扩展点交出去,用清晰克制的接口构建生态,才是让项目长期存活下去的正确路径。
