dsh-vscode-mode 0.5.0 → 0.5.2
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/README.md +15 -2
- package/lib/client.js +518 -71
- package/lib/client.js.map +1 -1
- package/lib/index.js +193 -43
- package/lib/index.js.map +1 -1
- package/package.json +8 -7
- package/src/client/ai/inlineProvider.ts +42 -1
- package/src/client/index.ts +47 -45
- package/src/client/monaco/lsp/index.ts +33 -3
- package/src/client/monaco/lsp/lspClient.ts +3 -1
- package/src/client/monaco/lsp/providers.ts +32 -0
- package/src/client/officialSidebar.ts +67 -3
- package/src/client/sessionScope.ts +183 -0
- package/src/client/snippets/provider.ts +26 -4
- package/src/client/tabActions.ts +30 -5
- package/src/client/ui/AiSettings.ts +8 -0
- package/src/client/ui/EditorView.ts +51 -13
- package/src/compat.ts +3 -3
- package/src/dshVersion.ts +8 -1
- package/src/fileOpenSettings.ts +86 -10
- package/src/lsp/extmgr.ts +56 -3
- package/src/lsp/zip.ts +25 -9
- package/src/revert.ts +20 -8
- package/src/shared/ai.ts +25 -0
|
@@ -23,6 +23,16 @@ let pending = null
|
|
|
23
23
|
let registered = false
|
|
24
24
|
let disposer = null
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 片段 provider 注销器的全局挂点名。
|
|
28
|
+
*
|
|
29
|
+
* 为什么挂 window:`window.monaco` 由 loader 注入后**跨插件重载存活**,而模块级
|
|
30
|
+
* registered/disposer 会随 bundle 重新求值复位 —— 只判 registered 会在每次重载后
|
|
31
|
+
* 重复注册补全 provider(同一编辑器出现重复候补)。故注销器落 window 供跨代认领与清理。
|
|
32
|
+
* @author ddj 2026年09月18号
|
|
33
|
+
*/
|
|
34
|
+
const SNIPPET_GLOBAL = '__edrvSnippetsDisposer__'
|
|
35
|
+
|
|
26
36
|
/** 当前会话 id(补全按会话工作区叠加项目片段;EditorView 装配时注入)。 */
|
|
27
37
|
let sessionId = null
|
|
28
38
|
|
|
@@ -102,6 +112,12 @@ export function entriesForLanguage(entries, languageId) {
|
|
|
102
112
|
*/
|
|
103
113
|
export function registerSnippetProvider(monaco) {
|
|
104
114
|
if (registered || !monaco?.languages?.registerCompletionItemProvider) return
|
|
115
|
+
// 跨重载守卫:上一代 bundle 注册的 provider 仍在存活 window.monaco 上(注销器已落 window)
|
|
116
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
117
|
+
if (host && host[SNIPPET_GLOBAL]) {
|
|
118
|
+
registered = true
|
|
119
|
+
return
|
|
120
|
+
}
|
|
105
121
|
const snippetKind = monaco.languages.CompletionItemKind?.Snippet
|
|
106
122
|
const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet
|
|
107
123
|
// 缺少片段枚举(精简版 Monaco)时不注册:否则候选项会退化为纯文本插入,误导用户
|
|
@@ -135,6 +151,8 @@ export function registerSnippetProvider(monaco) {
|
|
|
135
151
|
}
|
|
136
152
|
},
|
|
137
153
|
})
|
|
154
|
+
// 注销器落 window:跨重载认领(见 SNIPPET_GLOBAL 说明)
|
|
155
|
+
if (host) host[SNIPPET_GLOBAL] = disposer
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
/**
|
|
@@ -147,13 +165,17 @@ export function setupSnippets(monaco) {
|
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
/**
|
|
150
|
-
* 卸载:注销 provider
|
|
151
|
-
*
|
|
168
|
+
* 卸载:注销 provider 并复位状态(插件热重载/卸载时调用)。
|
|
169
|
+
* 兼容上一代 bundle 遗留的 window 注销器(模块级 disposer 为空时仍能清干净)。
|
|
170
|
+
* @author ddj 2026年09月10号 / 2026年09月18号
|
|
152
171
|
*/
|
|
153
172
|
export function disposeSnippets() {
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
174
|
+
const target = (typeof disposer === 'function' ? disposer : null) ?? (host ? host[SNIPPET_GLOBAL] : null)
|
|
175
|
+
if (target && typeof target.dispose === 'function') {
|
|
176
|
+
try { target.dispose() } catch { /* 已注销 */ }
|
|
156
177
|
}
|
|
178
|
+
if (host) delete host[SNIPPET_GLOBAL]
|
|
157
179
|
disposer = null
|
|
158
180
|
registered = false
|
|
159
181
|
invalidateSnippets()
|
package/src/client/tabActions.ts
CHANGED
|
@@ -184,23 +184,48 @@ function isPinned(tab: TabLike): boolean {
|
|
|
184
184
|
/**
|
|
185
185
|
* 归一化持久化的页签数据:兼容旧版 `string[]`、去重、固定分区。
|
|
186
186
|
* 损坏项(非字符串/非对象/无 path)直接丢弃。
|
|
187
|
-
*
|
|
187
|
+
*
|
|
188
|
+
* G9:传入 cwd 时把每个路径收敛为**页签规范形态**(工作区相对路径),
|
|
189
|
+
* 以迁移历史持久化数据里残留的绝对路径(见 {@link tabPathOf})。
|
|
190
|
+
* @author ddj 2026年09月11号 / 2026年09月18号
|
|
188
191
|
* @param raw localStorage 解析结果(任意形状)
|
|
192
|
+
* @param cwd 会话工作区目录(可选;给出时同步归一化路径形态)
|
|
189
193
|
* @returns 归一化后的页签数组
|
|
190
194
|
*/
|
|
191
|
-
export function normalizeTabs(raw: unknown): TabLike[] {
|
|
195
|
+
export function normalizeTabs(raw: unknown, cwd?: string | null): TabLike[] {
|
|
192
196
|
if (!Array.isArray(raw)) return []
|
|
193
197
|
const seen = new Set<string>()
|
|
194
198
|
const out: TabLike[] = []
|
|
195
199
|
for (const item of raw) {
|
|
196
200
|
const tab = tabOf(item)
|
|
197
|
-
if (!tab
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
if (!tab) continue
|
|
202
|
+
const path = tabPathOf(tab.path, cwd)
|
|
203
|
+
if (!path || seen.has(path)) continue
|
|
204
|
+
seen.add(path)
|
|
205
|
+
out.push(tab.pinned === true ? { path, pinned: true } : { path })
|
|
200
206
|
}
|
|
201
207
|
return pinnedFirst(out)
|
|
202
208
|
}
|
|
203
209
|
|
|
210
|
+
/**
|
|
211
|
+
* 页签规范形态 = 工作区相对路径(G9)。
|
|
212
|
+
*
|
|
213
|
+
* 为什么需要:差异记录路径取自工具结果 `target.displayPath`(官方恒为绝对拼写),
|
|
214
|
+
* 而资源管理器树给出的是工作区相对路径。两者进页签后,地址栏形态不一致,且
|
|
215
|
+
* `insertTab` 按原串去重 → 同一文件可能出两个页签;绝对路径还会被
|
|
216
|
+
* `isTreeRevealable` 判为不可定位,「在资源管理器视图中显示」对差异入口失效。
|
|
217
|
+
* 统一经 {@link relativeOf} 收敛后,页签/地址栏/去重/持久化/`sameFile` 口径一致。
|
|
218
|
+
*
|
|
219
|
+
* 工作区外文件(全局片段/规则)由 relativeOf 自然回退为原绝对路径,语义不变。
|
|
220
|
+
* @author ddj 2026年09月18号
|
|
221
|
+
* @param path 原始路径(绝对或相对,两种分隔符均可)
|
|
222
|
+
* @param cwd 会话工作区目录(可空;无 cwd 时仅做分隔符归一)
|
|
223
|
+
* @returns 页签规范路径
|
|
224
|
+
*/
|
|
225
|
+
export function tabPathOf(path: string, cwd?: string | null): string {
|
|
226
|
+
return relativeOf(path, cwd)
|
|
227
|
+
}
|
|
228
|
+
|
|
204
229
|
/**
|
|
205
230
|
* 解释单条持久化页签:字符串 = 路径(旧版),对象取 path/pinned。
|
|
206
231
|
* @author ddj 2026年09月11号
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import React from 'react'
|
|
10
10
|
import { rpc } from '../rpc.js'
|
|
11
11
|
import { setAiInlineEnabled } from '../ai/inlineProvider.js'
|
|
12
|
+
import { isDelistedModel } from '../../shared/ai.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* AI 补全设置卡片(设置页「AI 补全」Tab 主体)。
|
|
@@ -79,9 +80,16 @@ export function AiSettings() {
|
|
|
79
80
|
if (!cfg) return React.createElement('div', { className: 'vsm-mcp-empty' }, error || '正在读取 AI 补全配置…')
|
|
80
81
|
|
|
81
82
|
const modelValue = cfg.provider && cfg.model ? cfg.provider + '/' + cfg.model : ''
|
|
83
|
+
// G7:配置指向已从官方默认列表下架的模型(DSH 0.1.6-alpha.2 移除 V4 Flash 系列)。
|
|
84
|
+
// 仅提示,不擅自改写用户已保存的配置(避免覆盖其意图);清空即回到自动路由。
|
|
85
|
+
const delisted = isDelistedModel(cfg.model)
|
|
82
86
|
return React.createElement('section', { className: 'vsm-lsp-card' },
|
|
83
87
|
React.createElement('h3', null, 'AI 自动补全(实验)'),
|
|
84
88
|
React.createElement('p', { className: 'vsm-lsp-note' }, '编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。'),
|
|
89
|
+
(delisted
|
|
90
|
+
? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
|
|
91
|
+
'当前模型「' + cfg.model + '」已从 DSH 默认模型列表移除(0.1.6-alpha.2 起)。该模型可能不再可用;将「模型」改回「自动」即可使用第一个可用模型(现有配置不会被自动改动)。')
|
|
92
|
+
: null),
|
|
85
93
|
React.createElement('div', { className: 'vsm-lsp-row' },
|
|
86
94
|
React.createElement('button', { className: cfg.enabled ? 'vsm-primary' : '', disabled: busy, onClick: toggle },
|
|
87
95
|
cfg.enabled ? '已开启(点击关闭)' : '已关闭(点击开启)'),
|
|
@@ -56,7 +56,7 @@ import { SnippetsPicker } from './SnippetsPicker.js'
|
|
|
56
56
|
import { invalidateSnippets, setSnippetsSession, setupSnippets } from '../snippets/provider.js'
|
|
57
57
|
import {
|
|
58
58
|
absoluteOf, ancestorDirsOf, applyClose, baseNameOf, closeAll, closeOthers, closeRight, closeSaved,
|
|
59
|
-
insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, togglePin,
|
|
59
|
+
insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
|
|
60
60
|
} from '../tabActions.js'
|
|
61
61
|
import { buildTabMenu } from '../tabMenu.js'
|
|
62
62
|
import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
|
|
@@ -307,6 +307,33 @@ export function EditorView(props) {
|
|
|
307
307
|
if (select) setActive(path)
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* 页签规范路径(G9):统一收敛为工作区相对路径后再进页签。
|
|
312
|
+
*
|
|
313
|
+
* 差异记录路径取自工具结果 `target.displayPath`(官方恒绝对),而资源管理器树给相对路径;
|
|
314
|
+
* 不归一会导致地址栏形态不一致、同文件出现两个页签(insertTab 按原串去重)、
|
|
315
|
+
* 且绝对路径被 isTreeRevealable 判为不可定位(「在资源管理器视图中显示」失效)。
|
|
316
|
+
* 工作区外文件由 relativeOf 回退原绝对路径,语义不变。
|
|
317
|
+
* @author ddj 2026年09月18号
|
|
318
|
+
* @param path 原始路径(绝对或相对)
|
|
319
|
+
* @returns 页签规范路径
|
|
320
|
+
*/
|
|
321
|
+
const tabPath = (path) => tabPathOf(path, cwd)
|
|
322
|
+
/**
|
|
323
|
+
* 打开文件入口的统一收敛(G9):所有「进页签」路径都经此归一,避免逐点打补丁。
|
|
324
|
+
* @author ddj 2026年09月18号
|
|
325
|
+
* @param path 原始路径
|
|
326
|
+
* @param select 是否设为活动页签
|
|
327
|
+
* @returns 归一后的路径(空值返回 null)
|
|
328
|
+
*/
|
|
329
|
+
const addTabNorm = (path, select) => {
|
|
330
|
+
if (!path) return null
|
|
331
|
+
const normalized = tabPath(path)
|
|
332
|
+
if (!normalized) return null
|
|
333
|
+
addTab(normalized, select)
|
|
334
|
+
return normalized
|
|
335
|
+
}
|
|
336
|
+
|
|
310
337
|
/**
|
|
311
338
|
* 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
|
|
312
339
|
* @author ddj 2026年08月28号
|
|
@@ -388,7 +415,7 @@ export function EditorView(props) {
|
|
|
388
415
|
flushSave()
|
|
389
416
|
saveViewState(active)
|
|
390
417
|
navPendingRef.current = entry
|
|
391
|
-
|
|
418
|
+
addTabNorm(entry.path, true)
|
|
392
419
|
setFocusRequest((value) => value + 1) // 目标已是活动文件时也触发恢复
|
|
393
420
|
}
|
|
394
421
|
|
|
@@ -832,20 +859,23 @@ export function EditorView(props) {
|
|
|
832
859
|
const onOpen = (e) => {
|
|
833
860
|
const p = e?.detail?.path
|
|
834
861
|
if (!p) return
|
|
862
|
+
// G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
|
|
863
|
+
const normalized = tabPath(p)
|
|
864
|
+
if (!normalized) return
|
|
835
865
|
if (e?.detail?.focusDiff === true) {
|
|
836
866
|
recordNav()
|
|
837
|
-
pendingFocusRef.current = { path:
|
|
867
|
+
pendingFocusRef.current = { path: normalized, region: null }
|
|
838
868
|
setFocusRequest((value) => value + 1)
|
|
839
|
-
addTab(
|
|
869
|
+
addTab(normalized, true)
|
|
840
870
|
return
|
|
841
871
|
}
|
|
842
872
|
if (e?.detail?.line != null) {
|
|
843
873
|
// LSP/搜索跳转:打开并定位到行列(endLine/endColumn 为目标区间,供落地高亮)
|
|
844
|
-
openFileAt(
|
|
874
|
+
openFileAt(normalized, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
|
|
845
875
|
return
|
|
846
876
|
}
|
|
847
877
|
recordNav()
|
|
848
|
-
addTab(
|
|
878
|
+
addTab(normalized, true)
|
|
849
879
|
}
|
|
850
880
|
const onShowLauncher = (event) => {
|
|
851
881
|
const tab = event?.detail?.tab
|
|
@@ -939,10 +969,13 @@ export function EditorView(props) {
|
|
|
939
969
|
?? localStorage.getItem(CACHE_KEY.editorLegacy + String(scope))
|
|
940
970
|
if (raw) {
|
|
941
971
|
const saved = JSON.parse(raw)
|
|
942
|
-
|
|
972
|
+
// G9:迁移历史持久化里的绝对路径页签(旧版差异入口写入),并顺带按新形态去重
|
|
973
|
+
const restored = normalizeTabs(saved?.tabs, cwd)
|
|
943
974
|
if (restored.length) {
|
|
944
975
|
setTabs(restored)
|
|
945
|
-
|
|
976
|
+
// 活动路径同形态归一,否则恢复后匹配不到任何页签(pickActive 回退首个)
|
|
977
|
+
const wanted = typeof saved?.active === 'string' ? tabPathOf(saved.active, cwd) : saved?.active
|
|
978
|
+
setActive(pickActive(restored, wanted))
|
|
946
979
|
}
|
|
947
980
|
}
|
|
948
981
|
} catch (e) { /* 损坏忽略 */ }
|
|
@@ -2495,8 +2528,9 @@ export function EditorView(props) {
|
|
|
2495
2528
|
// 打开文件即离开基线差异审阅态(差异视图是「当前文件」的临时视图)
|
|
2496
2529
|
setSvnDiff(null)
|
|
2497
2530
|
recordNav()
|
|
2498
|
-
|
|
2499
|
-
|
|
2531
|
+
// G9:统一归一为页签规范形态(差异栏/启动器/树/命令栏等入口一致)
|
|
2532
|
+
const normalized = addTabNorm(path, true)
|
|
2533
|
+
if (focusDiff && normalized) pendingFocusRef.current = { path: normalized, region: null }
|
|
2500
2534
|
}
|
|
2501
2535
|
|
|
2502
2536
|
/**
|
|
@@ -2512,8 +2546,10 @@ export function EditorView(props) {
|
|
|
2512
2546
|
const openFileAt = (path, line, column, endLine, endColumn) => {
|
|
2513
2547
|
if (!path) return
|
|
2514
2548
|
recordNav()
|
|
2515
|
-
|
|
2516
|
-
|
|
2549
|
+
// G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
|
|
2550
|
+
const normalized = addTabNorm(path, true)
|
|
2551
|
+
if (!normalized) return
|
|
2552
|
+
pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
|
|
2517
2553
|
setFocusRequest((value) => value + 1)
|
|
2518
2554
|
}
|
|
2519
2555
|
|
|
@@ -2683,7 +2719,9 @@ export function EditorView(props) {
|
|
|
2683
2719
|
}, '保留本地')))
|
|
2684
2720
|
}
|
|
2685
2721
|
|
|
2686
|
-
|
|
2722
|
+
// G9:记录路径为绝对、active 为页签规范形态(相对),必须经 sameFile 归一比较,
|
|
2723
|
+
// 否则当前活动文件会被误列入「其他差异文件」。
|
|
2724
|
+
const otherFiles = sum.pendingFiles.filter((f) => !sameFile(f.path, active))
|
|
2687
2725
|
|
|
2688
2726
|
/**
|
|
2689
2727
|
* 渲染编辑器/文件加载进度面板。
|
package/src/compat.ts
CHANGED
|
@@ -10,7 +10,7 @@ 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, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
13
|
+
import { loadSettingsDeps, schemaLibName, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
14
14
|
import { readDevForm } from './devForm.js'
|
|
15
15
|
import { compareDshVersions, detectDshVersion, familyLabel, parseDshVersion } from './dshVersion.js'
|
|
16
16
|
import { SKILL_PREFIXES, skillGroupState } from './skills.js'
|
|
@@ -59,7 +59,7 @@ export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[
|
|
|
59
59
|
const skills = skillGroupState()
|
|
60
60
|
return [
|
|
61
61
|
{ name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
|
|
62
|
-
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section
|
|
62
|
+
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装(schema: ' + (schemaLibName() || '未知') + ')' : '未安装:fileOpenTool 持久化降级为配置值' },
|
|
63
63
|
{ name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
|
|
64
64
|
{ name: '文件浏览器打开(subprocess 服务)', active: typeof sub?.spawn === 'function', note: typeof sub?.spawn === 'function' ? '可定位/打开 OS 文件浏览器' : '不可用(右键「在文件浏览器中打开」将提示失败)' },
|
|
65
65
|
// 新增项一律追加在末尾:既有下标被 tests/compat.test.ts 断言,不得前插。
|
|
@@ -133,7 +133,7 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
136
|
-
const TESTED_DSH_MAX = '0.1.6-alpha.
|
|
136
|
+
const TESTED_DSH_MAX = '0.1.6-alpha.2'
|
|
137
137
|
|
|
138
138
|
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
|
|
139
139
|
export function versionAdapters(dshVersion: string): CompatAdapter[] {
|
package/src/dshVersion.ts
CHANGED
|
@@ -82,10 +82,17 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
|
|
|
82
82
|
return true
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
87
|
+
* 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
|
|
88
|
+
* @author ddj 2026年09月02号 / 2026年09月18号
|
|
89
|
+
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
90
|
+
* @returns 版本线标签
|
|
91
|
+
*/
|
|
86
92
|
export function familyLabel(input: string): string {
|
|
87
93
|
const version = parseDshVersion(input)
|
|
88
94
|
if (!version) return '未知'
|
|
95
|
+
if (inDshRange(version, { from: '0.1.6-alpha.2' })) return '0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)'
|
|
89
96
|
if (inDshRange(version, { from: '0.1.6-alpha.1' })) return '0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)'
|
|
90
97
|
if (inDshRange(version, { from: '0.1.5-alpha.1' })) return '0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)'
|
|
91
98
|
if (inDshRange(version, { from: '0.1.3-alpha.1' })) return '0.1.3-alpha 及更新(对话文件链接=remote.session.openWorkspacePath)'
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -73,20 +73,96 @@ async function hostImport(specifier: string): Promise<unknown> {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* schema 库候选名(新名在前)。
|
|
77
|
+
* DSH 官方自 0.1.5 起把 vendored 包改名为 @deepseek-ai/schemastery 并全树改用新名,
|
|
78
|
+
* 安装树里**没有**裸 schemastery;裸名保留给 rc 线(其 dsh-settings 仍 peers 旧名),
|
|
79
|
+
* 也让开发形态(插件 node_modules 有 devDependency 副本)继续可用。
|
|
80
|
+
*/
|
|
81
|
+
const SCHEMA_SPECIFIERS = ['@deepseek-ai/schemastery', 'schemastery'] as const
|
|
82
|
+
|
|
83
|
+
/** 设置持久化包名(installSettingsSection 仅 rc 线提供,alpha 线缺失属正常)。 */
|
|
84
|
+
const SETTINGS_SPECIFIER = '@deepseek-ai/dsh-settings'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 逐个尝试候选名,返回首个加载成功的模块及其包名(全失败返回 null,不抛错)。
|
|
88
|
+
* @author ddj 2026年09月18号
|
|
89
|
+
* @param specifiers 候选包名(按优先级)
|
|
90
|
+
* @param importFn 加载函数(测试注入)
|
|
91
|
+
* @returns 命中的模块与包名;全失败返回 null
|
|
92
|
+
*/
|
|
93
|
+
async function firstImport(
|
|
94
|
+
specifiers: readonly string[],
|
|
95
|
+
importFn: (specifier: string) => Promise<unknown> = hostImport,
|
|
96
|
+
): Promise<{ specifier: string; module: unknown } | null> {
|
|
97
|
+
for (const specifier of specifiers) {
|
|
98
|
+
try {
|
|
99
|
+
return { specifier, module: await importFn(specifier) }
|
|
100
|
+
} catch {
|
|
101
|
+
/* 该候选不可解析:尝试下一个 */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 抹平 schema 库的 ESM/CJS 互操作形态取默认导出。
|
|
109
|
+
* 三种实测形态:真 ESM(`default` 即 z)、CJS 经 import()(`default` 与
|
|
110
|
+
* `module.exports` 同为 z)、双层包装(`default.default` 才是 z)。
|
|
111
|
+
* @author ddj 2026年09月18号
|
|
112
|
+
* @param module 加载到的模块命名空间(可空)
|
|
113
|
+
* @returns 具备 object/string 等构造器的 z;取不到返回 null
|
|
114
|
+
*/
|
|
115
|
+
export function pickSchema(module: unknown): SettingsDeps['z'] | null {
|
|
116
|
+
const layers = [module, (module as { default?: unknown } | null)?.default, (module as Record<string, unknown> | null)?.['module.exports']]
|
|
117
|
+
for (const layer of layers) {
|
|
118
|
+
const z = ((layer as { default?: unknown } | null)?.default ?? layer) as SettingsDeps['z'] | undefined
|
|
119
|
+
if (z && typeof z.object === 'function' && typeof z.string === 'function') return z
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 实际命中的 schema 库名(供兼容性报告展示;未命中为空串)。 */
|
|
125
|
+
let schemaLib = ''
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 读取实际命中的 schema 库名(报告文案用)。
|
|
129
|
+
* @author ddj 2026年09月18号
|
|
130
|
+
* @returns 包名;未解析到为空串
|
|
131
|
+
*/
|
|
132
|
+
export function schemaLibName(): string {
|
|
133
|
+
return schemaLib
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 复位依赖缓存与命中库名(测试隔离用)。 */
|
|
137
|
+
export function resetSettingsDeps(): void {
|
|
138
|
+
depsPromise = undefined
|
|
139
|
+
schemaLib = ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 动态加载设置依赖(模块级缓存;schema 库缺失返回 null 而非抛错)。
|
|
144
|
+
* ⚠️ 两个依赖**独立解析**:@deepseek-ai/dsh-settings 仅 rc 线跑 legacy 策略时需要,
|
|
145
|
+
* alpha 线走 settings 服务 installSection 用不到它;原先 Promise.all 让该包缺失
|
|
146
|
+
* 拖垮整体 → 用户端(npm 安装)settings section 永不装配。
|
|
147
|
+
* 同理,schema 库按候选链解析(安装树只有新名 @deepseek-ai/schemastery)。
|
|
148
|
+
* @author ddj 2026年08月24号 / 2026年09月15号 / 2026年09月18号
|
|
149
|
+
* @param importFn 加载函数(测试注入;缺省宿主锚点动态导入)
|
|
80
150
|
* @returns 设置依赖或 null
|
|
81
151
|
*/
|
|
82
|
-
export function loadSettingsDeps(): Promise<SettingsDeps | null> {
|
|
152
|
+
export function loadSettingsDeps(importFn: (specifier: string) => Promise<unknown> = hostImport): Promise<SettingsDeps | null> {
|
|
83
153
|
if (!depsPromise) {
|
|
84
|
-
depsPromise = Promise.all([
|
|
85
|
-
.then(([
|
|
154
|
+
depsPromise = Promise.all([firstImport([SETTINGS_SPECIFIER], importFn), firstImport(SCHEMA_SPECIFIERS, importFn)])
|
|
155
|
+
.then(([settingsHit, schemaHit]) => {
|
|
156
|
+
const z = pickSchema(schemaHit?.module)
|
|
157
|
+
if (!z) {
|
|
158
|
+
schemaLib = ''
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
schemaLib = schemaHit?.specifier ?? ''
|
|
86
162
|
// dsh-settings 类型声明随版本变化(rc.8 有 d.ts、alpha 已移除导出),统一经 unknown 松绑
|
|
87
|
-
installSettingsSection
|
|
88
|
-
|
|
89
|
-
})
|
|
163
|
+
const installSettingsSection = (settingsHit?.module as unknown as { installSettingsSection?: SettingsDeps['installSettingsSection'] } | undefined)?.installSettingsSection
|
|
164
|
+
return { installSettingsSection, z }
|
|
165
|
+
})
|
|
90
166
|
.catch(() => null)
|
|
91
167
|
}
|
|
92
168
|
return depsPromise
|
package/src/lsp/extmgr.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* 已装扩展作为 LSP provider 的"扩展源"(kind=extension),见 providers.ts。
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { basename, dirname, join } from 'node:path'
|
|
10
|
-
import { unzip } from './zip.js'
|
|
10
|
+
import { unzip, type ZipEntry } from './zip.js'
|
|
11
11
|
import { dshHome, PLUGIN_ID } from '../paths.js'
|
|
12
12
|
|
|
13
13
|
/** 单个已装扩展信息(edrv.lsp.ext.list 载荷元素)。 */
|
|
@@ -57,9 +57,61 @@ export function vsixManifest(vsix: Buffer): Record<string, unknown> {
|
|
|
57
57
|
throw new Error('VSIX 缺少 extension/package.json')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 判定解包路径是否为可执行入口(无 mode 时的启发式兜底)。
|
|
62
|
+
* 只认确定语义:`bin/` 段下的文件、语言服务器/运行时常见可执行名、`.sh` 脚本。
|
|
63
|
+
* @author ddj 2026年09月18号
|
|
64
|
+
* @param rel 相对路径(/ 分隔)
|
|
65
|
+
* @returns 是否应补执行位
|
|
66
|
+
*/
|
|
67
|
+
export function looksExec(rel: string): boolean {
|
|
68
|
+
const path = String(rel ?? '').replace(/\\/g, '/')
|
|
69
|
+
if (!path) return false
|
|
70
|
+
if (path.split('/').includes('bin')) return true
|
|
71
|
+
if (/\.(sh|bash|zsh)$/i.test(path)) return true
|
|
72
|
+
const name = path.split('/').pop() ?? ''
|
|
73
|
+
const base = name.replace(/\.exe$/i, '')
|
|
74
|
+
return ['lua-language-server', 'emmylua_ls', 'OmniSharp', 'dotnet', 'node'].includes(base)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 计算解包后要施加的权限位(纯函数,可单测)。
|
|
79
|
+
* 优先信任归档自带的 Unix mode 中的执行位(VS Code 自身解 VSIX 即此口径);
|
|
80
|
+
* 归档无 mode(Windows 打包的 VSIX)时按路径启发式补 0o755;
|
|
81
|
+
* 其余保持 undefined(交给 umask 默认,不越权改动)。
|
|
82
|
+
* @author ddj 2026年09月18号
|
|
83
|
+
* @param entry 解压条目
|
|
84
|
+
* @returns 目标权限位;无需处理返回 undefined
|
|
85
|
+
*/
|
|
86
|
+
export function execModeOf(entry: ZipEntry): number | undefined {
|
|
87
|
+
if (entry.isDirectory) return undefined
|
|
88
|
+
const mode = entry.mode
|
|
89
|
+
if (mode !== undefined && (mode & 0o111) !== 0) return mode
|
|
90
|
+
return looksExec(entry.path) ? 0o755 : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 落盘后补可执行位(POSIX 专属;Windows 无此概念,直接跳过)。
|
|
95
|
+
* 必须 best-effort:只读文件系统/无权限时不能拖垮整个扩展安装。
|
|
96
|
+
* @author ddj 2026年09月18号
|
|
97
|
+
* @param target 落盘绝对路径
|
|
98
|
+
* @param entry 解压条目
|
|
99
|
+
* @param platform 目标平台(测试注入)
|
|
100
|
+
*/
|
|
101
|
+
export function applyExecBit(target: string, entry: ZipEntry, platform: NodeJS.Platform = process.platform): void {
|
|
102
|
+
if (platform === 'win32') return
|
|
103
|
+
const mode = execModeOf(entry)
|
|
104
|
+
if (mode === undefined) return
|
|
105
|
+
try {
|
|
106
|
+
chmodSync(target, mode)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
/* 权限补不上不阻塞安装(后续 spawn 会给出真实错误) */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
60
112
|
/**
|
|
61
113
|
* 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
|
|
62
|
-
* @author ddj 2026年08月27号
|
|
114
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
63
115
|
* @param vsix vsix 字节
|
|
64
116
|
* @param dest 目标目录(已存在则先清空)
|
|
65
117
|
* @returns 清单对象
|
|
@@ -80,6 +132,7 @@ export function unpackVsix(vsix: Buffer, dest: string): Record<string, unknown>
|
|
|
80
132
|
}
|
|
81
133
|
mkdirSync(dirname(target), { recursive: true })
|
|
82
134
|
writeFileSync(target, entry.data)
|
|
135
|
+
applyExecBit(target, entry)
|
|
83
136
|
if (rel === 'package.json') manifest = JSON.parse(entry.data.toString('utf8')) as Record<string, unknown>
|
|
84
137
|
}
|
|
85
138
|
if (!manifest) throw new Error('VSIX 解包后缺少 package.json')
|
package/src/lsp/zip.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { inflateRawSync } from 'node:zlib'
|
|
8
8
|
|
|
9
|
-
/** 单条解压结果:相对路径 + 内容(目录条目 path 以 /
|
|
9
|
+
/** 单条解压结果:相对路径 + 内容(目录条目 path 以 / 结尾)+ Unix 权限位(可缺失)。 */
|
|
10
10
|
export interface ZipEntry {
|
|
11
11
|
path: string
|
|
12
12
|
data: Buffer
|
|
13
13
|
isDirectory: boolean
|
|
14
|
+
/** Unix 权限位(中央目录 external attributes 高 16 位;0 或非 Unix 归档时缺失)。 */
|
|
15
|
+
mode?: number
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const EOCD_SIG = 0x06054b50
|
|
@@ -19,16 +21,16 @@ const LHDR_SIG = 0x04034b50
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
|
|
22
|
-
* @author ddj 2026年08月27号
|
|
24
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
23
25
|
* @param buf zip 字节
|
|
24
|
-
* @returns
|
|
26
|
+
* @returns 条目元数据(含 Unix mode,见 modeOf)
|
|
25
27
|
*/
|
|
26
|
-
export function zipEntries(buf: Buffer): { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] {
|
|
28
|
+
export function zipEntries(buf: Buffer): { name: string; method: number; compressed: number; uncompressed: number; localOffset: number; mode?: number }[] {
|
|
27
29
|
const eocd = findEocd(buf)
|
|
28
30
|
if (eocd < 0) throw new Error('不是有效的 ZIP 文件(找不到 EOCD)')
|
|
29
31
|
const total = buf.readUInt16LE(eocd + 10)
|
|
30
32
|
let offset = buf.readUInt32LE(eocd + 16)
|
|
31
|
-
const out: { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] = []
|
|
33
|
+
const out: { name: string; method: number; compressed: number; uncompressed: number; localOffset: number; mode?: number }[] = []
|
|
32
34
|
for (let i = 0; i < total; i++) {
|
|
33
35
|
if (buf.readUInt32LE(offset) !== CDIR_SIG) throw new Error('ZIP 中央目录损坏')
|
|
34
36
|
const method = buf.readUInt16LE(offset + 10)
|
|
@@ -39,17 +41,31 @@ export function zipEntries(buf: Buffer): { name: string; method: number; compres
|
|
|
39
41
|
const commentLen = buf.readUInt16LE(offset + 32)
|
|
40
42
|
const localOffset = buf.readUInt32LE(offset + 42)
|
|
41
43
|
const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString('utf8')
|
|
42
|
-
|
|
44
|
+
const mode = modeOf(buf.readUInt32LE(offset + 38))
|
|
45
|
+
out.push({ name, method, compressed, uncompressed, localOffset, ...(mode === undefined ? {} : { mode }) })
|
|
43
46
|
offset += 46 + nameLen + extraLen + commentLen
|
|
44
47
|
}
|
|
45
48
|
return out
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 中央目录 external attributes → Unix 权限位。
|
|
53
|
+
* ZIP 把 Unix mode 放在 `st_mode << 16`(含 S_IFREG 等类型位),低 16 位仅 DOS 属性;
|
|
54
|
+
* 故必须右移 16 再取低 9 位权限。非 Unix 归档(Windows 打包)该字段为 0 或纯 DOS 位。
|
|
55
|
+
* @author ddj 2026年09月18号
|
|
56
|
+
* @param attributes 中央目录 offset+38 的 4 字节
|
|
57
|
+
* @returns 0o000~0o777 权限位;无有效 mode 返回 undefined
|
|
58
|
+
*/
|
|
59
|
+
export function modeOf(attributes: number): number | undefined {
|
|
60
|
+
const mode = (attributes >>> 16) & 0o777
|
|
61
|
+
return mode === 0 ? undefined : mode
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
/**
|
|
49
65
|
* 解压 ZIP → 条目列表(目录条目含尾部 /)。
|
|
50
|
-
* @author ddj 2026年08月27号
|
|
66
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
51
67
|
* @param buf zip 字节
|
|
52
|
-
* @returns
|
|
68
|
+
* @returns 条目(含内容与可选 mode)
|
|
53
69
|
*/
|
|
54
70
|
export function unzip(buf: Buffer): ZipEntry[] {
|
|
55
71
|
const metas = zipEntries(buf)
|
|
@@ -66,7 +82,7 @@ export function unzip(buf: Buffer): ZipEntry[] {
|
|
|
66
82
|
const raw = buf.subarray(dataStart, dataStart + meta.compressed)
|
|
67
83
|
data = meta.method === 8 ? Buffer.from(inflateRawSync(raw)) : meta.method === 0 ? Buffer.from(raw) : Buffer.alloc(0)
|
|
68
84
|
}
|
|
69
|
-
out.push({ path: meta.name, data, isDirectory: isDir })
|
|
85
|
+
out.push({ path: meta.name, data, isDirectory: isDir, ...(meta.mode === undefined ? {} : { mode: meta.mode }) })
|
|
70
86
|
}
|
|
71
87
|
return out
|
|
72
88
|
}
|
package/src/revert.ts
CHANGED
|
@@ -11,9 +11,26 @@ import { applyLocations, locateHunks, preciseHunk } from './shared/diff.js'
|
|
|
11
11
|
/** 回滚结果;stale=true 表示该 hunk 的新文本已不在文件中(无可回滚内容,不算失败)。 */
|
|
12
12
|
export type Result = { ok: true } | { ok: false; error: string; stale?: boolean }
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 删除单文件的 argv(纯函数,platform 可注入便于单测)。
|
|
16
|
+
* Windows 走 PowerShell Remove-Item;其余平台走 /bin/rm。
|
|
17
|
+
* 原先无条件先发 powershell:macOS/Linux 上必然 spawn 失败后才回落,
|
|
18
|
+
* 每删一个文件多一次无谓 spawn 与失败噪声。
|
|
19
|
+
* @author ddj 2026年09月18号
|
|
20
|
+
* @param absPath 绝对路径
|
|
21
|
+
* @param platform 目标平台(缺省当前进程平台)
|
|
22
|
+
* @returns 删除命令 argv
|
|
23
|
+
*/
|
|
24
|
+
export function removeFileArgv(absPath: string, platform: NodeJS.Platform = process.platform): string[] {
|
|
25
|
+
if (platform === 'win32') {
|
|
26
|
+
return ['powershell', '-NoProfile', '-NonInteractive', '-Command', 'Remove-Item -LiteralPath "' + absPath + '" -Force']
|
|
27
|
+
}
|
|
28
|
+
return ['/bin/rm', '-f', '--', absPath]
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
|
|
16
|
-
* @author ddj 2026年08月20号
|
|
33
|
+
* @author ddj 2026年08月20号 / 2026年09月18号
|
|
17
34
|
* @returns 成功或失败原因
|
|
18
35
|
*/
|
|
19
36
|
export async function deleteCreated(ctx: Ctx, session: Session, record: DiffRecord): Promise<Result> {
|
|
@@ -40,15 +57,10 @@ export async function deleteCreated(ctx: Ctx, session: Session, record: DiffReco
|
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
try {
|
|
43
|
-
await attempt(
|
|
60
|
+
await attempt(removeFileArgv(p))
|
|
44
61
|
return { ok: true }
|
|
45
62
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
await attempt(['/bin/rm', '-f', '--', p])
|
|
48
|
-
return { ok: true }
|
|
49
|
-
} catch (error2) {
|
|
50
|
-
return { ok: false, error: '删除失败(文件仍存在):' + String(error) + ' / ' + String(error2) }
|
|
51
|
-
}
|
|
63
|
+
return { ok: false, error: '删除失败(文件仍存在):' + String(error) }
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
|