dsh-vscode-mode 0.1.42 → 0.1.44
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/lib/client.js +107 -66
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -26
- package/lib/index.js.map +1 -1
- package/package.json +1 -5
- package/src/client/monaco/lsp/providers.ts +129 -62
- package/src/client/styles/editor.css +0 -6
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/compat.ts +46 -5
- package/src/dshVersion.ts +121 -0
- package/src/fileOpenSettings.ts +143 -24
- package/src/host-deps.d.ts +5 -5
- package/src/index.ts +2 -1
- package/src/lsp/settings.ts +12 -6
- package/src/shared/compat.ts +9 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -85,10 +85,6 @@
|
|
|
85
85
|
"patch": "./cordis.patch.yml"
|
|
86
86
|
},
|
|
87
87
|
"client": {
|
|
88
|
-
"inject": [
|
|
89
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
90
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
91
|
-
],
|
|
92
88
|
"platform": "web"
|
|
93
89
|
}
|
|
94
90
|
}
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — Monaco LSP provider 注册 + 跳转/引用命令。
|
|
4
4
|
* 数据层:Definition/Reference/DocumentSymbol/Hover provider(同一文件内跳转 Monaco 原生可用)。
|
|
5
|
-
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 /
|
|
6
|
-
*
|
|
5
|
+
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 / 右键)/ Ctrl+点击引用导航,
|
|
6
|
+
* 跨文件跳转自研(target 经 openFileAt 打开并定位),多条引用走 Monaco 原生 References Peek
|
|
7
|
+
* (左侧代码预览 + 右侧引用列表;registerEditorOpener 接管 edrv:// 点击跳转)。
|
|
7
8
|
* 作者 ddj 2026-08-27
|
|
8
9
|
*/
|
|
9
10
|
import {
|
|
10
11
|
pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
|
|
11
12
|
fetchDocumentSymbols, fetchHover, fetchSemanticTokens,
|
|
12
13
|
targetOpenPath, targetMonoPosition, lspStatusFor, refreshStatus,
|
|
14
|
+
lspUriToAbs,
|
|
13
15
|
} from './lspClient.js'
|
|
14
|
-
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../../../shared/lsp.js'
|
|
16
|
+
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES, monoToLsp } from '../../../shared/lsp.js'
|
|
15
17
|
|
|
16
18
|
const LSP_LANGS = ['lua', 'csharp']
|
|
17
19
|
const SEMANTIC_LEGEND = {
|
|
@@ -20,7 +22,6 @@ const SEMANTIC_LEGEND = {
|
|
|
20
22
|
}
|
|
21
23
|
let registered = false
|
|
22
24
|
const disposables = []
|
|
23
|
-
let overlayEl = null
|
|
24
25
|
|
|
25
26
|
/** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
|
|
26
27
|
function openAt(path, line, column) {
|
|
@@ -72,6 +73,20 @@ export function registerLspProviders(monaco) {
|
|
|
72
73
|
|
|
73
74
|
// —— 数据 provider ——
|
|
74
75
|
disposables.push(
|
|
76
|
+
// 原生跳转(peek 参考文献列表点击 / 原生 go to definition 等)的 edrv:// 打开兜底:
|
|
77
|
+
// 目标 uri 由本插件自己定义(edrv:// 工作区相对路径),Monaco 无法自行加载,
|
|
78
|
+
// 必须经事件通道交给 EditorView openFileAt 打开并定位。
|
|
79
|
+
monaco.editor.registerEditorOpener({
|
|
80
|
+
openCodeEditor: (source, resource, selectionOrPosition) => {
|
|
81
|
+
const path = lspUriToAbs(resource)
|
|
82
|
+
if (!path) return false
|
|
83
|
+
const start = selectionOrPosition?.getStartPosition?.() ?? selectionOrPosition
|
|
84
|
+
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
85
|
+
const column = Math.max(1, start?.column ?? 1)
|
|
86
|
+
openAt(path, line, column)
|
|
87
|
+
return true
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
75
90
|
monaco.languages.registerDefinitionProvider(LSP_LANGS, {
|
|
76
91
|
provideDefinition: (model, position, token) => {
|
|
77
92
|
const path = pathOfModel(model)
|
|
@@ -168,12 +183,108 @@ export async function runGoToDefinition(ed) {
|
|
|
168
183
|
await goToDefinition(ed, ed.getPosition(), true)
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Ctrl+鼠标点击:查询其它内容对该项的引用并导航。
|
|
188
|
+
* 0 条 → 无特殊效果;1 条 → 直接跳转;多条 → 打开 Monaco 原生 References Peek
|
|
189
|
+
* (左侧代码预览 + 右侧引用列表,点击经 registerEditorOpener 跳转)。
|
|
190
|
+
* @author ddj 2026年09月02号
|
|
191
|
+
* @param ed Monaco 编辑器
|
|
192
|
+
* @param position 点击位置(Monaco 1-based)
|
|
193
|
+
*/
|
|
194
|
+
export async function gotoRefsAt(ed, position) {
|
|
195
|
+
const model = ed.getModel()
|
|
196
|
+
const path = pathOfModel(model)
|
|
197
|
+
if (!path || !position) return
|
|
198
|
+
const all = await findReferences(path, model.getValue(), position, false)
|
|
199
|
+
const others = filterOtherRefs(all, path, position)
|
|
200
|
+
if (!others.length) return
|
|
201
|
+
if (others.length === 1) {
|
|
202
|
+
jumpToLocation(others[0])
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
// 计数器就绪:光标移到点击处(原生 Peek 按光标词查询),走原生两栏引用视图
|
|
206
|
+
ed.setPosition(position)
|
|
207
|
+
await triggerReferencePeek(ed)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 触发 Monaco 原生 References Peek(左侧代码 + 右侧引用列表,原生点击跳转)。
|
|
212
|
+
* @author ddj 2026年09月02号
|
|
213
|
+
* @param ed Monaco 编辑器
|
|
214
|
+
* @returns 是否成功触发(动作缺失时返回 false)
|
|
215
|
+
*/
|
|
216
|
+
export async function triggerReferencePeek(ed) {
|
|
217
|
+
const action = ed.getAction('editor.action.referenceSearch.trigger')
|
|
218
|
+
if (!action || typeof action.run !== 'function') return false
|
|
219
|
+
await action.run()
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 过滤「其它内容」引用:剔除当前点击处自身(同文件且点击位置落入其 range),并按路径+起点去重。
|
|
225
|
+
* @author ddj 2026年09月02号
|
|
226
|
+
* @param locations 引用结果
|
|
227
|
+
* @param path 当前文档路径
|
|
228
|
+
* @param position 点击位置(Monaco 1-based)
|
|
229
|
+
* @returns 过滤后的其它引用
|
|
230
|
+
*/
|
|
231
|
+
function filterOtherRefs(locations, path, position) {
|
|
232
|
+
const lspPos = monoToLsp(position.lineNumber, position.column)
|
|
233
|
+
const seen = new Set()
|
|
234
|
+
const out = []
|
|
235
|
+
for (const loc of locations) {
|
|
236
|
+
const rel = targetOpenPath(loc)
|
|
237
|
+
if (sameFile(rel, path) && posInRange(lspPos, loc.lspRange)) continue
|
|
238
|
+
const start = loc.lspRange?.start ?? {}
|
|
239
|
+
const key = String(rel).toLowerCase() + ':' + (start.line ?? 0) + ':' + (start.character ?? 0)
|
|
240
|
+
if (seen.has(key)) continue
|
|
241
|
+
seen.add(key)
|
|
242
|
+
out.push(loc)
|
|
243
|
+
}
|
|
244
|
+
return out
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 两个路径是否指向同一文件(容忍相对/绝对混合与大小写差异)。
|
|
249
|
+
* @author ddj 2026年09月02号
|
|
250
|
+
* @param a 路径 A
|
|
251
|
+
* @param b 路径 B
|
|
252
|
+
* @returns 是否同一文件
|
|
253
|
+
*/
|
|
254
|
+
function sameFile(a, b) {
|
|
255
|
+
if (!a || !b) return false
|
|
256
|
+
const na = String(a).replace(/\\/g, '/').toLowerCase()
|
|
257
|
+
const nb = String(b).replace(/\\/g, '/').toLowerCase()
|
|
258
|
+
if (na === nb) return true
|
|
259
|
+
const absA = /^[A-Za-z]:\//.test(na) || na.startsWith('/')
|
|
260
|
+
const absB = /^[A-Za-z]:\//.test(nb) || nb.startsWith('/')
|
|
261
|
+
// 同为相对/同为绝对但不等 → 不同文件(避免根目录文件误匹配同名子目录文件)
|
|
262
|
+
if (absA === absB) return false
|
|
263
|
+
// 一方绝对一方相对:绝对路径以相对路径结尾视为同文件
|
|
264
|
+
return absA ? na.endsWith('/' + nb) : nb.endsWith('/' + na)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 0-based LSP 位置是否落在 range 内(end 排除;零宽 range 按点匹配)。
|
|
269
|
+
* @author ddj 2026年09月02号
|
|
270
|
+
* @param pos LSP 位置(0-based)
|
|
271
|
+
* @param range LSP range
|
|
272
|
+
* @returns 是否在范围内
|
|
273
|
+
*/
|
|
274
|
+
function posInRange(pos, range) {
|
|
275
|
+
const start = range?.start
|
|
276
|
+
if (!start) return false
|
|
277
|
+
const end = range?.end ?? start
|
|
278
|
+
if (start.line === end.line && start.character === end.character) {
|
|
279
|
+
return pos.line === start.line && pos.character === start.character
|
|
280
|
+
}
|
|
281
|
+
if (pos.line < start.line || pos.line > end.line) return false
|
|
282
|
+
if (pos.line === start.line && pos.character < start.character) return false
|
|
283
|
+
if (pos.line === end.line && pos.character >= end.character) return false
|
|
284
|
+
return true
|
|
174
285
|
}
|
|
175
286
|
|
|
176
|
-
/** 给一个 Monaco 编辑器绑定 Ctrl/Cmd
|
|
287
|
+
/** 给一个 Monaco 编辑器绑定 Ctrl/Cmd+鼠标引用导航(幂等)。 */
|
|
177
288
|
export function bindLspEditor(ed) {
|
|
178
289
|
if (!ed || ed.__edrvLspClick) return
|
|
179
290
|
ed.__edrvLspClick = true
|
|
@@ -187,68 +298,24 @@ export function bindLspEditor(ed) {
|
|
|
187
298
|
if (!native || !position || !isLeftButton || (!ctrl && !meta)) return
|
|
188
299
|
native.preventDefault?.()
|
|
189
300
|
native.stopPropagation?.()
|
|
190
|
-
void
|
|
301
|
+
void gotoRefsAt(ed, position)
|
|
191
302
|
})
|
|
192
303
|
disposables.push(disposable)
|
|
193
304
|
}
|
|
194
305
|
|
|
195
|
-
/** Shift+F12 /
|
|
306
|
+
/** Shift+F12 / 右键「查找所有引用」:走 Monaco 原生 References Peek(两栏引用视图)。 */
|
|
196
307
|
export async function runFindReferences(ed) {
|
|
197
|
-
const model = ed.getModel()
|
|
198
|
-
const path = pathOfModel(model)
|
|
199
308
|
const position = ed.getPosition()
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
309
|
+
if (!position) return
|
|
310
|
+
ed.setPosition(position)
|
|
311
|
+
await triggerReferencePeek(ed)
|
|
203
312
|
}
|
|
204
313
|
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
const root = ed.getDomNode()
|
|
213
|
-
if (!root) return
|
|
214
|
-
const el = document.createElement('div')
|
|
215
|
-
el.className = 'edrv-refs'
|
|
216
|
-
const header = document.createElement('div')
|
|
217
|
-
header.className = 'edrv-refs-head'
|
|
218
|
-
header.textContent = '引用 ' + locations.length + ' 处'
|
|
219
|
-
el.appendChild(header)
|
|
220
|
-
const list = document.createElement('div')
|
|
221
|
-
list.className = 'edrv-refs-list'
|
|
222
|
-
for (const loc of locations) {
|
|
223
|
-
const row = document.createElement('div')
|
|
224
|
-
row.className = 'edrv-refs-row'
|
|
225
|
-
const pos = targetMonoPosition(loc)
|
|
226
|
-
const rel = targetOpenPath(loc)
|
|
227
|
-
const name = String(rel).split('/').pop() || rel
|
|
228
|
-
row.textContent = name + ':' + pos.line + ':' + pos.column
|
|
229
|
-
row.title = rel + ':' + pos.line
|
|
230
|
-
row.addEventListener('click', () => {
|
|
231
|
-
hideReferencesOverlay()
|
|
232
|
-
jumpToLocation(loc)
|
|
233
|
-
})
|
|
234
|
-
list.appendChild(row)
|
|
235
|
-
}
|
|
236
|
-
el.appendChild(list)
|
|
237
|
-
root.appendChild(el)
|
|
238
|
-
overlayEl = el
|
|
239
|
-
const onKey = (e) => { if (e.key === 'Escape') hideReferencesOverlay() }
|
|
240
|
-
window.addEventListener('keydown', onKey)
|
|
241
|
-
el.addEventListener('click', (e) => { if (e.target === el) hideReferencesOverlay() })
|
|
242
|
-
el._onKey = onKey
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function hideReferencesOverlay() {
|
|
246
|
-
if (overlayEl) {
|
|
247
|
-
window.removeEventListener('keydown', overlayEl._onKey)
|
|
248
|
-
overlayEl.remove()
|
|
249
|
-
overlayEl = null
|
|
250
|
-
}
|
|
251
|
-
}
|
|
314
|
+
/**
|
|
315
|
+
* 兼容占位(历史导出;自绘引用浮窗已移除,改走 Monaco 原生 Peek)。
|
|
316
|
+
* @author ddj 2026年09月02号
|
|
317
|
+
*/
|
|
318
|
+
export function hideReferencesOverlay() {}
|
|
252
319
|
|
|
253
320
|
/** 编辑器状态栏提示(复用 EditorView 的 status 通道)。 */
|
|
254
321
|
function setStatus(text) {
|
|
@@ -291,11 +291,5 @@
|
|
|
291
291
|
[data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
|
|
292
292
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
293
293
|
|
|
294
|
-
/* LSP 引用结果浮动面板(Shift+F12 / 右键「查找所有引用」) */
|
|
295
|
-
[data-edrv-view] .edrv-refs { position: absolute; right: 12px; top: 40px; z-index: 60; width: 360px; max-height: 60%; display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 8px; background: var(--dsw-alias-bg-panel, #161b22); box-shadow: 0 6px 24px rgba(0,0,0,.4); overflow: hidden; font-size: 12px; }
|
|
296
|
-
[data-edrv-view] .edrv-refs-head { padding: 6px 10px; font-weight: 600; color: var(--dsw-alias-label-primary, #eee); border-bottom: 1px solid var(--dsw-alias-border-l2, #333); background: var(--dsw-alias-bg-layer-1, transparent); }
|
|
297
|
-
[data-edrv-view] .edrv-refs-list { overflow-y: auto; }
|
|
298
|
-
[data-edrv-view] .edrv-refs-row { padding: 4px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #c0c8d0); font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
299
|
-
[data-edrv-view] .edrv-refs-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.10)); color: var(--dsw-alias-label-primary, #fff); }
|
|
300
294
|
|
|
301
295
|
|
|
@@ -211,11 +211,15 @@ function CompatSection({ getSummary }) {
|
|
|
211
211
|
)
|
|
212
212
|
const warnings = report?.warnings ?? []
|
|
213
213
|
const devForm = report?.devForm
|
|
214
|
+
const versionText = report?.dshVersion
|
|
215
|
+
? '(DSH ' + report.dshVersion + ' · 插件 ' + (report?.pluginVersion ?? '') + ')'
|
|
216
|
+
: report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : ''
|
|
214
217
|
return React.createElement('section', { className: 'vsm-general-page' },
|
|
215
218
|
React.createElement('h2', null, '兼容性'),
|
|
216
|
-
React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' +
|
|
219
|
+
React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + versionText),
|
|
217
220
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
218
221
|
devForm?.enabled && group('开发形态', React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path))),
|
|
222
|
+
group('版本适配', renderItems(report?.adapters)),
|
|
219
223
|
group('外部插件与服务', renderItems(report?.external)),
|
|
220
224
|
group('客户端适配', renderItems(getSummary?.() ?? [])),
|
|
221
225
|
group('护栏', renderItems(report?.guards)),
|
package/src/compat.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { readFileSync } from 'node:fs'
|
|
|
10
10
|
import { dirname, join } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { entriesOf } from './mcp.js'
|
|
13
|
-
import { loadSettingsDeps } from './fileOpenSettings.js'
|
|
13
|
+
import { loadSettingsDeps, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
14
14
|
import { readDevForm } from './devForm.js'
|
|
15
|
+
import { compareDshVersions, detectDshVersion, familyLabel, parseDshVersion } from './dshVersion.js'
|
|
15
16
|
import type { CompatAdapter, CompatReport } from './shared/compat.js'
|
|
16
17
|
import type { Ctx } from './store.js'
|
|
17
18
|
import { ROUTE_PREFIX } from './paths.js'
|
|
@@ -127,24 +128,64 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
|
|
|
127
128
|
]
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
132
|
+
const TESTED_DSH_MAX = '0.1.2-alpha.4'
|
|
133
|
+
|
|
134
|
+
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
|
|
135
|
+
export function versionAdapters(dshVersion: string): CompatAdapter[] {
|
|
136
|
+
const adapters: CompatAdapter[] = []
|
|
137
|
+
const parsed = parseDshVersion(dshVersion)
|
|
138
|
+
if (dshVersion === '') {
|
|
139
|
+
adapters.push({ name: 'DSH 版本', active: false, note: '未探测到版本号(能力探测模式运行)' })
|
|
140
|
+
} else if (!parsed) {
|
|
141
|
+
adapters.push({ name: 'DSH 版本', active: false, note: '无法解析版本号 ' + dshVersion + '(能力探测模式运行)' })
|
|
142
|
+
} else {
|
|
143
|
+
adapters.push({ name: 'DSH 版本', active: true, note: dshVersion + ' · ' + familyLabel(dshVersion) })
|
|
144
|
+
}
|
|
145
|
+
const strategy = settingsInstallStrategy()
|
|
146
|
+
adapters.push({
|
|
147
|
+
name: '设置 section 安装(版本适配)',
|
|
148
|
+
active: strategy === 'legacy' || strategy === 'service',
|
|
149
|
+
note: settingsInstallNote(),
|
|
150
|
+
})
|
|
151
|
+
return adapters
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
/**
|
|
131
155
|
* 构建完整兼容性报告(RPC 与启动日志共用)。
|
|
132
|
-
* @author ddj 2026年08月24号
|
|
156
|
+
* @author ddj 2026年08月24号 / 2026年09月02号
|
|
133
157
|
* @param ctx DSH host 上下文
|
|
134
|
-
* @param options 测试注入:depsAvailable 跳过动态导入、version
|
|
158
|
+
* @param options 测试注入:depsAvailable 跳过动态导入、version 固定插件版本、dshVersion 固定 DSH 版本
|
|
135
159
|
* @returns 兼容性报告
|
|
136
160
|
*/
|
|
137
161
|
export async function buildReport(
|
|
138
162
|
ctx: Ctx,
|
|
139
|
-
options?: { depsAvailable?: boolean; version?: string },
|
|
163
|
+
options?: { depsAvailable?: boolean; version?: string; dshVersion?: string },
|
|
140
164
|
): Promise<CompatReport> {
|
|
141
165
|
const deps = options?.depsAvailable ?? (await loadSettingsDeps()) !== null
|
|
166
|
+
const dshVersion = options?.dshVersion ?? detectDshVersion()
|
|
142
167
|
const external = detectExternal(ctx, deps)
|
|
143
168
|
const guards = detectGuards(ctx)
|
|
169
|
+
const adapters = versionAdapters(dshVersion)
|
|
144
170
|
const warnings: string[] = []
|
|
145
171
|
if (!deps) warnings.push('未安装 @deepseek-ai/dsh-settings:文件打开工具设置持久化不可用(降级为配置值)')
|
|
172
|
+
const strategy = settingsInstallStrategy()
|
|
173
|
+
if (strategy === 'none') warnings.push('设置 section 安装不可用:' + settingsInstallNote())
|
|
174
|
+
const parsed = parseDshVersion(dshVersion)
|
|
175
|
+
const testedMax = parseDshVersion(TESTED_DSH_MAX)
|
|
176
|
+
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) {
|
|
177
|
+
warnings.push('DSH ' + dshVersion + ' 高于已实测版本(' + TESTED_DSH_MAX + '):设置 API 按能力探测运行,异常时请回报适配矩阵')
|
|
178
|
+
}
|
|
146
179
|
for (const guard of guards) {
|
|
147
180
|
if (!guard.active && guard.note) warnings.push(guard.note)
|
|
148
181
|
}
|
|
149
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
184
|
+
external,
|
|
185
|
+
guards,
|
|
186
|
+
adapters,
|
|
187
|
+
warnings,
|
|
188
|
+
devForm: readDevForm(),
|
|
189
|
+
dshVersion,
|
|
190
|
+
}
|
|
150
191
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — DSH 核心版本探测与区间匹配(版本适配机制的地基)。
|
|
3
|
+
* 约定:以运行时可解析的 @deepseek-ai/dsh-settings 包版本代表 DSH 核心版本——
|
|
4
|
+
* 核心包发布锁步同版(rc.2 / alpha.2 全树同版实证),且该包是插件动态依赖面、
|
|
5
|
+
* exports 已放行 ./package.json。解析失败按候选降级(dsh-web-app / dsh-base / dsh),
|
|
6
|
+
* 全部失败返回空串(报告显示"未探测",功能按能力探测运行)。
|
|
7
|
+
* 纯函数(parse / compare / inRange / familyLabel)可在 node 环境单测。
|
|
8
|
+
* 作者 ddj 2026-09-02
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { createRequire } from 'node:module'
|
|
12
|
+
|
|
13
|
+
/** 归一化版本:主/次/补丁 + 预发布标识数组(0.1.2-alpha.2 → pre=['alpha','2'])。 */
|
|
14
|
+
export interface DshVersion {
|
|
15
|
+
major: number
|
|
16
|
+
minor: number
|
|
17
|
+
patch: number
|
|
18
|
+
pre: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
22
|
+
|
|
23
|
+
/** 解析版本串;非法返回 null(不抛错)。 */
|
|
24
|
+
export function parseDshVersion(input: string): DshVersion | null {
|
|
25
|
+
const match = VERSION_RE.exec(input.trim())
|
|
26
|
+
if (!match) return null
|
|
27
|
+
return {
|
|
28
|
+
major: Number(match[1]),
|
|
29
|
+
minor: Number(match[2]),
|
|
30
|
+
patch: Number(match[3]),
|
|
31
|
+
pre: match[4] ? match[4].split('.') : [],
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 预发布标识比较:纯数字按值;数字段 < 字母段;字母按字典序(semver 规则)。 */
|
|
36
|
+
function comparePreId(a: string, b: string): number {
|
|
37
|
+
const numericA = /^\d+$/.test(a)
|
|
38
|
+
const numericB = /^\d+$/.test(b)
|
|
39
|
+
if (numericA && numericB) return Number(a) - Number(b)
|
|
40
|
+
if (numericA !== numericB) return numericA ? -1 : 1
|
|
41
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 比较两个已解析版本:a<b → -1;相等 → 0;a>b → 1。 */
|
|
45
|
+
export function compareDshVersions(a: DshVersion, b: DshVersion): number {
|
|
46
|
+
for (const key of ['major', 'minor', 'patch'] as const) {
|
|
47
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1
|
|
48
|
+
}
|
|
49
|
+
const samePre = a.pre.length === b.pre.length && a.pre.every((part, index) => part === b.pre[index])
|
|
50
|
+
if (samePre) return 0
|
|
51
|
+
if (a.pre.length === 0) return 1
|
|
52
|
+
if (b.pre.length === 0) return -1
|
|
53
|
+
const length = Math.max(a.pre.length, b.pre.length)
|
|
54
|
+
for (let i = 0; i < length; i++) {
|
|
55
|
+
const left = a.pre[i]
|
|
56
|
+
const right = b.pre[i]
|
|
57
|
+
if (left === undefined) return -1
|
|
58
|
+
if (right === undefined) return 1
|
|
59
|
+
const cmp = comparePreId(left, right)
|
|
60
|
+
if (cmp !== 0) return cmp
|
|
61
|
+
}
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 版本区间(含端点):from/to 为空表示该侧不设界。 */
|
|
66
|
+
export interface DshRange {
|
|
67
|
+
from?: string
|
|
68
|
+
to?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 版本是否落在区间内(入参解析失败一律不命中)。 */
|
|
72
|
+
export function inDshRange(version: DshVersion | null | undefined, range: DshRange): boolean {
|
|
73
|
+
if (!version) return false
|
|
74
|
+
if (range.from) {
|
|
75
|
+
const lower = parseDshVersion(range.from)
|
|
76
|
+
if (!lower || compareDshVersions(version, lower) < 0) return false
|
|
77
|
+
}
|
|
78
|
+
if (range.to) {
|
|
79
|
+
const upper = parseDshVersion(range.to)
|
|
80
|
+
if (!upper || compareDshVersions(version, upper) > 0) return false
|
|
81
|
+
}
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。 */
|
|
86
|
+
export function familyLabel(input: string): string {
|
|
87
|
+
const version = parseDshVersion(input)
|
|
88
|
+
if (!version) return '未知'
|
|
89
|
+
if (inDshRange(version, { from: '0.1.2-alpha.1' })) return '0.1.2-alpha 及更新(设置 API=settings.installSection)'
|
|
90
|
+
return '0.1.0/0.1.1 rc 线(设置 API=installSettingsSection)'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 探测候选:按与插件运行环境相关性排序,逐个尝试解析 package.json。 */
|
|
94
|
+
const VERSION_CANDIDATES = [
|
|
95
|
+
'@deepseek-ai/dsh-settings/package.json',
|
|
96
|
+
'@deepseek-ai/dsh-web-app/package.json',
|
|
97
|
+
'@deepseek-ai/dsh-base/package.json',
|
|
98
|
+
'@deepseek-ai/dsh/package.json',
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
let detectedVersion: string | undefined
|
|
102
|
+
|
|
103
|
+
/** 探测运行中 DSH 核心版本(模块级缓存;失败空串,不抛错)。 */
|
|
104
|
+
export function detectDshVersion(): string {
|
|
105
|
+
if (detectedVersion !== undefined) return detectedVersion
|
|
106
|
+
detectedVersion = ''
|
|
107
|
+
const require = createRequire(import.meta.url)
|
|
108
|
+
for (const specifier of VERSION_CANDIDATES) {
|
|
109
|
+
try {
|
|
110
|
+
const file = require.resolve(specifier)
|
|
111
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
|
|
112
|
+
if (typeof pkg.version === 'string' && parseDshVersion(pkg.version) !== null) {
|
|
113
|
+
detectedVersion = pkg.version
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
/* exports 未放行或包缺失:尝试下一个候选 */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return detectedVersion
|
|
121
|
+
}
|