dsh-remote-plugin 0.6.2 → 0.6.3

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.
Binary file
package/gateway.cjs CHANGED
@@ -1653,10 +1653,20 @@ function proxyApi(req, res, url) {
1653
1653
  }
1654
1654
 
1655
1655
  // ---------- 其它 ----------
1656
- function serveHealth(res) {
1656
+ async function serveHealth(res) {
1657
+ let upstreamOk = false
1658
+ try {
1659
+ const ctrl = new AbortController()
1660
+ const timer = setTimeout(() => ctrl.abort(), 2000)
1661
+ const probe = await fetch(UPSTREAM.origin + '/healthz', { signal: ctrl.signal, cache: 'no-store' })
1662
+ clearTimeout(timer)
1663
+ upstreamOk = probe.ok
1664
+ } catch {
1665
+ upstreamOk = false
1666
+ }
1657
1667
  cors(res)
1658
1668
  res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
1659
- res.end(JSON.stringify({ ok: true, service: 'dsh-remote', version: VERSION, upstream: UPSTREAM.origin }))
1669
+ res.end(JSON.stringify({ ok: true, service: 'dsh-remote', version: VERSION, pid: process.pid, upstream: UPSTREAM.origin, upstreamOk }))
1660
1670
  }
1661
1671
 
1662
1672
  function lanAddresses() {
package/index.mjs CHANGED
@@ -5,8 +5,8 @@
5
5
  * 网关不可用时回退到插件模式主机状态
6
6
  * 浏览器侧入口由 client half 注册在 DSH 原生侧边栏(见 client.js)。
7
7
  */
8
- import { spawn } from 'node:child_process'
9
- import { createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { execFileSync, spawn } from 'node:child_process'
9
+ import { appendFileSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
10
10
  import { stat } from 'node:fs/promises'
11
11
  import { homedir, hostname, networkInterfaces } from 'node:os'
12
12
  import { dirname, extname, normalize, resolve } from 'node:path'
@@ -128,9 +128,59 @@ function runExit(cmd, args) {
128
128
 
129
129
  async function gatewayRunning() {
130
130
  try {
131
- const res = await fetch(`${GATEWAY_BASE}/health`, { signal: AbortSignal.timeout(800) })
132
- return res.ok
131
+ const res = await fetch(`${GATEWAY_BASE}/health`, { signal: AbortSignal.timeout(3000) })
132
+ if (!res.ok) return { running: false }
133
+ const data = await res.json().catch(() => ({}))
134
+ return {
135
+ running: true,
136
+ pid: Number(data.pid) || 0,
137
+ upstream: typeof data.upstream === 'string' ? data.upstream : '',
138
+ upstreamOk: data.upstreamOk === true,
139
+ }
140
+ } catch {
141
+ return { running: false }
142
+ }
143
+ }
144
+
145
+ function gatewayPidFile() { return `${homedir()}/.dsh-remote/plugin-gateway.pid` }
146
+
147
+ function readGatewayPid() {
148
+ try {
149
+ const pid = Number(readFileSync(gatewayPidFile(), 'utf8').trim())
150
+ return Number.isFinite(pid) && pid > 0 ? pid : 0
133
151
  } catch {
152
+ return 0
153
+ }
154
+ }
155
+
156
+ function writeGatewayPid(pid) {
157
+ try {
158
+ mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
159
+ writeFileSync(gatewayPidFile(), String(pid) + '\n')
160
+ } catch {}
161
+ }
162
+
163
+ function logGateway(msg) {
164
+ try {
165
+ mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
166
+ appendFileSync(`${homedir()}/.dsh-remote/plugin-gateway.log`, `[${new Date().toISOString()}] ${msg}\n`)
167
+ } catch {}
168
+ }
169
+
170
+ async function killGateway(health) {
171
+ const pid = (health && Number(health.pid)) || readGatewayPid()
172
+ if (!pid) return false
173
+ try {
174
+ if (process.platform === 'win32') {
175
+ await runExit('taskkill', ['/F', '/PID', String(pid)])
176
+ } else {
177
+ process.kill(pid)
178
+ }
179
+ logGateway('已停止旧网关 PID=' + pid)
180
+ await sleep(300)
181
+ return true
182
+ } catch (e) {
183
+ logGateway('停止旧网关失败 PID=' + pid + ' ' + (e?.message || String(e)))
134
184
  return false
135
185
  }
136
186
  }
@@ -157,7 +207,9 @@ function setGatewayEnabled(on) {
157
207
 
158
208
  /** 启动随插件分发的 gateway.cjs; 已运行则直接返回。 */
159
209
  async function startGateway() {
160
- if (await gatewayRunning()) {
210
+ const upstream = `http://${dshListen.host}:${dshListen.port}`
211
+ const health = await gatewayRunning()
212
+ if (health.running) {
161
213
  setGatewayEnabled(true)
162
214
  return { ok: true, running: true, started: false }
163
215
  }
@@ -166,6 +218,7 @@ async function startGateway() {
166
218
  return { ok: false, running: false, error: '插件包缺少 gateway.cjs, 请升级插件' }
167
219
  }
168
220
  const port = process.env.DSH_REMOTE_GATEWAY_PORT || '8787'
221
+ logGateway('启动网关, 上游: ' + upstream)
169
222
 
170
223
  // 首选 systemd-run: 网关成为独立 user 单元, DSH 重启/升级不会连带杀掉它
171
224
  let sysd = false
@@ -173,10 +226,16 @@ async function startGateway() {
173
226
  await runExit('systemctl', ['--user', 'reset-failed', 'dsh-remote-gateway'])
174
227
  sysd = (await runExit('systemd-run', [
175
228
  '--user', '--unit=dsh-remote-gateway', '--service-type=exec',
176
- '--setenv=PORT=' + port, '--setenv=HOST=0.0.0.0',
229
+ '--setenv=PORT=' + port, '--setenv=HOST=0.0.0.0', '--setenv=DSH_UPSTREAM=' + upstream,
177
230
  '--', process.execPath, script,
178
231
  ])) === 0
179
232
  } catch {}
233
+ if (sysd) {
234
+ try {
235
+ const pid = Number(execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', 'dsh-remote-gateway'], { encoding: 'utf8' }).trim())
236
+ if (Number.isFinite(pid) && pid > 1) writeGatewayPid(pid)
237
+ } catch {}
238
+ }
180
239
 
181
240
  // 无 systemd 的机器回退: detached 子进程
182
241
  if (!sysd) {
@@ -188,14 +247,17 @@ async function startGateway() {
188
247
  cwd: dirname(script),
189
248
  detached: true,
190
249
  stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'],
191
- env: { ...process.env, PORT: port },
250
+ env: { ...process.env, PORT: port, DSH_UPSTREAM: upstream },
192
251
  })
193
252
  child.unref()
253
+ writeGatewayPid(child.pid)
194
254
  }
195
255
  // 最多等 4 秒; 超过可能是端口冲突或首次初始化, 前端稍后刷新即可
196
256
  for (let i = 0; i < 16; i++) {
197
257
  await sleep(250)
198
- if (await gatewayRunning()) {
258
+ const h = await gatewayRunning()
259
+ if (h.running) {
260
+ if (h.pid) writeGatewayPid(h.pid)
199
261
  setGatewayEnabled(true)
200
262
  return { ok: true, running: true, started: true }
201
263
  }
@@ -210,9 +272,24 @@ function ensureGateway() {
210
272
  if (ensurePromise) return ensurePromise
211
273
  ensurePromise = (async () => {
212
274
  try {
213
- if (await gatewayRunning()) return true
214
- const out = await startGateway()
215
- return !!out.running
275
+ const health = await gatewayRunning()
276
+ if (!health.running) {
277
+ const out = await startGateway()
278
+ return !!out.running
279
+ }
280
+ const upstream = `http://${dshListen.host}:${dshListen.port}`
281
+ const oldUpstream = health.upstream || ''
282
+ if (health.upstreamOk === false || (oldUpstream && oldUpstream !== upstream) || (!oldUpstream && upstream)) {
283
+ logGateway(`网关上游需刷新: 旧=${oldUpstream || '?'} 新=${upstream}`)
284
+ await killGateway(health)
285
+ for (let i = 0; i < 10; i++) {
286
+ if (!(await gatewayRunning()).running) break
287
+ await sleep(200)
288
+ }
289
+ const out = await startGateway()
290
+ return !!out.running
291
+ }
292
+ return true
216
293
  } finally {
217
294
  setTimeout(() => { ensurePromise = null }, 4000)
218
295
  }
@@ -417,7 +494,8 @@ async function serveStatic(req, res, ctx) {
417
494
  // 本地网关开关(仅插件内嵌页使用): GET 状态 / POST {action:'start'|'stop'}
418
495
  if (pathname === `${MOUNT}/admin/api/gateway`) {
419
496
  if (req.method === 'GET') {
420
- sendJson(res, 200, { ok: true, running: await gatewayRunning() })
497
+ const h = await gatewayRunning()
498
+ sendJson(res, 200, { ok: true, running: h.running, upstream: h.upstream || '', upstreamOk: h.upstreamOk === true })
421
499
  return
422
500
  }
423
501
  if (req.method === 'POST') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.2",
2
+ "version": "0.6.3",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "cfd99b7f3e38a55f956e9ca7867da0e0c841629b00675fcbfad1dfb9bc74c183",
5
- "releasedAt": "2026-08-18T18:55:03.782Z",
6
- "notes": "桌面端 WebUI:设置页清理与输入区饱满化;新增会话功能(goal/todo/subagent/模型切换/思考深度);预设提示词管理与空状态引导;更新弹窗改为按正式版逐版展示历史(App/桌面端);移除通知分组;聊天宽度约 680px、输入框按钮对齐。",
4
+ "sha256": "e658387db7cee1e0b5392fad3719056b463d40ce55b5aac0d313d797c07438f2",
5
+ "releasedAt": "2026-08-19T03:20:39.386Z",
6
+ "notes": "修复 DSH 重启后网关 upstream 端口不刷新(Issue #1):/health 增加 upstream/upstreamOk/pid 探测;插件 ensureGateway 检测上游变化或不可达时自动杀旧网关并按新 DSH_UPSTREAM 重启;启动/刷新写插件日志与 PID 文件。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.3",
10
+ "notes": "修复 DSH 重启后网关 upstream 端口不刷新(Issue #1):/health 增加 upstream/upstreamOk/pid 探测;插件 ensureGateway 检测上游变化或不可达时自动杀旧网关并按新 DSH_UPSTREAM 重启;启动/刷新写插件日志与 PID 文件。"
11
+ },
8
12
  {
9
13
  "version": "0.6.2",
10
14
  "notes": "桌面端 WebUI:设置页清理与输入区饱满化;新增会话功能(goal/todo/subagent/模型切换/思考深度);预设提示词管理与空状态引导;更新弹窗改为按正式版逐版展示历史(App/桌面端);移除通知分组;聊天宽度约 680px、输入框按钮对齐。"
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.2"
2
+ "version": "0.6.3"
3
3
  }