修复 Claude Code TypeScript LSP 在 Windows 上启动失败的问题

修复 Claude Code TypeScript LSP 在 Windows 上的 ENOENT 问题

问题现象

在 Windows 上使用 Claude Code 时,LSP(Language Server Protocol)工具无法启动 typescript-language-server,报错:

复制代码
Error performing documentSymbol: ENOENT: no such file or directory, uv_spawn 'typescript-language-server'

根因分析

1. npm 在 Windows 上安装了什么?

当你在 Windows 上安装 typescript-language-server,npm 只会创建以下入口文件:

复制代码
├── typescript-language-server          ← POSIX shell 脚本
├── typescript-language-server.cmd      ← Windows 批处理文件
└── typescript-language-server.ps1      ← PowerShell 脚本

注意:npm 不会创建 .exe 文件。

2. Claude Code 只认 .exe

Claude Code 的 LSP 客户端在 Windows 上启动 Language Server 时,默认只查找 .exe 后缀的可执行文件

它内部调用类似 uv_spawn('typescript-language-server') 的 API。由于目标目录中只有 .cmd.ps1,没有 .exe,进程创建失败,直接抛出:

复制代码
ENOENT: no such file or directory, uv_spawn 'typescript-language-server'

解决方案:创建 PE .exe 包装器

核心思路:编译一个真正的 Windows PE 可执行文件(.exe),让它作为代理,启动同目录下的 typescript-language-server.cmd

Claude Code 要 .exe,npm 只给了 .cmd。我们就自己造一个 .exe,内部代理执行 .cmd,让两边都能满意。

步骤一:准备编译环境

你需要一个 Windows C 编译器。以下任选其一:

方案 A:WinLibs(推荐,解压即用)

  1. 访问 https://winlibs.com
  2. 下载最新 x86_64-...-posix-seh-ucrt-... 版本的 .zip.7z
  3. 解压到 C:\mingw64
  4. C:\mingw64\bin 添加到系统 PATH

方案 B:MSYS2(带包管理器)

  1. 访问 https://www.msys2.org 下载安装

  2. 打开 MSYS2 UCRT64 终端

  3. 执行:

    bash 复制代码
    pacman -Syu
    pacman -S mingw-w64-ucrt-x86_64-gcc
  4. C:\msys64\ucrt64\bin 添加到系统 PATH

验证安装:

bash 复制代码
gcc --version

步骤二:创建包装器源代码

在项目根目录(或任意目录)创建文件 typescript-language-server.c

c 复制代码
/**
 * TypeScript Language Server Windows Launcher
 *
 * 解决 Claude Code 在 Windows 上无法启动 typescript-language-server 的问题。
 * 原理:编译为 .exe 后,Windows CreateProcess 会优先找到 .exe 文件,
 * 本程序再代理执行同目录下的 typescript-language-server.cmd。
 */

#include <windows.h>
#include <string.h>

#define TARGET_CMD "typescript-language-server.cmd"

int main() {
    char exeDir[MAX_PATH];
    char cmdPath[MAX_PATH];
    char commandLine[MAX_PATH * 4];

    // 获取当前 .exe 的完整路径
    char exePath[MAX_PATH];
    DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
    if (len == 0 || len >= MAX_PATH) {
        return 1;
    }

    // 提取 .exe 所在目录
    strcpy(exeDir, exePath);
    char *lastSlash = strrchr(exeDir, '\\');
    if (lastSlash == NULL) {
        lastSlash = strrchr(exeDir, '/');
    }
    if (lastSlash != NULL) {
        *(lastSlash + 1) = '\0';
    } else {
        exeDir[0] = '.';
        exeDir[1] = '\\';
        exeDir[2] = '\0';
    }

    // 拼接目标 .cmd 路径
    int ret = snprintf(cmdPath, sizeof(cmdPath), "%s%s", exeDir, TARGET_CMD);
    if (ret < 0 || ret >= sizeof(cmdPath)) {
        return 1;
    }

    // 检查 .cmd 文件是否存在
    DWORD attr = GetFileAttributesA(cmdPath);
    if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
        return 1;
    }

    // 获取原始命令行参数(去掉程序名本身)
    LPSTR rawCmdLine = GetCommandLineA();
    char *p = rawCmdLine;

    // 跳过第一个 token(程序名),处理带引号和不带引号的情况
    if (*p == '"') {
        p++;
        while (*p && *p != '"') p++;
        if (*p == '"') p++;
    } else {
        while (*p && *p != ' ') p++;
    }
    while (*p == ' ') p++;

    // 构建完整的命令行: cmd.exe /c "path.cmd" [args...]
    ret = snprintf(commandLine, sizeof(commandLine),
                   "cmd.exe /c \"%s\" %s", cmdPath, p);
    if (ret < 0 || ret >= sizeof(commandLine)) {
        return 1;
    }

    // 启动进程
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };

    BOOL success = CreateProcessA(
        NULL,           // 不指定模块名,使用命令行解析
        commandLine,    // 完整命令行
        NULL, NULL,     // 默认安全属性
        TRUE,           // 继承句柄(关键:确保 stdin/stdout/stderr 透传)
        0,              // 创建标志
        NULL,           // 使用父进程环境
        NULL,           // 使用父进程目录
        &si,
        &pi
    );

    if (!success) {
        return 1;
    }

    // 等待进程结束并透传退出码
    WaitForSingleObject(pi.hProcess, INFINITE);

    DWORD exitCode = 0;
    GetExitCodeProcess(pi.hProcess, &exitCode);

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return (int)exitCode;
}

步骤三:编译

在 Git Bash 或 CMD 中执行:

bash 复制代码
gcc -O2 -s -o typescript-language-server.exe typescript-language-server.c

参数说明:

  • -O2:优化级别 2
  • -s:去除符号表,减小体积
  • -o:指定输出文件名

编译后你会得到一个约 17 KBtypescript-language-server.exe 文件。

步骤四:全局安装并定位

1. 全局安装 typescript-language-server

bash 复制代码
npm install -g typescript-language-server

2. 找到实际安装位置

在 CMD 或 PowerShell 中执行:

cmd 复制代码
where typescript-language-server

输出示例:

第一个是无扩展名的 POSIX 脚本,第二个是 .cmd 批处理文件。记下这个目录路径(比如 C:\Users\xxx\AppData\Roaming\npm)。

3. 复制 .exe 包装器到该目录

部署后该目录结构:

复制代码
%APPDATA%\npm\
├── typescript-language-server          ← POSIX shell 脚本(npm 原文件)
├── typescript-language-server.cmd      ← Windows 批处理(npm 原文件)
├── typescript-language-server.exe      ← ← 我们的包装器(新增)
└── typescript-language-server.ps1      ← PowerShell 脚本(npm 原文件)

为什么推荐全局安装? 全局安装一次,所有项目都能用,不需要每个项目重复复制。而且 typescript-language-server 作为 TypeScript 的语言服务,本来就是全局工具,不依赖特定项目的 node_modules

步骤五:验证

1. 基础启动测试

cmd 复制代码
typescript-language-server.exe --version
# 预期输出: 5.3.0(或你安装的版本号)

2. LSP JSON-RPC 协议测试

使用 Node.js 发送标准的 LSP 初始化请求:

bash 复制代码
node -e "
const msg = JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{processId:null,rootUri:'file:///E:/your-project',capabilities:{}}});
process.stdout.write('Content-Length: ' + Buffer.byteLength(msg) + '\r\n\r\n' + msg);
setTimeout(() => {
  const s = JSON.stringify({jsonrpc:'2.0',id:2,method:'shutdown'});
  const e = JSON.stringify({jsonrpc:'2.0',method:'exit'});
  process.stdout.write('Content-Length: ' + Buffer.byteLength(s) + '\r\n\r\n' + s);
  process.stdout.write('Content-Length: ' + Buffer.byteLength(e) + '\r\n\r\n' + e);
}, 2000);
" | typescript-language-server.exe --stdio

预期能看到类似以下的 JSON-RPC 响应:

json 复制代码
Content-Length: 206

{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Using Typescript version (workspace) 6.0.3 from path \"...\""}}
Content-Length: 1593

{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":2,"completionProvider":...,"definitionProvider":true,...}}}
Content-Length: 38

{"jsonrpc":"2.0","id":2,"result":null}

3. 在 Claude Code 中测试

重启 Claude Code,尝试使用 TypeScript LSP(如代码跳转、类型提示、符号搜索等),不再报 ENOENT 即说明修复成功。

原理总结

层级 行为
npm 在 Windows 上只创建 .cmd / .ps1,不创建 .exe
Claude Code LSP 在 Windows 上只识别 .exe 后缀的可执行文件
修复前 两边不匹配:Claude Code 找 .exe,npm 只给了 .cmdENOENT
修复后 自己提供 .exe:Claude Code 找到 .exe.exe 代理执行 .cmd → LSP 正常工作

常见问题

Q: 为什么不做成通用的 LSP 启动器?

A: 硬编码 typescript-language-server.cmd 更可靠。如果做成根据 .exe 自身文件名推导 .cmd 的通用版本,当文件名不规则时可能出错。每个 LSP 单独编译一个 .exe 虽然多几个文件,但逻辑最简单、最稳定。

Q: 可以用符号链接代替 .exe 吗?

A: 不能 。Windows CreateProcess 在解析 .exe 符号链接时,会检查目标文件是否为有效的 PE 格式。如果目标是 .cmd 批处理文件,CreateProcess 会拒绝执行。

Q: 这个方案对其他 LSP(如 Vue、Rust、Go)也有效吗?

A: 有效 。只要 Claude Code 在 Windows 上对该 LSP 也只认 .exe,而 npm 只提供了 .cmd,就可以用同样的方法解决。只需把 TARGET_CMD 宏改成对应的 .cmd 文件名,重新编译即可。

Q: 需要管理员权限吗?

A: 不需要 。编译、复制到 node_modules/.bin/ 都是普通用户权限即可完成的操作。

参考

相关推荐
a58808112 小时前
【nano11】Windows 11_25H2_26200.5074_极致精简版介绍与安装教程
windows
comcoo2 小时前
避坑指南:OpenClaw v2.7.9 Windows/macOS 零基础安装全过程
人工智能·windows·macos·github·开源软件·open claw·open claw部署包
惢雨2 小时前
ts中的特殊符号说明并举例,如 ?. 、?:、??等
前端·typescript
海棠AI实验室3 小时前
AI 时代文献综述:从检索到成稿的 RAG 五步法
windows·算法·自动化·llm·rag
取名好樊3 小时前
Windows Docker PostgreSQL 端口绑定失败问题记录
windows·docker·postgresql
c++之路3 小时前
CMake 系列教程(三):变量、条件与控制流
java·windows·spring
百事牛科技3 小时前
Word只打需要的部分:4种打印范围设置方法
windows·word
sun00770013 小时前
SniffMaster(读取苹果的ats文件) 和 wireshark
windows