oh-my-openagent 缺陷 6376 修复实战:打包器内联导致的 import.meta.url 路径解析错位与完整 QA 证据链

发布时间:2026/9/18 6:21:12
oh-my-openagent 缺陷 6376 修复实战:打包器内联导致的 import.meta.url 路径解析错位与完整 QA 证据链 oh-my-openagent 缺陷 #6376 修复实战打包器内联导致的 import.meta.url 路径解析错位与完整 QA 证据链【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent本篇基于 oh-my-openagentOmO仓库中 issue #6376 的 QA 证据文档qa-summary.md完整复盘一个典型的打包产物路径解析错位缺陷bunx oh-my-openagent install的 CLI 打包产物中sharedSkillsRootPath()返回了不存在的dist/cli/skills/导致 ast-grep 的sg二进制从未被安装。读完本文你将掌握如何用驱动真实构建产物的方式验证源码层面无法复现的缺陷、为何修复点要选在唯一咽喉函数而非单个调用方以及一套可复制的证据化 QA 方法论RED/GREEN/负对照、回归扫描、隔离性证明、必须会失败的验证驱动脚本。1. 问题现象install 静默跳过 sg 安装doctor 报 AST-Grep unavailableQA 采集环境为 Windows 11、bun 1.3.14、node v22.14.0、opencode 1.18.5基线为upstream/dev4a6b2bed2。用户报告的缺陷路径如下bunx oh-my-openagent install从CLI 打包产物运行会调用installAstGrepForOpenCodeinstall-ast-grep-sg.ts。该函数在第 26 行以sharedSkillsRootPath()的返回值拼接出 skill 目录join(sharedSkillsRootPath(), ast-grep)并期望在其中找到ast-grep/install.sh安装脚本。由于解析到了错误目录dist/cli/skills/实际不存在安装脚本找不到CLI 记录日志[ast-grep] skipped sg provisioning: missing .../dist/cli/skills/ast-grep/install.sh。sg因此从未被 provisioning后续doctor命令报告AST-Grep unavailable。注意这里有两个值得注意的静默特征installAstGrepForOpenCode的设计是失败即跳过捕获异常并打印自己的诊断日志不抛出所以安装流程本身不会报错而doctor中的checkAstGrepCli只读取sg是否已经存在于运行时目录并不执行 provisioning——它是下游观测值不是本缺陷的判别器这一点在 QA 文档中被明确标注后文验证方案正是围绕这个盲区设计的。2. 根因打包器内联让 import.meta.url 指向消费方打包产物修复前的 packages/shared-skills/index.mjs 全文仅 5 行import { fileURLToPath } from node:url; export function sharedSkillsRootPath() { return fileURLToPath(new URL(./skills/, import.meta.url)); }问题在于两个构建步骤都会把该模块内联inline进各自的 bundle。内联后import.meta.url指向的是消费方打包产物的 URL而不是packages/shared-skills/目录的 URL。而构建只把共享 skills 复制到dist/skills这一处。结果在三个随包分发的 bundle 之间形成了不对称bundle所在位置sharedSkillsRootPath()返回值是否正确dist/index.jsplugindist/dist/skills/正确dist/cli/index.jsdist/cli/dist/cli/skills/错误不存在dist/cli-node/index.jsdist/cli-node/dist/cli-node/skills/错误不存在bunx oh-my-openagent install恰好运行自 CLI bundledist/cli/或dist/cli-node/因此必然踩中错误的两个分支。QA 文档同时解释了为什么这个 bug无法从源码复现直接从packages/omo-opencode/src/cli/index.ts运行源码时import.meta.url解析到源码树而packages/shared-skills/skills是真实的同级目录一切正常。缺陷只存在于构建产物中——这决定了后文的验证必须驱动真实的dist/输出。3. 修复方案有界回退探测bounded fallback probe修复只改动一个产品文件 packages/shared-skills/index.mjs8 / -1。QA 文档记录的原始修复版本为export function sharedSkillsRootPath() { const sibling fileURLToPath(new URL(./skills/, import.meta.url)); if (existsSync(sibling)) return sibling; const parent fileURLToPath(new URL(../skills/, import.meta.url)); return existsSync(parent) ? parent : sibling; }这一版修复有三个刻意为之的设计属性同级优先Sibling first。所有当前能正常工作的布局源码树、dist/根部的 plugin bundle都会在第一次探测就命中./skills/返回值与修复前逐字节一致。唯一发生行为变化的只有同级不存在但父级存在这一种情况——恰好就是坏掉的 CLI bundle 情形。回退严格限定为一层Bounded to exactly one level。如果做泛化的向上逐级搜索可能绑定到某个不相干的祖先skills/目录反而在本来正常的目录树中悄悄改变行为。而所有真实 bundle 的深度只可能是 0 或 1。两者都不存在时返回同级路径保持不变。函数签名是(): string从不抛异常调用方本来就容忍目录缺失并各自打印诊断。返回同级路径可以让所有既有错误消息继续指向同一个主位置。当前仓库的后续演化从当前 index.mjs 源码看该函数后来演化为探测三个 specifiers——./skills/、../skills/、../../skills/按最近优先顺序取第一个存在的目录。注释中新增了第三种布局Codex marketplace 把plugin/复制到plugins/omo/skills 落在plugins/omo/skills而根部 CLI 运行时被打包到plugins/omo/dist/cli深度为 2。shared-skills-root-path.test.ts 中对应增加了两个用例marketplace 两层布局解析、以及更近与更远两个 skills 目录并存时最近者胜出。这说明限定深度并非死板限制每当出现一种新的可见构建布局就显式扩展探测表并补充测试而不是放开无界向上搜索。4. 为什么修函数而不是修调用点四个消费方共享同一咽喉QA 文档将本缺陷归类为chokepoint咽喉点缺陷——四个调用点共用同一个函数修好函数即覆盖全部消费方运行自修复前状态install-ast-grep-sg.tsCLI bundle实际损坏即本次 issueskill-file-loader.tsplugin bundle潜伏loader.tsplugin bundle潜伏builtin-skill-converter.tsplugin bundle潜伏只有第一个调用点今天可观测地坏了因为它是唯一运行在非根部 bundle 里的。另外三个从dist/index.js加载、当前解析正确但它们携带同一个缺陷任何未来把它们放进非根部 bundle 的构建变更都会让它们同时损坏。如果只打补丁到 ast-grep 的调用点其余三处将长期裸露在外。从源码结构看这三个 plugin 侧调用点分别用于内置 skill 模板的按需读取createSharedSkillTemplateLoader带 Map 缓存、共享 skill 发现discoverSharedSkills、以及 builtin skill 到 LoadedSkill 的路径解析——全部是skills 根目录解析这一语义天然适合收敛到单一函数。5. 验证证据链从单元到真实构建产物5.1 单元测试驱动的是真实产物而非重新实现新测试文件 shared-skills-root-path.test.ts 的核心技巧是一个loadFrom辅助函数L21-L26把仓库里真实的index.mjs复制进一次性构造的 fixture 目录布局如tmp/cli/再动态import副本——于是模块内部的import.meta.url就会像打包器内联时一样相对于 fixture 位置解析。这正是模拟dist/cli/index.js深度 1与dist/index.js深度 0的同一机制。一个容易踩坑的实现细节被测试注释明确指出macOS 上临时目录位于符号链接的/var之下动态 import 会规范化模块 URL因此 fixture 根目录必须经realpathSync规范化后再参与比较L33-L37。5.2 RED / GREEN / 负对照 / 类型检查#场景产物文件观察结果RED未修改基线上跑新测试red-6376.txt2 通过 / 1 失败。bundle 位于 skills 目录下一层的用例返回了tmp\cli\skills而期望是tmp\skills——与dist/cli/skillsvsdist/skills的分歧完全同构。是行为失败不是编译错误GREEN同一测试加修复green-6376.txt3/3 通过负对照只回滚产品文件、保留测试negative-control-6376.txt同一用例再次失败exit 1类型检查tsgo --noEmit -p packages/omo-opencodetypecheck-6376.txtexit 0回归3 个套件干净基线 vs PRregression-comparison.txt无新增失败见下负对照有一个方法论细节值得学习修复已经提交在分支上时git stash会找不到任何东西而静默产出假绿所以回滚必须用git checkout upstream/dev -- file显式还原产品文件并在产物中打印回滚后 diff 为空的断言。5.3 回归扫描区分新引入的失败与基线既有失败套件干净upstream/dev本 PRpackages/shared-skills68 通过 / 1 失败新增的 RED 用例69 通过 / 0 失败packages/skills-loader-core235 / 0235 / 0packages/omo-opencode/src/cli659 通过 /2失败659 通过 /2失败那 2 个 CLI 失败是executeOnCompleteHook uses powershell when PowerShell is detected on Windows及其 cmd.exe 对应用例在未修改的upstream/dev上、同一台 Windows 主机上以完全相同的方式失败与 skill 路径解析无关。regression-comparison.txt 末尾的 NEW failures introduced by this PR (must be empty) 区段为空是本 PR 不引入任何回归的直接证据。5.4 构建产物证据唯一存在该 bug 的表面单元测试证明了解析逻辑但最终证据必须打在真实dist/输出上。live-driver.sh 的做法是运行bun run build然后把随包分发的index.mjs副本放进每个 bundle 自己的目录中再 import——这样它的import.meta.url就复现了该 bundle 对真实构建资产的解析。资产不对称bug 的前置条件在修复前后一致dist/skills/ast-grep/install.sh : EXISTS dist/cli/skills/ast-grep/install.sh : MISSING dist/cli-node/skills/ast-grep/install.sh : MISSING而在各真实 bundle 目录中的解析结果对比 live-driver-before.txt 与 live-driver-after.txtBEFORE AFTER dist/index.js dist\skills\ FOUND dist\skills\ FOUND (unchanged) dist/cli/index.js dist\cli\skills\ MISSING dist\skills\ FOUND dist/cli-node/... dist\cli-node\... MISSING dist\skills\ FOUND内联字面量计数则确认回退逻辑确实被打进了产物修复前每个 bundle 只有./skills/一处修复后../skills/也各有一处BEFORE AFTER dist/index.js ./skills/1 ../skills/0 ./skills/1 ../skills/1 dist/cli/index.js ./skills/1 ../skills/0 ./skills/1 ../skills/1 dist/cli-node/... ./skills/1 ../skills/0 ./skills/1 ../skills/1驱动脚本还真实执行了node dist/cli-node/index.js doctor在 live-driver-after.txt 中完整展示了一次沙箱内的 doctor 运行报告 3 个与注册/版本/模型缓存相关的发现exit 1 属正常——doctor 只要报出 findings 就退出 1。隔离性证明驱动脚本把HOME、USERPROFILE、APPDATA、LOCALAPPDATA和所有XDG_*变量重定向进mktemp -d沙箱退出时删除并清理自己复制进dist/的探测文件。由于 QA 本身运行在一个真实的 opencode 会话内、写同一个数据库db-session-count-proof.txt 额外设置了环境对照真实 session 表在两次采集前后均为 2729环境对照间隔也是 2729——驱动脚本自身贡献了 0 个新会话。6. 评审回合让证据驱动脚本必须会失败2026-08-03 的评审提出两条有效意见并均已修复见 qa-summary.md 的 Review round 章节P2证据驱动脚本原本不可能失败。旧版live-driver.sh以set -uo pipefail无-e运行以echo wrote $OUT收尾且对缺失的 bundle 目录用continue跳过——于是 MISSING 探测仍以 exit 0 结束陈旧或缺失的dist/会产出看似完整的假绿产物。修订后的驱动脚本前置强制要求dist/index.js、dist/cli/index.js、dist/cli-node/index.js、dist/skills/ast-grep/install.sh存在否则 exit 2 并点名缺失路径跟踪每一次探测失败任何随包 bundle 解析出的 skills 根目录缺少ast-grep/install.sh即判失败doctor 退出码大于 1 视为崩溃等于 1 只是报出 findings以该状态码退出。由于本机bun run build会卡在 vendoredlsp-tools-mcp步骤其脚本以rm -rf开头驱动脚本无法在内部构建因此改为显式要求提供已构建产物——这正是评审给出的另一选项。driver-failure-propagation.txt 用同一台机器上的双向运行证明修复生效A. fixed branch driver exit0 FOUND / FOUND / FOUND RESULT: PASS B. product reverted, rebuilt driver exit1 FOUND / MISSING / MISSING RESULT: FAIL parent-literal count in dist/cli/index.js: 0 (confirms the rebuild used the base source)用例 B 使用的正是此前会在同一状态下 exit 0 的脚本本体——失败传播被实证而非断言。P1补充 OpenCode harness 真实会话用例。live-opencode-run.sh实现了 opencode-qa Case A加载本分支dist/index.js作为插件在隔离沙箱中运行真实的opencode run reply with exactly OK --format json并在前后比对真实 DB 会话数。live-opencode-run-output.txt 的结果dist/index.js parent-fallback ../skills/ occurrences1 (0 unfixed base, 1 fix present) opencode run exit0 real sessions before: 2831 - after: 2831 count unchanged: true RESULT: PASS该用例的范围被诚实地陈述在产物里它证明从本分支构建的插件能在真实 OpenCode 会话中加载、且会话完成时未触碰真实 DB但它不能给出逐 skill 清单——最小非交互运行只返回 assistant 消息且插件日志没有落到被重定向的TMPDIRWindows 上 node 的os.tmpdir()忽略该变量。逐 bundle 的 skills 根解析仍由live-driver.sh第 3 节承担它在每个 bundle 自己的目录下执行随包分发的解析器对照真实构建资产。产出该用例过程中还抓到并修复了一个 harness 自身的 bug把脚本复制到/tmp以去除 CRLF破坏了BASH_SOURCE相对解析REPO变成/配置的插件路径变成/dist/index.js——第一次运行根本没加载任何插件却差点产出绿色假产物。脚本现改为尊重OMO_REPO_ROOT并在解析根下找不到dist/index.js时硬失败杜绝此类静默假绿。7. 残余风险被显式接受、而非被遗忘QA 文档将以下残余风险逐条列为有意为之的权衡仅限定一层修复版语义。若未来构建把 bundle 嵌套得比dist/one-dir/更深解析会回退到不存在的同级路径skill 目录再次被静默错过。这是有意的更深的嵌套是一次可见的构建变更应重新审视该函数而无界向上搜索可能绑定不相干的skills/目录、破坏当前正常的布局。当前仓库已按此原则演进为三级探测表以覆盖 marketplace 布局见第 3 节。回退路径上的两次stat调用。该函数在模块初始化、每次 install、每次 skill 发现各被调用一次从不出现在紧循环里且同级消费方本就会对同一结果做existsSync探测故不做记忆化。符号链接。existsSync跟随符号链接因此一个损坏的./skills软链会被当作缺失而走上父级回退——可接受。资产不被复制。CLI bundle 现在读共享的dist/skills目录而非各自拥有一份副本dist/skills在任何完整构建产物中都存在且解析发生在运行时因此构建步骤的先后顺序无关紧要。未受影响面。已提交的packages/omo-senpi/plugin/extensions/omo.js中该代码出现次数为零senpi-compatibilityjob 不受触碰Codex skill 同步脚本从源码运行、未变更。8. 方法论小结什么证据才算足够QA 文档的 Why this is enough 一节给出了本案例的充分性论证也是可复用的验收标准单元层测试驱动的是随包分发的真实index.mjs产物复制进受控 fixture 布局让其自身的import.meta.url选中被测分支而不是对函数的重新实现——这与打包器的内联机制同源。产物层构建产物采集在真实dist/输出上展示用户可见的失败及其消除——那是唯一存在该 bug 的表面。反证层负对照回滚产品文件后测试必须重新变红、驱动脚本失败传播证明同一脚本在 base 上必须非零退出保证证据链中的每个绿都不是结构性必然。副作用层环境变量全量重定向 真实 DB 会话数前后比对 环境对照间隔证明 QA 过程零污染。保密层What was omitted无密钥、令牌或环境转储隔离记录只含会话计数与文件路径OpenCode 用例将开发者auth.json复制进沙箱以完成真实鉴权但其内容从不被读取或打印沙箱退出即删。对任何源码跑得好好的、打包产物却坏了的缺陷路径解析、资源定位、__dirname/import.meta依赖类问题这套单元驱动真实产物 逐 bundle 位置解析 字面量计数 隔离运行 可失败驱动脚本的组合比单纯堆叠测试用例更接近用户真正遭遇的失败模式。参考路径索引内容仓库路径QA 证据总述.omo/evidence/20260727-fix-6376/qa-summary.md解析函数当前仓库状态三级探测packages/shared-skills/index.mjs解析函数测试5 用例含 marketplace 布局packages/shared-skills/shared-skills-root-path.test.tsast-grep 安装入口缺陷调用点packages/omo-opencode/src/cli/install-ast-grep-sg.ts三个 plugin 侧潜在调用点skill-file-loader.ts、loader.ts、builtin-skill-converter.tsRED / GREEN / 负对照 / 类型检查采集red-6376.txt、green-6376.txt、negative-control-6376.txt、typecheck-6376.txt构建产物 before/after 与驱动脚本live-driver.sh、live-driver-before.txt、live-driver-after.txt回归扫描 / 失败传播 / DB 隔离证明regression-comparison.txt、driver-failure-propagation.txt、db-session-count-proof.txt真实 opencode 会话用例live-opencode-run.sh、live-opencode-run-output.txt【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考