dsh-vscode-mode 0.1.46 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -11,7 +11,7 @@ import '../styles/mcp.css'
11
11
 
12
12
  const LANGUAGES = [
13
13
  { id: 'lua', label: 'Lua(EmmyLua / LuaLS)', hint: '优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server' },
14
- { id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
14
+ { id: 'csharp', label: 'C#(Roslyn / DotRush / OmniSharp)', hint: '优先自动发现 ms-dotnettools.csharp / DotRush 扩展(DotRush 需 .NET 10 运行时);也可手动指定' },
15
15
  ]
16
16
 
17
17
  const PHASE_LABEL = {
@@ -25,8 +25,18 @@ const TABS = [
25
25
  { id: 'updates', label: '更新' },
26
26
  ]
27
27
 
28
- /** 语言卡片:状态 + 启用开关 + 命令/路径配置 + 重新检测。 */
29
- function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect }) {
28
+ /** 环境安装按钮文案(按安装进行态)。 */
29
+ function envButtonLabel(req, envStates) {
30
+ const state = (envStates ?? []).find((s) => s.id === req.id)
31
+ if (state?.phase === 'downloading') return '下载中…'
32
+ if (state?.phase === 'extracting') return '安装中…'
33
+ if (state?.phase === 'done') return '已完成'
34
+ if (state?.phase === 'failed') return '重试'
35
+ return '一键安装'
36
+ }
37
+
38
+ /** 语言卡片:状态 + 缺失环境提示(一键安装/官网下载) + 启用开关 + 命令/路径配置 + 重新检测。 */
39
+ function LangCard({ lang, config, status, busy, envStates, onInstallEnv, onToggle, onSave, onRedetect }) {
30
40
  const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
31
41
  const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
32
42
  React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
@@ -44,6 +54,21 @@ function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect })
44
54
  status?.version ? ' · v' + status.version : '',
45
55
  (phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
46
56
  status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
57
+ Array.isArray(status?.missingEnv) && status.missingEnv.length
58
+ ? React.createElement('div', { className: 'vsm-lsp-hint' },
59
+ '缺少运行环境(一键安装后自动生效):',
60
+ status.missingEnv.map((req) => React.createElement('div', { key: req.id, className: 'vsm-mcp-actions' },
61
+ React.createElement('span', null, req.label, req.detail ? '(' + req.detail + ')' : ''),
62
+ req.installable ? React.createElement('button', {
63
+ className: 'vsm-primary vsm-small',
64
+ disabled: busy === 'env:' + req.id,
65
+ onClick: () => onInstallEnv(lang.id, req),
66
+ }, envButtonLabel(req, envStates)) : null,
67
+ req.manualUrl ? React.createElement('a', {
68
+ href: req.manualUrl, target: '_blank', rel: 'noreferrer',
69
+ style: { color: 'inherit', textDecoration: 'underline', alignSelf: 'center' },
70
+ }, req.manualLabel ?? '官网下载') : null)))
71
+ : null,
47
72
  React.createElement('div', { className: 'vsm-lsp-form' },
48
73
  React.createElement('label', null, React.createElement('span', null, '可执行文件路径(绝对路径,优先)'), React.createElement('input', { value: pathDraft, placeholder: '如 C:/.../lua-language-server.exe', onChange: (e) => setPathDraft(e.target.value) })),
49
74
  React.createElement('label', null, React.createElement('span', null, '命令名(PATH 内查找)'), React.createElement('input', { value: commandDraft, placeholder: '如 lua-language-server', onChange: (e) => setCommandDraft(e.target.value) })),
@@ -96,6 +121,8 @@ export function LspSettings() {
96
121
  const [busy, setBusy] = React.useState('')
97
122
  const [error, setError] = React.useState('')
98
123
  const [loading, setLoading] = React.useState(true)
124
+ const [envStates, setEnvStates] = React.useState([])
125
+ const [envLang, setEnvLang] = React.useState('')
99
126
 
100
127
  const refreshServers = React.useCallback(() => {
101
128
  Promise.all([rpc('edrv.lsp.configGet', {}), rpc('edrv.lsp.status', {})])
@@ -117,6 +144,15 @@ export function LspSettings() {
117
144
  .catch((e) => setError(String(e)))
118
145
  }, [])
119
146
 
147
+ const refreshEnvStates = React.useCallback(() => {
148
+ return rpc('edrv.lsp.envState', {})
149
+ .then((res) => {
150
+ if (res?.ok) setEnvStates(res.states ?? [])
151
+ return res?.states ?? []
152
+ })
153
+ .catch(() => [])
154
+ }, [])
155
+
120
156
  const refresh = React.useCallback(() => {
121
157
  setLoading(true)
122
158
  Promise.all([refreshServers(), refreshExt()]).finally(() => setLoading(false))
@@ -140,10 +176,52 @@ export function LspSettings() {
140
176
  }
141
177
 
142
178
  /** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
143
- const notifyResync = (languageId) => {
179
+ const notifyResync = React.useCallback((languageId) => {
144
180
  window.dispatchEvent(new CustomEvent('edrv:lsp-redetect', { detail: { languageId } }))
181
+ }, [])
182
+
183
+ /** 一键安装缺失环境:启动内置安装器(成功后进入轮询,完成自动重同步)。 */
184
+ const installEnv = (languageId, req) => {
185
+ const key = 'env:' + req.id
186
+ setBusy(key)
187
+ setError('')
188
+ setEnvLang(languageId)
189
+ rpc('edrv.lsp.envInstall', { languageId, id: req.id })
190
+ .then((res) => {
191
+ if (!res?.ok) { setError(res?.error ?? '无法启动安装'); return }
192
+ void refreshEnvStates()
193
+ void refreshServers()
194
+ })
195
+ .catch((e) => setError(String(e)))
196
+ .finally(() => setBusy(''))
145
197
  }
146
198
 
199
+ // 环境安装进行中:2s 轮询进度并刷新状态;某项转入 done 时由下方过渡检测重同步
200
+ React.useEffect(() => {
201
+ const states = envStates ?? []
202
+ const installing = states.some((s) => s.phase === 'downloading' || s.phase === 'extracting')
203
+ if (!installing) return
204
+ const timer = setInterval(() => {
205
+ void refreshEnvStates()
206
+ void refreshServers()
207
+ }, 2000)
208
+ return () => clearInterval(timer)
209
+ }, [envStates, refreshEnvStates, refreshServers])
210
+
211
+ // 环境安装完成过渡检测:刷新状态并通知编辑器重新同步对应语言
212
+ const prevEnvPhases = React.useRef({})
213
+ React.useEffect(() => {
214
+ const states = envStates ?? []
215
+ for (const s of states) {
216
+ const prev = prevEnvPhases.current[s.id]
217
+ if (prev && prev !== 'done' && s.phase === 'done' && envLang) {
218
+ void refreshServers()
219
+ notifyResync(envLang)
220
+ }
221
+ prevEnvPhases.current[s.id] = s.phase
222
+ }
223
+ }, [envStates, envLang, refreshServers, notifyResync])
224
+
147
225
  const saveLang = (languageId, path, command) => {
148
226
  setBusy(languageId)
149
227
  setError('')
@@ -271,7 +349,7 @@ export function LspSettings() {
271
349
  key: lang.id, lang,
272
350
  config: config[lang.id] ?? {},
273
351
  status: servers.find((s) => s.languageId === lang.id),
274
- busy, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
352
+ busy, envStates, onInstallEnv: installEnv, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
275
353
  })),
276
354
  React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
277
355
  } else if (tab === 'installed') {
package/src/lsp/config.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Ctx } from '../store.js'
7
7
  import { PROVIDER_RESOLVERS, type LspProviderSpec } from './providers.js'
8
+ import { provisionRuntime } from './dotnetProvision.js'
8
9
 
9
10
  /** 单语言配置。 */
10
11
  export interface LspLangConfig {
@@ -91,7 +92,10 @@ export function resolveProviderSpec(ctx: Ctx, pluginConfig: unknown, languageId:
91
92
  if (lang && lang.enabled === false) {
92
93
  return { languageId, kind: 'none', argv: [], ready: false, reason: '已在设置中禁用' }
93
94
  }
94
- return resolver({ command: lang?.command, path: lang?.path })
95
+ const spec = resolver({ command: lang?.command, path: lang?.path })
96
+ // DotRush 缺运行时:后台自动下载官方 .NET runtime(用户零操作),完成后经监听者重载
97
+ if (spec.provisionRuntime) provisionRuntime(spec.provisionRuntime)
98
+ return spec
95
99
  }
96
100
 
97
101
  /** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
@@ -0,0 +1,224 @@
1
+ /**
2
+ * dsh-vscode-mode host — C# 服务器 .NET 运行时自动配置。
3
+ * DotRush 等服务器需要 .NET 运行时;缺失时从官方 release 元数据解析下载地址,
4
+ * sha512 校验后解压到用户级 dotnet 目录(win: %LOCALAPPDATA%\Microsoft\dotnet,
5
+ * unix: ~/.dotnet),成功后通知监听者重载 provider——用户零手动操作。
6
+ * 失败静默降级(保留原 reason 文案),10 分钟退避重试。
7
+ * 安装全程有状态机(envInstallStates)供设置页一键安装与进度轮询使用。
8
+ * 作者 ddj 2026年09月03号
9
+ */
10
+ import { spawn } from 'node:child_process'
11
+ import { createHash } from 'node:crypto'
12
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { homedir } from 'node:os'
15
+ import { dirname, join } from 'node:path'
16
+ import { dotnetWithRuntime } from './providers.js'
17
+ import type { LspEnvInstallState } from '../shared/lsp.js'
18
+ import { unzip } from './zip.js'
19
+
20
+ /** release 元数据 files 元素子集(官方 releases.json)。 */
21
+ export interface RuntimeAsset {
22
+ name: string
23
+ url: string
24
+ hash: string
25
+ }
26
+
27
+ const META_BASE = 'https://builds.dotnet.microsoft.com/dotnet/release-metadata'
28
+ const RETRY_MS = 10 * 60 * 1000
29
+
30
+ /** 同 major 去重(in-flight)。 */
31
+ const inFlight = new Map<number, Promise<boolean>>()
32
+ /** 失败时间戳(退避重试)。 */
33
+ const failedAt = new Map<number, number>()
34
+ /** 配置成功监听者(重载 provider 缓存与服务器)。 */
35
+ const listeners = new Set<() => void>()
36
+ /** 安装进行态(major → phase/message),供设置页轮询展示。 */
37
+ const installStates = new Map<number, { phase: LspEnvInstallState['phase']; message?: string }>()
38
+
39
+ /** 需求 id(与 envRequirements 的 id 一致)。 */
40
+ export function runtimeRequirementId(major: number): string {
41
+ return 'dotnet-runtime:' + major
42
+ }
43
+
44
+ /** 当前安装进行态快照(edrv.lsp.envState 载荷)。 */
45
+ export function envInstallStates(): LspEnvInstallState[] {
46
+ return [...installStates.entries()].map(([major, state]) => ({
47
+ id: runtimeRequirementId(major),
48
+ phase: state.phase,
49
+ message: state.message,
50
+ }))
51
+ }
52
+
53
+ /** 注册配置成功监听(成功后由 rpc 侧清缓存并重置服务器)。 */
54
+ export function onRuntimeProvisioned(listener: () => void): void {
55
+ listeners.add(listener)
56
+ }
57
+
58
+ /** 平台 → 官方运行时资产文件名。 */
59
+ function assetNameFor(platform: string, arch: string): string {
60
+ const archPart = arch === 'arm64' ? 'arm64' : 'x64'
61
+ if (platform === 'win32') return 'dotnet-runtime-win-' + archPart + '.zip'
62
+ if (platform === 'darwin') return 'dotnet-runtime-osx-' + archPart + '.tar.gz'
63
+ return 'dotnet-runtime-linux-' + archPart + '.tar.gz'
64
+ }
65
+
66
+ /** 用户级 dotnet 安装目录(与 providers 的发现候选位置一致)。 */
67
+ export function userDotnetDir(): string {
68
+ if (process.platform === 'win32') {
69
+ return join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'Microsoft', 'dotnet')
70
+ }
71
+ return join(homedir(), '.dotnet')
72
+ }
73
+
74
+ /** 从 release 元数据 files 里挑当前平台资产(纯函数,测试可注入平台)。 */
75
+ export function pickRuntimeAsset(files: RuntimeAsset[], platform = process.platform, arch = process.arch): RuntimeAsset | null {
76
+ const name = assetNameFor(platform, arch)
77
+ return files.find((f) => f.name === name) ?? null
78
+ }
79
+
80
+ /** 是否允许自动配置(测试隔离 + 显式开关)。 */
81
+ function allowed(): boolean {
82
+ if (process.env.DSH_LSP_AUTO_RUNTIME === '0') return false
83
+ if (process.env.DSH_LSP_EXT_DIRS) return false // 测试环境不下载
84
+ return true
85
+ }
86
+
87
+ function setState(major: number, phase: LspEnvInstallState['phase'], message?: string): void {
88
+ installStates.set(major, { phase, message })
89
+ }
90
+
91
+ function notifyListeners(): void {
92
+ for (const listener of listeners) {
93
+ try {
94
+ listener()
95
+ } catch { /* 单个监听失败不影响其它 */ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 触发运行时自动配置(幂等:同 major 去重;失败 10 分钟退避;已满足则直接成功回调)。
101
+ * 静默自动路径与设置页一键安装共用;进度经 envInstallStates 暴露。
102
+ * @author ddj 2026年09月03号
103
+ * @param major 运行时大版本
104
+ */
105
+ export function provisionRuntime(major: number): void {
106
+ if (inFlight.has(major)) return
107
+ const failed = failedAt.get(major)
108
+ if (failed && Date.now() - failed < RETRY_MS) return
109
+ const task = runProvision(major)
110
+ .catch(() => false)
111
+ .then((ok) => {
112
+ inFlight.delete(major)
113
+ if (ok) {
114
+ failedAt.delete(major)
115
+ setState(major, 'done')
116
+ notifyListeners()
117
+ } else {
118
+ failedAt.set(major, Date.now())
119
+ setState(major, 'failed')
120
+ }
121
+ return ok
122
+ })
123
+ inFlight.set(major, task)
124
+ }
125
+
126
+ /**
127
+ * 一键安装入口(设置页按钮):校验允许后触发 provisionRuntime。
128
+ * @author ddj 2026年09月03号
129
+ * @param major 运行时大版本
130
+ * @returns 是否成功启动(已满足/进行中/被禁用时返回 false)
131
+ */
132
+ export function startEnvInstall(major: number): boolean {
133
+ if (dotnetWithRuntime(major)) {
134
+ setState(major, 'done')
135
+ return false
136
+ }
137
+ if (!allowed() || inFlight.has(major)) return false
138
+ const failed = failedAt.get(major)
139
+ if (failed && Date.now() - failed < RETRY_MS && installStates.get(major)?.phase === 'failed') {
140
+ // 退避期内失败态:允许用户手动重试(清除退避标记)
141
+ failedAt.delete(major)
142
+ }
143
+ provisionRuntime(major)
144
+ return inFlight.has(major)
145
+ }
146
+
147
+ async function runProvision(major: number): Promise<boolean> {
148
+ if (dotnetWithRuntime(major)) return true
149
+ setState(major, 'downloading')
150
+ const meta = await fetchJson(META_BASE + '/' + major + '.0/releases.json')
151
+ const release = meta?.releases?.[0]
152
+ const files = release?.runtime?.files
153
+ if (!Array.isArray(files)) return false
154
+ const asset = pickRuntimeAsset(files as RuntimeAsset[])
155
+ if (!asset?.url || !asset.hash) return false
156
+ const buf = await downloadVerified(asset.url, asset.hash)
157
+ if (!buf) return false
158
+ setState(major, 'extracting')
159
+ const dir = userDotnetDir()
160
+ const created = !existsSync(dir)
161
+ try {
162
+ const ok = await extractTo(buf, dir)
163
+ if (!ok) throw new Error('解压失败')
164
+ } catch (error) {
165
+ if (created) rmSync(dir, { recursive: true, force: true })
166
+ return false
167
+ }
168
+ return dotnetWithRuntime(major) !== null
169
+ }
170
+
171
+ /** 拉取 JSON(30s 超时,失败返回 null)。 */
172
+ async function fetchJson(url: string): Promise<{ releases?: { runtime?: { files?: unknown } }[] } | null> {
173
+ try {
174
+ const res = await fetch(url, { signal: AbortSignal.timeout(30000) })
175
+ if (!res.ok) return null
176
+ return await res.json()
177
+ } catch {
178
+ return null
179
+ }
180
+ }
181
+
182
+ /** 下载并按官方 sha512 校验(5 分钟超时,不匹配返回 null)。 */
183
+ async function downloadVerified(url: string, sha512: string): Promise<Buffer | null> {
184
+ try {
185
+ const res = await fetch(url, { signal: AbortSignal.timeout(300000) })
186
+ if (!res.ok) return null
187
+ const buf = Buffer.from(await res.arrayBuffer())
188
+ const actual = createHash('sha512').update(buf).digest('hex').toLowerCase()
189
+ return actual === sha512.toLowerCase() ? buf : null
190
+ } catch {
191
+ return null
192
+ }
193
+ }
194
+
195
+ /** 解压到目录:win 走 zip(PK 头),unix 落临时包交系统 tar。 */
196
+ async function extractTo(buf: Buffer, dir: string): Promise<boolean> {
197
+ mkdirSync(dir, { recursive: true })
198
+ if (buf.subarray(0, 2).toString('ascii') === 'PK') {
199
+ for (const entry of unzip(buf)) {
200
+ if (!entry.path || entry.path.startsWith('..')) continue
201
+ const parts = entry.path.split(/[\\/]/).filter((p) => p && p !== '..' && p !== '.')
202
+ if (!parts.length) continue
203
+ const target = join(dir, ...parts)
204
+ if (entry.isDirectory) {
205
+ mkdirSync(target, { recursive: true })
206
+ continue
207
+ }
208
+ mkdirSync(dirname(target), { recursive: true })
209
+ writeFileSync(target, entry.data)
210
+ }
211
+ return true
212
+ }
213
+ const tmp = join(tmpdir(), 'dsh-dotnet-' + Date.now() + '.tar.gz')
214
+ try {
215
+ writeFileSync(tmp, buf)
216
+ return await new Promise<boolean>((resolve) => {
217
+ const child = spawn('tar', ['-xzf', tmp, '-C', dir], { stdio: 'ignore' })
218
+ child.on('exit', (code) => resolve(code === 0))
219
+ child.on('error', () => resolve(false))
220
+ })
221
+ } finally {
222
+ rmSync(tmp, { force: true })
223
+ }
224
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * dsh-vscode-mode host — LSP 环境需求检测注册表。
3
+ * 每个语言注册一个检测器:扫描当前环境,返回未满足的需求清单(含一键安装与官网链接信息)。
4
+ * 新接入 LSP:在 CHECKERS 加一个检测器条目;需要内置安装器时在 INSTALLERS 注册同前缀安装函数。
5
+ * 检测在 status/redetect/configUpdate 时执行(不缓存),保证安装完成后状态即时刷新。
6
+ * 作者 ddj 2026年09月03号
7
+ */
8
+ import { dirname } from 'node:path'
9
+ import { candidateCSharpServers, dotnetWithRuntime, runtimeMajorOf } from './providers.js'
10
+ import { startEnvInstall } from './dotnetProvision.js'
11
+ import type { LspMissingEnv } from '../shared/lsp.js'
12
+
13
+ /** dotnet 运行时需求构造(DotRush / ms-Roslyn 共用)。 */
14
+ function dotnetRuntimeRequirement(major: number, detail: string): LspMissingEnv {
15
+ return {
16
+ id: 'dotnet-runtime:' + major,
17
+ label: '.NET 运行时 ' + major + '.0',
18
+ detail,
19
+ installable: true,
20
+ manualUrl: 'https://dotnet.microsoft.com/download/dotnet/' + major + '.0',
21
+ manualLabel: '官网下载',
22
+ }
23
+ }
24
+
25
+ /** csharp 检测器:DotRush 缺运行时 / ms-Roslyn 缺 dotnet。 */
26
+ function checkCSharp(home: string): LspMissingEnv[] {
27
+ const found = candidateCSharpServers(home)
28
+ const missing: LspMissingEnv[] = []
29
+ if (found.dotrushDll) {
30
+ const major = runtimeMajorOf(dirname(found.dotrushDll))
31
+ if (major !== null && dotnetWithRuntime(major) === null) {
32
+ missing.push(dotnetRuntimeRequirement(major, 'DotRush 语言服务器运行所需'))
33
+ }
34
+ return missing
35
+ }
36
+ if (found.roslynDll && !found.dotnet) {
37
+ // 官方 Roslyn 服务器分支未实测,保守按 LTS 8 基线提示
38
+ missing.push(dotnetRuntimeRequirement(8, 'Roslyn 语言服务器运行所需'))
39
+ }
40
+ return missing
41
+ }
42
+
43
+ /** lua 检测器:EmmyLua/LuaLS 自带运行时,无外部依赖。 */
44
+ function checkLua(): LspMissingEnv[] {
45
+ return []
46
+ }
47
+
48
+ /** 检测器注册表(按 languageId;新 LSP 在此加条目即可)。 */
49
+ const CHECKERS: Record<string, (home: string) => LspMissingEnv[]> = {
50
+ lua: checkLua,
51
+ csharp: checkCSharp,
52
+ }
53
+
54
+ /** 安装器注册表(按需求 id 前缀匹配;新依赖类型在此加条目)。 */
55
+ const INSTALLERS: { prefix: string; run: (id: string) => boolean }[] = [
56
+ {
57
+ prefix: 'dotnet-runtime:',
58
+ run: (id) => {
59
+ const major = Number.parseInt(id.slice('dotnet-runtime:'.length), 10)
60
+ return Number.isInteger(major) ? startEnvInstall(major) : false
61
+ },
62
+ },
63
+ ]
64
+
65
+ /**
66
+ * 检测某语言未满足的环境需求(仅返回缺失项;检测器异常按无需求处理)。
67
+ * @author ddj 2026年09月03号
68
+ * @param languageId 语言 id
69
+ * @param home DSH home(测试可注入临时目录)
70
+ * @returns 缺失需求清单
71
+ */
72
+ export function envRequirementsFor(languageId: string, home: string): LspMissingEnv[] {
73
+ const checker = CHECKERS[languageId]
74
+ if (!checker) return []
75
+ try {
76
+ return checker(home)
77
+ } catch {
78
+ return []
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 尝试一键安装某条需求(存在内置安装器时启动,进行中/已满足返回 false)。
84
+ * @author ddj 2026年09月03号
85
+ * @param id 需求标识(如 dotnet-runtime:10)
86
+ * @returns 是否成功启动安装
87
+ */
88
+ export function installRequirement(id: string): boolean {
89
+ const installer = INSTALLERS.find((it) => id.startsWith(it.prefix))
90
+ if (!installer) return false
91
+ return installer.run(id)
92
+ }