Mini Plugin Loader Demo¶
从 3302 行的
src/utils/plugins/pluginLoader.ts提炼到 ~250 行的可运行 plugin loader。 展示 路径安全 + 缓存 + 多源合并 三大可扩展性核心。
这是什么?¶
CC 的 pluginLoader.ts 负责加载所有 plugin、合并 settings、保证安全。真实代码 3302 行,本 demo 用 ~250 行 实现核心 4 件套,让你:
- 手写一个 plugin manifest(plugin.json),被 loader 加载
- 看 1 级缓存如何工作(真实 4 级)
- 看 path traversal 攻击如何被拦截
- 看多插件 settings 合并如何处理冲突
- 写自己的第一个 CC plugin(最小可行版)
文件结构¶
mini-plugin/
├── src/
│ ├── loader.ts ← PluginLoader 核心(~250 行)
│ └── cli.ts ← CLI 入口(3 模式:单加载 / 扫描 / 安全测试)
├── examples/
│ ├── greet-plugin/
│ │ └── plugin.json ← 示例 plugin 1(hello / goodbye 命令)
│ └── translate-plugin/
│ └── plugin.json ← 示例 plugin 2(hello / thanks,演示命令冲突)
├── package.json
├── tsconfig.json
└── README.md
怎么跑?¶
# 1. 装依赖
npm install
# 2. 编译
npm run build
# 3. 模式 1:加载单个插件
node dist/cli.js examples/greet-plugin
# 4. 模式 2:扫描目录找所有插件
node dist/cli.js --discover examples
# 5. 模式 3:安全测试(演示 path traversal 拦截)
npm test
真实代码对照表¶
| Demo 文件 | 真实文件 | 行数对比 | 简化了啥 |
|---|---|---|---|
src/loader.ts |
src/utils/plugins/pluginLoader.ts |
250 vs 3302 | 4 级缓存 → 1 级;6 种 source → 1 种(local);JS manifest → JSON |
examples/*/plugin.json |
~/.claude/plugins/*/plugin.json |
~15 vs ~50 | 简化 schema(commands 用 template 字符串,不用 JS handler) |
validatePath() |
validatePluginPaths() 30 行 |
12 vs 30 | 去掉 realpath / 符号链接 / 跨平台处理 |
核心设计 4 件套¶
1️⃣ 路径安全 = allowlist 校验¶
export function validatePath(target: string, allowedRoots: string[]): string {
const resolved = path.resolve(target); // 解析 ../ 和符号链接
const isAllowed = allowedRoots.some((root) => {
const rootResolved = path.resolve(root);
return resolved === rootResolved || resolved.startsWith(rootResolved + path.sep);
});
if (!isAllowed) throw new SecurityError(...);
return resolved;
}
为什么重要:
- 防止 ../../etc/passwd 攻击
- 防止用户加载 /root/.ssh/id_rsa 之类的敏感路径
- 真实 pluginLoader 用 30 行(realpath + 符号链接 + 跨平台),本 demo 12 行
测试:运行 npm test 看到 4 个攻击场景(normal / traversal / /etc/ / 符号链接)都被拦截。
2️⃣ 缓存 = Map¶
private cache = new Map<string, Plugin>();
async loadPlugin(pluginDir: string): Promise<Plugin> {
const safePath = validatePath(pluginDir, this.opts.allowedRoots);
if (this.cache.has(safePath)) return this.cache.get(safePath)!; // 命中
// ... 读 manifest + 解析 + 验证
this.cache.set(safePath, plugin); // 缓存
return plugin;
}
真实 pluginLoader 是 4 级缓存:
1. 内存 Map(最快)
2. 磁盘 ~/.claude/plugins/cache/<hash>.json(跨进程)
3. 远程 registry 缓存
4. 重新加载
为什么 4 级:CC 是商业产品,要支持 100+ 插件且不能每次启动都重读磁盘。
3️⃣ 跨插件合并 + 冲突检测¶
mergeSettings(plugins: Plugin[]): MergedSettings {
const commands: Record<string, PluginCommand> = {};
for (const plugin of plugins) {
for (const cmd of plugin.manifest.commands ?? []) {
if (commands[cmd.name]) {
console.warn(`[loader] command conflict: '${cmd.name}' in ${plugin.manifest.name} overrides ...`);
}
commands[cmd.name] = cmd; // 后加载的赢
}
}
// ...
}
例子:greet-plugin 和 translate-plugin 都有 hello 命令 → 后加载的赢 + 警告。
真实 pluginLoader:除覆盖外还支持 priority 字段、disabled: true、dependency 解析等。
4️⃣ 递归扫描 + 过滤¶
async discoverPlugins(rootDir, recursive = true) {
await this.walkDir(rootDir, async (dir) => {
if (existsSync(`${dir}/plugin.json`)) {
await this.loadPlugin(dir);
}
}, recursive);
}
过滤规则:
- 跳过 . 开头的目录(.git / .cache)
- 跳过 node_modules
- 只找含 plugin.json 的目录
测试场景¶
模式 1: 单加载¶
$ node dist/cli.js examples/greet-plugin
[loader] loaded: [email protected] from .../examples/greet-plugin
📦 Plugin loaded:
{
"manifest": {
"name": "greet-plugin",
"version": "1.0.0",
"description": "A simple greeting plugin...",
"commands": [...]
}
}
模式 2: 扫描 + 合并¶
$ node dist/cli.js --discover examples
[loader] loaded: [email protected]
[loader] loaded: [email protected]
📦 Found 2 plugins
🔀 Merged settings:
commands: hello, goodbye, thanks
hooks: UserPromptSubmit
metadata: [{"name":"greet-plugin",...},{"name":"translate-plugin",...}]
模式 3: 安全测试¶
$ npm test
🛡️ Security tests:
正常路径 ✅ loaded
Path traversal ✅ BLOCKED (Path '...' is not within...)
绝对路径 (etc) ✅ BLOCKED
符号链接逃逸 ✅ BLOCKED
进阶练习¶
- 加版本约束:manifest 加
peerDependencies: { "cc-core": ">=1.0.0" },loader 拒绝不兼容 - 加命令去重:merge 时所有命令名加 plugin 名作为前缀(
greet:hello/translate:hello) - 加签名验证:用 ed25519 验签 plugin.tar.gz,防止恶意 plugin
- 加远程 source:支持从 npm registry 下载
cc-plugin-foo这种包 - 加 watch 模式:用
fs.watch监听目录,manifest 改了自动重载
相关阅读¶
- topics/deep-dive-plugin-loader.md —— 3302 行完整分析
- topics/deep-dive-marketplace.md —— Plugin 市场
- tutorials/build-plugin.md —— 手把手教程
- docs/PLUGIN_GUIDE.md —— 官方 plugin 规范