ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

oh-my-pi 扩展加载机制全解析:从路径发现到模块执行的完整链路

oh-my-pi 扩展加载机制全解析:从路径发现到模块执行的完整链路 oh-my-pi 扩展加载机制全解析从路径发现到模块执行的完整链路【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi导读oh-my-pi⌥ Coding agent with the IDE wired in在启动时通过一套统一的扩展加载子系统将原生扫描、Hook 工厂、已安装插件与显式配置四类 TypeScript/JavaScript 模块入口汇总为一条有序加载管线交给 Bun 运行时导入并执行其工厂函数。本文以 docs/extension-loading.md 为主线结合packages/coding-agent中的真实实现系统讲解扩展从「被发现」到「被绑定」再到「被运行」的完整生命周期并给出可直接落地的用户级与项目级目录布局示例。这个子系统做了什么扩展加载子系统的职责非常聚焦构建一份模块入口文件清单用 Bun 逐个 import 并执行工厂函数最终返回三样东西已加载的扩展定义extension definitions集合按路径收集的加载错误单个失败不会中断整体加载一个共享的扩展运行时对象extension runtime供后续ExtensionRunner使用。核心实现位于 loader.ts对外出口在 index.ts加载完成后的运行时/事件执行则交给 runner.ts。值得注意的是loader.ts顶部的导入注释明确写着一个约束运行时自引用PiCodingAgent只能在 loader 函数内部解引用以避免index.ts的循环依赖见 loader.ts。从入口函数的组装关系可以清晰看到这个子系统的骨架// loader.ts export async function discoverAndLoadExtensions( configuredPaths: string[], cwd: string, eventBus?: EventBus, disabledExtensionIds?: string[], options: DiscoverExtensionPathOptions {}, ): PromiseLoadExtensionsResult { const paths await discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds, options); return loadExtensions(paths, cwd, eventBus); }loader.ts——先做文件系统扫描得到路径再做模块导入与工厂绑定两阶段分离的设计也允许子代理复用父会话已经「预备」prepared好的工厂只重新绑定自己的运行时。扩展的四个来源discoverAndLoadExtensions()会按固定顺序向discoverExtensionPaths()汇聚四类路径实现见 loader.ts。1) 原生自动发现的扩展模块第一步通过 capability API 加载extension-module能力项但只保留 provider 为native的条目。loader 注释里说明了原因该能力还有 claude/codex/gemini/opencode 等 provider它们的条目在这里本来就会被丢弃直接按 provider 过滤可以跳过四次无关目录扫描见 loader.ts。原生extension-module的发现来源有三个项目目录cwd/.omp/extensions用户目录当前 agent 目录下的extensions/默认~/.omp/agent/extensions原生遗留 settings JSON 条目cwd/.omp/settings.json#extensions与当前 agent 目录的settings.json#extensions这里有两个重要的边界语义项目根是 cwd-only 的项目根即原生 provider 的.omp目录只扫描 cwd 本身不会向上遍历祖先目录用户根跟随 profile 与环境变量用户根通过getAgentDir()解析因此omp --profile name下会变成~/.omp/profiles/name/agent/extensions并且尊重PI_CODING_AGENT_DIR环境变量的覆盖详见 docs/config-usage.md。遗留兼容方面.pi仍然被pi.extensions清单与项目覆盖查找接受但.pi/extensions不再是本子系统的原生根目录。原生 provider 的具体实现可参考 builtin.ts它先Promise.all并行扫描各配置目录的extensions/子目录与settings.json再对 settings 中声明的路径做「目录还是文件」的二选一解析目录走discoverExtensionModulePaths扫描文件直接作为模块条目。2) 发现的 JS/TS Hook 工厂原生自动发现之后discoverExtensionPaths()还会把hook能力中入口路径是.ts/.js文件的 Hook 工厂追加进来让它们走同一条模块加载管线。关键差异Hook 能力加载时已经应用了自己专属的 disabled id 过滤所以这些路径不会再被disabledExtensions中的扩展模块名额外过滤一次。在 loader 中对应loadCapabilityHook(hookCapability.id, ...)后按文件后缀过滤的逻辑见 loader.ts而当非 ambient非环境性发现时还会扫描显式配置包根的hooks/pre、hooks/post目录discoverHooksInPackageRoot见 loader.ts。3) 已安装插件的扩展条目接着discoverExtensionPaths()通过getAllPluginExtensionPaths(cwd)追加已启用的已安装插件的扩展入口见 loader.ts。插件扩展条目来自包的omp.extensions/pi.extensions清单包括启用的 feature 条目。已安装插件的清单解析接受.ts、.js、.mjs、.cjs四种后缀若清单条目指向目录则识别index.ts、index.js、index.mjs、index.cjs扩展目录展开同样使用这四种后缀。这比原生与配置目录的自动扫描仅限.ts与.js更宽。4) 显式配置的路径最后追加显式配置路径。在主会话启动路径sdk.ts中配置路径来源有两类CLI 传入路径--extension/-e以及同样被视为扩展路径的--hook合并后的 settingsextensions数组。配置文件位置用户级当前 agent 目录的config.yml默认~/.omp/agent/config.yml--profile name时为~/.omp/profiles/name/agent/config.yml可用PI_CODING_AGENT_DIR覆盖 agent 目录项目/原生 settings 能力cwd/.omp/config.yml与cwd/.omp/settings.json。原生扩展模块发现还会读取遗留 JSON 扩展列表当前 agent 目录的settings.json默认~/.omp/agent/settings.json与cwd/.omp/settings.json。两种格式的配置示例完整继承自原文档# ~/.omp/agent/config.yml extensions: - ~/my-exts/safety.ts - ./local/ext-pack{ extensions: [./.omp/extensions/my-extra] }启用与禁用控制整体禁用扩展发现CLI--no-extensionsSDK 选项disableExtensionDiscovery两者的行为语义完全一致但拆分来看SDKdisableExtensionDiscoverytrue时ambient环境性扩展工厂被排除但additionalExtensionPaths仍正常解析包括带package.json#omp.extensions的包目录CLI--no-extensions遵循同样的显式契约——显式的-e/--extension与--hook路径照常加载且只有显式命名的扩展包中的兄弟能力根sibling capability roots仍被纳入项目/用户的extensions:settings 与已安装的 OMP 扩展包则从兄弟面中排除。源码层面的对应逻辑清晰可见discoverExtensionPaths()的options.ambient开关直接控制第 1/2/3 阶段是否执行第 4 阶段显式配置无条件执行见 loader.ts。需要强调的是这个开关只管辖扩展工厂与 OMP 扩展包的兄弟根不是整个进程的能力隔离开关。Skills、MCP 服务器、tools、prompts、rules 等由其他发现子系统持有的能力仍然拥有各自的启用/禁用控制。禁用指定的扩展模块disabledExtensions设置按扩展 id 格式过滤extension-module:derivedNamederivedName基于入口路径计算getExtensionNameFromPath例如/x/foo.ts→foo/x/bar/index.ts→bar配置示例disabledExtensions: - extension-module:foo在 loader 实现中addPaths会在入列前用isDisabledName(getExtensionNameFromPath(extPath))做过滤见 loader.ts。禁用其他能力的单项disabledExtensions并不仅限于扩展模块。每个定义了toExtensionId的能力都会向同一份列表贡献 id加载时会在条目进入会话前将其过滤掉。例如上下文文件使用context-file:level:basename格式其中level为user或projectdisabledExtensions: - context-file:user:CLAUDE.md该 id 不携带目录、不含深度信息因此一个project条目会禁用发现遍历到达的每一层深度下的同名文件详见 docs/context-files.md。路径与入口解析路径规范化对于显式配置路径依次执行规范化 Unicode 空格与支持的路径简写包括file://、/absolute/path以及绝对/相对路径前多余的:展开~相对路径基于当前cwd解析拒绝内部local://协议——它必须由协议处理器解析不能当作文件系统路径。配置路径是文件时直接作为模块入口候选使用。显式.ts、.js、.mjs、.cjs文件均受支持。配置路径是目录时解析顺序实现见resolveExtensionEntriesloader.ts该目录下package.json带omp.extensions或遗留pi.extensions→ 使用声明的条目index.tsindex.js否则扫描一层扩展条目直接*.ts/*.js文件子目录的index.ts/index.js子目录package.json带omp.extensions/pi.extensions。约束与规则均与源码行为一一对应不递归发现最多深入到一层子目录复杂包必须使用package.json清单discoverExtensionsInDir的注释明确写着 No recursion beyond one level见 loader.ts清单声明的extensions条目相对于该包目录解析声明条目仅在文件存在/可访问时才被纳入resolveExtensionEntries中对缺失路径continue跳过*/index.{ts,js}同时存在时TypeScript 优先于 JavaScript源码中先statindex.ts 再 index.js符号链接被视为合法的文件/目录候选发现循环中entry.isSymbolicLink()与普通文件/目录同等对待。忽略行为因来源而异原生自动发现discovery helpers 中的discoverExtensionModulePaths使用原生 glob带gitignore: true与hidden: false显式配置目录扫描loader.ts 内使用readdir规则不应用 gitignore 过滤。加载顺序与优先级discoverAndLoadExtensions()构建一条有序列表后统一交给loadExtensions()。顺序为原生自动发现的模块发现的 JS/TS Hook 工厂已安装插件的扩展条目显式配置路径按提供顺序。在sdk.ts中配置顺序为CLI 附加路径 → settingsextensions。去重规则基于绝对路径先到先得首次出现的路径获胜后续重复项被忽略。由此可以推导出一个重要结论同一个模块路径如果同时被自动发现和显式配置它只会在第一个位置自动发现阶段被加载一次。源码对应addPath中的seenSet 去重见 loader.ts。性能上的一个细节值得留意loadExtensions()中模块导入cold-start 的主要成本——文件 I/O 加模块求值是跨扩展并发执行的Promise.all而工厂绑定则按原始路径顺序串行执行bindPreparedExtensions的 for 循环从而保证注册语义同名校验的 last-wins 冲突、共享 runtime 的 flag 默认值是确定性的见 loader.ts。模块导入与工厂契约每个候选路径都经由loadLegacyPiModule()legacy-pi-compat.ts加载realpath 解析 动态 import 加?mtime缓存破坏符入口先解析真实路径兼容 macOS/var→/private/var、bun link/pnpm 安装再用带?mtimetag的后缀动态 import。这样编辑过源码后重新加载能拿到新内容POSIX 下使用裸文件系统路径而非file://前缀因为 Bun 会把?mtime视为模块身份的一部分而file://上的 query string 会被忽略、导致读到陈旧源码。图级 mtime 传播从 16.3.7 起同一个 mtime 标签会传播到扩展所属依赖图中的每一个模块——相对.//../导入、包imports别名#alias/*、以及扩展局部的裸依赖——通过图级onLoad重写实现。因此同进程内重新导入时整个依赖图而非仅入口文件都能感知编辑。而 host 解析的重写遗留 pi 包说明符、TypeBox shim保持不带标签的file://URL因为它们指向进程内 host 代码重载之间不会变化。作用域化的 BunonLoadHook 重写遗留说明符installLegacyPiSpecifierShim()legacy-pi-compat.ts注册的Bun.plugin在求值前把mariozechner/*、earendil-works/*及裸sinclair/typebox重写到 host 内置副本上。遗留兼容的完整链路还包括两个 shim移到oh-my-pi/pi-catalog/models的 catalog 符号calculateCost、modelsAreEqual、getBundledProviders以及getModel/getModels别名由遗留 pi-ai shimsrc/extensibility/legacy-pi-ai-shim.ts重新导出遗留oh-my-pi/pi-coding-agent导入包括DefaultResourceLoader解析到src/extensibility/legacy-pi-coding-agent-shim.ts中的兼容 loader。工厂的选择逻辑在getExtensionFactory()模块本身是函数则用它否则用module.default见 loader.ts。工厂必须是函数ExtensionFactory可返回void或 promise加载过程会 await 它完成后才继续下一个路径。若导出不是函数该路径以结构化错误失败加载继续。绑定阶段的另一个细节是runExtensionFactory()中的provider 注册回滚执行工厂前先对runtime.pendingProviderRegistrations做检查点快照工厂抛错时恢复完整注册队列——因为前一个扩展可能 unregister 了更早扩展排队的条目见 loader.ts。失败处理与隔离加载期间每个扩展路径的失败被捕获为{ path, error }不会阻止其他路径加载。常见失败场景import 失败 / 文件缺失无效工厂导出非函数工厂执行时抛出异常。在源码中importExtensionModule()与bindExtension()各自 try/catch 并返回结构化错误见 loader.tsbindPreparedExtensions收集到errors数组后继续循环。运行时隔离模型扩展不做沙箱隔离同一进程/运行时它们共享一个EventBus与一个ExtensionRuntime实例加载期间runtime 的动作方法会故意抛出ExtensionRuntimeNotInitializedError——动作接线要等ExtensionRunner.initialize()之后才发生。源码中ExtensionRuntime的所有动作方法sendMessage、sendUserMessage、appendEntry、setModel等在初始化前都是抛错桩见 loader.ts。加载之后事件经由ExtensionRunner运行时handler 异常会被捕获并作为扩展错误发出而不是让 runner 循环崩溃。runner 还会为每个事件设置独立的处理预算通用事件默认 30 秒EXTENSION_HANDLER_TIMEOUT_MS而session_shutdown使用独立的 2 秒短上限SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS因为它是 fire-and-forget 的清理逻辑挂起的 handler 绝不应拖住用户的 CtrlC //exit见 runner.ts。最小目录布局示例用户级~/.omp/agent/ config.yml extensions/ guardrails.ts audit/ index.ts项目级repo/ .omp/ settings.json extensions/ checks/ package.json lint-gates.ts其中checks/package.json使用清单声明扩展入口{ omp: { extensions: [./src/check-a.ts, ./src/check-b.js] } }遗留清单键仍然接受{ pi: { extensions: [./index.ts] } }小结oh-my-pi 的扩展加载是一条「发现 → 排序去重 → 并发导入 → 顺序绑定 → 运行时接线」的清晰流水线原生.omp目录、Hook 工厂、插件清单与显式配置四路汇流disabledExtensions提供模块级与能力级两种粒度的禁用路径解析严格区分文件/目录与清单声明?mtime缓存破坏与图级重写保证开发期热重载遗留 pi 说明符 shim 让旧版插件无缝运行。深入 loader.ts、legacy-pi-compat.ts 与 runner.ts可以进一步掌握每个阶段的实现细节为编写健壮的扩展或排查加载问题打下基础。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表