跳转至

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-plugintranslate-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: greet-plugin@1.0.0 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: greet-plugin@1.0.0
[loader] loaded: translate-plugin@0.5.0

📦 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

进阶练习

  1. 加版本约束:manifest 加 peerDependencies: { "cc-core": ">=1.0.0" },loader 拒绝不兼容
  2. 加命令去重:merge 时所有命令名加 plugin 名作为前缀(greet:hello / translate:hello
  3. 加签名验证:用 ed25519 验签 plugin.tar.gz,防止恶意 plugin
  4. 加远程 source:支持从 npm registry 下载 cc-plugin-foo 这种包
  5. 加 watch 模式:用 fs.watch 监听目录,manifest 改了自动重载

相关阅读