首页
/ claude-mem 安装与分发体系解析:从 TRIAGE-07 看插件首装失败的根因与修复

claude-mem 安装与分发体系解析:从 TRIAGE-07 看插件首装失败的根因与修复

2026-09-05 20:14:51作者:胡易黎Nicole

claude-mem 通过 Claude Code 插件机制分发给最终用户,"首次安装能否成功"直接决定了新用户的第一印象。本文基于仓库内的问题修复记录 TRIAGE-07-Installation-Distribution,系统梳理该修复周期解决的六类安装问题(对应 issue #1128、#1166、#1187、#1156、#979、#1041),并结合 plugin/package.jsonsrc/npx-cli/install/setup-runtime.tstests/infrastructure/plugin-distribution.test.ts 等仓库源码,还原每项修复的实现细节与回归验证方式。读完本文,你可以掌握:插件运行时依赖的审计方法、基于真实模块解析的 post-install 校验机制、三层分发链路,以及 SQLite 双迁移系统版本冲突的根治思路。

一、问题背景:新用户首装失败的三类典型症状

TRIAGE-07 文档开篇即列出了新用户遇到的安装失败场景,这三类症状分别对应分发链路上的不同环节:

  1. 缓存式安装后缺少 node_modules(issue #1128、#1166):插件从缓存目录提取到 marketplace 目录后,运行时依赖没有被安装,worker 和 hooks 脚本一启动就报 Cannot find module,插件完全无法工作;
  2. skills/ 目录未被复制(issue #1187):安装完成后 plugin/skills/mem-search/SKILL.md 缺失,导致 Claude Code 无法按约定发现 mem-search 技能;
  3. np(npm 发布工具)被错误地列为运行时依赖(issue #1156):发布工具只应在开发机上使用,出现在运行时依赖中会让消费者下载无用的包;
  4. 另外还涉及 迁移失败(issue #979,全新数据库初始化即报错)和 marketplace not found(issue #1041,插件根路径解析失败)。

修复记录中每项任务都附带了验证结论("Verified 2026-02-23" / "Fixed 2026-02-23"),这是理解下文各节的事实基础。

二、依赖审计:plugin/package.json 里只允许放真正的运行时依赖

修复的第一个动作是审计依赖面:将 plugin/package.jsondependenciesplugin/scripts/*.js 中的实际运行时 import 逐一比对,确认每个声明的依赖都有真实用途。

文档中特别指出了一条判定准则:自 v10.3.0 起,claude-mem 的向量检索走 chroma-mcp(通过 uvx 拉起的独立进程),不再依赖 npm 的 chromadb 包。因此审计时要在全仓搜索 require('chromadb'),确认无残留后再将其从依赖中移除,并同步更新任何提及它的错误提示。文档结论为:验证当日所有依赖均正确,无任何 chromadb 残留引用,"无需变更"。

对照当前仓库的 plugin/package.json,其 dependencies 目前只包含插件 hook 运行时真实用到的包:

  • zod(schema 校验,worker 运行时从插件目录内的副本解析);
  • 一组 tree-sitter-* 语言解析器(C、C++、Go、Java、JavaScript、Python、Ruby、Rust、TypeScript、Kotlin、Swift、PHP、Lua、Scala、Bash、Haskell、Zig、CSS、SCSS、TOML、YAML、SQL、Markdown)与 tree-sitter-cli,服务于代码结构解析;
  • shell-quote

注意 overridestree-sitter 固定到 ^0.25.0,并把 tree-sitter-cli 列入 trustedDependencies(它带 native 构建,需要 npm 信任才执行 install 脚本);engines 要求 node >= 20.12.0bun >= 1.0.0。这套"声明即所需"的依赖清单,正是审计任务要维护的不变量。

np 的归类验证:发布流程定义在根 package.json 中,"release": "np" 等脚本依赖 np@^11.2.0,而它确实只出现在 devDependencies(第 157 行),dependencies 仅有服务端认证面用到的 better-auth@better-auth/api-key。根 package.json 里还有一段 //dependencies-note 注释,明确解释了依赖归类原则:只有经 bundler 后仍作为存活 import/require 出现在发布产物中的依赖才进 dependencies,其余(express、bullmq、ioredis、react、zod 等)全部由 esbuild 内联进 worker/server/npx 产物,作为构建期 devDependency 存在,不会让消费者下载。这条注释本身就是一份可审计的依赖治理策略。

三、修复缓存安装缺少 node_modules:路径解析 + post-install 校验

3.1 根因:硬编码路径 + installCLI() 路径错误

修复记录给出的根因很具体:原 smart-install.js硬编码了 ~/.claude/plugins/marketplaces/thedotmack 这一路径,对缓存式(cache-based)安装完全不适用;同时 installCLI() 使用了错误的目标路径(ROOT/plugin/scripts/ 而非 ROOT/scripts/)。

修复方案是引入 resolveRoot(),采用多级回退策略解析插件根目录:

  1. 优先读取环境变量 CLAUDE_PLUGIN_ROOT —— Claude Code 为所有 hook 进程注入该变量,是最可靠的来源;
  2. 回退到脚本自身位置(import.meta.url 推导);
  3. 再回退到 XDG 路径与旧版(legacy)路径。

这一思路在仓库中仍有体现:plugin/hooks/hooks.jsonplugin/scripts/bun-runner.js 等分发产物中都使用 CLAUDE_PLUGIN_ROOT 来定位插件文件,避免依赖任何与用户名、安装方式相关的绝对路径。同时修复要求分发版与开发版使用同一套解析逻辑——即 plugin/scripts/ 下的分发副本与 scripts/ 下的开发副本保持相同实现,杜绝"开发机正常、用户机失败"的漂移。

3.2 verifyCriticalModules:把"装没装"从目录存在性升级为真实模块解析

修复的第二层防线是安装后校验。当前实现位于 src/npx-cli/install/setup-runtime.ts,其设计比"检查 node_modules/<dep> 目录是否存在"严谨得多,值得逐点拆解:

(1)以安装树为锚点的解析。 函数用 createRequire(join(nodeModulesPath, 'noop.js')) 创建一个锚定在安装目录内的 require 实例,使 require.resolve 遵守被安装 package.jsonexports 映射,而不是宿主环境的模块路径。这保证校验的是"用户机器上这份安装"的闭包完整性。

(2)逐依赖解析 + bin-only 包的二次确认。dependencies 中每个声明包先做裸名解析;失败后不直接判死,而是再尝试解析 <dep>/package.json——因为像 tree-sitter-cli 这类只有 bin 没有入口点(无 main/module/exports/index.js)的包,裸名解析天然会失败,但其 package.json 是可解析的,说明它其实装好了。两次都失败才算真正缺失(源码注释中将该行为追溯到 issue #2730)。

(3)zod 子路径导出的显式校验。 被打包的 worker 会经由 @modelcontextprotocol/sdk 等依赖传递性地 import 'zod/v3''zod/v4''zod/v4-mini'。注释指出一个隐蔽故障模式:陈旧或半截的安装可能让 zod 目录存在,但子路径导出解析失败,最终在运行时才以 Cannot find module 'zod/v3' 的形式爆发。因此校验对 ZOD_REQUIRED_SUBPATHS = ['zod/v3', 'zod/v4', 'zod/v4-mini'] 逐一 resolve,且不绑定具体版本号("version-agnostic: we resolve subpaths, never a pinned version")。

(4)失败要"响"(fail LOUD)。 所有无法解析的模块被收集进 unresolvable 列表,循环结束后统一抛出:

Post-install check failed: unresolvable modules: <逗号分隔的列表>

该函数在安装主流程的尾部被调用(setup-runtime.ts 第 458 行),失败即中止安装而不是留下一个"看起来装好了"的破损闭包;对应的回归测试在 tests/cli/verify-critical-modules.test.ts。文档中还记录了失败时的 npm 回退策略——校验不通过则触发重新 npm install 的兜底流程。

四、skills/ 缺失问题:技能是源文件,不是构建产物

对"安装后缺 skills/"的排查结论是:plugin/skills/mem-search/SKILL.md 本就已提交进 git,且就位于 plugin/ 分发目录内——构建脚本 scripts/build-hooks.js 无需(也不应该)复制它,因为技能是源文件而非构建输出。

真正的分发保障来自三条独立链路,任何一条失效都会被其他链路或测试兜住:

  1. marketplace/缓存同步scripts/sync-marketplace.cjs 将完整的 plugin/ 目录同步到 marketplace 与缓存两个路径(对应根 package.jsonsync-marketplace 脚本,可加 --force 强制);
  2. npm 发布面:根 package.jsonfiles 字段(第 35–52 行)显式包含 plugin/skillsplugin/hooksplugin/scripts/*.jsplugin/scripts/*.cjsplugin/.claude-pluginplugin/package.jsonplugin/bun.lock 等条目,npm 发布时这些内容随包体走;
  3. 约定式发现plugin.json 并不枚举技能文件,Claude Code 按 skills/*/SKILL.md 约定自动发现——这解释了为什么"构建不复制"是正确的,也说明分发完整性只依赖目录布局正确。

在此之上,修复又加了两道防回归的保险:

  • 构建期验证:在 scripts/build-hooks.js 中新增检查,若 plugin/skills/mem-search/SKILL.mdplugin/hooks/hooks.jsonplugin/.claude-plugin/plugin.json 任一缺失,构建直接失败;
  • 10 个回归测试tests/infrastructure/plugin-distribution.test.ts 覆盖技能文件存在性、YAML frontmatter 合法性(必须以 --- 开头且含 name:description:)、三层工作流文档完整性(搜索 search / timeline / get_observations 三个关键词)、必需分发文件清单、hooks.jsonCLAUDE_PLUGIN_ROOT 引用的一致性、package.jsonfiles 字段,以及构建脚本验证步骤本身。从该测试文件的源码结构看,它还会读取 plugin/hooks/hooks.jsonplugin/hooks/codex-hooks.json 中所有 type: "command" 的 hook 命令,逐一核对路径引用,属于典型的"契约型"分发测试。

五、SQLite 迁移失败(#979):两套并行迁移系统争夺同一张 schema_versions

这是本修复周期中根因最深刻的一项,值得单独展开。

5.1 根因:版本号冲突 + maxApplied > 0 门控

当时库中存在两套并行的迁移系统:旧 DatabaseManager 的迁移 1–7,与新 MigrationRunner 的迁移 4–22,两者共用同一张 schema_versions。版本号 5、6、7 发生语义冲突——旧系统的版本 5 是"删除孤儿表",新系统的版本 5 是"给 worker_port 加列"。一旦数据库里预记录了旧系统的版本号,就触发连锁故障:

  • initializeSchema() 内的门控条件(maxApplied > 0 时跳过建表)误判"已经有版本,核心表必然存在",跳过了核心表的创建;
  • 新系统的迁移 5–7 因版本号已存在而被视为"已应用",实际变更从未执行。

最终表现就是:全新数据库初始化失败,或库处于"表存在但列/约束缺失"的半损坏状态。

5.2 四条修复措施

  1. 移除 maxApplied === 0 门控:核心表的创建改为无条件执行 CREATE TABLE IF NOT EXISTS,与版本记录状态解耦。从当前仓库 src/services/sqlite/SessionStore.ts 的源码结构看,这一原则贯穿了后续所有演进——例如迁移 31–35、41(sync_state)、42(sync_outbox)、47–49 等的建表语句均为 CREATE TABLE IF NOT EXISTS ...,且每次应用都以 SELECT version FROM schema_versions WHERE version = ? 查询 + INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?) 写入构成幂等单元(如第 927–935 行处 schema_versionssdk_sessionsobservationssession_summaries 的初始建表)。
  2. 状态驱动而非记录驱动:迁移 5–7 不再只信 schema_versions 记录,而是检查实际数据库状态(列是否存在、约束是否存在)再决定是否执行变更。这让"版本表与真实结构不一致"的历史包袱被状态检查吸收。
  3. 崩溃安全(crash-safety):临时表重建类迁移(7、9、21)在创建 xxx_new 临时表前先 DROP TABLE IF EXISTS xxx_new,防止上一次中途崩溃留下的残留表导致 CREATE TABLE 报错;
  4. 补齐缺失迁移 + FK 级联:把只存在于 SessionStore 中的迁移 21(addOnUpdateCascadeToForeignKeys)补进 MigrationRunner,并在 initializeSchema() 的 FK 定义中加入 ON UPDATE CASCADE

所有改动同时落到 runner.tsSessionStore.ts 两个位置,并新增 13 个回归测试(tests/services/sqlite/migration-runner.test.ts 当时路径),覆盖六大场景:全新数据库初始化、幂等性(连续跑两遍)、版本冲突(预记录旧版本 1–7)、崩溃恢复(残留临时表)、FK 级联约束、数据完整性保持。

5.3 可迁移的工程经验

这段修复给出的通用教训是:任何"版本记录表 + 迁移脚本"体系,都应保证迁移幂等、建表语句使用 IF NOT EXISTS、破坏性/结构变更以真实 schema 状态为准。当系统经历过"多套迁移机制并存"的演化(如 claude-mem 从 DatabaseManager 迁到 MigrationRunner),版本表本身就可能成为不可信的单一事实来源,此时状态检查是唯一可靠的真相。

六、测试收尾:21 个失败用例的修复清单

TRIAGE-07 的最后一项任务是把 npm test 跑绿。文档记录了当日 8 个测试文件中 21 个失败的逐一归因,本身也是一份很好的"测试与实现漂移"案例集:

类别 失败数 根因与修复
服务端健康端点 12 ServerOptions 接口新增了 workerPathgetAiStatus,但 3 个测试文件中的 mock/内联对象未同步,补齐缺失属性
日志规范检查 1 src/services/transcripts/cli.ts 的用户可见 CLI 输出使用 console.log 属合理用法,被误判为后台服务,加入排除模式
MarkdownFormatter 2 源码重构后文案由 "MCP tools" 改为 "mem-search skill" / "claude-mem skill",更新测试断言
SettingsDefaultsManager 1 getBool 用例使用了默认值已变为 'false'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,改用默认 'true'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT
ChromaSync 3 重构为 ChromaMcpManager 单例后,测试仍在断言已不存在的内部 client/transport/connected 属性,改为校验 ChromaMcpManager.ts 源码中的 transport 清理逻辑
OpenClaw 2 测试预期的 memory_ 工具跳过与响应截断功能源码缺失,在 openclaw/src/index.ts 中补上 memory_ 前缀检查(防递归观察循环)与 MAX_TOOL_RESPONSE_LENGTH = 1000 截断

最终结果为 1008 通过、0 失败、3 跳过,共 57 个文件。注意其中"OpenClaw 两项"是测试先于实现的反向证据:测试把预期行为写死后,源码补齐了功能——这也提示维护者,断言与实现谁先漂移都应视为缺陷。

七、小结:安装分发的四道防线

把 TRIAGE-07 的修复串联起来,可以抽象出 claude-mem 在安装分发上的四层防御,每一层都能对应当前的仓库证据:

  1. 依赖面最小化——plugin/package.json 只声明 hook 运行时真实 import 的包,根 package.json//dependencies-note 固化"bundler 存活依赖才进 dependencies"的归类规则;发布工具(np)严格留在 devDependencies;
  2. 路径与安装方式无关——resolveRoot()CLAUDE_PLUGIN_ROOT 为首选来源多级回退,分发版与开发版共用同一解析实现,杜绝硬编码用户目录;
  3. 分发完整性多层保障——sync-marketplace.cjs 同步、npm files 白名单、skills/*/SKILL.md 约定式发现,再由构建期校验 + 契约型测试(tests/infrastructure/plugin-distribution.test.ts)兜底;
  4. 安装后校验与幂等持久层——verifyCriticalModules() 以真实模块解析(含 bin-only 包与 zod 子路径导出)在装完即验、失败即响;SQLite 侧则以 CREATE TABLE IF NOT EXISTS + 状态检查保证全新库与历史库都能收敛到一致 schema。

这套"预防 + 校验 + 幂等 + 测试"的组合,是把"新用户装不上"这类首印问题从偶发事故变成可回归、可审计的工程流程的关键。

登录后查看全文
热门项目推荐
相关项目推荐