跳转到内容

LSP 源码解读

模块概述

LSP(Language Server Protocol)模块让 OpenCode 获得了 IDE 级别的代码理解能力。通过标准化的 LSP 协议,OpenCode 可以获取精确的诊断信息(编译错误、类型错误、lint 警告)、跳转到定义、查找引用、获取悬浮信息、浏览符号层级。这让 AI 助手基于精确的代码语义做出决策,而非简单的文本匹配。

模块位于 packages/opencode/src/lsp/,由 5 个文件组成,总共约 2900 行代码。核心入口 LSP 命名空间采用 Effect Service 架构,通过 Instance.state() 管理全局单例状态。模块内置 30+ 种 Language Server 的定义(TypeScript、Gopls、Rust Analyzer、Pyright、Clangd、Lua LS 等),每种服务器有自己的项目根目录检测逻辑和 spawn 实现。

核心设计选择:

  • Effect Service 模式LSP 命名空间通过 ServiceMap.Service 注册为全局服务,Instance.state() 管理生命周期
  • 智能客户端复用getClients(file)(root, serverID) 去重,同一项目只启动一个服务器实例
  • 去抖诊断收集waitForDiagnostics() 使用 150ms 去抖 + 3 秒超时,避免 Agent 阻塞
  • 自动下载与隔离:Language Server 二进制文件自动下载到 Global.Path.bin,通过 Flag.OPENCODE_DISABLE_LSP_DOWNLOAD 控制

关键文件

文件路径行数职责
src/lsp/index.ts~559模块主入口:Effect Service 定义、InstanceState 状态管理、getClients 智能匹配、所有公共 API
src/lsp/client.ts~253LSP 客户端实现:vscode-jsonrpc 连接管理、诊断收集、文件版本追踪、连接关闭
src/lsp/server.ts~196830+ 种 Language Server 定义:LSPServer.Info 接口、NearestRoot 高阶函数、spawn 实现与自动下载
src/lsp/launch.ts~22spawn 辅助函数:包装 Process.spawn,简化服务器启动代码
src/lsp/language.ts~120LANGUAGE_EXTENSIONS 映射表:文件扩展名(如 .tsx)→ LSP languageId(如 typescriptreact

类型体系

Range 与 Diagnostic — 位置与诊断

// 文本范围(行号 + 字符偏移)
export const Range = z.object({
  start: z.object({ line: z.number(), character: z.number() }),
  end: z.object({ line: z.number(), character: z.number() }),
})

// 符号信息(用于 documentSymbol / workspaceSymbol)
export const Symbol = z.object({
  name: z.string(),
  kind: z.number(),       // LSP SymbolKind 枚举值
  location: z.object({
    uri: z.string(),
    range: Range,
  }),
})

// 服务器连接状态
export const Status = z.object({
  id: z.string(),         // 服务器唯一标识
  name: z.string(),       // 人类可读名称
  root: z.string(),       // 项目根目录
  status: z.union([
    z.literal("connected"),
    z.literal("error"),
  ]),
})

InstanceState — 全局状态

// LSP 模块的全局状态结构
{
  clients: LSPClient.Info[],                 // 所有活跃的客户端连接
  servers: Record<string, LSPServer.Info>,   // 已注册的服务器定义(key = serverID)
  broken: Set<string>,                       // 已知失败的服务器 ID,避免重复尝试
  spawning: Map<string, Promise<LSPClient.Info | undefined>>,  // 正在启动的客户端(去重)
}

关键字段说明:

  • clients:所有活跃的 LSP 客户端连接。每个客户端绑定到一个 (root, serverID) 组合
  • servers:从内置定义 + 用户配置合并后的服务器注册表。key 是服务器 ID(如 "typescript""gopls"
  • broken:启动失败的服务器 ID 集合。一旦进入此集合,后续 getClients() 直接跳过,不再重试
  • spawning:正在启动中的客户端 Promise。同一 (root, serverID) 的并发请求会共享同一个 Promise,避免重复启动

LSPServer.Info — 服务器定义

// server.ts 中每种 Language Server 的定义接口
interface Info {
  id: string              // 唯一标识,如 "typescript"、"gopls"、"rust-analyzer"
  extensions: string[]    // 关联的文件扩展名,如 [".ts", ".tsx"]
  root: RootFunction      // 确定项目根目录的函数
  spawn(root: string): Promise<Handle | undefined>  // 启动服务器进程
}

LSPClient.Info — 客户端实例

// client.ts 中客户端的核心结构
{
  serverID: string                           // 对应的服务器 ID
  root: string                               // 项目根目录
  connection: MessageConnection              // vscode-jsonrpc 消息连接
  diagnostics: Map<string, Diagnostic[]>     // 文件路径 → 诊断列表
  files: Record<string, number>              // 文件路径 → 版本号(递增计数器)
  process: ChildProcess                      // 服务器子进程
}

核心流程

getClients — 智能文件-客户端匹配

getClients(file) 是 LSP 模块最核心的函数,负责为给定文件找到或创建合适的 LSP 客户端。完整逻辑分为 8 个步骤

// index.ts — 简化后的核心逻辑
async function getClients(file: string): Promise<LSPClient.Info[]> {
  const state = await instanceState()
  const results: LSPClient.Info[] = []

  // 步骤 1:检查文件是否在 Instance 目录内
  // 不在项目目录内的文件不处理
  if (!file.startsWith(instance.directory)) return results

  // 步骤 2:提取文件扩展名
  const ext = path.extname(file)  // 如 ".ts"、".go"

  // 步骤 3:遍历所有注册服务器,检查扩展名匹配
  for (const [serverID, server] of Object.entries(state.servers)) {
    if (!server.extensions.includes(ext)) continue

    // 步骤 4:调用 server.root(file) 确定项目根目录
    const root = server.root(file)
    if (!root) continue  // 该文件不属于任何项目

    // 步骤 5:检查 broken 集合,跳过已知失败的服务器
    if (state.broken.has(serverID)) continue

    // 步骤 6:查找已有客户端(root + serverID 匹配)
    const existing = state.clients.find(
      c => c.root === root && c.serverID === serverID
    )
    if (existing) {
      results.push(existing)
      continue
    }

    // 步骤 7:检查 spawning Map 处理并发请求
    const spawnKey = `${root}:${serverID}`
    const spawning = state.spawning.get(spawnKey)
    if (spawning) {
      const client = await spawning
      if (client) results.push(client)
      continue
    }

    // 步骤 8:新建客户端并存入缓存
    const promise = LSPClient.create({ serverID, server: server.handle, root })
      .catch(() => {
        state.broken.add(serverID)  // 启动失败 → 加入 broken 集合
        return undefined
      })
    state.spawning.set(spawnKey, promise)
    const client = await promise
    state.spawning.delete(spawnKey)
    if (client) {
      state.clients.push(client)
      results.push(client)
    }
  }

  return results
}

这段逻辑实现了完整的客户端复用与去重

  • 同一项目下不会启动两个相同类型的 Language Server
  • 并发的 getClients 调用共享同一个 spawn Promise
  • 失败的服务器被记入 broken 集合,不再重试

LSPClient.create — JSON-RPC 连接建立

LSPClient.create() 是客户端创建的核心函数,建立与 Language Server 的通信通道:

// client.ts — 核心流程(简化)
async function create(input: {
  serverID: string
  server: LSPServer.Handle
  root: string
}): Promise<Info> {
  // 1. 启动服务器子进程
  const handle = await input.server.spawn(input.root)
  if (!handle) throw new Error("spawn failed")

  // 2. 建立 JSON-RPC 消息连接
  const connection = createMessageConnection(
    new StreamMessageReader(handle.process.stdout),
    new StreamMessageWriter(handle.process.stdin),
  )

  // 3. 启动消息监听
  connection.listen()

  // 4. 发送 initialize 请求(超时 45 秒)
  const initResult = await withTimeout(
    connection.sendRequest("initialize", {
      rootUri: pathToUri(input.root),
      capabilities: {
        textDocument: {
          synchronization: { didOpen: true, didChange: true, didClose: true },
          publishDiagnostics: { relatedInformation: true },
        },
        workspace: { configuration: true },
      },
    }),
    45_000,
  )

  // 5. 发送 initialized 通知
  connection.sendNotification("initialized", {})

  // 6. 创建客户端实例
  return {
    serverID: input.serverID,
    root: input.root,
    connection,
    process: handle.process,
    diagnostics: new Map(),
    files: {},
  }
}

连接建立后,客户端监听 textDocument/publishDiagnostics 通知并自动更新诊断映射。

诊断收集流程

诊断收集是 LSP 模块最核心的输出,直接影响 Agent 的代码修复能力。完整流程:

1. Agent 编辑文件
   └─ LSP.touchFile(filePath, content, true)

2. getClients(file) 匹配合适的客户端
   └─ 按扩展名 → 服务器 → root → 复用/新建客户端

3. 客户端发送文件变更通知
   ├─ 首次打开? → textDocument/didOpen
   └─ 已打开?   → textDocument/didChange (带版本号)

4. Language Server 分析代码后推送诊断
   └─ textDocument/publishDiagnostics 通知

5. 客户端更新 diagnostics Map
   └─ TypeScript 服务器特殊:首次诊断不发布事件(!exists 检查)

6. 通过 Bus 广播给上层
   └─ Bus.publish(Event.Diagnostics, { ... })

waitForDiagnostics 去抖机制

// client.ts — 诊断等待(简化)
async function waitForDiagnostics(
  client: LSPClient.Info,
  uri: string,
): Promise<Diagnostic[]> {
  const DIAGNOSTICS_DEBOUNCE_MS = 150   // 去抖间隔
  const DIAGNOSTICS_TIMEOUT_MS = 3000   // 总超时

  return new Promise((resolve) => {
    let timer: ReturnType<typeof setTimeout>

    // 订阅诊断事件
    const unsub = Bus.subscribe(Event.Diagnostics, (event) => {
      // 只关心当前文件
      if (event.uri !== uri) return

      // 收到诊断后重置计时器(去抖)
      clearTimeout(timer)
      timer = setTimeout(() => {
        unsub()
        resolve(client.diagnostics.get(uri) ?? [])
      }, DIAGNOSTICS_DEBOUNCE_MS)
    })

    // 总超时保护:3 秒后强制返回
    setTimeout(() => {
      unsub()
      clearTimeout(timer)
      resolve(client.diagnostics.get(uri) ?? [])
    }, DIAGNOSTICS_TIMEOUT_MS)
  })
}

150ms 去抖的原因:Language Server 通常先发送语法诊断,稍后发送语义诊断。去抖确保 Agent 拿到的是完整的诊断结果,而非中间状态。3 秒总超时避免 Agent 无限等待。

服务器定义与 NearestRoot

server.ts 是模块中最大的文件(~1968 行),定义了 30+ 种 Language Server。每种服务器的核心是两个函数:root()spawn()

NearestRoot 高阶函数

// server.ts — 项目根目录检测(简化)
function NearestRoot(
  files: string[],      // 目标文件名列表,如 ["package.json", "tsconfig.json"]
  exclude?: string[],   // 排除模式,如 ["deno.json"]
): (file: string) => string | undefined {
  return (file: string) => {
    // 从文件所在目录向上搜索直到 Instance.directory
    let dir = path.dirname(file)
    while (dir.startsWith(instance.directory)) {
      // 检查目标文件是否存在
      for (const name of files) {
        const target = path.join(dir, name)
        if (fs.existsSync(target)) {
          // 检查排除模式
          if (exclude?.some(ex => fs.existsSync(path.join(dir, ex)))) {
            continue  // 命中排除规则,跳过此目录
          }
          return dir
        }
      }
      dir = path.dirname(dir)
    }
    return undefined
  }
}

这个高阶函数支持 monorepo 场景:不同子目录下的文件可以匹配到不同的项目根目录,从而启动独立的 Language Server 实例。排除模式用于处理特殊情况(如 TypeScript 服务器排除 deno.json,避免在 Deno 项目中启动错误的 TS 服务器)。

内置服务器概览(30+ 种):

类别服务器根目录检测策略
前端TypeScript, Deno, Vue, Astro, SvelteNearestRoot(["package.json", "tsconfig.json"], ["deno.json"])
系统语言Gopls, Rust Analyzer, Clangd, SourceKit (Swift)NearestRoot(["go.mod"]) / Cargo.toml / compile_commands.json / Package.swift
脚本语言Pyright, JDTLS (Java), KotlinLS, Rubocop (Ruby), JuliaLSNearestRoot(["pyproject.toml", "setup.py"]) / pom.xml / build.gradle
函数式语言LuaLS, Zls (Zig), Gleam, Ocaml, HLS (Haskell)NearestRoot([".luarc.json"]) / build.zig / gleam.toml / dune / hie.yaml
基础设施TerraformLS, DockerfileLS, YamlLS, NixdNearestRoot(["*.tf"]) / Dockerfile / *.yml / flake.nix
数据/查询Prisma, TexLab (LaTeX), SQLSNearestRoot(["*.prisma"]) / *.tex / *.sql
其他Biome, ESLint, Dart, Clojure, PHPIntelephense, CSharp, FSharp各自的配置文件检测
排版Tinymist (Typst)NearestRoot(["*.typ"])

自动下载逻辑

部分 Language Server(如 Biome、Deno、Ty)支持自动下载,流程如下:

// server.ts — 自动下载(简化)
async function spawnWithDownload(input: {
  command: string
  args: string[]
  url: string    // 下载地址模板
}): Promise<Handle | undefined> {
  // 检查标志位
  if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) {
    // 只尝试本地查找,不下载
    return Process.spawn(input.command, input.args)
  }

  // 尝试本地启动
  const local = await Process.spawn(input.command, input.args)
  if (local) return local

  // 本地不存在 → 自动下载到 Global.Path.bin
  const binPath = path.join(Global.Path.bin, input.command)
  if (!fs.existsSync(binPath)) {
    await download(input.url, binPath)
  }

  return Process.spawn(binPath, input.args)
}

代码导航 API

LSP 模块提供完整的代码导航能力,所有 API 通过 run() 辅助函数路由到匹配的客户端:

// index.ts — run 辅助函数(简化)
async function run<T>(
  file: string,
  fn: (client: LSPClient.Info, uri: string) => Promise<T>,
): Promise<T[]> {
  const clients = await getClients(file)
  const uri = pathToUri(file)
  return Promise.all(clients.map(c => fn(c, uri)))
}
APILSP Method说明
definition(file, position)textDocument/definition从引用跳转到定义,返回 Location[]
references(file, position)textDocument/references查找符号的所有引用位置
hover(file, position)textDocument/hover获取符号的类型信息和文档
workspaceSymbol(query)workspace/symbol全局符号搜索,过滤 kind 5/6/11/12 等,最多 10 条
documentSymbol(uri)textDocument/documentSymbol文档内符号列表,返回 DocumentSymbolSymbol
incomingCalls(file, position)两步请求prepareCallHierarchycallHierarchy/incomingCalls
outgoingCalls(file, position)两步请求prepareCallHierarchycallHierarchy/outgoingCalls

workspaceSymbol 的过滤逻辑:

// index.ts — workspaceSymbol(简化)
async function workspaceSymbol(query: string): Promise<Symbol[]> {
  const allClients = await allClients()
  const results: Symbol[] = []

  for (const client of allClients) {
    const symbols = await client.connection.sendRequest(
      "workspace/symbol",
      { query },
    )
    for (const sym of symbols) {
      // 过滤到关键类型:Class(5), Function(6/12), Method(6), Enum(10)
      if ([5, 6, 11, 12].includes(sym.kind)) {
        results.push(sym)
      }
    }
  }

  // 最多返回 10 条,避免上下文膨胀
  return results.slice(0, 10)
}

调用链示例

链路 1:Agent 编辑文件 → 诊断收集 → Agent 修复

1. Agent 执行 EditTool 修改文件
   └─ SessionProcessor 处理 tool-result 事件

2. Agent 调用 LSP.touchFile(filePath, newContent, true)
   ├─ getClients(file)
   │  ├─ 提取扩展名: ".ts"
   │  ├─ 匹配服务器: Typescript (extensions 包含 ".ts")
   │  ├─ root(file) → NearestRoot(["package.json"]) → "/project/root"
   │  ├─ 查找已有客户端: root="/project/root", serverID="typescript" → 命中
   │  └─ 返回 [existingClient]
   └─ client.notify.didChange({ uri, version: 3, content: newContent })

3. Language Server 分析变更后的文件
   └─ 推送 textDocument/publishDiagnostics 通知

4. 客户端更新 diagnostics Map
   └─ Bus.publish(Event.Diagnostics, { uri, diagnostics: [...] })

5. Agent 调用 LSP.waitForDiagnostics(file)
   ├─ 订阅 Diagnostics 事件
   ├─ 收到通知 → 启动 150ms 去抖计时器
   ├─ 150ms 内无新诊断 → resolve
   └─ 返回 [{ message: "Type 'string' is not assignable to 'number'", range: {...} }]

6. Agent 根据诊断决定是否继续修复
   └─ SessionPrompt.loop() 下一次迭代中,Agent 看到诊断结果并决定修复策略

链路 2:首次打开文件 → 服务器启动 → 诊断就绪

1. Agent 首次读取 "main.go"
   └─ LSP.touchFile("main.go", content, true)

2. getClients("main.go")
   ├─ 扩展名: ".go"
   ├─ 匹配服务器: Gopls (extensions 包含 ".go")
   ├─ root("main.go") → NearestRoot(["go.mod"]) → "/project/root"
   ├─ 查找已有客户端: 无匹配(首次打开)
   ├─ 检查 spawning Map: 无(首次)
   └─ 新建客户端:
      ├─ LSPClient.create({ serverID: "gopls", root: "/project/root" })
      │  ├─ server.spawn(root) → Process.spawn("gopls", ["serve"])
      │  ├─ createMessageConnection(stdout, stdin)
      │  ├─ connection.sendRequest("initialize", { rootUri, capabilities })
      │  ├─ connection.sendNotification("initialized", {})
      │  └─ 返回新客户端
      ├─ state.clients.push(newClient)
      └─ 返回 [newClient]

3. 新客户端发送 textDocument/didOpen
   └─ connection.sendNotification("textDocument/didOpen", { textDocument: { uri, languageId: "go", version: 1, text: content } })

4. Gopls 分析后推送诊断
   └─ publishDiagnostics → diagnostics Map 更新 → Bus 广播

5. waitForDiagnostics("main.go")
   ├─ 150ms 去抖等待
   └─ 返回诊断结果

链路 3:Monorepo 中多个项目共存

项目结构:
/project
├── frontend/
│   ├── package.json
│   └── app.tsx       ← TypeScript 文件
├── backend/
│   ├── go.mod
│   └── main.go       ← Go 文件
└── shared/
    └── utils.ts      ← TypeScript 文件

1. Agent 打开 "frontend/app.tsx"
   ├─ getClients → Typescript 服务器
   ├─ root("frontend/app.tsx") → NearestRoot → "/project/frontend"
   └─ 启动 TS Client A (root="/project/frontend")

2. Agent 打开 "shared/utils.ts"
   ├─ getClients → Typescript 服务器
   ├─ root("shared/utils.ts") → NearestRoot → "/project"(向上搜索到项目根的 package.json)
   └─ 启动 TS Client B (root="/project") — 不同 root,不同实例

3. Agent 打开 "backend/main.go"
   ├─ getClients → Gopls 服务器
   ├─ root("backend/main.go") → NearestRoot → "/project/backend"
   └─ 启动 Go Client C (root="/project/backend")

结果:3 个独立的 LSP 客户端并存,互不干扰

链路 4:服务器启动失败 → broken 标记 → 后续跳过

1. Agent 打开 "broken.rs"
   └─ getClients → Rust Analyzer 服务器

2. LSPClient.create() → server.spawn() 失败
   ├─ catch 块捕获错误
   ├─ state.broken.add("rust-analyzer")
   └─ 返回 undefined

3. Agent 再次打开 "other.rs"
   └─ getClients("other.rs")
      ├─ 匹配服务器: Rust Analyzer
      ├─ 检查 broken 集合: "rust-analyzer" ∈ broken
      └─ 跳过,不重试 → 返回空列表

4. Agent 看到无诊断结果,继续正常工作

设计取舍

决策理由
Effect Service 而非普通模块LSP 状态需要跟随 Instance 生命周期(项目打开时初始化,关闭时清理)。Instance.state() + Effect.addFinalizer 确保客户端连接和子进程在项目关闭时被正确清理
broken 集合而非重试Language Server 启动失败通常是因为二进制不存在、端口冲突等环境问题。重复尝试只会浪费时间和资源,不如直接标记跳过
spawning Map 去重并发的 getClients 调用(如 Agent 同时编辑多个 .ts 文件)必须共享同一个 spawn Promise,否则会启动多个相同的 Language Server 进程
150ms 去抖诊断Language Server 通常先推送语法诊断(基于 AST),再推送语义诊断(基于类型检查)。去抖确保 Agent 看到完整的诊断集合
3 秒总超时Agent 不能因为 LSP 响应慢而无限等待。3 秒是”诊断通常足够完整”和”Agent 不能等太久”的平衡点
NearestRoot 高阶函数不同服务器需要检测不同的配置文件,高阶函数抽象了这个模式。排除模式(如 TypeScript 排除 deno.json)处理语言服务器之间的冲突
30+ 内置服务器主流开发语言开箱即用是 OpenCode 的核心卖点。用户也可以通过 Config.lsp 添加自定义服务器或禁用内置服务器
自动下载而非报错缺少 Language Server 二进制时自动下载(如 Biome、Deno)优于报错退出。Flag.OPENCODE_DISABLE_LSP_DOWNLOAD 允许用户在受限环境中禁用此行为
workspaceSymbol 过滤到 10 条Agent 的上下文窗口有限,过多符号会稀释有效信息。过滤到 Class/Function/Method/Enum 等关键类型并限制 10 条是信息密度和完整性的平衡

状态管理与生命周期

InstanceState 初始化

LSP 状态通过 Instance.state() 创建懒加载单例:

// index.ts — 状态初始化(简化)
const instanceState = Instance.state(async () => {
  const cfg = await Config.get()

  // 1. 收集内置服务器定义
  const servers: Record<string, LSPServer.Info> = {}
  for (const server of builtInServers) {
    servers[server.id] = server
  }

  // 2. 合并用户自定义服务器配置
  if (cfg.lsp) {
    for (const [id, custom] of Object.entries(cfg.lsp)) {
      if (custom.disabled) {
        delete servers[id]  // 禁用内置服务器
        continue
      }
      // 覆盖内置服务器的 command、extensions、env、initialization 等
      if (servers[id]) {
        servers[id] = mergeServerConfig(servers[id], custom)
      } else {
        servers[id] = createCustomServer(id, custom)
      }
    }
  }

  // 3. 全局禁用检查
  if (cfg.lsp === false) {
    return { clients: [], servers: {}, broken: new Set(), spawning: new Map() }
  }

  return { clients: [], servers, broken: new Set(), spawning: new Map() }
})

Effect.addFinalizer 清理

// index.ts — 生命周期清理(简化)
Effect.addFinalizer(async () => {
  const state = await instanceState()
  for (const client of state.clients) {
    await LSPClient.shutdown(client)
  }
  state.clients = []
  state.broken.clear()
  state.spawning.clear()
})

当 Instance(项目)关闭时,所有 LSP 客户端连接被断开,子进程被终止。

LSPClient.shutdown — 连接关闭

// client.ts — 关闭流程(简化)
async function shutdown(client: LSPClient.Info): Promise<void> {
  try {
    // 1. 发送 shutdown 请求(优雅关闭)
    await withTimeout(
      client.connection.sendRequest("shutdown"),
      5000,
    )
    // 2. 发送 exit 通知
    client.connection.sendNotification("exit")
  } catch {
    // 超时或错误 → 强制杀死进程
  } finally {
    // 3. 关闭连接
    client.connection.dispose()
    // 4. 杀死子进程
    client.process.kill()
  }
}

与其他模块的关系

  • Agent:Agent 在编辑文件后调用 LSP.touchFile() 并通过 LSP.diagnostics() 获取最新诊断,辅助判断是否需要修复代码。hover()definition() 帮助 Agent 理解符号的语义
  • Session / SessionPromptSessionPrompt 在构造用户消息时调用 LSP.documentSymbol()LSP.workspaceSymbol() 为 LLM 提供代码结构概览
  • Bus:LSP 通过 Bus.publish(Event.Diagnostics)BusEvent.define("lsp.client.diagnostics") 通知诊断状态变更,上层模块(Agent、UI)订阅这些事件
  • ConfigConfig.lsp 定义启用的服务器、自定义命令和环境变量;cfg.lsp === false 可全局禁用整个 LSP 模块
  • Instance:LSP 状态通过 Instance.state() 管理,跟随项目实例的生命周期。Instance.directoryNearestRoot 搜索的边界
  • Global/FlagGlobal.Path.bin 存放自动下载的 Language Server 二进制文件;Flag.OPENCODE_DISABLE_LSP_DOWNLOAD 控制是否允许自动下载
  • Processlaunch.ts 中的 spawn() 函数封装 Process.spawn(),统一处理子进程启动的错误处理和环境变量注入