@lenorin/dsh-tauri-launcher 1.0.4 → 1.0.6
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/launcher/README.md +57 -56
- package/launcher/bin/dsh-launcher.exe +0 -0
- package/lib/client.js +576 -252
- package/lib/index.js +488 -478
- package/package.json +9 -1
package/lib/index.js
CHANGED
|
@@ -1,478 +1,488 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-tauri-launcher — host half.
|
|
3
|
-
*
|
|
4
|
-
* 在 DSH Web 设置中启动/退出本机 DeepSeek Harness Tauri 桌面应用,并联动
|
|
5
|
-
* 桌面快捷方式(开启自动创建、关闭自动删除、可手动补建)。与桌面应用的
|
|
6
|
-
* 协作协议(launcher exe 同目录):
|
|
7
|
-
* - `.dsh-heartbeat`:桌面应用每秒写入的 Unix 时间戳,freshSecs 秒内新鲜视为运行中;
|
|
8
|
-
* - `.dsh-quit`:内容 `1` 且 60 秒内新鲜 → 桌面应用仅退出自身(消费时自删)。
|
|
9
|
-
*
|
|
10
|
-
* 浏览器侧通过本插件注册的 /api/dsh-tauri-launcher/* 路由通信(仅回环)。
|
|
11
|
-
*
|
|
12
|
-
* 可调参数全部走行配置(见 apply 的 config 处理与 README「配置」章节);
|
|
13
|
-
* 内置候选目录只是默认值,供未配置时自动探测。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
|
|
18
|
-
export const name = 'desktop-launcher'
|
|
19
|
-
|
|
20
|
-
/** 路由注册与文件/进程操作所需的主机服务(声明后加载器会等待就绪再 apply)。 */
|
|
21
|
-
export const inject = ['webServer', 'fs', 'subprocess']
|
|
22
|
-
|
|
23
|
-
const STDIO = { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }
|
|
24
|
-
|
|
25
|
-
/** 本包随附的预编译桌面应用 exe 目录(无论安装到哪个 profile 都能定位)。 */
|
|
26
|
-
const PACKAGE_BIN = fileURLToPath(new URL('../launcher/bin', import.meta.url))
|
|
27
|
-
|
|
28
|
-
/** 未配置 launcherDirs 时的默认候选目录(仅包内随附的预编译 exe;本地自行构建的 exe 通过行配置 launcherDirs/launcherExe 指定)。 */
|
|
29
|
-
const DEFAULT_DIRS = [PACKAGE_BIN]
|
|
30
|
-
|
|
31
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
32
|
-
const psQuote = (value) => "'" + String(value).replace(/'/g, "''") + "'"
|
|
33
|
-
|
|
34
|
-
function writeJson(res, status, body) {
|
|
35
|
-
const payload = JSON.stringify(body)
|
|
36
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
37
|
-
res.end(payload)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 请求体大小上限(回环接口的防御性约束)。 */
|
|
41
|
-
const MAX_BODY_BYTES = 1024 * 1024
|
|
42
|
-
|
|
43
|
-
async function readJsonBody(req) {
|
|
44
|
-
const chunks = []
|
|
45
|
-
let total = 0
|
|
46
|
-
for await (const chunk of req) {
|
|
47
|
-
total += chunk.length
|
|
48
|
-
if (total > MAX_BODY_BYTES) return {}
|
|
49
|
-
chunks.push(chunk)
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
53
|
-
return typeof parsed === 'object' && parsed !== null ? parsed : {}
|
|
54
|
-
} catch {
|
|
55
|
-
return {}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 仅允许本机回环请求(与 dsh-ssh 相同的信任边界)。 */
|
|
60
|
-
function isLoopback(req) {
|
|
61
|
-
const address = String(req.socket.remoteAddress ?? '')
|
|
62
|
-
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* CSRF 防线(#4):回环之外直接拒绝;回环内还须满足任一——
|
|
67
|
-
* 1. 携带本插件约定的自定义头(同源 fetch 可带,跨站 form POST 无法伪造);
|
|
68
|
-
* 2. Origin 为 DSH 自身页面地址(http://127.0.0.1|localhost|[::1]:port);
|
|
69
|
-
* 3. Referer 指向 DSH 自身页面地址。
|
|
70
|
-
* 三者全缺(如 <img>/<script> 等无头请求)拒绝。
|
|
71
|
-
*/
|
|
72
|
-
function isTrustedRequest(req, port) {
|
|
73
|
-
if (!isLoopback(req)) return false
|
|
74
|
-
const headers = req.headers || {}
|
|
75
|
-
if (headers['x-requested-with'] === 'dsh-tauri-launcher') return true
|
|
76
|
-
const allowed = new Set([
|
|
77
|
-
'http://127.0.0.1:' + port,
|
|
78
|
-
'http://localhost:' + port,
|
|
79
|
-
'http://[::1]:' + port,
|
|
80
|
-
])
|
|
81
|
-
if (headers.origin && allowed.has(headers.origin)) return true
|
|
82
|
-
const referer = headers.referer
|
|
83
|
-
if (referer) {
|
|
84
|
-
const base = referer.split('/').slice(0, 3).join('/')
|
|
85
|
-
if (allowed.has(base)) return true
|
|
86
|
-
}
|
|
87
|
-
return false
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function apply(ctx, config) {
|
|
91
|
-
// 注入的服务由加载器保证在 apply 前就绪(不能在 apply 时用 ctx.get 提前
|
|
92
|
-
// 捕获未就绪的服务,否则会永久拿到 undefined)。
|
|
93
|
-
const fs = ctx.fs
|
|
94
|
-
const subprocess = ctx.subprocess
|
|
95
|
-
const sandboxPolicy = ctx.get('sandboxPolicy')
|
|
96
|
-
|
|
97
|
-
const resolved = {
|
|
98
|
-
/** 直接指定桌面应用 exe 路径;空则按 launcherDirs/内置候选自动探测。 */
|
|
99
|
-
launcherExe: config && typeof config.launcherExe === 'string' ? config.launcherExe : '',
|
|
100
|
-
/** 候选 exe 目录列表;为空时使用内置默认候选。 */
|
|
101
|
-
launcherDirs: Array.isArray(config && config.launcherDirs) ? config.launcherDirs.filter((d) => typeof d === 'string') : [],
|
|
102
|
-
/** 心跳“新鲜窗口”(秒)。桌面应用每秒写心跳,默认 4 秒即 4 个周期的余量。 */
|
|
103
|
-
freshSecs: config && typeof config.freshSecs === 'number' && config.freshSecs > 1 ? config.freshSecs : 4,
|
|
104
|
-
/** 桌面快捷方式的文件名。 */
|
|
105
|
-
shortcutName: config && typeof config.shortcutName === 'string' && config.shortcutName !== '' ? config.shortcutName : 'DeepSeek Harness.lnk',
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let lastFsError = ''
|
|
109
|
-
let lastMarkerError = ''
|
|
110
|
-
let spawnHandle = null
|
|
111
|
-
let shortcutCache = { t: 0, value: false }
|
|
112
|
-
|
|
113
|
-
function baseDirs() {
|
|
114
|
-
const list = resolved.launcherDirs.slice()
|
|
115
|
-
if (resolved.launcherExe) {
|
|
116
|
-
const slash = resolved.launcherExe.lastIndexOf('\\')
|
|
117
|
-
if (slash > 0) list.push(resolved.launcherExe.slice(0, slash))
|
|
118
|
-
}
|
|
119
|
-
for (const dir of DEFAULT_DIRS) list.push(dir)
|
|
120
|
-
return [...new Set(list)]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function exeExists(dir) {
|
|
124
|
-
if (!fs) return false
|
|
125
|
-
try {
|
|
126
|
-
const target = await fs.resolve(dir + '\\dsh-launcher.exe')
|
|
127
|
-
const info = await fs.stat(target)
|
|
128
|
-
return Boolean(info && info.type === 'file')
|
|
129
|
-
} catch (error) {
|
|
130
|
-
lastFsError = String((error && error.message) || error)
|
|
131
|
-
return false
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function exeDirs() {
|
|
136
|
-
const checked = await Promise.all(baseDirs().map(async (dir) => ((await exeExists(dir)) ? dir : null)))
|
|
137
|
-
return checked.filter((dir) => dir !== null)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function fileText(dir, name) {
|
|
141
|
-
if (!fs) return null
|
|
142
|
-
try {
|
|
143
|
-
const target = await fs.resolve(dir + '\\' + name)
|
|
144
|
-
const info = await fs.stat(target)
|
|
145
|
-
if (!info) return null
|
|
146
|
-
const text = await fs.readText(target)
|
|
147
|
-
return String(text).trim()
|
|
148
|
-
} catch (error) {
|
|
149
|
-
lastFsError = String((error && error.message) || error)
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function hbState(dir) {
|
|
155
|
-
const text = await fileText(dir, '.dsh-heartbeat')
|
|
156
|
-
if (text === null || text === '') return null
|
|
157
|
-
const stamp = Number(text)
|
|
158
|
-
if (!Number.isFinite(stamp)) return null
|
|
159
|
-
const age = Math.abs(Date.now() / 1000 - stamp)
|
|
160
|
-
return age < resolved.freshSecs
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function isRunning() {
|
|
164
|
-
const dirs = await exeDirs()
|
|
165
|
-
const states = []
|
|
166
|
-
for (const dir of dirs) states.push(await hbState(dir))
|
|
167
|
-
if (states.includes(true)) return true
|
|
168
|
-
if (states.length === 0 || states.every((s) => s === null)) return null
|
|
169
|
-
return false
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function waitFresh(seconds) {
|
|
173
|
-
let sawFile = false
|
|
174
|
-
for (let i = 0; i < seconds * 2; i++) {
|
|
175
|
-
const state = await isRunning()
|
|
176
|
-
if (state === true) return true
|
|
177
|
-
if (state === false) sawFile = true
|
|
178
|
-
await sleep(500)
|
|
179
|
-
}
|
|
180
|
-
return sawFile ? false : null
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function waitGone(seconds) {
|
|
184
|
-
for (let i = 0; i < seconds * 2; i++) {
|
|
185
|
-
const state = await isRunning()
|
|
186
|
-
if (state === false) return true
|
|
187
|
-
if (state === null) return null
|
|
188
|
-
await sleep(500)
|
|
189
|
-
}
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function pickExe() {
|
|
194
|
-
if (resolved.launcherExe) {
|
|
195
|
-
try {
|
|
196
|
-
const target = await fs.resolve(resolved.launcherExe)
|
|
197
|
-
const info = await fs.stat(target)
|
|
198
|
-
if (info && info.type === 'file') return resolved.launcherExe
|
|
199
|
-
} catch (error) {
|
|
200
|
-
lastFsError = String((error && error.message) || error)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const dirs = await exeDirs()
|
|
204
|
-
if (dirs.length > 0) return dirs[0] + '\\dsh-launcher.exe'
|
|
205
|
-
return null
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function runPowerShell(script, cwd) {
|
|
209
|
-
if (!subprocess) throw new Error('subprocess 服务不可用')
|
|
210
|
-
const handle = subprocess.spawn({
|
|
211
|
-
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
212
|
-
cwd: cwd || (await pickExe()) || '',
|
|
213
|
-
stdio: STDIO,
|
|
214
|
-
graceMs: 3000,
|
|
215
|
-
})
|
|
216
|
-
if (handle && handle.done) await handle.done.catch(() => {})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function runPowerShellExitCode(script, cwd) {
|
|
220
|
-
if (!subprocess) return null
|
|
221
|
-
try {
|
|
222
|
-
const handle = subprocess.spawn({
|
|
223
|
-
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
224
|
-
cwd: cwd || process.cwd(),
|
|
225
|
-
stdio: STDIO,
|
|
226
|
-
graceMs: 3000,
|
|
227
|
-
})
|
|
228
|
-
const outcome = handle && handle.done ? await handle.done.catch(() => null) : null
|
|
229
|
-
return outcome && typeof outcome.exitCode === 'number' ? outcome.exitCode : null
|
|
230
|
-
} catch (error) {
|
|
231
|
-
lastMarkerError = String((error && error.message) || error)
|
|
232
|
-
return null
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async function quitScriptFor(value) {
|
|
237
|
-
const exe = await pickExe()
|
|
238
|
-
if (!exe) return null
|
|
239
|
-
const dir = exe.slice(0, exe.lastIndexOf('\\'))
|
|
240
|
-
return {
|
|
241
|
-
dir,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
'$
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
'
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
lines
|
|
336
|
-
lines.push('
|
|
337
|
-
lines.push('
|
|
338
|
-
lines.push('
|
|
339
|
-
lines.push('
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
lines.push('
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
writeJson(res,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
writeJson(res,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tauri-launcher — host half.
|
|
3
|
+
*
|
|
4
|
+
* 在 DSH Web 设置中启动/退出本机 DeepSeek Harness Tauri 桌面应用,并联动
|
|
5
|
+
* 桌面快捷方式(开启自动创建、关闭自动删除、可手动补建)。与桌面应用的
|
|
6
|
+
* 协作协议(launcher exe 同目录):
|
|
7
|
+
* - `.dsh-heartbeat`:桌面应用每秒写入的 Unix 时间戳,freshSecs 秒内新鲜视为运行中;
|
|
8
|
+
* - `.dsh-quit`:内容 `1` 且 60 秒内新鲜 → 桌面应用仅退出自身(消费时自删)。
|
|
9
|
+
*
|
|
10
|
+
* 浏览器侧通过本插件注册的 /api/dsh-tauri-launcher/* 路由通信(仅回环)。
|
|
11
|
+
*
|
|
12
|
+
* 可调参数全部走行配置(见 apply 的 config 处理与 README「配置」章节);
|
|
13
|
+
* 内置候选目录只是默认值,供未配置时自动探测。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
export const name = 'desktop-launcher'
|
|
19
|
+
|
|
20
|
+
/** 路由注册与文件/进程操作所需的主机服务(声明后加载器会等待就绪再 apply)。 */
|
|
21
|
+
export const inject = ['webServer', 'fs', 'subprocess']
|
|
22
|
+
|
|
23
|
+
const STDIO = { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }
|
|
24
|
+
|
|
25
|
+
/** 本包随附的预编译桌面应用 exe 目录(无论安装到哪个 profile 都能定位)。 */
|
|
26
|
+
const PACKAGE_BIN = fileURLToPath(new URL('../launcher/bin', import.meta.url))
|
|
27
|
+
|
|
28
|
+
/** 未配置 launcherDirs 时的默认候选目录(仅包内随附的预编译 exe;本地自行构建的 exe 通过行配置 launcherDirs/launcherExe 指定)。 */
|
|
29
|
+
const DEFAULT_DIRS = [PACKAGE_BIN]
|
|
30
|
+
|
|
31
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
32
|
+
const psQuote = (value) => "'" + String(value).replace(/'/g, "''") + "'"
|
|
33
|
+
|
|
34
|
+
function writeJson(res, status, body) {
|
|
35
|
+
const payload = JSON.stringify(body)
|
|
36
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
37
|
+
res.end(payload)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 请求体大小上限(回环接口的防御性约束)。 */
|
|
41
|
+
const MAX_BODY_BYTES = 1024 * 1024
|
|
42
|
+
|
|
43
|
+
async function readJsonBody(req) {
|
|
44
|
+
const chunks = []
|
|
45
|
+
let total = 0
|
|
46
|
+
for await (const chunk of req) {
|
|
47
|
+
total += chunk.length
|
|
48
|
+
if (total > MAX_BODY_BYTES) return {}
|
|
49
|
+
chunks.push(chunk)
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
53
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {}
|
|
54
|
+
} catch {
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 仅允许本机回环请求(与 dsh-ssh 相同的信任边界)。 */
|
|
60
|
+
function isLoopback(req) {
|
|
61
|
+
const address = String(req.socket.remoteAddress ?? '')
|
|
62
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* CSRF 防线(#4):回环之外直接拒绝;回环内还须满足任一——
|
|
67
|
+
* 1. 携带本插件约定的自定义头(同源 fetch 可带,跨站 form POST 无法伪造);
|
|
68
|
+
* 2. Origin 为 DSH 自身页面地址(http://127.0.0.1|localhost|[::1]:port);
|
|
69
|
+
* 3. Referer 指向 DSH 自身页面地址。
|
|
70
|
+
* 三者全缺(如 <img>/<script> 等无头请求)拒绝。
|
|
71
|
+
*/
|
|
72
|
+
function isTrustedRequest(req, port) {
|
|
73
|
+
if (!isLoopback(req)) return false
|
|
74
|
+
const headers = req.headers || {}
|
|
75
|
+
if (headers['x-requested-with'] === 'dsh-tauri-launcher') return true
|
|
76
|
+
const allowed = new Set([
|
|
77
|
+
'http://127.0.0.1:' + port,
|
|
78
|
+
'http://localhost:' + port,
|
|
79
|
+
'http://[::1]:' + port,
|
|
80
|
+
])
|
|
81
|
+
if (headers.origin && allowed.has(headers.origin)) return true
|
|
82
|
+
const referer = headers.referer
|
|
83
|
+
if (referer) {
|
|
84
|
+
const base = referer.split('/').slice(0, 3).join('/')
|
|
85
|
+
if (allowed.has(base)) return true
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function apply(ctx, config) {
|
|
91
|
+
// 注入的服务由加载器保证在 apply 前就绪(不能在 apply 时用 ctx.get 提前
|
|
92
|
+
// 捕获未就绪的服务,否则会永久拿到 undefined)。
|
|
93
|
+
const fs = ctx.fs
|
|
94
|
+
const subprocess = ctx.subprocess
|
|
95
|
+
const sandboxPolicy = ctx.get('sandboxPolicy')
|
|
96
|
+
|
|
97
|
+
const resolved = {
|
|
98
|
+
/** 直接指定桌面应用 exe 路径;空则按 launcherDirs/内置候选自动探测。 */
|
|
99
|
+
launcherExe: config && typeof config.launcherExe === 'string' ? config.launcherExe : '',
|
|
100
|
+
/** 候选 exe 目录列表;为空时使用内置默认候选。 */
|
|
101
|
+
launcherDirs: Array.isArray(config && config.launcherDirs) ? config.launcherDirs.filter((d) => typeof d === 'string') : [],
|
|
102
|
+
/** 心跳“新鲜窗口”(秒)。桌面应用每秒写心跳,默认 4 秒即 4 个周期的余量。 */
|
|
103
|
+
freshSecs: config && typeof config.freshSecs === 'number' && config.freshSecs > 1 ? config.freshSecs : 4,
|
|
104
|
+
/** 桌面快捷方式的文件名。 */
|
|
105
|
+
shortcutName: config && typeof config.shortcutName === 'string' && config.shortcutName !== '' ? config.shortcutName : 'DeepSeek Harness.lnk',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let lastFsError = ''
|
|
109
|
+
let lastMarkerError = ''
|
|
110
|
+
let spawnHandle = null
|
|
111
|
+
let shortcutCache = { t: 0, value: false }
|
|
112
|
+
|
|
113
|
+
function baseDirs() {
|
|
114
|
+
const list = resolved.launcherDirs.slice()
|
|
115
|
+
if (resolved.launcherExe) {
|
|
116
|
+
const slash = resolved.launcherExe.lastIndexOf('\\')
|
|
117
|
+
if (slash > 0) list.push(resolved.launcherExe.slice(0, slash))
|
|
118
|
+
}
|
|
119
|
+
for (const dir of DEFAULT_DIRS) list.push(dir)
|
|
120
|
+
return [...new Set(list)]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function exeExists(dir) {
|
|
124
|
+
if (!fs) return false
|
|
125
|
+
try {
|
|
126
|
+
const target = await fs.resolve(dir + '\\dsh-launcher.exe')
|
|
127
|
+
const info = await fs.stat(target)
|
|
128
|
+
return Boolean(info && info.type === 'file')
|
|
129
|
+
} catch (error) {
|
|
130
|
+
lastFsError = String((error && error.message) || error)
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function exeDirs() {
|
|
136
|
+
const checked = await Promise.all(baseDirs().map(async (dir) => ((await exeExists(dir)) ? dir : null)))
|
|
137
|
+
return checked.filter((dir) => dir !== null)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fileText(dir, name) {
|
|
141
|
+
if (!fs) return null
|
|
142
|
+
try {
|
|
143
|
+
const target = await fs.resolve(dir + '\\' + name)
|
|
144
|
+
const info = await fs.stat(target)
|
|
145
|
+
if (!info) return null
|
|
146
|
+
const text = await fs.readText(target)
|
|
147
|
+
return String(text).trim()
|
|
148
|
+
} catch (error) {
|
|
149
|
+
lastFsError = String((error && error.message) || error)
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function hbState(dir) {
|
|
155
|
+
const text = await fileText(dir, '.dsh-heartbeat')
|
|
156
|
+
if (text === null || text === '') return null
|
|
157
|
+
const stamp = Number(text)
|
|
158
|
+
if (!Number.isFinite(stamp)) return null
|
|
159
|
+
const age = Math.abs(Date.now() / 1000 - stamp)
|
|
160
|
+
return age < resolved.freshSecs
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function isRunning() {
|
|
164
|
+
const dirs = await exeDirs()
|
|
165
|
+
const states = []
|
|
166
|
+
for (const dir of dirs) states.push(await hbState(dir))
|
|
167
|
+
if (states.includes(true)) return true
|
|
168
|
+
if (states.length === 0 || states.every((s) => s === null)) return null
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function waitFresh(seconds) {
|
|
173
|
+
let sawFile = false
|
|
174
|
+
for (let i = 0; i < seconds * 2; i++) {
|
|
175
|
+
const state = await isRunning()
|
|
176
|
+
if (state === true) return true
|
|
177
|
+
if (state === false) sawFile = true
|
|
178
|
+
await sleep(500)
|
|
179
|
+
}
|
|
180
|
+
return sawFile ? false : null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function waitGone(seconds) {
|
|
184
|
+
for (let i = 0; i < seconds * 2; i++) {
|
|
185
|
+
const state = await isRunning()
|
|
186
|
+
if (state === false) return true
|
|
187
|
+
if (state === null) return null
|
|
188
|
+
await sleep(500)
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function pickExe() {
|
|
194
|
+
if (resolved.launcherExe) {
|
|
195
|
+
try {
|
|
196
|
+
const target = await fs.resolve(resolved.launcherExe)
|
|
197
|
+
const info = await fs.stat(target)
|
|
198
|
+
if (info && info.type === 'file') return resolved.launcherExe
|
|
199
|
+
} catch (error) {
|
|
200
|
+
lastFsError = String((error && error.message) || error)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const dirs = await exeDirs()
|
|
204
|
+
if (dirs.length > 0) return dirs[0] + '\\dsh-launcher.exe'
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runPowerShell(script, cwd) {
|
|
209
|
+
if (!subprocess) throw new Error('subprocess 服务不可用')
|
|
210
|
+
const handle = subprocess.spawn({
|
|
211
|
+
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
212
|
+
cwd: cwd || (await pickExe()) || '',
|
|
213
|
+
stdio: STDIO,
|
|
214
|
+
graceMs: 3000,
|
|
215
|
+
})
|
|
216
|
+
if (handle && handle.done) await handle.done.catch(() => {})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function runPowerShellExitCode(script, cwd) {
|
|
220
|
+
if (!subprocess) return null
|
|
221
|
+
try {
|
|
222
|
+
const handle = subprocess.spawn({
|
|
223
|
+
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
224
|
+
cwd: cwd || process.cwd(),
|
|
225
|
+
stdio: STDIO,
|
|
226
|
+
graceMs: 3000,
|
|
227
|
+
})
|
|
228
|
+
const outcome = handle && handle.done ? await handle.done.catch(() => null) : null
|
|
229
|
+
return outcome && typeof outcome.exitCode === 'number' ? outcome.exitCode : null
|
|
230
|
+
} catch (error) {
|
|
231
|
+
lastMarkerError = String((error && error.message) || error)
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function quitScriptFor(value) {
|
|
237
|
+
const exe = await pickExe()
|
|
238
|
+
if (!exe) return null
|
|
239
|
+
const dir = exe.slice(0, exe.lastIndexOf('\\'))
|
|
240
|
+
return {
|
|
241
|
+
dir,
|
|
242
|
+
// dir/value 一律经 psQuote 转义(单引号翻倍),避免含单引号的路径破坏脚本。
|
|
243
|
+
script: 'Set-Content -LiteralPath ' + psQuote(dir + '\\.dsh-quit') + ' -Value ' + psQuote(value) + ' -NoNewline',
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function writeQuitMarker(value) {
|
|
248
|
+
const job = await quitScriptFor(value)
|
|
249
|
+
if (!job) {
|
|
250
|
+
lastMarkerError = 'writeQuitMarker: no exe dir'
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await runPowerShell(job.script, job.dir)
|
|
255
|
+
lastMarkerError = ''
|
|
256
|
+
} catch (error) {
|
|
257
|
+
lastMarkerError = String((error && error.message) || error)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function writeQuitMarkerNoWait(value) {
|
|
262
|
+
void quitScriptFor(value).then((job) => {
|
|
263
|
+
if (!job) return
|
|
264
|
+
try {
|
|
265
|
+
subprocess.spawn({
|
|
266
|
+
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', job.script],
|
|
267
|
+
cwd: job.dir,
|
|
268
|
+
stdio: STDIO,
|
|
269
|
+
graceMs: 3000,
|
|
270
|
+
})
|
|
271
|
+
} catch (error) {
|
|
272
|
+
lastMarkerError = String((error && error.message) || error)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function quitFileGone() {
|
|
278
|
+
const exe = await pickExe()
|
|
279
|
+
if (!exe) return true
|
|
280
|
+
const dir = exe.slice(0, exe.lastIndexOf('\\'))
|
|
281
|
+
return (await fileText(dir, '.dsh-quit')) === null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function quitMarkerIsOne() {
|
|
285
|
+
const exe = await pickExe()
|
|
286
|
+
if (!exe) return false
|
|
287
|
+
const dir = exe.slice(0, exe.lastIndexOf('\\'))
|
|
288
|
+
return (await fileText(dir, '.dsh-quit')) === '1'
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function shortcutExistsScript() {
|
|
292
|
+
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(resolved.shortcutName) + "; if (Test-Path -LiteralPath $lnk) { exit 0 } else { exit 1 }"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function shortcutCreateScript(exe) {
|
|
296
|
+
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(resolved.shortcutName) + "; " +
|
|
297
|
+
"$exe=" + psQuote(exe) + "; " +
|
|
298
|
+
'$ws=New-Object -ComObject WScript.Shell; $sc=$ws.CreateShortcut($lnk); ' +
|
|
299
|
+
'$sc.TargetPath=$exe; $sc.WorkingDirectory=Split-Path $exe; ' +
|
|
300
|
+
"$sc.IconLocation=($exe + ',0'); $sc.Description=" + psQuote('DeepSeek Harness 桌面启动器') + '; ' +
|
|
301
|
+
'$sc.Save(); if (Test-Path -LiteralPath $lnk) { exit 0 } else { exit 1 }'
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function shortcutDeleteScript() {
|
|
305
|
+
return "$desktop=[Environment]::GetFolderPath('Desktop'); $lnk=Join-Path $desktop " + psQuote(resolved.shortcutName) + '; ' +
|
|
306
|
+
'Remove-Item -LiteralPath $lnk -Force -ErrorAction SilentlyContinue; ' +
|
|
307
|
+
'if (Test-Path -LiteralPath $lnk) { exit 1 } else { exit 0 }'
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function shortcutExists() {
|
|
311
|
+
const now = Date.now()
|
|
312
|
+
if (now - shortcutCache.t < 5000) return shortcutCache.value
|
|
313
|
+
const code = await runPowerShellExitCode(shortcutExistsScript(), process.cwd())
|
|
314
|
+
shortcutCache = { t: Date.now(), value: code === 0 }
|
|
315
|
+
return shortcutCache.value
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function ensureShortcut() {
|
|
319
|
+
if (await shortcutExists()) return true
|
|
320
|
+
const exe = await pickExe()
|
|
321
|
+
if (!exe) return false
|
|
322
|
+
const code = await runPowerShellExitCode(shortcutCreateScript(exe), process.cwd())
|
|
323
|
+
shortcutCache = { t: Date.now(), value: code === 0 }
|
|
324
|
+
return code === 0
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function removeShortcut() {
|
|
328
|
+
if (!(await shortcutExists())) return true
|
|
329
|
+
const code = await runPowerShellExitCode(shortcutDeleteScript(), process.cwd())
|
|
330
|
+
shortcutCache = { t: Date.now(), value: code !== 0 }
|
|
331
|
+
return code === 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function buildDiag() {
|
|
335
|
+
const lines = []
|
|
336
|
+
lines.push('services: fs=' + !!fs + ' subprocess=' + !!subprocess + ' sandboxPolicy=' + !!sandboxPolicy)
|
|
337
|
+
lines.push('workspaceRoot: ' + (sandboxPolicy ? sandboxPolicy.workspaceRoot : '(none)'))
|
|
338
|
+
lines.push('freshWindowSecs: ' + resolved.freshSecs)
|
|
339
|
+
lines.push('launcherExe: ' + (resolved.launcherExe || '(auto)'))
|
|
340
|
+
lines.push('shortcut: ' + (await shortcutExists()))
|
|
341
|
+
const dirs = await exeDirs()
|
|
342
|
+
lines.push('exeDirs: ' + JSON.stringify(dirs))
|
|
343
|
+
const exe = await pickExe()
|
|
344
|
+
lines.push('pickExe: ' + (exe || '(null)'))
|
|
345
|
+
if (lastFsError) lines.push('fsError: ' + lastFsError)
|
|
346
|
+
lines.push('markerWrite: ' + (lastMarkerError || 'ok'))
|
|
347
|
+
for (const dir of dirs) {
|
|
348
|
+
lines.push('heartbeat(' + dir.split('\\').slice(-2).join('\\') + '): ' + String(await hbState(dir)))
|
|
349
|
+
}
|
|
350
|
+
if (dirs.length === 0) lines.push('heartbeat: (no exe dirs)')
|
|
351
|
+
return lines.join('\n')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function getState() {
|
|
355
|
+
const running = await isRunning()
|
|
356
|
+
const exe = await pickExe()
|
|
357
|
+
const shortcut = await shortcutExists()
|
|
358
|
+
return { ok: true, desktop: running, shortcut, exe: exe || null }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function getDiag() {
|
|
362
|
+
return { ok: true, diag: await buildDiag() }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function setDesktop(enabled) {
|
|
366
|
+
if (enabled) {
|
|
367
|
+
if (await quitMarkerIsOne()) await writeQuitMarker('0')
|
|
368
|
+
const exe = await pickExe()
|
|
369
|
+
if (!exe) {
|
|
370
|
+
return { ok: false, error: '未找到桌面应用可执行文件(可通过行配置 launcherExe 或 launcherDirs 指定)。', diag: await buildDiag() }
|
|
371
|
+
}
|
|
372
|
+
if (!subprocess) {
|
|
373
|
+
return { ok: false, error: 'subprocess 服务不可用,无法启动桌面应用。', diag: await buildDiag() }
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const dir = exe.slice(0, exe.lastIndexOf('\\'))
|
|
377
|
+
spawnHandle = subprocess.spawn({ argv: [exe], cwd: dir, stdio: STDIO, graceMs: 3000 })
|
|
378
|
+
} catch (error) {
|
|
379
|
+
return { ok: false, error: '启动桌面应用失败:' + String((error && error.message) || error), diag: await buildDiag() }
|
|
380
|
+
}
|
|
381
|
+
const state = await waitFresh(20)
|
|
382
|
+
if (state === true) {
|
|
383
|
+
if (!(await ensureShortcut())) {
|
|
384
|
+
lastMarkerError = 'shortcut sync failed (create)'
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
desktop: true,
|
|
388
|
+
shortcut: false,
|
|
389
|
+
error: '桌面应用已启动,但创建桌面快捷方式失败(可稍后在设置中手动重试)。',
|
|
390
|
+
diag: await buildDiag(),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { ok: true, desktop: true, shortcut: true, diag: '' }
|
|
394
|
+
}
|
|
395
|
+
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag: '' }
|
|
396
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const owned = spawnHandle
|
|
400
|
+
spawnHandle = null
|
|
401
|
+
if (owned) {
|
|
402
|
+
try { owned.terminate() } catch {}
|
|
403
|
+
}
|
|
404
|
+
await writeQuitMarker('1')
|
|
405
|
+
let confirmed = false
|
|
406
|
+
for (let i = 0; i < 12; i++) {
|
|
407
|
+
if (await quitFileGone()) { confirmed = true; break }
|
|
408
|
+
if ((await isRunning()) === false) { confirmed = true; break }
|
|
409
|
+
await sleep(500)
|
|
410
|
+
}
|
|
411
|
+
if (confirmed) {
|
|
412
|
+
if (owned && owned.done) await Promise.race([owned.done.catch(() => {}), sleep(3000)])
|
|
413
|
+
writeQuitMarkerNoWait('0')
|
|
414
|
+
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
415
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
416
|
+
}
|
|
417
|
+
writeQuitMarkerNoWait('0')
|
|
418
|
+
const state = await waitGone(20)
|
|
419
|
+
if (state === true) {
|
|
420
|
+
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
421
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
422
|
+
}
|
|
423
|
+
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag: '' }
|
|
424
|
+
try {
|
|
425
|
+
const exe = await pickExe()
|
|
426
|
+
const dir = exe ? exe.slice(0, exe.lastIndexOf('\\')) : process.cwd()
|
|
427
|
+
// 仅终止与探测到的 exe 路径匹配的进程,避免误杀其他同名程序。
|
|
428
|
+
const script = "$p = Get-Process -Name dsh-launcher -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq " + psQuote(exe || '') + " }; if ($p) { $p | Stop-Process -Force -ErrorAction SilentlyContinue }"
|
|
429
|
+
await runPowerShell(script, dir)
|
|
430
|
+
} catch (error) {
|
|
431
|
+
lastMarkerError = String((error && error.message) || error)
|
|
432
|
+
}
|
|
433
|
+
const state2 = await waitGone(8)
|
|
434
|
+
if (state2 === true) {
|
|
435
|
+
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
436
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
437
|
+
}
|
|
438
|
+
return { ok: false, error: '桌面应用仍在运行(退出请求与强制结束均未生效)。', shortcut: await shortcutExists(), diag: await buildDiag() }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function setShortcut() {
|
|
442
|
+
const ok = await ensureShortcut()
|
|
443
|
+
if (ok) return { ok: true, shortcut: true, diag: '' }
|
|
444
|
+
return { ok: false, error: '创建桌面快捷方式失败。', shortcut: await shortcutExists(), diag: await buildDiag() }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
ctx.effect(() => {
|
|
448
|
+
const routes = [
|
|
449
|
+
{
|
|
450
|
+
kind: 'exact',
|
|
451
|
+
path: '/api/dsh-tauri-launcher/state',
|
|
452
|
+
handler: async (req, res) => {
|
|
453
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
454
|
+
writeJson(res, 200, await getState())
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
kind: 'exact',
|
|
459
|
+
path: '/api/dsh-tauri-launcher/set-desktop',
|
|
460
|
+
handler: async (req, res) => {
|
|
461
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
462
|
+
const body = await readJsonBody(req)
|
|
463
|
+
writeJson(res, 200, await setDesktop(Boolean(body.enabled)))
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
kind: 'exact',
|
|
468
|
+
path: '/api/dsh-tauri-launcher/set-shortcut',
|
|
469
|
+
handler: async (req, res) => {
|
|
470
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
471
|
+
writeJson(res, 200, await setShortcut())
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
kind: 'exact',
|
|
476
|
+
path: '/api/dsh-tauri-launcher/diagnose',
|
|
477
|
+
handler: async (req, res) => {
|
|
478
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
479
|
+
writeJson(res, 200, await getDiag())
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
const disposers = routes.map((route) => ctx.webServer.register(route))
|
|
484
|
+
return () => {
|
|
485
|
+
for (const dispose of disposers) dispose()
|
|
486
|
+
}
|
|
487
|
+
}, 'dsh-tauri-launcher: routes')
|
|
488
|
+
}
|