@lenorin/dsh-tauri-launcher 1.0.9 → 1.0.10
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 +17 -0
- package/launcher/bin/dsh-launcher.exe +0 -0
- package/lib/index.js +123 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,23 @@ dsh plugin --profile web remove @lenorin/dsh-tauri-launcher
|
|
|
137
137
|
> 注:原 `shortcutName` 配置项已移除(2026-09)——快捷方式文件名由插件与桌面应用
|
|
138
138
|
> 共用同一常量(`DeepSeek Harness.lnk`):两侧各自维护名字会导致各建出一个 .lnk。
|
|
139
139
|
|
|
140
|
+
### 候选目录的自动探测
|
|
141
|
+
|
|
142
|
+
心跳、退出标记与桌面应用配置都在**桌面应用 exe 同目录**,而桌面应用可能存在多份
|
|
143
|
+
副本(npm 包内预编译 exe、本地 `build.ps1` 产物)。插件按下述优先级挑目录,因此
|
|
144
|
+
无论实际运行的是哪一份副本,状态判定、退出标记与启动目标都会对齐到同一个实例:
|
|
145
|
+
|
|
146
|
+
1. 行配置 `launcherExe` 所在目录(硬指定);
|
|
147
|
+
2. **正在运行的** `dsh-launcher` 进程所在目录(`Get-Process` 取 `Path`);
|
|
148
|
+
3. 桌面快捷方式 `DeepSeek Harness.lnk` 的目标目录(WScript.Shell 读 `TargetPath`);
|
|
149
|
+
4. 行配置 `launcherDirs`(候选列表);
|
|
150
|
+
5. 本次插件运行中发现过的目录(实例退出后仍扫描其心跳,避免状态在「已停止/未知」间抖动);
|
|
151
|
+
6. 包内 `launcher/bin`(兜底)。
|
|
152
|
+
|
|
153
|
+
只有真的含 `dsh-launcher.exe` 的目录会被采用——指向已删除副本的失效快捷方式会自动
|
|
154
|
+
跳过,回退到下一候选。运行中实例/快捷方式目标的探测结果缓存 5 秒;探测不可用时
|
|
155
|
+
(无 `subprocess` 服务等)自动回退到第 4/6 项,行为与旧版一致。
|
|
156
|
+
|
|
140
157
|
## 桌面应用构建与发布
|
|
141
158
|
|
|
142
159
|
- npm 包自带预编译 exe(`launcher/bin/dsh-launcher.exe`),插件安装后自动探测、装上即用;
|
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 浏览器侧通过本插件注册的 /api/dsh-tauri-launcher/* 路由通信(仅回环)。
|
|
11
11
|
*
|
|
12
|
+
* 标记协议以 **exe 同目录**为界,而桌面应用可能有多个副本(包内预编译 exe、
|
|
13
|
+
* 本地构建产物)。因此候选目录按优先级自动探测:行配置 launcherExe →
|
|
14
|
+
* 运行中实例目录 → 桌面快捷方式目标目录 → 行配置 launcherDirs →
|
|
15
|
+
* 本会话发现过的目录 → 包内 `launcher/bin`,见 baseDirs。
|
|
16
|
+
*
|
|
12
17
|
* 可调参数全部走行配置(见 apply 的 config 处理与 README「配置」章节);
|
|
13
18
|
* 内置候选目录只是默认值,供未配置时自动探测。
|
|
14
19
|
*/
|
|
@@ -28,6 +33,26 @@ const PACKAGE_BIN = fileURLToPath(new URL('../launcher/bin', import.meta.url))
|
|
|
28
33
|
/** 未配置 launcherDirs 时的默认候选目录(仅包内随附的预编译 exe;本地自行构建的 exe 通过行配置 launcherDirs/launcherExe 指定)。 */
|
|
29
34
|
const DEFAULT_DIRS = [PACKAGE_BIN]
|
|
30
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 运行中实例 / 桌面快捷方式目标的探测缓存时长(毫秒)。状态轮询与启停等待
|
|
38
|
+
* 循环都会反复取候选目录,探测结果按此窗口复用,避免每次读取都拉起 PowerShell。
|
|
39
|
+
*/
|
|
40
|
+
const DISCOVERY_TTL_MS = 5000
|
|
41
|
+
|
|
42
|
+
/** 会话内累积的候选目录上限(含已退出实例的目录)。 */
|
|
43
|
+
const MAX_KNOWN_DIRS = 8
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* PowerShell 输出编码前置:Windows PowerShell 5.1 默认按控制台代码页写重定向
|
|
47
|
+
* 输出,非 ASCII 路径会乱码;`ctx.subprocess` 的收集模式按 UTF-8 解码,故显式
|
|
48
|
+
* 钉成 UTF-8(与 `@deepseek-ai/dsh-pwsh-local` 的 ENCODING_PREAMBLE 同做法)。
|
|
49
|
+
*/
|
|
50
|
+
const PS_UTF8_PREAMBLE = '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [System.Text.UTF8Encoding]::new($false); '
|
|
51
|
+
|
|
52
|
+
/** 列出运行中的桌面应用 exe 路径(同名 exe 可能有多份副本同时在跑)。 */
|
|
53
|
+
const RUNNING_EXE_SCRIPT =
|
|
54
|
+
'Get-Process -Name dsh-launcher -ErrorAction SilentlyContinue | ForEach-Object { try { $_.Path } catch {} }'
|
|
55
|
+
|
|
31
56
|
/**
|
|
32
57
|
* 桌面快捷方式文件名。**单一来源**:必须与桌面应用 `src-tauri/src/settings.rs`
|
|
33
58
|
* 的 `SHORTCUT_NAME` 常量一致——两侧各自维护名字会导致插件与桌面应用各建
|
|
@@ -129,15 +154,97 @@ export function apply(ctx, config) {
|
|
|
129
154
|
|
|
130
155
|
let lastFsError = ''
|
|
131
156
|
let lastMarkerError = ''
|
|
157
|
+
let lastProbeError = ''
|
|
132
158
|
let spawnHandle = null
|
|
133
159
|
let shortcutCache = { t: 0, value: false }
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
/**
|
|
162
|
+
* 已在本次插件运行中发现过的候选目录(最近使用在前)。实例退出后仍保留:
|
|
163
|
+
* 该目录里的心跳文件会变成「过期」而非「不存在」,让状态稳定落在
|
|
164
|
+
* 「已停止」而不是回跳到 null(未知),退出流程的确认逻辑也不会中途丢目标。
|
|
165
|
+
*/
|
|
166
|
+
const knownDirs = []
|
|
167
|
+
/** 「运行中实例目录 + 桌面快捷方式目标目录」的探测缓存。 */
|
|
168
|
+
const discovery = { t: 0, runningDirs: [], shortcutDir: '' }
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 运行采集模式 PowerShell 并取回 stdout 文本(短探测脚本)。
|
|
172
|
+
* 任何失败(服务缺失、spawn 失败、provider 未提供收集流)都返回空串,
|
|
173
|
+
* 调用方据此回退到既有候选目录——探测不可用不改变原有行为。
|
|
174
|
+
*/
|
|
175
|
+
async function capturePowerShell(script) {
|
|
176
|
+
if (!subprocess) return ''
|
|
177
|
+
try {
|
|
178
|
+
const handle = subprocess.spawn({
|
|
179
|
+
argv: ['powershell', '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', PS_UTF8_PREAMBLE + script],
|
|
180
|
+
cwd: process.cwd(),
|
|
181
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: 16384 }, stderr: { maxBytes: 4096 } },
|
|
182
|
+
graceMs: 3000,
|
|
183
|
+
})
|
|
184
|
+
if (handle && handle.done) await handle.done.catch(() => null)
|
|
185
|
+
const reader = handle && handle.collected ? handle.collected.stdout : null
|
|
186
|
+
if (!reader) return ''
|
|
187
|
+
return String(reader.readFrom(0).text || '')
|
|
188
|
+
} catch (error) {
|
|
189
|
+
lastProbeError = String((error && error.message) || error)
|
|
190
|
+
return ''
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 记住一个已发现的候选目录(去重、最近使用在前、限量)。 */
|
|
195
|
+
function rememberDir(dir) {
|
|
196
|
+
if (!dir) return
|
|
197
|
+
const hit = knownDirs.indexOf(dir)
|
|
198
|
+
if (hit >= 0) knownDirs.splice(hit, 1)
|
|
199
|
+
knownDirs.unshift(dir)
|
|
200
|
+
if (knownDirs.length > MAX_KNOWN_DIRS) knownDirs.length = MAX_KNOWN_DIRS
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 探测正在运行的桌面应用目录与桌面快捷方式目标目录(TTL 内复用)。
|
|
205
|
+
*
|
|
206
|
+
* 为什么需要:标记协议以 exe 同目录为界,而桌面应用存在多份副本(包内预编译
|
|
207
|
+
* exe、本地构建产物)。只看包内目录时,若实际运行的是另一份副本,就会读到空
|
|
208
|
+
* 心跳 → 状态「未知」、退出标记写进错误目录、开关反而拉起第二份实例。
|
|
209
|
+
*/
|
|
210
|
+
async function probe() {
|
|
211
|
+
if (Date.now() - discovery.t < DISCOVERY_TTL_MS) return discovery
|
|
212
|
+
const [runningText, targetText] = await Promise.all([
|
|
213
|
+
capturePowerShell(RUNNING_EXE_SCRIPT),
|
|
214
|
+
capturePowerShell(shortcutTargetScript()),
|
|
215
|
+
])
|
|
216
|
+
const runningDirs = []
|
|
217
|
+
for (const line of String(runningText).split(/\r?\n/)) {
|
|
218
|
+
const dir = dirOf(line.trim())
|
|
219
|
+
if (dir && !runningDirs.includes(dir)) runningDirs.push(dir)
|
|
220
|
+
}
|
|
221
|
+
const targetLine = String(targetText).split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== '')[0] || ''
|
|
222
|
+
discovery.t = Date.now()
|
|
223
|
+
discovery.runningDirs = runningDirs
|
|
224
|
+
discovery.shortcutDir = dirOf(targetLine)
|
|
225
|
+
for (const dir of runningDirs) rememberDir(dir)
|
|
226
|
+
if (discovery.shortcutDir) rememberDir(discovery.shortcutDir)
|
|
227
|
+
return discovery
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 候选 exe 目录,按启动优先级排列:
|
|
232
|
+
* 1. 行配置 launcherExe 所在目录;2. 运行中实例目录;3. 桌面快捷方式目标目录;
|
|
233
|
+
* 4. 行配置 launcherDirs;5. 本会话发现过的目录;6. 包内随附 exe 目录。
|
|
234
|
+
* 只有真的含 dsh-launcher.exe 的目录才会被 exeDirs() 采用——失效的快捷方式
|
|
235
|
+
* 目标会被自动跳过,回退到下一候选。
|
|
236
|
+
*/
|
|
237
|
+
async function baseDirs() {
|
|
238
|
+
const found = await probe()
|
|
239
|
+
const list = []
|
|
137
240
|
if (resolved.launcherExe) {
|
|
138
241
|
const dir = dirOf(resolved.launcherExe)
|
|
139
242
|
if (dir) list.push(dir)
|
|
140
243
|
}
|
|
244
|
+
for (const dir of found.runningDirs) list.push(dir)
|
|
245
|
+
if (found.shortcutDir) list.push(found.shortcutDir)
|
|
246
|
+
for (const dir of resolved.launcherDirs) list.push(dir)
|
|
247
|
+
for (const dir of knownDirs) list.push(dir)
|
|
141
248
|
for (const dir of DEFAULT_DIRS) list.push(dir)
|
|
142
249
|
return [...new Set(list)]
|
|
143
250
|
}
|
|
@@ -155,7 +262,8 @@ export function apply(ctx, config) {
|
|
|
155
262
|
}
|
|
156
263
|
|
|
157
264
|
async function exeDirs() {
|
|
158
|
-
const
|
|
265
|
+
const dirs = await baseDirs()
|
|
266
|
+
const checked = await Promise.all(dirs.map(async (dir) => ((await exeExists(dir)) ? dir : null)))
|
|
159
267
|
return checked.filter((dir) => dir !== null)
|
|
160
268
|
}
|
|
161
269
|
|
|
@@ -324,6 +432,12 @@ export function apply(ctx, config) {
|
|
|
324
432
|
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(SHORTCUT_NAME) + "; if (Test-Path -LiteralPath $lnk) { exit 0 } else { exit 1 }"
|
|
325
433
|
}
|
|
326
434
|
|
|
435
|
+
/** 读桌面快捷方式的目标 exe 路径;快捷方式不存在或目标为空时无输出。 */
|
|
436
|
+
function shortcutTargetScript() {
|
|
437
|
+
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(SHORTCUT_NAME) + '; ' +
|
|
438
|
+
'if (Test-Path -LiteralPath $lnk) { $ws=New-Object -ComObject WScript.Shell; $sc=$ws.CreateShortcut($lnk); if ($sc.TargetPath) { $sc.TargetPath } }'
|
|
439
|
+
}
|
|
440
|
+
|
|
327
441
|
function shortcutCreateScript(exe) {
|
|
328
442
|
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(SHORTCUT_NAME) + "; " +
|
|
329
443
|
"$exe=" + psQuote(exe) + "; " +
|
|
@@ -370,11 +484,15 @@ export function apply(ctx, config) {
|
|
|
370
484
|
lines.push('freshWindowSecs: ' + resolved.freshSecs)
|
|
371
485
|
lines.push('launcherExe: ' + (resolved.launcherExe || '(auto)'))
|
|
372
486
|
lines.push('shortcut: ' + (await shortcutExists()))
|
|
487
|
+
const found = await probe()
|
|
488
|
+
lines.push('runningDirs: ' + JSON.stringify(found.runningDirs))
|
|
489
|
+
lines.push('shortcutTarget: ' + (found.shortcutDir || '(none)'))
|
|
373
490
|
const dirs = await exeDirs()
|
|
374
491
|
lines.push('exeDirs: ' + JSON.stringify(dirs))
|
|
375
492
|
const exe = await pickExe()
|
|
376
493
|
lines.push('pickExe: ' + (exe || '(null)'))
|
|
377
494
|
if (lastFsError) lines.push('fsError: ' + lastFsError)
|
|
495
|
+
if (lastProbeError) lines.push('probeError: ' + lastProbeError)
|
|
378
496
|
lines.push('markerWrite: ' + (lastMarkerError || 'ok'))
|
|
379
497
|
for (const dir of dirs) {
|
|
380
498
|
lines.push('heartbeat(' + dir.split('\\').slice(-2).join('\\') + '): ' + String(await hbState(dir)))
|
|
@@ -406,6 +524,8 @@ export function apply(ctx, config) {
|
|
|
406
524
|
}
|
|
407
525
|
try {
|
|
408
526
|
const dir = dirOf(exe)
|
|
527
|
+
// 拉起前记住该目录:即使探测缓存仍是旧的,心跳扫描也已覆盖新实例的目录。
|
|
528
|
+
rememberDir(dir)
|
|
409
529
|
spawnHandle = subprocess.spawn({ argv: [exe], cwd: dir, stdio: STDIO, graceMs: 3000 })
|
|
410
530
|
} catch (error) {
|
|
411
531
|
return { ok: false, error: '启动桌面应用失败:' + String((error && error.message) || error), diag: await buildDiag() }
|