首页
/ claude-mem Windows 平台支持实战:从 WMIC 移除、uvx 启动修复到 FTS5 优雅降级

claude-mem Windows 平台支持实战:从 WMIC 移除、uvx 启动修复到 FTS5 优雅降级

2026-09-05 18:45:49作者:齐添朝

本文围绕 claude-mem 的 Windows 平台专项修复记录(Playbook Phase 06),系统讲解该项目在 Windows 上遭遇的四大类核心平台问题——Windows 11 25H2+ 移除 WMIC 导致孤儿进程清理失效、uvx 子进程无法直接 spawn、PowerShell 管道语法在 Git Bash 下被误解析、以及 Bun 运行时 FTS5 扩展可用性不确定——并逐条给出仓库中对应的源码级修复方案:taskkill/Get-CimInstance 替代 WMIC、绝对路径直接 spawn uvx.exe、WQL -Filter 服务端过滤、windowsHide: true 全局纪律,以及 FTS5 运行时探测加搜索降级策略。读完本文,你可以掌握一个跨平台 Node/Bun 项目在 Windows 上做进程管理、子进程启动和数据库能力探测的完整工程范式。

背景:一次覆盖约 20 个 Issue 的平台修复战役

claude-mem 是一个为 AI Agent 提供跨会话持久上下文的工具:它捕获 Agent 在会话中的操作,用 AI 压缩后,再把相关上下文注入未来的会话。其核心组件是一个常驻的 worker daemon(依赖 bun:sqlite),以及一个通过 MCP 拉起的 ChromaDB 向量搜索子进程。这两个"常驻 + 子进程"的设计在 Windows 上恰好踩中了所有平台差异的雷区。

仓库中的修复记录 TRIAGE-06-Windows-Platform-Support.md 明确列出了本阶段解决的问题面:约 20 个 Windows 相关 Issue,其中最高优先级的修复包括:

问题域 关联 Issue 根因
WMIC 被移除 #785 Win11 25H2+ 移除了 wmic,孤儿进程清理(orphan reaper)失效
PowerShell 语法错误 #1024 孤儿清理函数中的 $_ 管道语法出错
uvx 启动失败 #1190、#1192、#1199 uvx.cmd 这类 shim 无法被无 shell 的 spawn() 解析
FTS5 不可用 #791 bun:sqlite 在 Windows 上可能没有 FTS5 扩展
worker 启动 / 控制台弹窗 / Git Bash #1139、#1048、#1062 spawn 缺少 windowsHide;PowerShell 管道中的 $_ 被 Git Bash 解释

该 Playbook 的根因验证部分给出了两条关键判断:MCP SDK v1.26.0 的 StdioClientTransport 不支持 shell: true 选项(因此最初方案是路由到 cmd.exe /c uvx,让 cmd.exe 原生处理 .cmd 扩展名解析与 PATH 查找);而 WMIC 的移除是 Windows 11 25H2+ 的真实平台回归,所有 wmic 用法必须替换。

以下按问题域逐条展开,每条都附仓库中当前生效的源码证据。

问题一:uvx.cmd 无法被 spawn —— MCP SDK 无 shell 选项时的替代路径

问题本质

ChromaDB 的 MCP server 通过 uvx(uv 的包执行器)拉起。在 Windows 上,uvx 实际上是一个 uvx.cmd 批处理 shim,而 Node 的无 shell spawn() 不会PATHEXT 解析,直接 spawn 裸的 uvxuvx.cmd 都会失败。由于 MCP SDK 的 StdioClientTransport 内部固定使用 spawn() 且不提供 shell 选项,问题无法在传输层解决。

仓库中的最终解法

最初 Playbook 记录的方案是"路由到 cmd.exe /c uvx"。但在后续迭代中(Issue #2696 修订),源码演进了一个更彻底的方案:在 Windows 上直接 spawn uvx.exe 的绝对路径,完全绕开 cmd.exe shell 包装

ChromaMcpManager.ts 的源码注释解释了为什么必须放弃 cmd.exe 包装:

cmd.exe 会在 uvx 看到之前,把依赖覆盖规格(如 onnxruntime>=1.20protobuf<7)中的 >/< 解析为 shell 重定向;而 Node 的 child_process 针对 cmd.exe 的参数转义又会破坏预先加引号的参数,最终 cmd.exe 在约 10ms 内以 "The directory name is invalid" 崩溃。

resolveUvxCommand() 的实现策略是:

  • 非 Windows:直接返回 uvx,依赖 PATH;
  • Windows:从 uv 的安装 bin 目录(通过 uvx-bin-dirs.ts 枚举)中解析出 uvx.exe 的绝对路径并直接 spawn;若 uv 的 bin 目录不在 worker 继承的 PATH 中,spawn 环境会显式补上这些目录,保证即使 worker 启动早于用户把 uv 加入 PATH,子进程也能找到 uvx

配套的 isUvxAvailable() 预检(含可注入的 uvxAvailabilityProbe 测试缝)在启动前对候选路径做 stat 校验,避免带着坏路径进入 MCP 启动流程。相关测试见 chroma-windows-lifecycle.test.ts

问题二:WMIC 移除 —— 用 taskkill + Get-CimInstance 重建进程管理能力

Windows 11 25H2+ 移除 wmic 后,claude-mem 的进程管理需要完全迁移到三个现代工具上。从当前源码看,迁移落在两个共享模块中。

2.1 进程树清理:taskkill /T /F

kill-process-tree.ts 是全仓库统一的进程树拆除实现(从 ChromaMcpManager 提取而来,供所有 teardown 路径复用)。选择它的动机在文件头注释中写得很清楚:

Windows 没有进程组,Node 的 process.kill(pid, signal) 只能强杀单个 PID。任何超过一层的 spawn 链(uvx -> uv -> python -> chroma-mcp,或包裹真实二进制的 .cmd shim)都会留下存活的后代进程——它们继承监听套接字,卡死 worker 端口。

Windows 分支的实现要点(kill-process-tree.ts#L138-L165):

await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], {
  timeout: 5_000,
  windowsHide: true
});
  • /T 递归杀整棵子树,/F 强制;
  • 退出码语义精确区分taskkill 在目标不存在时退出码为 128,这是唯一代表"已经死了"的非零状态;代码只把 128 或 stderr 匹配 not found|no running instance|no tasks 的情形当作成功。注释特意说明:不能匹配 could not be terminated 前缀,因为 taskkill 对"实例不存在"和"Access is denied"都输出该前缀——匹配前缀会把访问拒绝吞掉,而"拒绝访问"恰恰是要向上抛出的真实失败。其余任何失败(访问拒绝、超时、/T 遍历卡死)都会抛出 ProcessTreeKillError,确保 server stop 这类调用方不会在杀进程失败时误报成功。

2.2 进程身份识别:Get-CimInstance 替代 wmic 查启动时间

仅杀 PID 在 Windows 上不安全——OS 会回收并重新发放 PID 编号,快照时刻记录的 PID 到杀进程时刻可能已经指向无关进程。process-identity.ts 的注释直接点明了迁移原因:

Windows 没有廉价的 /proc 式启动时间读取,也没有 ps lstart,所以我们 shell 到 PowerShell 的 CIM(wmic 已在 Windows 11 上移除)。

其实现分三层:

  1. 捕获 start tokenqueryWindowsCreationDate(pid) 执行
    (Get-CimInstance Win32_Process -Filter "ProcessId=<pid>").CreationDate.ToString('yyyyMMddHHmmss.ffffff')
    
    注意这里用的正是 Playbook 中提到的 WQL -Filter 服务端过滤,而非 Where-Object { $_ } 客户端管道——这正是同时修复 #1024(PowerShell 语法错误)和 #1062(Git Bash 把 $_ 解释为 shell 变量)的根因级改动。CreationDate 是 CIM DATETIME,在 (pid, 一次开机) 内足够唯一,可用来检测 PID 复用。
  2. 缓存策略:单次 CIM 查询约 100–300ms,因此 token 按 PID 缓存 5 秒(WINDOWS_START_TOKEN_CACHE_TTL_MS);
  3. 校验必须绕过缓存isSameProcess(pid, snapshotToken) 内部故意绕过缓存重新读 OS。注释解释得很尖锐:如果走缓存,快照捕获会填充缓存条目,随后的重新校验读回同一条目,在 5 秒 TTL 内 100% 命中——一个被复用的 PID 会被认证为原进程,然后 taskkill /PID <pid> /T /F 会连坐杀掉一个无关进程及其整棵子树。未导出的无缓存读取器只通过该谓词可达,调用方无法拿到裸探针用于其他位置。

2.3 进程表枚举:一次查询同时拿到父子关系与身份

kill-process-tree.ts#L414-L447readProcessTableWindows() 用一条 PowerShell 命令一次性取回全表:

Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,
  @{Name='StartToken';Expression={$_.CreationDate.ToString('yyyyMMddHHmmss.ffffff')}} |
ConvertTo-Csv -NoTypeInformation

这里的设计考量值得注意:枚举 PID 后再逐个探测 token 比不检查还糟——如果探测间隙 PID 退出并被重新发放,探测拿到的是替代进程的 token,后续比对等于拿替代进程和自己比,必然"认证通过",反而给无关进程发了击杀许可证。把父子关系(ParentProcessId)和身份(CreationDate)放进同一次观察,才是原子的。CSV 的 StartToken 格式与 captureProcessStartToken() 的格式逐字节一致,这一"约定"由测试断言而非假设。

collectDescendantIdentities() 随后做自底向上的子树遍历(叶子在前),POSIX 分支用 /proc(Linux,stat 的 starttime 字段)或 ps -eo pid=,ppid=,lstart=(macOS,LC_ALL=C 固定 locale 防止本地化日期导致比对失败),Windows 分支即上述 CIM 全表查询。

问题三:控制台窗口弹窗(#1048)与 Git Bash 兼容(#1062)

windowsHide 作为 spawn 纪律

Playbook 要求"给 Windows 上所有 exec/spawn 调用加 windowsHide: true"。从源码看这条纪律已经渗透到全部平台敏感调用点,例如:

  • ProcessManager.ts#L37-L41lookupBinaryInPath():Windows 分支用 where <bin>、其他平台用 which <bin> 做 PATH 查找,execSyncwindowsHide: true
  • kill-process-tree.tstaskkillpspowershell.exe 的每一处 execFileAsync 都带 windowsHide: true
  • process-identity.tsqueryWindowsCreationDate() 与 macOS 分支的 ps -p <pid> -o lstart= 均带 windowsHide: true,且统一经 sanitizeEnv() 做 spawn 环境纪律。

这条纪律有专门的回归测试守护:windows-hide-regressions.test.tsworker-wrapper-windows-hide.test.ts,防止未来新增的调用点漏配。

Windows 分支的额外适配

除弹窗问题外,Windows 分支还有两处源码级适配值得了解:

  1. 超时时钟差异ProcessManager.ts#L181-L184 提供 getPlatformTimeout(),Windows 下对基础超时统一乘 2.0——Windows 上进程启动与 CIM 查询显著更慢(单次查询 100–300ms)。
  2. daemon 启动的引号地狱ProcessManager.ts#L360-L415buildWindowsDaemonStartCommand() 构造
    Start-Process -FilePath '<runtime>' -ArgumentList @('"<scriptPath>"','--daemon') -WindowStyle Hidden
    
    并通过 powershell -NoProfile -EncodedCommand <base64(utf16le)> 传递。注释解释了为什么必须在单引号 PS 字符串内嵌字面双引号:Windows PowerShell 5.1 拼接 -ArgumentList 元素时用裸空格且不自动加引号,%USERPROFILE% 路径中的空格会把脚本路径拆成多个 argv,导致 bun 立即以 "Module not found" 退出(Issue #3195)。-FilePath 参数作为单字符串参数不经过该拼接,可安全裸传。

Git Bash 兼容(#1062)则是上述 WQL -Filter 改动的附带收益:进程查询不再经过含 $_ 的 PowerShell 管道,改用始终在 PATH 中的 tasklist.exe/taskkill.exe 二进制与 WQL 过滤,Git Bash 不再有机会把 $_ 解释成自己的环境变量。

问题四:FTS5 在 Windows + Bun 上的运行时探测与搜索降级(#791)

Playbook 的修复策略是:启动时探测 FTS5 是否可用;不可用时跳过 FTS 建表,搜索降级为 LIKE 结构化查询 + ChromaDB 向量检索——FTS5 只是全量文本搜索的加速层,缺失不应让搜索功能整体瘫痪。

SessionSearch.ts 中的运行时探针实现了一个"建临时表再删"的活性检测:

private isFts5Available(): boolean {
  // 探测:尝试创建临时 FTS5 虚拟表
  this.db.run('CREATE VIRTUAL TABLE _fts5_probe USING fts5(test_column)');
  // ... 成功后删除并返回 true;失败则返回 false
}

构造时执行一次(this._fts5Available = this.isFts5Available()),后续所有 FTS5 建表(observations_ftssession_summaries_fts)在 ensureFTSTables() 中先检查该标志,不可用则静默跳过。Playbook 同时要求在迁移路径上补防御:migrations.ts(migration006)、migrations/runner.tsSessionStore.ts(其中 user_prompts_fts 等 FTS5 表创建)都包了 try/catch 守卫,保证老库升级时在无 FTS5 环境下迁移不中断。

降级后的搜索路径仍然完整:文本全文检索由 ChromaDB 向量搜索承担,结构化过滤走 LIKE 查询(如 SessionStore.ts#L861concepts LIKE '%:%' AND json_valid(concepts) 这类 JSON 过滤),两者均不依赖 FTS5。

验证:测试矩阵与结果

该阶段修复的验证结果记录在 Playbook 末尾,并与仓库测试文件一一对应:

此外,针对 Windows 平台纪律还有专项守护测试:windows-hide-regressions.test.ts(windowsHide 回归)、worker-wrapper-windows-hide.test.ts(wrapper 隐藏窗口)、codex-transcript-watcher-windows.test.tsnpm-install-windows-hide.test.ts

小结:一套可复用的 Windows 平台工程范式

claude-mem 的 Phase 06 修复给出了一条清晰的跨平台进程/存储工程路线,任何在 Windows 上跑常驻进程 + 子进程链 + SQLite 的项目都可以对照借鉴:

  1. 永远不用 WMIC:进程列表用 tasklist /FO CSV /NH,杀进程用 taskkill /PID <pid> /T /F,需要命令行/父 PID 过滤(tasklist 做不到)时用 Get-CimInstance + WQL -Filter 服务端过滤,既避开 $_ 管道语法又天然兼容 Git Bash;
  2. PID 必须配 start token:快照与击杀之间任何 PID 都可能被复用,击杀前用启动时间 token 重新认证,且认证读取必须绕过缓存;
  3. .cmd shim 不能靠 spawn 解析:MCP SDK 无 shell 选项时,解析绝对路径直接 spawn 原生可执行文件(uvx.exe),并警惕 cmd.exe 包装会劫持 >/< 参数;
  4. windowsHide: true 是纪律不是可选项:每一处 exec/spawn/execFile 都要带,并用回归测试防漏配;
  5. 可选扩展要探测 + 降级:FTS5 这类"锦上添花"能力用临时表探测可用性,建表路径全部加守卫,查询路径准备好 LIKE + 向量检索的替代方案。

以上所有实现均可在仓库对应文件中进一步查证,建议从 kill-process-tree.tsprocess-identity.ts 的头部注释读起——两处注释完整保留了每次设计决策的问题背景(关联 Issue 编号),是理解这套 Windows 平台防御体系最直接的入口。

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