61. 扩展、技能与资源发现¶
这个模块干什么¶
这套系统把“发现文件”和“让文件参与运行”拆成两层:DefaultResourceLoader 收集扩展、skills、prompts、themes、context files,ExtensionRunner 再把扩展的注册结果绑定为活的命令、工具、provider、快捷键和事件处理器。packages/coding-agent/src/core/resource-loader.ts:333-493 packages/coding-agent/src/core/extensions/runner.ts:311-408
扩展是 TypeScript 模块的 factory;它拿到 ExtensionAPI,通过注册方法声明能力,并在指定 hook 到达时获得受控的 context。packages/coding-agent/src/core/extensions/types.ts:1179-1465 packages/coding-agent/src/core/extensions/runner.ts:788-1221
skills 和 prompt templates 不必写代码:loader 发现它们,skill 的元数据进入 system prompt,交互界面又把它们变成 /skill:name 与普通 /name 命令。packages/coding-agent/src/core/skills.ts:168-361 packages/coding-agent/src/core/slash-commands.ts:19-42
内建 llama.cpp 是最有代表性的内建扩展:它隐藏在启动列表中,却照样注册 provider 和 /llama 命令,并只在 TUI 模式运行其界面流程。packages/coding-agent/src/extensions/index.ts:1-4 packages/coding-agent/src/extensions/llama/index.ts:42-220
项目资源不是默认可信;发现顺序、包资源和项目目录会受到 trust gate 约束。packages/coding-agent/src/core/package-manager.ts:901-963 packages/coding-agent/src/core/package-manager.ts:2303-2467
核心文件速查¶
| 文件 | 一句话职责 | 必读优先级 |
|---|---|---|
packages/coding-agent/src/core/extensions/types.ts |
定义 ExtensionAPI、事件/上下文、已加载扩展和所有注册合同。packages/coding-agent/src/core/extensions/types.ts:1179-1465 packages/coding-agent/src/core/extensions/types.ts:1488-1683 |
⭐⭐⭐ |
packages/coding-agent/src/core/extensions/loader.ts |
查找并执行 extension module/factory,处理加载期 provider 注册与缓存。packages/coding-agent/src/core/extensions/loader.ts:170-392 packages/coding-agent/src/core/extensions/loader.ts:546-721 |
⭐⭐⭐ |
packages/coding-agent/src/core/extensions/runner.ts |
绑定真实 actions、执行 hooks、处理命令/快捷键冲突并使旧 context 失效。packages/coding-agent/src/core/extensions/runner.ts:311-408 packages/coding-agent/src/core/extensions/runner.ts:490-776 packages/coding-agent/src/core/extensions/runner.ts:788-1221 |
⭐⭐⭐ |
packages/coding-agent/src/core/resource-loader.ts |
编排启动与 reload:trust、settings、packages、扩展、skills、prompts、themes、context 和 system prompt。packages/coding-agent/src/core/resource-loader.ts:333-493 |
⭐⭐⭐ |
packages/coding-agent/src/core/package-manager.ts |
决定包 manifest/惯例目录如何变成资源路径、优先级、去重和 trust-gated 项目资源。packages/coding-agent/src/core/package-manager.ts:347-460 packages/coding-agent/src/core/package-manager.ts:2230-2467 |
⭐⭐⭐ |
packages/coding-agent/src/core/skills.ts |
找 SKILL.md、验证 frontmatter、加载内容并生成 system-prompt XML。packages/coding-agent/src/core/skills.ts:168-361 |
⭐⭐⭐ |
packages/coding-agent/src/core/slash-commands.ts |
集中定义内建 slash command 的名称、描述和参数提示。packages/coding-agent/src/core/slash-commands.ts:19-42 |
⭐⭐ |
packages/coding-agent/src/extensions/index.ts |
把隐藏的 llama.cpp factory 放入 builtInExtensions。packages/coding-agent/src/extensions/index.ts:1-4 |
⭐⭐ |
packages/coding-agent/src/extensions/llama/index.ts |
用真实内建功能演示 provider、command、TUI UI 和生命周期检查的组合。packages/coding-agent/src/extensions/llama/index.ts:42-220 |
⭐⭐⭐ |
packages/coding-agent/src/extensions/llama/provider.ts |
负责 llama.cpp provider 注册、服务端模型 catalog 刷新和网络条件判断。packages/coding-agent/src/extensions/llama/provider.ts:58-134 |
⭐⭐ |
精读¶
1. extensions/types.ts:扩展拿到的不是内部对象,而是一份合同¶
ExtensionAPI 集中定义 extension factory 可注册的能力,包括 tool、command、shortcut、flag、renderer 和 provider。packages/coding-agent/src/core/extensions/types.ts:1179-1465
因此扩展的简化入口是 export default function (pi: ExtensionAPI): void | Promise<void>;真正的加载器接受同步或异步 factory。packages/coding-agent/src/core/extensions/types.ts:1488-1683 packages/coding-agent/src/core/extensions/loader.ts:546-721
registerTool(...) 把自定义工具描述加入扩展运行时,registerCommand(...) 声明 slash 命令,registerShortcut(...) 声明按键处理器。packages/coding-agent/src/core/extensions/types.ts:1179-1465
registerFlag(...) 解释了为什么 CLI parser 会保存未知长选项:扩展在加载后可把它们定义成自己的 boolean 或 string flag。packages/coding-agent/src/core/extensions/types.ts:1179-1465 packages/coding-agent/src/cli/args.ts:188-201
registerProvider(...) 与 unregisterProvider(...) 属于同一 API 面,允许 extension 把 auth/model/provider 配置带入共享 model runtime。packages/coding-agent/src/core/extensions/types.ts:1342-1405
扩展还可注册 message/entry renderer、autocomplete wrapper、editor factory、header/footer/widget 等 UI 能力;这些能力是否可用由运行模式给出的 ExtensionUIContext 决定。packages/coding-agent/src/core/extensions/types.ts:1179-1465 packages/coding-agent/src/core/extensions/types.ts:1488-1683
事件是显式名字的 union,而不是任意字符串;runner 的实现覆盖 context、provider 请求/headers、agent 开始、资源发现、input、tool、message、project trust 和会话前置事件。packages/coding-agent/src/core/extensions/runner.ts:788-1221
需要改写或阻止行为的 hook 会返回该事件合同允许的结果;例如 before_provider_request、before_provider_headers、before_agent_start、input 与 tool hooks 都由 runner 单独处理。packages/coding-agent/src/core/extensions/runner.ts:788-1221
不要把 ExtensionContext 缓存到以后无条件使用:runner 在 reload 或 session 替换后会标记旧 context 为 stale。packages/coding-agent/src/core/extensions/runner.ts:539-546 packages/coding-agent/src/core/extensions/runner.ts:665-776
这个限制的意义是扩展必须在每次 handler 调用获得的上下文中读取 session、model、cwd 和 UI,而不是持有已过期的运行时引用。packages/coding-agent/src/core/extensions/runner.ts:539-546 packages/coding-agent/src/core/extensions/runner.ts:665-776
2. extensions/loader.ts:先运行 factory,但先不给它完整 runtime¶
loader 的职责是把 extension 路径或内联 factory 变成已加载扩展,并缓存以 cwd 与 generation 区分的结果。packages/coding-agent/src/core/extensions/loader.ts:546-721
它支持 package manifest 的 pi.extensions、目录入口 index.ts/index.js、直接文件,以及项目/用户的常规 extensions 目录。packages/coding-agent/src/core/extensions/loader.ts:546-721
factory 执行时得到的 runtime actions 不是最终 session actions;loader 先建立会抛错的占位 action,防止加载期错误地调用尚未绑定的会话能力。packages/coding-agent/src/core/extensions/loader.ts:170-223
加载期的 registerProvider() 是特殊例外:它先进入队列,不需要等待完整 runner。packages/coding-agent/src/core/extensions/loader.ts:230-392 packages/coding-agent/src/core/extensions/types.ts:1342-1405
这样异步 factory 可以先抓取或准备远程模型列表再注册 provider,但不能把“加载期”误当成“已经可执行 command/tool”的阶段。packages/coding-agent/src/core/extensions/loader.ts:170-392
模块加载或 factory 执行失败会作为加载结果的错误保存,而非让所有其它资源发现立刻中止。packages/coding-agent/src/core/extensions/loader.ts:546-721
启动入口会把这类 extension errors 转成 runtime diagnostics,并在失败消息中提示用 -ne 禁用发现后重试。packages/coding-agent/src/main.ts:679-687 packages/coding-agent/src/main.ts:790-797
3. extensions/runner.ts:把注册表接到活的运行时¶
ExtensionRunner 在绑定阶段接收真实 actions,再把 loader 阶段缓存的 provider 注册逐一 flush 到实际 model runtime。packages/coding-agent/src/core/extensions/runner.ts:311-408
provider 队列清空后,后续 registerProvider() / unregisterProvider() 会直接作用于已绑定 runtime,不再停留在 loader 的临时队列。packages/coding-agent/src/core/extensions/runner.ts:311-408 packages/coding-agent/src/core/extensions/types.ts:1342-1405
这就是“两阶段”的边界:loader 负责把声明收集起来,runner 负责让声明能操作当前 session 和 provider 系统。packages/coding-agent/src/core/extensions/loader.ts:170-392 packages/coding-agent/src/core/extensions/runner.ts:311-408
runner 为事件调用创建受控 context,具体 handler 通过 on(event, handler) 预先登记,运行时按事件种类执行对应的 handler 列表。packages/coding-agent/src/core/extensions/types.ts:1488-1683 packages/coding-agent/src/core/extensions/runner.ts:788-1221
context hook 用来调整上下文,before_provider_request/before_provider_headers 在请求前处理 provider 输入,before_agent_start 在 agent 开始前运行。packages/coding-agent/src/core/extensions/runner.ts:788-1221
resources_discover 和 project_trust 分别参与资源发现与信任决策;后者在 project trust 流程里先于保存的决定与默认策略。packages/coding-agent/src/core/extensions/runner.ts:190-230 packages/coding-agent/src/core/extensions/runner.ts:788-1221
input、tool_call、tool_result、message_end 等 hook 对应用户输入、工具执行和消息结束的可插入点。packages/coding-agent/src/core/extensions/runner.ts:788-1221
会话切换、fork、tree、compact 前的 hook 也由同一 runner 执行;它们使用当前的 context,而不是让 extension 自己监听内部对象。packages/coding-agent/src/core/extensions/runner.ts:788-1221
两个扩展注册同名命令时,runner 会把冲突命令重命名为 name:1、name:2 等 invocation name,而非静默覆盖。packages/coding-agent/src/core/extensions/runner.ts:595-629
快捷键也不是“后注册者覆盖前注册者”:runner 检查冲突,并保留编辑器全局按键不让 extension 抢占。packages/coding-agent/src/core/extensions/runner.ts:490-532
构造 command context 时使用 property descriptor 保留 lazy getter,避免创建时把 model、cwd 等动态值过早拍成快照。packages/coding-agent/src/core/extensions/runner.ts:665-776
这也是阅读 extension API 时应优先看 runner 而不是只看 types 的原因:types 说明“能注册什么”,runner 才说明“何时生效、冲突怎么办、旧引用何时失效”。packages/coding-agent/src/core/extensions/types.ts:1179-1683 packages/coding-agent/src/core/extensions/runner.ts:311-408 packages/coding-agent/src/core/extensions/runner.ts:490-776
4. resource-loader.ts:所有可发现资源共用一次 reload¶
DefaultResourceLoader 的启动/reload 流程包含可选 pre-trust pass、settings reload、package 解析、CLI 临时 extension,以及 skills/prompts/themes/context/system prompt 的重新构建。packages/coding-agent/src/core/resource-loader.ts:333-493
它在 reload 时清空 extension cache,但保留当前 trust 语义,使“改资源后重载”不会悄悄改变项目是否可信。packages/coding-agent/src/core/resource-loader.ts:341-493
资源的禁用开关在 loader 入口统一生效:noExtensions、noSkills、noPromptTemplates、noThemes、noContextFiles 都来自同一条 options 通道。packages/coding-agent/src/main.ts:633-677 packages/coding-agent/src/core/resource-loader.ts:333-493
CLI 的 -e、--skill、--prompt-template、--theme 先被 main() 按 cwd 解析为附加路径,再交给 resource loader;它们不是直接绕过 loader 的另一套机制。packages/coding-agent/src/main.ts:611-677
内联 factory 也由 resource loader 载入,因此内建能力和 SDK 传入的 extension 能走与文件扩展同一套 registry/runner。packages/coding-agent/src/core/resource-loader.ts:892-914
具名 inline entry 可以标记 hidden,启动资源列表可据此不显示它;hidden 不表示未加载。packages/coding-agent/src/core/resource-loader.ts:892-914 packages/coding-agent/src/modes/interactive/interactive-mode.ts:1460-1619
5. package-manager.ts:目录规则、包 manifest、优先级与信任¶
package manager 把资源来源统一成路径元数据,而非只扫描一个 .pi 目录。packages/coding-agent/src/core/package-manager.ts:2230-2451
skills 的用户目录包括 ~/.pi/agent/skills,并会从 cwd 向上寻找 .agents/skills;项目 .pi/skills 则属于受信任条件控制的项目资源。packages/coding-agent/src/core/package-manager.ts:347-460 packages/coding-agent/src/core/package-manager.ts:2230-2451
向上走 .agents/skills 时会在 git root 停止,因此不会无限越过工作树边界拾取祖先目录。packages/coding-agent/src/core/package-manager.ts:441-460
extension、skill、prompt、theme 的包来源既可由 package.json 的 pi manifest 指定,也可按约定目录自动发现。packages/coding-agent/src/core/package-manager.ts:2303-2467
解析时项目资源优先于用户资源,包资源优先于自动发现资源;同一 canonical path 会按优先级去重。packages/coding-agent/src/core/package-manager.ts:901-963 packages/coding-agent/src/core/package-manager.ts:2303-2467
项目本地 .pi 和项目包资源在未信任时不会作为普通资源加载,这不是 extension 自己必须重复实现的安全策略。packages/coding-agent/src/core/package-manager.ts:901-963 packages/coding-agent/src/core/package-manager.ts:2230-2451
6. skills.ts:一个目录何时算一项 skill¶
loadSkills() / loadSkillsFromDir() 以 SKILL.md 为 skill 根文件,而不是把目录内每个 Markdown 都当能力。packages/coding-agent/src/core/skills.ts:168-325
一旦某个目录命中 SKILL.md,递归就在这里停止,避免把 skill 内部的文档或子文件重复当成独立 skill。packages/coding-agent/src/core/skills.ts:168-325
loader 会解析并校验名称与 description;没有有效元数据的文件会形成诊断而不是伪装成可调用 skill。packages/coding-agent/src/core/skills.ts:168-325
frontmatter 的 disable-model-invocation 会影响该 skill 是否暴露给模型自动选择;这是“文件已加载”和“模型可见”之间的区别。packages/coding-agent/src/core/skills.ts:168-325
formatSkillsForPrompt() 只格式化可见 skills,生成放进 system prompt 的 XML 块,而不会把所有 SKILL.md 正文直接塞入初始 prompt。packages/coding-agent/src/core/skills.ts:335-361
skill 的正文在用户或模型通过对应命令选择后才成为具体指令内容,因此 skills 是按需能力包,而非启动时全量上下文。packages/coding-agent/src/core/skills.ts:168-361
7. slash-commands.ts:内建命令只是命令集合的一部分¶
BUILTIN_SLASH_COMMANDS 集中保存内建命令条目,交互层可用它生成基础 autocomplete。packages/coding-agent/src/core/slash-commands.ts:19-42
extension command、prompt template 与 skill command 在最终命令面中并列,而不是硬编码进该常量。packages/coding-agent/src/core/slash-commands.ts:19-42 packages/coding-agent/src/modes/interactive/interactive-mode.ts:592-628
交互模式把每个可用 skill 映射为 skill:<name>,存下 name 到 filePath 的关系,再交给 combined autocomplete provider。packages/coding-agent/src/modes/interactive/interactive-mode.ts:610-628
prompt template 在同一处被映射成普通 slash command,extension command 则取 runner 已解决冲突后的 invocationName。packages/coding-agent/src/modes/interactive/interactive-mode.ts:592-608
这意味着 /foo 与 /skill:foo 的前缀差异不是纯展示:前者是 template/内建/extension 名字空间,后者明确属于 skill 名字空间。packages/coding-agent/src/modes/interactive/interactive-mode.ts:592-628
8. extensions/llama:内建特性怎样保持“只是扩展”¶
builtInExtensions 当前把 llama.cpp factory 作为 hidden 内建扩展注册,所以它会被启动时合并进 extension factories。packages/coding-agent/src/extensions/index.ts:1-4 packages/coding-agent/src/main.ts:473-476
llama extension 在初始化时注册 provider 与 /llama command,并根据 mode 判断,避免在非 TUI 环境启动交互界面。packages/coding-agent/src/extensions/llama/index.ts:42-220
这不是核心代码的特权路径:它仍然通过 ExtensionAPI 的 provider/command/UI 注册面工作。packages/coding-agent/src/extensions/llama/index.ts:42-220 packages/coding-agent/src/core/extensions/types.ts:1179-1465
provider.ts 负责将 llama.cpp provider 放进运行时,并在允许网络且凭据条件满足时从服务端刷新模型 catalog。packages/coding-agent/src/extensions/llama/provider.ts:58-134
ui.ts 提供模型浏览与进度 UI,client.ts 实现 REST/SSE 的加载、下载与等待循环;command 只负责把这些部件组织起来。packages/coding-agent/src/extensions/llama/ui.ts:276-542 packages/coding-agent/src/extensions/llama/client.ts:156-332
Hugging Face 搜索与 GGUF quantization 提取放在 huggingface.ts,所以 provider 注册本身不被下载/搜索细节淹没。packages/coding-agent/src/extensions/llama/huggingface.ts:46-158
这组文件说明“内建 feature 也是 extension”的实际收益:核心只提供稳定 API 与 lifecycle,复杂 provider/UI 功能可以独立演化。packages/coding-agent/src/core/extensions/types.ts:1179-1683 packages/coding-agent/src/extensions/llama/index.ts:42-220
数据流¶
下面的链路把一个项目中的扩展与 skill 从磁盘带到可调用界面。packages/coding-agent/src/core/resource-loader.ts:333-493 packages/coding-agent/src/core/extensions/runner.ts:311-408
main() 合并 builtInExtensions 与 SDK inline factories
-> DefaultResourceLoader.reload()
-> pre-trust / settings / package resource resolution
-> 按 manifest、约定目录、CLI 路径发现 extensions、skills、prompts、themes
-> extension loader 执行 factory,收集注册表和加载期 provider 队列
-> skills loader 在 SKILL.md 处停止递归,解析 frontmatter
-> ExtensionRunner.bind(...)
-> 接入真实 actions,flush provider 队列,建立事件 handler/命令/快捷键
-> TUI: bindExtensions(mode: "tui")
-> 命令补全 = built-ins + templates + runner commands + /skill:name
-> extension hook 到来时,runner 创建当前 context 并执行 handler
启动合并发生在 main() 的 builtInExtensions 与传入 factories 合并处。packages/coding-agent/src/main.ts:469-476
资源发现/reload 和 extension factory 载入的边界分别在 resource loader 与 extension loader。packages/coding-agent/src/core/resource-loader.ts:333-493 packages/coding-agent/src/core/extensions/loader.ts:546-721
“声明变活”的绑定点是 runner 的真实 actions/provider flush,而不是文件被 import 的时刻。packages/coding-agent/src/core/extensions/runner.ts:311-408
TUI 将扩展绑定为 mode: "tui",重建 autocomplete 并显示资源;所以 UI 权限来自 mode bind,而不是扩展文件路径。packages/coding-agent/src/modes/interactive/interactive-mode.ts:1629-1707
自测¶
-
为什么 factory 执行阶段的 actions 是占位实现,而
registerProvider()又能例外地先排队?packages/coding-agent/src/core/extensions/loader.ts:170-392 -
extension context 为什么不能跨 reload/session 替换长期保存?
packages/coding-agent/src/core/extensions/runner.ts:539-546packages/coding-agent/src/core/extensions/runner.ts:665-776 -
两个 extension 都注册
/review时,用户实际可调用的名字如何确定?packages/coding-agent/src/core/extensions/runner.ts:595-629 -
为什么
.agents/skills与.pi/skills的发现范围和 trust 条件不同?packages/coding-agent/src/core/package-manager.ts:347-460packages/coding-agent/src/core/package-manager.ts:2230-2451 -
llama.cpp 为什么既是内建功能又能证明它遵循 extension API?
packages/coding-agent/src/extensions/index.ts:1-4packages/coding-agent/src/extensions/llama/index.ts:42-220