@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 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: state.diag || '' }
114
+ return { desktop: state.desktop, shortcut: Boolean(state.shortcut), error: '', diag: '' }
109
115
  } catch (error) {
110
- return { desktop: null, shortcut: false, error: '读取状态失败:' + errText(error), diag: '' }
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 时的默认候选目录(包内 exe 优先,本机常见布局兜底,可被行配置覆盖)。 */
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
- for await (const chunk of req) chunks.push(chunk)
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 dirs = []
114
- for (const dir of baseDirs()) {
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() ? 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
- const diag = await buildDiag()
338
- return { ok: true, desktop: running, shortcut, exe: exe || null, diag }
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: await buildDiag() }
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: await buildDiag() }
363
- return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: await buildDiag() }
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: await buildDiag() }
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: await buildDiag() }
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: await buildDiag() }
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
- await runPowerShell('Stop-Process -Name dsh-launcher -Force -ErrorAction SilentlyContinue', dir)
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: await buildDiag() }
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: await buildDiag() }
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 (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
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 (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
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 (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenorin/dsh-tauri-launcher",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "DSH Web 插件:基于 Tauri 2 的 DeepSeek Harness 桌面启动器,自动联动桌面快捷方式,exe 路径与探测窗口可配置。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",