dsh-remote-plugin 0.6.24 → 0.6.26

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/index.mjs CHANGED
@@ -77,6 +77,14 @@ try {
77
77
  let dshListen = { host: '127.0.0.1', port: 3080 }
78
78
  let dshConnection = null
79
79
 
80
+ // 监听通配地址不是连接目标;保留地址族,避免 IPv6-only 服务退回 IPv4。
81
+ export function upstreamUrlForListener({ host, port }) {
82
+ let target = String(host || '127.0.0.1').replace(/^\[|\]$/g, '')
83
+ if (target === '0.0.0.0') target = '127.0.0.1'
84
+ if (net.isIP(target) === 6 && new URL(`http://[${target}]`).hostname === '[::]') target = '::1'
85
+ return `http://${net.isIP(target) === 6 ? `[${target}]` : target}:${port}`
86
+ }
87
+
80
88
  function dshUpstreamCookieFile() {
81
89
  return process.env.DSH_REMOTE_DSH_COOKIE_FILE || `${homedir()}/.dsh-remote/dsh-upstream.cookie`
82
90
  }
@@ -88,7 +96,7 @@ function dshUpstreamCookieFile() {
88
96
  */
89
97
  async function refreshDshUpstreamCookie() {
90
98
  if (typeof dshConnection?.authenticatedUrl !== 'function') return false
91
- const upstream = `http://${dshListen.host}:${dshListen.port}`
99
+ const upstream = upstreamUrlForListener(dshListen)
92
100
  try {
93
101
  const loginUrl = dshConnection.authenticatedUrl(upstream)
94
102
  const response = await fetch(loginUrl, {
@@ -254,11 +262,16 @@ function portInUse(port) {
254
262
 
255
263
  async function gatewayRunning() {
256
264
  try {
257
- const res = await fetch(`${gatewayBase()}/health`, { signal: AbortSignal.timeout(3000) })
265
+ const base = gatewayBase()
266
+ const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(6000) })
258
267
  if (!res.ok) return { running: false }
259
268
  const data = await res.json().catch(() => ({}))
269
+ if (data.service !== 'dsh-remote' || !Number.isInteger(data.pid) || data.pid <= 1
270
+ || typeof data.version !== 'string' || typeof data.upstream !== 'string'
271
+ || !data.events?.mux || !data.events?.host) return { running: false }
260
272
  return {
261
273
  running: true,
274
+ base,
262
275
  pid: Number(data.pid) || 0,
263
276
  version: typeof data.version === 'string' ? data.version : '',
264
277
  upstream: typeof data.upstream === 'string' ? data.upstream : '',
@@ -274,15 +287,6 @@ async function gatewayRunning() {
274
287
 
275
288
  function gatewayPidFile() { return `${homedir()}/.dsh-remote/plugin-gateway.pid` }
276
289
 
277
- function readGatewayPid() {
278
- try {
279
- const pid = Number(readFileSync(gatewayPidFile(), 'utf8').trim())
280
- return Number.isFinite(pid) && pid > 0 ? pid : 0
281
- } catch {
282
- return 0
283
- }
284
- }
285
-
286
290
  function writeGatewayPid(pid) {
287
291
  try {
288
292
  mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
@@ -298,19 +302,22 @@ function logGateway(msg) {
298
302
  }
299
303
 
300
304
  async function killGateway(health) {
301
- const pid = (health && Number(health.pid)) || readGatewayPid()
302
- if (!pid) return false
305
+ // PID 文件和公开健康响应不能证明进程归属;仅走网关认证关闭接口。
306
+ if (!health?.running || !Number.isInteger(health.pid) || health.pid <= 1) return false
307
+ const token = gatewayToken()
308
+ if (!token) return false
303
309
  try {
304
- if (process.platform === 'win32') {
305
- await runExit('taskkill', ['/F', '/PID', String(pid)])
306
- } else {
307
- process.kill(pid)
308
- }
309
- logGateway('已停止旧网关 PID=' + pid)
310
+ const res = await fetch(`${health.base || gatewayBase()}/admin/api/shutdown`, {
311
+ method: 'POST', headers: { authorization: `Bearer ${token}`, 'x-dsh-remote-client': 'admin' },
312
+ signal: AbortSignal.timeout(2000),
313
+ })
314
+ const body = await res.json().catch(() => ({}))
315
+ if (!res.ok || body.ok !== true || body.bye !== true) return false
316
+ logGateway('已请求旧网关认证关闭')
310
317
  await sleep(300)
311
318
  return true
312
319
  } catch (e) {
313
- logGateway('停止旧网关失败 PID=' + pid + ' ' + (e?.message || String(e)))
320
+ logGateway('认证关闭旧网关失败: ' + (e?.message || String(e)))
314
321
  return false
315
322
  }
316
323
  }
@@ -337,7 +344,7 @@ function setGatewayEnabled(on) {
337
344
 
338
345
  /** 启动随插件分发的 gateway.cjs; 已运行则直接返回。 */
339
346
  async function startGateway() {
340
- const upstream = `http://${dshListen.host}:${dshListen.port}`
347
+ const upstream = upstreamUrlForListener(dshListen)
341
348
  const health = await gatewayRunning()
342
349
  if (health.running) {
343
350
  setGatewayEnabled(true)
@@ -413,7 +420,7 @@ function ensureGateway() {
413
420
  const out = await startGateway()
414
421
  return !!out.running
415
422
  }
416
- const upstream = `http://${dshListen.host}:${dshListen.port}`
423
+ const upstream = upstreamUrlForListener(dshListen)
417
424
  const oldUpstream = health.upstream || ''
418
425
  const oldVersion = health.version || '?'
419
426
  const versionMismatch = oldVersion !== version
@@ -421,7 +428,10 @@ function ensureGateway() {
421
428
  // 重启只能制造额外断线,网关应保持运行并通过 /health 暴露 degraded 状态。
422
429
  if (versionMismatch || (oldUpstream && oldUpstream !== upstream) || (!oldUpstream && upstream)) {
423
430
  logGateway(`网关需刷新: 版本 ${oldVersion} -> ${version}, 上游 ${oldUpstream || '?'} -> ${upstream}`)
424
- await killGateway(health)
431
+ if (!await killGateway(health)) {
432
+ logGateway('拒绝自动重启: 无法通过认证关闭旧网关,请检查令牌或端口占用')
433
+ return false
434
+ }
425
435
  for (let i = 0; i < 10; i++) {
426
436
  if (!(await gatewayRunning()).running) break
427
437
  await sleep(200)
@@ -439,20 +449,10 @@ function ensureGateway() {
439
449
 
440
450
  /** 通过网关自身的 /admin/api/shutdown 优雅停止(不管它当初是谁拉起的); 并写入 off 防自愈拉起。 */
441
451
  async function stopGateway() {
442
- const token = gatewayToken()
443
- if (!token) return { ok: false, running: false, error: '找不到 ~/.dsh-remote/token, 无法认证网关' }
444
- try {
445
- const res = await fetch(`${gatewayBase()}/admin/api/shutdown`, {
446
- method: 'POST',
447
- headers: { authorization: `Bearer ${token}`, 'x-dsh-remote-client': 'admin' },
448
- signal: AbortSignal.timeout(2000),
449
- })
450
- const json = await res.json().catch(() => ({}))
451
- if (res.ok) setGatewayEnabled(false)
452
- return { ok: res.ok, running: false, ...json }
453
- } catch (e) {
454
- return { ok: false, running: false, error: '网关不可达: ' + (e?.message || e) }
455
- }
452
+ const health = await gatewayRunning()
453
+ if (!await killGateway(health)) return { ok: false, running: health.running, error: '无法确认网关身份或认证关闭失败,请检查令牌与端口占用' }
454
+ setGatewayEnabled(false)
455
+ return { ok: true, running: false, bye: true }
456
456
  }
457
457
 
458
458
  // ---------- 统计事件投递(实时 assistant/message + usage -> 网关 /stats/ingest) ----------
@@ -750,6 +750,10 @@ async function serveStatic(req, res, ctx) {
750
750
  // 先在切换配置前读取旧端口上的健康状态;写入新端口后 gatewayRunning()
751
751
  // 只会探测新端口,否则旧网关会变成孤儿进程继续占用旧端口。
752
752
  const oldHealth = process.env.DSH_REMOTE_GATEWAY ? { running: false } : await gatewayRunning()
753
+ if (!process.env.DSH_REMOTE_GATEWAY_PORT && port !== oldPort && oldHealth.running && !await killGateway(oldHealth)) {
754
+ sendJson(res, 409, { ok: false, error: '无法通过认证关闭旧网关,端口配置未修改' })
755
+ return
756
+ }
753
757
  try {
754
758
  mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
755
759
  writeFileSync(gatewayPortFile(), String(port) + '\n')
@@ -758,9 +762,6 @@ async function serveStatic(req, res, ctx) {
758
762
  return
759
763
  }
760
764
  const effectivePort = Number(readGatewayPort())
761
- if (effectivePort !== oldPort) {
762
- if (oldHealth.running) await killGateway(oldHealth)
763
- }
764
765
  let running = (await gatewayRunning()).running
765
766
  if (gatewayAutostart()) {
766
767
  const startOut = await startGateway()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.24",
3
+ "version": "0.6.26",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",