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.
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +12 -2
- package/index.mjs +90 -12
- package/package.json +1 -1
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
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(
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/public/update.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.3",
|
|
3
3
|
"apkUrl": "dsh-remote.apk",
|
|
4
|
-
"sha256": "
|
|
5
|
-
"releasedAt": "2026-08-
|
|
6
|
-
"notes": "
|
|
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、输入框按钮对齐。"
|
package/public/version.json
CHANGED