@lenorin/dsh-tauri-launcher 1.0.1 → 1.0.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.en.md +4 -0
- package/README.md +4 -0
- package/launcher/bin/dsh-launcher.exe +0 -0
- package/lib/client.js +16 -5
- package/lib/index.js +66 -33
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -145,6 +145,10 @@ fit common deployments):
|
|
|
145
145
|
|
|
146
146
|
- The npm package ships a pre-built exe (`launcher/bin/dsh-launcher.exe`); the plugin auto-detects it on install — ready to use out of the box;
|
|
147
147
|
- Local build: `pwsh -File launcher/build.ps1` (optional `-Offline -CargoHome <dir>`);
|
|
148
|
+
- ⚠️ Maintenance rule: **after editing Rust sources under `launcher/src-tauri`, you must
|
|
149
|
+
rebuild and re-sync `launcher/bin/dsh-launcher.exe`** (see [docs/release.md](docs/release.md)),
|
|
150
|
+
or the shipped exe will drift from the sources; keep the version in `package.json` /
|
|
151
|
+
`Cargo.toml` / `tauri.conf.json` in sync.
|
|
148
152
|
- GitHub Release: push a `v*` tag; CI auto-builds the Windows exe and publishes it
|
|
149
153
|
(Linux/macOS builds are not enabled yet — see [docs/release.md](docs/release.md)).
|
|
150
154
|
|
package/README.md
CHANGED
|
@@ -138,6 +138,10 @@ dsh plugin --profile web remove @lenorin/dsh-tauri-launcher
|
|
|
138
138
|
|
|
139
139
|
- npm 包自带预编译 exe(`launcher/bin/dsh-launcher.exe`),插件安装后自动探测、装上即用;
|
|
140
140
|
- 本地自行构建:`pwsh -File launcher/build.ps1`(可选 `-Offline -CargoHome <dir>`);
|
|
141
|
+
- ⚠️ 维护纪律:**修改 `launcher/src-tauri` 下的 Rust 源码后,必须重新构建并同步
|
|
142
|
+
`launcher/bin/dsh-launcher.exe`**(发布流程见 [docs/release.md](docs/release.md)),
|
|
143
|
+
否则 npm 包内 exe 与源码漂移;版本号需在 `package.json` / `Cargo.toml` /
|
|
144
|
+
`tauri.conf.json` 三处同步。
|
|
141
145
|
- GitHub Release:推送 `v*` tag,CI 自动构建 Windows exe 并发布(Linux/macOS 构建
|
|
142
146
|
尚未启用,见 [docs/release.md](docs/release.md))。
|
|
143
147
|
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -64,22 +64,28 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
`
|
|
65
65
|
|
|
66
66
|
const API_BASE = '/api/dsh-tauri-launcher'
|
|
67
|
+
/** 自定义请求头:同源 fetch 可带,跨站 form POST 无法伪造(CSRF 防线)。 */
|
|
68
|
+
const CSRF_HEADERS = { 'x-requested-with': 'dsh-tauri-launcher' }
|
|
67
69
|
|
|
68
70
|
const api = {
|
|
69
71
|
async state() {
|
|
70
|
-
const response = await fetch(API_BASE + '/state')
|
|
72
|
+
const response = await fetch(API_BASE + '/state', { headers: CSRF_HEADERS })
|
|
71
73
|
return response.json()
|
|
72
74
|
},
|
|
73
75
|
async setDesktop(enabled) {
|
|
74
76
|
const response = await fetch(API_BASE + '/set-desktop', {
|
|
75
77
|
method: 'POST',
|
|
76
|
-
headers: { 'content-type': 'application/json' },
|
|
78
|
+
headers: { ...CSRF_HEADERS, 'content-type': 'application/json' },
|
|
77
79
|
body: JSON.stringify({ enabled }),
|
|
78
80
|
})
|
|
79
81
|
return response.json()
|
|
80
82
|
},
|
|
81
83
|
async setShortcut() {
|
|
82
|
-
const response = await fetch(API_BASE + '/set-shortcut', { method: 'POST' })
|
|
84
|
+
const response = await fetch(API_BASE + '/set-shortcut', { method: 'POST', headers: CSRF_HEADERS })
|
|
85
|
+
return response.json()
|
|
86
|
+
},
|
|
87
|
+
async diagnose() {
|
|
88
|
+
const response = await fetch(API_BASE + '/diagnose', { headers: CSRF_HEADERS })
|
|
83
89
|
return response.json()
|
|
84
90
|
},
|
|
85
91
|
}
|
|
@@ -105,9 +111,14 @@ window.__ModuleLoader__.load({
|
|
|
105
111
|
async function fetchState() {
|
|
106
112
|
try {
|
|
107
113
|
const state = await api.state()
|
|
108
|
-
return { desktop: state.desktop, shortcut: Boolean(state.shortcut), error: '', diag:
|
|
114
|
+
return { desktop: state.desktop, shortcut: Boolean(state.shortcut), error: '', diag: '' }
|
|
109
115
|
} catch (error) {
|
|
110
|
-
|
|
116
|
+
let diag = ''
|
|
117
|
+
try {
|
|
118
|
+
const diagnosed = await api.diagnose()
|
|
119
|
+
diag = (diagnosed && diagnosed.diag) || ''
|
|
120
|
+
} catch { /* 诊断失败不影响错误展示 */ }
|
|
121
|
+
return { desktop: null, shortcut: false, error: '读取状态失败:' + errText(error), diag }
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
package/lib/index.js
CHANGED
|
@@ -25,12 +25,8 @@ const STDIO = { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }
|
|
|
25
25
|
/** 本包随附的预编译桌面应用 exe 目录(无论安装到哪个 profile 都能定位)。 */
|
|
26
26
|
const PACKAGE_BIN = fileURLToPath(new URL('../launcher/bin', import.meta.url))
|
|
27
27
|
|
|
28
|
-
/** 未配置 launcherDirs
|
|
29
|
-
const DEFAULT_DIRS = [
|
|
30
|
-
PACKAGE_BIN,
|
|
31
|
-
'F:\\DeepSeek Harness\\DeepSeek Harness Tauri\\dsh-launcher\\src-tauri\\target\\release',
|
|
32
|
-
'F:\\DeepSeek Harness\\DeepSeek Harness Tauri\\dsh-launcher\\src-tauri\\target\\debug',
|
|
33
|
-
]
|
|
28
|
+
/** 未配置 launcherDirs 时的默认候选目录(仅包内随附的预编译 exe;本地自行构建的 exe 通过行配置 launcherDirs/launcherExe 指定)。 */
|
|
29
|
+
const DEFAULT_DIRS = [PACKAGE_BIN]
|
|
34
30
|
|
|
35
31
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
36
32
|
const psQuote = (value) => "'" + String(value).replace(/'/g, "''") + "'"
|
|
@@ -41,9 +37,17 @@ function writeJson(res, status, body) {
|
|
|
41
37
|
res.end(payload)
|
|
42
38
|
}
|
|
43
39
|
|
|
40
|
+
/** 请求体大小上限(回环接口的防御性约束)。 */
|
|
41
|
+
const MAX_BODY_BYTES = 1024 * 1024
|
|
42
|
+
|
|
44
43
|
async function readJsonBody(req) {
|
|
45
44
|
const chunks = []
|
|
46
|
-
|
|
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
|
+
}
|
|
47
51
|
try {
|
|
48
52
|
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
49
53
|
return typeof parsed === 'object' && parsed !== null ? parsed : {}
|
|
@@ -58,6 +62,31 @@ function isLoopback(req) {
|
|
|
58
62
|
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
59
63
|
}
|
|
60
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
|
+
|
|
61
90
|
export function apply(ctx, config) {
|
|
62
91
|
// 注入的服务由加载器保证在 apply 前就绪(不能在 apply 时用 ctx.get 提前
|
|
63
92
|
// 捕获未就绪的服务,否则会永久拿到 undefined)。
|
|
@@ -88,12 +117,6 @@ export function apply(ctx, config) {
|
|
|
88
117
|
if (slash > 0) list.push(resolved.launcherExe.slice(0, slash))
|
|
89
118
|
}
|
|
90
119
|
for (const dir of DEFAULT_DIRS) list.push(dir)
|
|
91
|
-
const root = sandboxPolicy && sandboxPolicy.workspaceRoot
|
|
92
|
-
if (root) {
|
|
93
|
-
const r = String(root).replace(/[\\/]+$/, '')
|
|
94
|
-
list.push(r + '\\dsh-launcher\\src-tauri\\target\\release')
|
|
95
|
-
list.push(r + '\\dsh-launcher\\src-tauri\\target\\debug')
|
|
96
|
-
}
|
|
97
120
|
return [...new Set(list)]
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -110,11 +133,8 @@ export function apply(ctx, config) {
|
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
async function exeDirs() {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
if (await exeExists(dir)) dirs.push(dir)
|
|
116
|
-
}
|
|
117
|
-
return dirs
|
|
136
|
+
const checked = await Promise.all(baseDirs().map(async (dir) => ((await exeExists(dir)) ? dir : null)))
|
|
137
|
+
return checked.filter((dir) => dir !== null)
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
async function fileText(dir, name) {
|
|
@@ -189,7 +209,7 @@ export function apply(ctx, config) {
|
|
|
189
209
|
if (!subprocess) throw new Error('subprocess 服务不可用')
|
|
190
210
|
const handle = subprocess.spawn({
|
|
191
211
|
argv: ['powershell', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
192
|
-
cwd: cwd || (await pickExe()
|
|
212
|
+
cwd: cwd || (await pickExe()) || '',
|
|
193
213
|
stdio: STDIO,
|
|
194
214
|
graceMs: 3000,
|
|
195
215
|
})
|
|
@@ -334,8 +354,11 @@ export function apply(ctx, config) {
|
|
|
334
354
|
const running = await isRunning()
|
|
335
355
|
const exe = await pickExe()
|
|
336
356
|
const shortcut = await shortcutExists()
|
|
337
|
-
|
|
338
|
-
|
|
357
|
+
return { ok: true, desktop: running, shortcut, exe: exe || null }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function getDiag() {
|
|
361
|
+
return { ok: true, diag: await buildDiag() }
|
|
339
362
|
}
|
|
340
363
|
|
|
341
364
|
async function setDesktop(enabled) {
|
|
@@ -357,10 +380,10 @@ export function apply(ctx, config) {
|
|
|
357
380
|
const state = await waitFresh(20)
|
|
358
381
|
if (state === true) {
|
|
359
382
|
if (!(await ensureShortcut())) lastMarkerError = 'shortcut sync failed (create)'
|
|
360
|
-
return { ok: true, desktop: true, shortcut: await shortcutExists(), diag:
|
|
383
|
+
return { ok: true, desktop: true, shortcut: await shortcutExists(), diag: '' }
|
|
361
384
|
}
|
|
362
|
-
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag:
|
|
363
|
-
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag:
|
|
385
|
+
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag: '' }
|
|
386
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
364
387
|
}
|
|
365
388
|
|
|
366
389
|
const owned = spawnHandle
|
|
@@ -379,33 +402,35 @@ export function apply(ctx, config) {
|
|
|
379
402
|
if (owned && owned.done) await Promise.race([owned.done.catch(() => {}), sleep(3000)])
|
|
380
403
|
writeQuitMarkerNoWait('0')
|
|
381
404
|
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
382
|
-
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag:
|
|
405
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
383
406
|
}
|
|
384
407
|
writeQuitMarkerNoWait('0')
|
|
385
408
|
const state = await waitGone(20)
|
|
386
409
|
if (state === true) {
|
|
387
410
|
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
388
|
-
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag:
|
|
411
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
389
412
|
}
|
|
390
|
-
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag:
|
|
413
|
+
if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag: '' }
|
|
391
414
|
try {
|
|
392
415
|
const exe = await pickExe()
|
|
393
416
|
const dir = exe ? exe.slice(0, exe.lastIndexOf('\\')) : process.cwd()
|
|
394
|
-
|
|
417
|
+
// 仅终止与探测到的 exe 路径匹配的进程,避免误杀其他同名程序。
|
|
418
|
+
const script = "$p = Get-Process -Name dsh-launcher -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq " + psQuote(exe || '') + " }; if ($p) { $p | Stop-Process -Force -ErrorAction SilentlyContinue }"
|
|
419
|
+
await runPowerShell(script, dir)
|
|
395
420
|
} catch (error) {
|
|
396
421
|
lastMarkerError = String((error && error.message) || error)
|
|
397
422
|
}
|
|
398
423
|
const state2 = await waitGone(8)
|
|
399
424
|
if (state2 === true) {
|
|
400
425
|
if (!(await removeShortcut())) lastMarkerError = 'shortcut sync failed (delete)'
|
|
401
|
-
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag:
|
|
426
|
+
return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
|
|
402
427
|
}
|
|
403
428
|
return { ok: false, error: '桌面应用仍在运行(退出请求与强制结束均未生效)。', shortcut: await shortcutExists(), diag: await buildDiag() }
|
|
404
429
|
}
|
|
405
430
|
|
|
406
431
|
async function setShortcut() {
|
|
407
432
|
const ok = await ensureShortcut()
|
|
408
|
-
if (ok) return { ok: true, shortcut: true, diag:
|
|
433
|
+
if (ok) return { ok: true, shortcut: true, diag: '' }
|
|
409
434
|
return { ok: false, error: '创建桌面快捷方式失败。', shortcut: await shortcutExists(), diag: await buildDiag() }
|
|
410
435
|
}
|
|
411
436
|
|
|
@@ -415,7 +440,7 @@ export function apply(ctx, config) {
|
|
|
415
440
|
kind: 'exact',
|
|
416
441
|
path: '/api/dsh-tauri-launcher/state',
|
|
417
442
|
handler: async (req, res) => {
|
|
418
|
-
if (!
|
|
443
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
419
444
|
writeJson(res, 200, await getState())
|
|
420
445
|
},
|
|
421
446
|
},
|
|
@@ -423,7 +448,7 @@ export function apply(ctx, config) {
|
|
|
423
448
|
kind: 'exact',
|
|
424
449
|
path: '/api/dsh-tauri-launcher/set-desktop',
|
|
425
450
|
handler: async (req, res) => {
|
|
426
|
-
if (!
|
|
451
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
427
452
|
const body = await readJsonBody(req)
|
|
428
453
|
writeJson(res, 200, await setDesktop(Boolean(body.enabled)))
|
|
429
454
|
},
|
|
@@ -432,10 +457,18 @@ export function apply(ctx, config) {
|
|
|
432
457
|
kind: 'exact',
|
|
433
458
|
path: '/api/dsh-tauri-launcher/set-shortcut',
|
|
434
459
|
handler: async (req, res) => {
|
|
435
|
-
if (!
|
|
460
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
436
461
|
writeJson(res, 200, await setShortcut())
|
|
437
462
|
},
|
|
438
463
|
},
|
|
464
|
+
{
|
|
465
|
+
kind: 'exact',
|
|
466
|
+
path: '/api/dsh-tauri-launcher/diagnose',
|
|
467
|
+
handler: async (req, res) => {
|
|
468
|
+
if (!isTrustedRequest(req, ctx.webServer.port)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
469
|
+
writeJson(res, 200, await getDiag())
|
|
470
|
+
},
|
|
471
|
+
},
|
|
439
472
|
]
|
|
440
473
|
const disposers = routes.map((route) => ctx.webServer.register(route))
|
|
441
474
|
return () => {
|