dsh-vscode-mode 0.1.52 → 0.1.53
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 +71 -0
- package/assets/shell/dsh-open.cs +182 -0
- package/assets/shell/dsh-open.ps1 +52 -0
- package/assets/shell/dsh-open.sh +70 -0
- package/assets/shell/dsh-whale.ico +0 -0
- package/lib/client.js +718 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +1086 -5
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/client/chooseDialog.ts +70 -0
- package/src/client/externalOpen.ts +130 -0
- package/src/client/index.ts +4 -0
- package/src/client/openFlow.ts +330 -0
- package/src/client/ui/IntegrationSettings.ts +243 -0
- package/src/client/ui/McpSettings.ts +2 -0
- package/src/externalHandoff.ts +70 -0
- package/src/fileOpenSettings.ts +10 -2
- package/src/index.ts +3 -0
- package/src/integrate.ts +576 -0
- package/src/rpc.ts +90 -3
- package/src/shared/externalOpen.ts +62 -0
- package/src/shared/integration.ts +73 -0
- package/src/shared/rpc.ts +26 -3
- package/src/unityBridge.ts +234 -0
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +267 -0
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs.meta +11 -0
- package/unity/com.dsh.editor/Editor/com.dsh.editor.asmdef +15 -0
- package/unity/com.dsh.editor/Editor/com.dsh.editor.asmdef.meta +7 -0
- package/unity/com.dsh.editor/Editor.meta +8 -0
- package/unity/com.dsh.editor/README.md +33 -0
- package/unity/com.dsh.editor/README.md.meta +7 -0
- package/unity/com.dsh.editor/package.json +7 -0
- package/unity/com.dsh.editor/package.json.meta +7 -0
package/src/integrate.ts
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 资源管理器右键菜单「在 DSH 文件编辑中打开」(跨平台)。
|
|
3
|
+
* - Windows:launcher(assets/shell/dsh-open.cs 经 csc 编译 WinExe,csc 缺失降级 ps1)安装到
|
|
4
|
+
* ~/.dsh/dsh-vscode-mode/shell/,HKCU\Software\Classes 三类菜单键(`*`/Directory/Directory\Background)。
|
|
5
|
+
* - Linux:dsh-open.sh + GNOME Files(Nautilus)右键脚本 + KDE Dolphin 服务菜单(纯文件写入)。
|
|
6
|
+
* - macOS:dsh-open.sh(Finder 快速操作由用户按配方手动创建,自动检测/移除)。
|
|
7
|
+
* 生命周期:注册成功写 marker(shell/registered.json)→ 插件卸载/reload 清理注册痕迹,
|
|
8
|
+
* 启动时 marker 存在则自动恢复(更新插件不丢注册);用户显式「移除注册」删 marker 永不自动恢复。
|
|
9
|
+
* 所有 reg/csc 调用经 subprocess 服务 argv 数组(卸载清理用 child_process 直调,teardown 可靠)。
|
|
10
|
+
* 纯函数(键路径/命令行/ini/脚本内容)可单测。
|
|
11
|
+
* 作者 ddj 2026-09-07
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process'
|
|
14
|
+
import { chmod, copyFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { assetsDirOf, dshHome, PLUGIN_ID } from './paths.js'
|
|
18
|
+
import { INTEGRATION_BASE_DEFAULT, SHELL_MENU_LEAF } from './shared/integration.js'
|
|
19
|
+
import { unitySourceDir } from './unityBridge.js'
|
|
20
|
+
import type { ShellIntegrationStatus, ShellLauncherState, ShellMenuEntry } from './shared/integration.js'
|
|
21
|
+
import type { Ctx } from './store.js'
|
|
22
|
+
|
|
23
|
+
/** 菜单显示名(Windows 键默认值 / Linux 菜单项名 / macOS workflow 名)。 */
|
|
24
|
+
export const MENU_LABEL = '在 DSH 文件编辑中打开'
|
|
25
|
+
/** 编译 launcher 文件名(Windows)。 */
|
|
26
|
+
export const LAUNCHER_EXE = 'dsh-open.exe'
|
|
27
|
+
/** launcher C# 源文件名。 */
|
|
28
|
+
export const LAUNCHER_CS = 'dsh-open.cs'
|
|
29
|
+
/** 降级 launcher 脚本名(Windows)。 */
|
|
30
|
+
export const LAUNCHER_PS1 = 'dsh-open.ps1'
|
|
31
|
+
/** POSIX launcher 脚本名(Linux/macOS 通用)。 */
|
|
32
|
+
export const LAUNCHER_SH = 'dsh-open.sh'
|
|
33
|
+
/** 深链基址配置文件名。 */
|
|
34
|
+
export const INI_NAME = 'dsh-open.ini'
|
|
35
|
+
/** 注册 marker 文件名(存在 = 用户注册过且未显式移除)。 */
|
|
36
|
+
export const MARKER_NAME = 'registered.json'
|
|
37
|
+
/** command 中文件路径占位符(多选时资源管理器逐项展开重复该占位符)。 */
|
|
38
|
+
export const FILE_TOKEN = '%1'
|
|
39
|
+
/** 文件夹空白处场景的路径占位符。 */
|
|
40
|
+
export const BACKGROUND_TOKEN = '%V'
|
|
41
|
+
/** Icon 值名。 */
|
|
42
|
+
export const REG_VALUE_NAME_ICON = 'Icon'
|
|
43
|
+
/** Nautilus 右键脚本文件名。 */
|
|
44
|
+
export const NAUTILUS_NAME = MENU_LABEL
|
|
45
|
+
/** Dolphin 服务菜单文件名。 */
|
|
46
|
+
export const DOLPHIN_NAME = 'dsh-editor.desktop'
|
|
47
|
+
/** Finder 快速操作(workflow 包)目录名。 */
|
|
48
|
+
export const FINDER_NAME = MENU_LABEL + '.workflow'
|
|
49
|
+
/** DSH 小鲸鱼图标文件名(右键菜单 Icon,注册时复制到安装目录)。 */
|
|
50
|
+
export const WHALE_ICO = 'dsh-whale.ico'
|
|
51
|
+
|
|
52
|
+
/** 一次 argv 运行结果(退出码 + 输出)。 */
|
|
53
|
+
interface RunOutcome { code: number | null; output: string }
|
|
54
|
+
|
|
55
|
+
/** 注册 marker 内容。 */
|
|
56
|
+
interface RegistrationMarker { baseUrl: string; at: string }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* reg.exe 路径(SystemRoot 派生;env 可注入便于测试)。
|
|
60
|
+
* @author ddj 2026年09月07号
|
|
61
|
+
* @param env 环境映射(缺省 process.env)
|
|
62
|
+
* @returns reg.exe 绝对路径
|
|
63
|
+
*/
|
|
64
|
+
export function regExe(env: Record<string, string | undefined> = process.env): string {
|
|
65
|
+
return join(systemRoot(env), 'System32', 'reg.exe')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* csc.exe 候选列表(.NET Framework 4.x 常驻路径,按存在性取首个)。
|
|
70
|
+
* @author ddj 2026年09月07号
|
|
71
|
+
* @param env 环境映射(缺省 process.env)
|
|
72
|
+
* @returns 候选绝对路径列表
|
|
73
|
+
*/
|
|
74
|
+
export function cscCandidates(env: Record<string, string | undefined> = process.env): string[] {
|
|
75
|
+
const root = systemRoot(env)
|
|
76
|
+
return [
|
|
77
|
+
join(root, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'),
|
|
78
|
+
join(root, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe'),
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Windows 三类菜单键完整路径。
|
|
84
|
+
* @author ddj 2026年09月07号
|
|
85
|
+
* @param leaf 菜单键叶子名(缺省 DSHEditor)
|
|
86
|
+
* @returns files/dir/background 三个键路径
|
|
87
|
+
*/
|
|
88
|
+
export function menuKeyPaths(leaf: string = SHELL_MENU_LEAF): { files: string; dir: string; background: string } {
|
|
89
|
+
const root = 'HKCU\\Software\\Classes'
|
|
90
|
+
return {
|
|
91
|
+
files: root + '\\*\\shell\\' + leaf,
|
|
92
|
+
dir: root + '\\Directory\\shell\\' + leaf,
|
|
93
|
+
background: root + '\\Directory\\Background\\shell\\' + leaf,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* launcher 安装目录(~/.dsh/dsh-vscode-mode/shell)。
|
|
99
|
+
* @author ddj 2026年09月07号
|
|
100
|
+
* @param home DSH home(测试可注入)
|
|
101
|
+
* @returns 安装目录绝对路径
|
|
102
|
+
*/
|
|
103
|
+
export function shellInstallDir(home = dshHome()): string {
|
|
104
|
+
return join(home, PLUGIN_ID, 'shell')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Nautilus 右键脚本路径(~/.local/share/nautilus/scripts/)。
|
|
109
|
+
* @author ddj 2026年09月07号
|
|
110
|
+
* @param home 用户 home(测试可注入)
|
|
111
|
+
*/
|
|
112
|
+
export function nautilusPath(home = homedir()): string {
|
|
113
|
+
return join(home, '.local', 'share', 'nautilus', 'scripts', NAUTILUS_NAME)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Dolphin 服务菜单路径(新 kio/servicemenus + 旧 kservices5,两个都写)。
|
|
118
|
+
* @author ddj 2026年09月07号
|
|
119
|
+
* @param home 用户 home(测试可注入)
|
|
120
|
+
*/
|
|
121
|
+
export function dolphinMenuPaths(home = homedir()): string[] {
|
|
122
|
+
return [
|
|
123
|
+
join(home, '.local', 'share', 'kio', 'servicemenus', DOLPHIN_NAME),
|
|
124
|
+
join(home, '.local', 'share', 'kservices5', 'ServiceMenus', DOLPHIN_NAME),
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Finder 快速操作 workflow 包路径(~/Library/Services/)。
|
|
130
|
+
* @author ddj 2026年09月07号
|
|
131
|
+
* @param home 用户 home(测试可注入)
|
|
132
|
+
*/
|
|
133
|
+
export function finderWorkflowPath(home = homedir()): string {
|
|
134
|
+
return join(home, 'Library', 'Services', FINDER_NAME)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 注册 marker 文件路径(shell/registered.json)。
|
|
139
|
+
* @author ddj 2026年09月07号
|
|
140
|
+
* @param home DSH home(测试可注入)
|
|
141
|
+
*/
|
|
142
|
+
export function markerFile(home = dshHome()): string {
|
|
143
|
+
return join(shellInstallDir(home), MARKER_NAME)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* command 默认值(Windows 编译 exe 版)。
|
|
148
|
+
* @author ddj 2026年09月07号
|
|
149
|
+
* @param exePath launcher 绝对路径
|
|
150
|
+
* @param token 路径占位符
|
|
151
|
+
* @returns 注册表 command 值
|
|
152
|
+
*/
|
|
153
|
+
export function commandValue(exePath: string, token: string = FILE_TOKEN): string {
|
|
154
|
+
return '"' + exePath + '" "' + token + '"'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* command 默认值(Windows 降级 ps1 版:隐藏窗口运行脚本)。
|
|
159
|
+
* @author ddj 2026年09月07号
|
|
160
|
+
* @param ps1Path launcher 脚本绝对路径
|
|
161
|
+
* @param token 路径占位符
|
|
162
|
+
* @param powerShell powershell.exe 路径(测试可注入)
|
|
163
|
+
* @returns 注册表 command 值
|
|
164
|
+
*/
|
|
165
|
+
export function ps1CommandValue(ps1Path: string, token: string = FILE_TOKEN, powerShell = powerShellExe()): string {
|
|
166
|
+
return '"' + powerShell + '" -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + ps1Path + '" "' + token + '"'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* dsh-open.ini 内容(base 行驱动 launcher 深链基址,三平台共用)。
|
|
171
|
+
* @author ddj 2026年09月07号
|
|
172
|
+
* @param baseUrl 深链基址
|
|
173
|
+
* @returns ini 文件全文
|
|
174
|
+
*/
|
|
175
|
+
export function iniContent(baseUrl: string): string {
|
|
176
|
+
return '[dsh]\nbase=' + (baseUrl.trim() || INTEGRATION_BASE_DEFAULT) + '\n'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Nautilus 右键脚本内容(多选经 NAUTILUS_SCRIPT_SELECTED_FILE_PATHS 一次传给 launcher)。
|
|
181
|
+
* @author ddj 2026年09月07号
|
|
182
|
+
* @param launcherPath dsh-open.sh 绝对路径
|
|
183
|
+
* @returns 脚本全文(#!/usr/bin/env bash)
|
|
184
|
+
*/
|
|
185
|
+
export function nautilusScript(launcherPath: string): string {
|
|
186
|
+
return [
|
|
187
|
+
'#!/usr/bin/env bash',
|
|
188
|
+
'# 由 dsh-vscode-mode 生成:GNOME Files(Nautilus)右键脚本 → DSH 文件编辑',
|
|
189
|
+
'launcher="$HOME/.dsh/dsh-vscode-mode/shell/' + LAUNCHER_SH + '"',
|
|
190
|
+
'args=()',
|
|
191
|
+
'while IFS= read -r line; do',
|
|
192
|
+
' [ -n "$line" ] && args+=("$line")',
|
|
193
|
+
'done <<< "${NAUTILUS_SCRIPT_SELECTED_FILE_PATHS:-}"',
|
|
194
|
+
'exec "$launcher" "${args[@]}"',
|
|
195
|
+
'',
|
|
196
|
+
].join('\n')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Dolphin 服务菜单内容(%F 多选传参)。
|
|
201
|
+
* @author ddj 2026年09月07号
|
|
202
|
+
* @param launcherPath dsh-open.sh 绝对路径
|
|
203
|
+
* @returns .desktop 全文
|
|
204
|
+
*/
|
|
205
|
+
export function dolphinMenu(launcherPath: string): string {
|
|
206
|
+
return [
|
|
207
|
+
'[Desktop Entry]',
|
|
208
|
+
'Type=Service',
|
|
209
|
+
'X-KDE-ServiceTypes=KonqPopupMenu/Plugin',
|
|
210
|
+
'MimeType=all/allfiles;inode/directory;',
|
|
211
|
+
'Actions=openInDshEditor',
|
|
212
|
+
'[Desktop Action openInDshEditor]',
|
|
213
|
+
'Name=' + MENU_LABEL,
|
|
214
|
+
'Icon=utilities-terminal',
|
|
215
|
+
'Exec="' + launcherPath + '" %F',
|
|
216
|
+
'',
|
|
217
|
+
].join('\n')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** SystemRoot 解析(env 可注入)。 */
|
|
221
|
+
function systemRoot(env: Record<string, string | undefined>): string {
|
|
222
|
+
return env.SystemRoot || 'C:\\Windows'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** powershell.exe 路径(Windows 降级注册用)。 */
|
|
226
|
+
function powerShellExe(env: Record<string, string | undefined> = process.env): string {
|
|
227
|
+
return join(systemRoot(env), 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** POSIX launcher 脚本安装路径。 */
|
|
231
|
+
function shLauncherPath(home = dshHome()): string {
|
|
232
|
+
return join(shellInstallDir(home), LAUNCHER_SH)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 经 subprocess 服务运行一个命令(argv 数组,无 shell 插值)。
|
|
237
|
+
* ⚠️ stdio 必须 'inherit':受管 host 环境禁止管道 stdio(node spawn pipe → EPERM),
|
|
238
|
+
* inherit 直写宿主控制台且不建管道;本模块只需退出码(reg query 缺键 = 退出码 1)。
|
|
239
|
+
* done 契约宽松归一化:{exitCode,signal} / {code} / undefined / 抛错(spawn 失败)。
|
|
240
|
+
* @author ddj 2026年09月07号
|
|
241
|
+
* @param ctx DSH 上下文
|
|
242
|
+
* @param argv 命令与参数
|
|
243
|
+
* @param cwd 工作目录(须已存在)
|
|
244
|
+
* @returns 退出码(0=成功)
|
|
245
|
+
*/
|
|
246
|
+
async function runArgv(ctx: Ctx, argv: string[], cwd = dshHome()): Promise<RunOutcome> {
|
|
247
|
+
const sub = ctx.get('subprocess')
|
|
248
|
+
if (!sub || typeof sub.spawn !== 'function') throw new Error('缺少 subprocess 服务')
|
|
249
|
+
let handle: { done: Promise<unknown> }
|
|
250
|
+
try {
|
|
251
|
+
handle = sub.spawn({
|
|
252
|
+
argv,
|
|
253
|
+
cwd,
|
|
254
|
+
stdio: { stdout: 'inherit', stderr: 'inherit', stdin: 'ignore' },
|
|
255
|
+
graceMs: 10000,
|
|
256
|
+
})
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { code: 1, output: String(error) }
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
return normalizeOutcome(await handle.done)
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return { code: 1, output: String(error) }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 归一化 done 结果(契约宽松:{exitCode,signal} / {code} / 字符串 / undefined)。 */
|
|
268
|
+
function normalizeOutcome(result: unknown): RunOutcome {
|
|
269
|
+
if (result == null) return { code: 0, output: '' }
|
|
270
|
+
if (typeof result === 'string') return { code: 0, output: result }
|
|
271
|
+
const record = result as { exitCode?: unknown; code?: unknown }
|
|
272
|
+
if (typeof record.exitCode === 'number') return { code: record.exitCode, output: '' }
|
|
273
|
+
if (typeof record.code === 'number') return { code: record.code, output: '' }
|
|
274
|
+
return { code: 0, output: '' }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 文件存在性探测(best-effort)。 */
|
|
278
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
279
|
+
try {
|
|
280
|
+
return (await stat(path)).isFile()
|
|
281
|
+
} catch {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Windows:探测一个注册表键是否存在(reg query 缺键 = 退出码 1,无需读输出)。 */
|
|
287
|
+
async function keyExists(ctx: Ctx, key: string): Promise<boolean> {
|
|
288
|
+
const outcome = await runArgv(ctx, [regExe(), 'query', key, '/ve'])
|
|
289
|
+
return outcome.code === 0
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 读注册 marker(缺失/非法 → null)。 */
|
|
293
|
+
async function readMarker(home = dshHome()): Promise<RegistrationMarker | null> {
|
|
294
|
+
try {
|
|
295
|
+
const data = JSON.parse(await readFile(markerFile(home), 'utf8')) as { baseUrl?: unknown; at?: unknown }
|
|
296
|
+
if (typeof data?.baseUrl !== 'string' || !data.baseUrl) return null
|
|
297
|
+
return { baseUrl: data.baseUrl, at: typeof data.at === 'string' ? data.at : '' }
|
|
298
|
+
} catch {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 写注册 marker(父目录已存在)。 */
|
|
304
|
+
async function saveMarker(baseUrl: string, home = dshHome()): Promise<void> {
|
|
305
|
+
const data: RegistrationMarker = { baseUrl, at: new Date().toISOString() }
|
|
306
|
+
await writeFile(markerFile(home), JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 复制 POSIX launcher 并加执行位(三平台 ini 同写)。 */
|
|
310
|
+
async function installPosix(dir: string, baseUrl: string): Promise<void> {
|
|
311
|
+
await copyFile(join(assetsDirOf(import.meta.url), 'shell', LAUNCHER_SH), join(dir, LAUNCHER_SH))
|
|
312
|
+
await chmod(join(dir, LAUNCHER_SH), 0o755)
|
|
313
|
+
await writeFile(join(dir, INI_NAME), iniContent(baseUrl), 'utf8')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** 写 Nautilus 右键脚本(父目录按需创建 + 执行位)。 */
|
|
317
|
+
async function writeNautilus(home: string, launcherPath: string): Promise<void> {
|
|
318
|
+
const script = nautilusPath(home)
|
|
319
|
+
await mkdir(join(script, '..'), { recursive: true })
|
|
320
|
+
await writeFile(script, nautilusScript(launcherPath), 'utf8')
|
|
321
|
+
await chmod(script, 0o755)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 写 Dolphin 服务菜单(新/旧两个路径,父目录按需创建)。 */
|
|
325
|
+
async function writeDolphinMenus(home: string, launcherPath: string): Promise<void> {
|
|
326
|
+
for (const menu of dolphinMenuPaths(home)) {
|
|
327
|
+
await mkdir(join(menu, '..'), { recursive: true })
|
|
328
|
+
await writeFile(menu, dolphinMenu(launcherPath), 'utf8')
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Linux:移除 Nautilus 脚本与 Dolphin 服务菜单(仅本插件自有文件)。 */
|
|
333
|
+
async function removePosixFiles(home: string): Promise<void> {
|
|
334
|
+
await rm(nautilusPath(home), { force: true })
|
|
335
|
+
for (const menu of dolphinMenuPaths(home)) await rm(menu, { force: true })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** macOS:移除 Finder 快速操作(仅当其内容引用本插件 launcher,防误删用户手工产物)。 */
|
|
339
|
+
async function dropFinderMenu(home: string): Promise<void> {
|
|
340
|
+
const dir = finderWorkflowPath(home)
|
|
341
|
+
let text = ''
|
|
342
|
+
try {
|
|
343
|
+
text = await readFile(join(dir, 'Contents', 'document.wflow'), 'utf8')
|
|
344
|
+
} catch {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
if (!text.includes('dsh-open')) return
|
|
348
|
+
await rm(dir, { recursive: true, force: true })
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 卸载清理专用:直调 reg delete(subprocess 服务在 teardown 可能已卸载)。⚠️ stdio 'ignore':受管环境禁管道。 */
|
|
352
|
+
function regDeleteDirect(key: string): Promise<void> {
|
|
353
|
+
return new Promise((resolve) => {
|
|
354
|
+
try {
|
|
355
|
+
const child = spawn(regExe(), ['delete', key, '/f'], { windowsHide: true, timeout: 5000, stdio: 'ignore' })
|
|
356
|
+
child.once('close', () => resolve())
|
|
357
|
+
child.once('error', () => resolve())
|
|
358
|
+
} catch {
|
|
359
|
+
resolve()
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 查询系统集成状态(按平台探测条目注册状态 + launcher 安装态)。
|
|
366
|
+
* @author ddj 2026年09月07号
|
|
367
|
+
* @param ctx DSH 上下文
|
|
368
|
+
* @param baseUrl 深链基址(设置 integrationBaseUrl)
|
|
369
|
+
* @returns 集成状态
|
|
370
|
+
*/
|
|
371
|
+
export async function shellMenuStatus(ctx: Ctx, baseUrl: string): Promise<ShellIntegrationStatus> {
|
|
372
|
+
const dir = shellInstallDir()
|
|
373
|
+
const platform = process.platform
|
|
374
|
+
let launcher: ShellLauncherState
|
|
375
|
+
let entries: ShellMenuEntry[] = []
|
|
376
|
+
if (platform === 'win32') {
|
|
377
|
+
const exe = join(dir, LAUNCHER_EXE)
|
|
378
|
+
const ps1 = join(dir, LAUNCHER_PS1)
|
|
379
|
+
const compiled = await fileExists(exe)
|
|
380
|
+
const ps1Present = await fileExists(ps1)
|
|
381
|
+
launcher = { present: compiled || ps1Present, compiled, path: compiled ? exe : ps1 }
|
|
382
|
+
entries = [
|
|
383
|
+
{ id: 'files', label: '文件', registered: await keyExists(ctx, menuKeyPaths().files) },
|
|
384
|
+
{ id: 'dir', label: '文件夹', registered: await keyExists(ctx, menuKeyPaths().dir) },
|
|
385
|
+
{ id: 'dirBackground', label: '文件夹空白处', registered: await keyExists(ctx, menuKeyPaths().background) },
|
|
386
|
+
]
|
|
387
|
+
} else {
|
|
388
|
+
const sh = shLauncherPath()
|
|
389
|
+
launcher = { present: await fileExists(sh), compiled: false, path: sh }
|
|
390
|
+
if (platform === 'linux') {
|
|
391
|
+
const menus = dolphinMenuPaths()
|
|
392
|
+
entries = [
|
|
393
|
+
{ id: 'nautilus', label: 'GNOME Files(Nautilus)', registered: await fileExists(nautilusPath()) },
|
|
394
|
+
{ id: 'dolphin', label: 'KDE Dolphin', registered: (await fileExists(menus[0])) || (await fileExists(menus[1])) },
|
|
395
|
+
]
|
|
396
|
+
} else if (platform === 'darwin') {
|
|
397
|
+
entries = [{ id: 'finder', label: 'Finder 快速操作', registered: await fileExists(finderWorkflowPath()), manual: true }]
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { platform, launcher, entries, baseUrl, unityPackagePath: unitySourceDir() }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 注册右键菜单(平台分发):写 ini → 安装 launcher → 首次备份(Windows)→ 写菜单;
|
|
405
|
+
* 成功后写 marker(驱动卸载清理与重启自动恢复)。
|
|
406
|
+
* @author ddj 2026年09月07号
|
|
407
|
+
* @param ctx DSH 上下文
|
|
408
|
+
* @param baseUrl 深链基址
|
|
409
|
+
* @returns 注册后状态
|
|
410
|
+
*/
|
|
411
|
+
export async function shellMenuRegister(ctx: Ctx, baseUrl: string): Promise<ShellIntegrationStatus> {
|
|
412
|
+
const dir = shellInstallDir()
|
|
413
|
+
await mkdir(dir, { recursive: true })
|
|
414
|
+
if (process.platform === 'win32') {
|
|
415
|
+
await writeFile(join(dir, INI_NAME), iniContent(baseUrl), 'utf8')
|
|
416
|
+
await installIcon(dir)
|
|
417
|
+
const compiled = await installLauncher(ctx, dir, true)
|
|
418
|
+
if (!(await readMarker())) await backupKeys(ctx, dir)
|
|
419
|
+
await writeMenuKeys(ctx, dir, compiled)
|
|
420
|
+
} else if (process.platform === 'linux') {
|
|
421
|
+
await installPosix(dir, baseUrl)
|
|
422
|
+
const launcherPath = join(dir, LAUNCHER_SH)
|
|
423
|
+
await writeNautilus(homedir(), launcherPath)
|
|
424
|
+
await writeDolphinMenus(homedir(), launcherPath)
|
|
425
|
+
} else if (process.platform === 'darwin') {
|
|
426
|
+
// macOS 菜单由用户按 Automator 配方手动创建(UI 提供一键复制);这里仅安装 launcher
|
|
427
|
+
await installPosix(dir, baseUrl)
|
|
428
|
+
}
|
|
429
|
+
await saveMarker(baseUrl)
|
|
430
|
+
return shellMenuStatus(ctx, baseUrl)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 移除右键菜单注册(平台分发)+ 删除 marker(显式移除后重启不再自动恢复)。
|
|
435
|
+
* @author ddj 2026年09月07号
|
|
436
|
+
* @param ctx DSH 上下文
|
|
437
|
+
* @param baseUrl 深链基址
|
|
438
|
+
* @returns 移除后状态
|
|
439
|
+
*/
|
|
440
|
+
export async function shellMenuRemove(ctx: Ctx, baseUrl: string): Promise<ShellIntegrationStatus> {
|
|
441
|
+
if (process.platform === 'win32') {
|
|
442
|
+
const paths = menuKeyPaths()
|
|
443
|
+
for (const key of [paths.files, paths.dir, paths.background]) {
|
|
444
|
+
await runArgv(ctx, [regExe(), 'delete', key, '/f'])
|
|
445
|
+
}
|
|
446
|
+
} else if (process.platform === 'linux') {
|
|
447
|
+
await removePosixFiles(homedir())
|
|
448
|
+
} else if (process.platform === 'darwin') {
|
|
449
|
+
await dropFinderMenu(homedir())
|
|
450
|
+
}
|
|
451
|
+
await rm(markerFile(), { force: true })
|
|
452
|
+
return shellMenuStatus(ctx, baseUrl)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 系统集成生命周期(index.ts 经 ctx.effect 挂载):
|
|
457
|
+
* 启动时 marker 存在 → 幂等自动恢复注册(插件更新/reload 不丢注册);
|
|
458
|
+
* 返回的 disposer 在插件卸载/reload 时清理注册痕迹(marker 保留供下次恢复)。
|
|
459
|
+
* @author ddj 2026年09月07号
|
|
460
|
+
* @param ctx DSH 上下文
|
|
461
|
+
* @returns 卸载清理 disposer
|
|
462
|
+
*/
|
|
463
|
+
export function shellMenuLifecycle(ctx: Ctx): () => void {
|
|
464
|
+
void restoreOnLoad(ctx).catch(() => {})
|
|
465
|
+
return () => {
|
|
466
|
+
void cleanupOnUnload().catch(() => {})
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 启动自动恢复:marker 存在才执行(幂等;exe 已存在跳过编译)。 */
|
|
471
|
+
async function restoreOnLoad(ctx: Ctx): Promise<void> {
|
|
472
|
+
const marker = await readMarker()
|
|
473
|
+
if (!marker) return
|
|
474
|
+
await shellMenuRegister(ctx, marker.baseUrl)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 卸载清理:marker 存在 → 按平台移除注册痕迹(launcher 文件与 marker 保留)。 */
|
|
478
|
+
async function cleanupOnUnload(): Promise<void> {
|
|
479
|
+
const marker = await readMarker()
|
|
480
|
+
if (!marker) return
|
|
481
|
+
if (process.platform === 'win32') {
|
|
482
|
+
const paths = menuKeyPaths()
|
|
483
|
+
for (const key of [paths.files, paths.dir, paths.background]) await regDeleteDirect(key)
|
|
484
|
+
} else if (process.platform === 'linux') {
|
|
485
|
+
await removePosixFiles(homedir())
|
|
486
|
+
} else if (process.platform === 'darwin') {
|
|
487
|
+
await dropFinderMenu(homedir())
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 安装 Windows launcher:优先 csc 编译 exe(无控制台闪窗),任一候选失败降级复制 ps1。
|
|
493
|
+
* @author ddj 2026年09月07号
|
|
494
|
+
* @param ctx DSH 上下文
|
|
495
|
+
* @param dir 安装目录(已存在)
|
|
496
|
+
* @param force 是否强制重编译(自动恢复时 false:exe 已存在直接复用)
|
|
497
|
+
* @returns 是否编译成功(false=降级 ps1)
|
|
498
|
+
*/
|
|
499
|
+
async function installLauncher(ctx: Ctx, dir: string, force: boolean): Promise<boolean> {
|
|
500
|
+
const exe = join(dir, LAUNCHER_EXE)
|
|
501
|
+
if (!force && await fileExists(exe)) return true
|
|
502
|
+
const shellAssets = join(assetsDirOf(import.meta.url), 'shell')
|
|
503
|
+
for (const candidate of cscCandidates()) {
|
|
504
|
+
if (!(await fileExists(candidate))) continue
|
|
505
|
+
try {
|
|
506
|
+
await copyFile(join(shellAssets, LAUNCHER_CS), join(dir, LAUNCHER_CS))
|
|
507
|
+
const outcome = await runArgv(ctx, [
|
|
508
|
+
candidate,
|
|
509
|
+
'/nologo',
|
|
510
|
+
'/target:winexe',
|
|
511
|
+
'/r:System.Windows.Forms.dll',
|
|
512
|
+
'/r:System.Web.Extensions.dll',
|
|
513
|
+
'/out:' + exe,
|
|
514
|
+
join(dir, LAUNCHER_CS),
|
|
515
|
+
], dir)
|
|
516
|
+
if (outcome.code === 0 && await fileExists(exe)) return true
|
|
517
|
+
} catch { /* 任一候选失败继续降级 */ }
|
|
518
|
+
}
|
|
519
|
+
await copyFile(join(shellAssets, LAUNCHER_PS1), join(dir, LAUNCHER_PS1))
|
|
520
|
+
return false
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 首次注册前备份 Windows 三类既有键(best-effort,键不存在时导出失败忽略)。
|
|
525
|
+
* @author ddj 2026年09月07号
|
|
526
|
+
* @param ctx DSH 上下文
|
|
527
|
+
* @param dir 备份文件落盘目录
|
|
528
|
+
*/
|
|
529
|
+
async function backupKeys(ctx: Ctx, dir: string): Promise<void> {
|
|
530
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
531
|
+
const paths = menuKeyPaths()
|
|
532
|
+
const targets: Array<[string, string]> = [
|
|
533
|
+
[paths.files, 'files'],
|
|
534
|
+
[paths.dir, 'dir'],
|
|
535
|
+
[paths.background, 'background'],
|
|
536
|
+
]
|
|
537
|
+
for (const [key, name] of targets) {
|
|
538
|
+
await runArgv(ctx, [regExe(), 'export', key, join(dir, 'backup-' + name + '-' + stamp + '.reg'), '/y'], dir)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** 复制 DSH 小鲸鱼 ico 到安装目录(右键菜单 Icon 用;缺失不阻塞注册)。 */
|
|
543
|
+
async function installIcon(dir: string): Promise<void> {
|
|
544
|
+
try {
|
|
545
|
+
await copyFile(join(assetsDirOf(import.meta.url), 'shell', WHALE_ICO), join(dir, WHALE_ICO))
|
|
546
|
+
} catch { /* 图标缺失不阻塞注册 */ }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* 写 Windows 三类菜单键(显示名 + Icon + command 子键)。
|
|
551
|
+
* @author ddj 2026年09月07号
|
|
552
|
+
* @param ctx DSH 上下文
|
|
553
|
+
* @param dir launcher 安装目录
|
|
554
|
+
* @param compiled 是否为编译 exe(否则 ps1 command)
|
|
555
|
+
*/
|
|
556
|
+
async function writeMenuKeys(ctx: Ctx, dir: string, compiled: boolean): Promise<void> {
|
|
557
|
+
const launcherPath = compiled ? join(dir, LAUNCHER_EXE) : join(dir, LAUNCHER_PS1)
|
|
558
|
+
const iconPath = (await fileExists(join(dir, WHALE_ICO))) ? join(dir, WHALE_ICO) : launcherPath
|
|
559
|
+
const keys: Array<[string, string]> = [
|
|
560
|
+
[menuKeyPaths().files, FILE_TOKEN],
|
|
561
|
+
[menuKeyPaths().dir, FILE_TOKEN],
|
|
562
|
+
[menuKeyPaths().background, BACKGROUND_TOKEN],
|
|
563
|
+
]
|
|
564
|
+
for (const [key, token] of keys) {
|
|
565
|
+
const command = compiled ? commandValue(launcherPath, token) : ps1CommandValue(launcherPath, token)
|
|
566
|
+
await addMenuKey(ctx, key, iconPath, command)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** 写一个 Windows 菜单键:默认值(显示名)+ Icon + command 子键。 */
|
|
571
|
+
async function addMenuKey(ctx: Ctx, key: string, iconPath: string, command: string): Promise<void> {
|
|
572
|
+
const reg = regExe()
|
|
573
|
+
await runArgv(ctx, [reg, 'add', key, '/ve', '/t', 'REG_SZ', '/d', MENU_LABEL, '/f'])
|
|
574
|
+
await runArgv(ctx, [reg, 'add', key, '/v', REG_VALUE_NAME_ICON, '/t', 'REG_SZ', '/d', iconPath, '/f'])
|
|
575
|
+
await runArgv(ctx, [reg, 'add', key + '\\command', '/ve', '/t', 'REG_SZ', '/d', command, '/f'])
|
|
576
|
+
}
|