loader 静默丢弃函数式插件的 inject 声明,首次访问依赖才报错 #5239
Replies: 2 comments
|
查了一下 vendor/loader/src/index.ts 里 unwrapExports 的实现,确认你说的这个机制是真的: exports = exports.default ?? exports 如果一个插件同时有 export default 和同级的 inject 这些具名导出,只要 default 存在,整个 namespace 就会被收缩成 default 那一个东西,inject、Config 这些具名导出全部被丢弃。插件本身照常加载不会报错,等到代码里第一次访问那个本该被 inject 声明保护的服务时才会炸,跟你描述的现象一致。 不过这个不是没人知道的隐藏 bug,仓库里已经有一篇完整的 postmortem 写这个事:docs/postmortem/0001-acp-default-export-drops-inject.md。里面把根因、判断方法、规避方式都写清楚了,结论是命名空间插件(name/inject/Config/apply)和 default export 互斥,两个不能同时用,选一个。 而且这不是纸上谈兵,仓库自己也做了大量测试挡这个问题,我数了一下至少 30 个包(tool-web、tool-lsp、mcp-client、todo、workflow 等等)的测试里都有专门的 unwrapExports 往返断言,就是防止哪天有人手滑加了个 export default 把 inject 弄丢。 想确认一下你踩这个坑的具体位置:是仓库自带的某个包(那样的话就是一个真实的回归,值得单独开一个报告,因为理论上应该被上面说的测试挡住),还是你自己写的第三方插件(那就是上面 postmortem 里说的那个经典坑,照着它的建议改成纯命名空间导出就行)? |
|
我在精确 alpha.2 基线
已通过:Node 22.19 与当前 Node 24 聚焦测试、Loader/HMR/app-boot TypeScript 构建、仓库 lint、32/32 文档门禁、完整 host/client 构建,以及独立精确 diff 复核(无剩余发现)。中英文 Agent Note、Loader README 和 vendor divergence 记录也随提交提供。 官方仓库当前关闭 Pull Requests,所以先把可复核/可 cherry-pick 的精确提交留在原讨论中;是否采用 warning-only 方案仍由维护者决定。这个提交不宣称修复第三方插件本身,现有安全规避仍是改为纯命名空间导出,或把元数据附到 default 值上。 |
Uh oh!
There was an error while loading. Please reload this page.
unwrapExports 解包默认导出时静默丢弃同级的 inject 等命名导出,插件照常加载但依赖声明丢失,首次访问服务才报错。
复现、预期与验收
写一个函数式插件(ESM,最常见的书写方式——默认导出函数、元数据具名导出):
经 loader 加载:
触发任意一次
llm/stream(例如手动/compact或任意模型调用)。Error: cannot get property "llm" without inject(本例发生在 compaction/start 后 1ms)。Loader.unwrapExports(vendor/loader/src/index.ts,当前 master 原样保留;npm 构建 @deepseek-ai/cordis-plugin-loader@1.0.2 lib/index.js:736):exports = exports.default ?? exports—— 只要存在默认导出,就返回裸 apply 函数,模块命名空间上的同级inject/name/Config全部被丢弃;ctx.registry.plugin(@deepseek-ai/cordis lib/index.js:1634)以Inject.resolve(plugin.inject)构建 fiber ——plugin.inject为undefined,依赖图为空;fiber.inject,抛错。export default class P { static inject = [...] })与命名空间式(无默认导出,export const inject+export function apply)都正常;只有"默认导出函数 + 具名元数据"这一形态静默失败。export default apply事故,PR 本人在此声明,所有黑deepseek的言论都是我发的 #41 修复)。但 0001 的修复是删掉那一个插件里的多余默认导出 + 补 e2e 测试,loader 的静默丢弃行为本身未变(今日已核对 master 上 unwrapExports 原样保留);其"不要加 export default"的教训只存在于仓库 README/postmortem 内部,外部插件作者从发布物、loader 报错或文档中都无法发现。llm注入,手动/compact在 compaction/start 后 1ms 失败;自动压缩的摘要级同样被卡,数日内实际只有剪枝级在运行,排查耗时数小时。预期结果(两层,其一即可):
最小(诊断):当
unwrapExports解包到.default、且被丢弃的命名空间带有 cordis 元数据键(至少inject,含name/Config)而解包值自身未定义时,在加载时经ctx.logger输出警告,指明模块与被丢弃的键,并指向 postmortem 0001 / 插件编写文档。把"数小时后在首个 LLM 调用处炸"变成"加载时立刻可见"。完整(健壮):保留元数据——解包出的默认值自身没有
inject/name/Config时,从被丢弃的命名空间继承(双方都定义时以默认值自身为准或告警),使自然 ESM 形态直接可用。环境:
dsh 0.1.1-rc.2;@deepseek-ai/cordis-plugin-loader 1.0.2;@deepseek-ai/cordis(随 dsh 安装);Node 22+;任意 profile(web/headless 均可复现)。
验收条件:
export default apply+export const inject形态的插件,断言声明的 inject 被生效(或加载时出现指明被丢弃键的告警)。All reactions