claude-mem 安装与分发体系解析:从 TRIAGE-07 看插件首装失败的根因与修复
claude-mem 通过 Claude Code 插件机制分发给最终用户,"首次安装能否成功"直接决定了新用户的第一印象。本文基于仓库内的问题修复记录 TRIAGE-07-Installation-Distribution,系统梳理该修复周期解决的六类安装问题(对应 issue #1128、#1166、#1187、#1156、#979、#1041),并结合 plugin/package.json、src/npx-cli/install/setup-runtime.ts、tests/infrastructure/plugin-distribution.test.ts 等仓库源码,还原每项修复的实现细节与回归验证方式。读完本文,你可以掌握:插件运行时依赖的审计方法、基于真实模块解析的 post-install 校验机制、三层分发链路,以及 SQLite 双迁移系统版本冲突的根治思路。
一、问题背景:新用户首装失败的三类典型症状
TRIAGE-07 文档开篇即列出了新用户遇到的安装失败场景,这三类症状分别对应分发链路上的不同环节:
- 缓存式安装后缺少
node_modules(issue #1128、#1166):插件从缓存目录提取到 marketplace 目录后,运行时依赖没有被安装,worker 和 hooks 脚本一启动就报Cannot find module,插件完全无法工作; skills/目录未被复制(issue #1187):安装完成后plugin/skills/mem-search/SKILL.md缺失,导致 Claude Code 无法按约定发现 mem-search 技能;np(npm 发布工具)被错误地列为运行时依赖(issue #1156):发布工具只应在开发机上使用,出现在运行时依赖中会让消费者下载无用的包;- 另外还涉及 迁移失败(issue #979,全新数据库初始化即报错)和 marketplace not found(issue #1041,插件根路径解析失败)。
修复记录中每项任务都附带了验证结论("Verified 2026-02-23" / "Fixed 2026-02-23"),这是理解下文各节的事实基础。
二、依赖审计:plugin/package.json 里只允许放真正的运行时依赖
修复的第一个动作是审计依赖面:将 plugin/package.json 的 dependencies 与 plugin/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。
注意 overrides 将 tree-sitter 固定到 ^0.25.0,并把 tree-sitter-cli 列入 trustedDependencies(它带 native 构建,需要 npm 信任才执行 install 脚本);engines 要求 node >= 20.12.0、bun >= 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(),采用多级回退策略解析插件根目录:
- 优先读取环境变量
CLAUDE_PLUGIN_ROOT—— Claude Code 为所有 hook 进程注入该变量,是最可靠的来源; - 回退到脚本自身位置(
import.meta.url推导); - 再回退到 XDG 路径与旧版(legacy)路径。
这一思路在仓库中仍有体现:plugin/hooks/hooks.json、plugin/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.json 的 exports 映射,而不是宿主环境的模块路径。这保证校验的是"用户机器上这份安装"的闭包完整性。
(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 无需(也不应该)复制它,因为技能是源文件而非构建输出。
真正的分发保障来自三条独立链路,任何一条失效都会被其他链路或测试兜住:
- marketplace/缓存同步:scripts/sync-marketplace.cjs 将完整的
plugin/目录同步到 marketplace 与缓存两个路径(对应根package.json的sync-marketplace脚本,可加--force强制); - npm 发布面:根 package.json 的
files字段(第 35–52 行)显式包含plugin/skills、plugin/hooks、plugin/scripts/*.js、plugin/scripts/*.cjs、plugin/.claude-plugin、plugin/package.json、plugin/bun.lock等条目,npm 发布时这些内容随包体走; - 约定式发现:
plugin.json并不枚举技能文件,Claude Code 按skills/*/SKILL.md约定自动发现——这解释了为什么"构建不复制"是正确的,也说明分发完整性只依赖目录布局正确。
在此之上,修复又加了两道防回归的保险:
- 构建期验证:在 scripts/build-hooks.js 中新增检查,若
plugin/skills/mem-search/SKILL.md、plugin/hooks/hooks.json或plugin/.claude-plugin/plugin.json任一缺失,构建直接失败; - 10 个回归测试:tests/infrastructure/plugin-distribution.test.ts 覆盖技能文件存在性、YAML frontmatter 合法性(必须以
---开头且含name:与description:)、三层工作流文档完整性(搜索search/timeline/get_observations三个关键词)、必需分发文件清单、hooks.json中CLAUDE_PLUGIN_ROOT引用的一致性、package.json的files字段,以及构建脚本验证步骤本身。从该测试文件的源码结构看,它还会读取plugin/hooks/hooks.json与plugin/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 四条修复措施
- 移除
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_versions、sdk_sessions、observations、session_summaries的初始建表)。 - 状态驱动而非记录驱动:迁移 5–7 不再只信
schema_versions记录,而是检查实际数据库状态(列是否存在、约束是否存在)再决定是否执行变更。这让"版本表与真实结构不一致"的历史包袱被状态检查吸收。 - 崩溃安全(crash-safety):临时表重建类迁移(7、9、21)在创建
xxx_new临时表前先DROP TABLE IF EXISTS xxx_new,防止上一次中途崩溃留下的残留表导致CREATE TABLE报错; - 补齐缺失迁移 + FK 级联:把只存在于
SessionStore中的迁移 21(addOnUpdateCascadeToForeignKeys)补进MigrationRunner,并在initializeSchema()的 FK 定义中加入ON UPDATE CASCADE。
所有改动同时落到 runner.ts 与 SessionStore.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 接口新增了 workerPath 与 getAiStatus,但 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 在安装分发上的四层防御,每一层都能对应当前的仓库证据:
- 依赖面最小化——
plugin/package.json只声明 hook 运行时真实 import 的包,根package.json用//dependencies-note固化"bundler 存活依赖才进 dependencies"的归类规则;发布工具(np)严格留在 devDependencies; - 路径与安装方式无关——
resolveRoot()以CLAUDE_PLUGIN_ROOT为首选来源多级回退,分发版与开发版共用同一解析实现,杜绝硬编码用户目录; - 分发完整性多层保障——
sync-marketplace.cjs同步、npmfiles白名单、skills/*/SKILL.md约定式发现,再由构建期校验 + 契约型测试(tests/infrastructure/plugin-distribution.test.ts)兜底; - 安装后校验与幂等持久层——
verifyCriticalModules()以真实模块解析(含 bin-only 包与 zod 子路径导出)在装完即验、失败即响;SQLite 侧则以CREATE TABLE IF NOT EXISTS+ 状态检查保证全新库与历史库都能收敛到一致 schema。
这套"预防 + 校验 + 幂等 + 测试"的组合,是把"新用户装不上"这类首印问题从偶发事故变成可回归、可审计的工程流程的关键。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00