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/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +106 -84
- package/gateway.cjs +151 -44
- package/index.mjs +41 -40
- package/package.json +1 -1
- package/public/app.js +153 -244
- package/public/desktop/desktop.html +2 -0
- package/public/desktop/desktop.js +50 -2
- package/public/genui.css +8 -0
- package/public/genui.js +254 -0
- package/public/index.html +11 -26
- package/public/md.js +16 -3
- package/public/styles.css +4 -11
- package/public/update.json +12 -12
- package/public/version.json +1 -1
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
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('
|
|
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 =
|
|
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 =
|
|
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
|
|
443
|
-
if (!
|
|
444
|
-
|
|
445
|
-
|
|
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.
|
|
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",
|