dsh-remote-plugin 0.6.3 → 0.6.5
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 +36 -3
- package/index.mjs +102 -10
- package/package.json +1 -1
- package/public/admin.html +32 -4
- package/public/admin.js +69 -1
- package/public/app.js +178 -54
- package/public/desktop/desktop.css +10 -0
- package/public/desktop/desktop.html +3 -0
- package/public/desktop/desktop.js +133 -9
- package/public/index.html +17 -3
- package/public/md.js +115 -0
- package/public/styles.css +7 -0
- package/public/update.json +12 -4
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway.cjs
CHANGED
|
@@ -43,6 +43,7 @@ const ROOT = __dirname
|
|
|
43
43
|
const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
44
44
|
const PORT = Number(process.env.PORT) || 8787
|
|
45
45
|
const HOST = process.env.HOST || '0.0.0.0'
|
|
46
|
+
const WS_IDLE_MS = Number(process.env.GATEWAY_WS_IDLE_MS) || 60000
|
|
46
47
|
const UPSTREAM = new URL(process.env.DSH_UPSTREAM || 'http://127.0.0.1:3080')
|
|
47
48
|
const TOKEN_FILE = process.env.TOKEN_FILE || path.join(os.homedir(), '.dsh-remote', 'token')
|
|
48
49
|
const NOTES_FILE = process.env.DSH_REMOTE_NOTES || path.join(os.homedir(), '.dsh-remote', 'device-notes.json')
|
|
@@ -1778,11 +1779,43 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
1778
1779
|
upSocket.setNoDelay(true)
|
|
1779
1780
|
upSocket.pipe(socket)
|
|
1780
1781
|
socket.pipe(upSocket)
|
|
1781
|
-
|
|
1782
|
+
// 双向 idle 检测: 任一侧 60s 无数据即视为死连接, 同时销毁两侧
|
|
1783
|
+
let upIdle = null
|
|
1784
|
+
let clientIdle = null
|
|
1785
|
+
const clearIdle = () => {
|
|
1786
|
+
clearTimeout(upIdle)
|
|
1787
|
+
clearTimeout(clientIdle)
|
|
1788
|
+
upIdle = null
|
|
1789
|
+
clientIdle = null
|
|
1790
|
+
}
|
|
1791
|
+
const destroyBoth = () => {
|
|
1792
|
+
clearIdle()
|
|
1793
|
+
upSocket.destroy()
|
|
1794
|
+
socket.destroy()
|
|
1795
|
+
}
|
|
1796
|
+
const close = () => {
|
|
1797
|
+
clearIdle()
|
|
1798
|
+
upSocket.destroy()
|
|
1799
|
+
socket.destroy()
|
|
1800
|
+
}
|
|
1801
|
+
const touchUp = () => {
|
|
1802
|
+
clearTimeout(upIdle)
|
|
1803
|
+
upIdle = setTimeout(destroyBoth, WS_IDLE_MS)
|
|
1804
|
+
upIdle?.unref?.()
|
|
1805
|
+
}
|
|
1806
|
+
const touchClient = () => {
|
|
1807
|
+
clearTimeout(clientIdle)
|
|
1808
|
+
clientIdle = setTimeout(destroyBoth, WS_IDLE_MS)
|
|
1809
|
+
clientIdle?.unref?.()
|
|
1810
|
+
}
|
|
1811
|
+
upSocket.on('data', touchUp)
|
|
1812
|
+
socket.on('data', touchClient)
|
|
1813
|
+
touchUp()
|
|
1814
|
+
touchClient()
|
|
1782
1815
|
upSocket.on('error', close)
|
|
1783
1816
|
socket.on('error', close)
|
|
1784
|
-
upSocket.on('close', () => socket.end())
|
|
1785
|
-
socket.on('close', () => upSocket.end())
|
|
1817
|
+
upSocket.on('close', () => { clearIdle(); if (!socket.destroyed) socket.end() })
|
|
1818
|
+
socket.on('close', () => { clearIdle(); if (!upSocket.destroyed) upSocket.end() })
|
|
1786
1819
|
})
|
|
1787
1820
|
|
|
1788
1821
|
upstreamReq.on('error', () => {
|
package/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { execFileSync, spawn } from 'node:child_process'
|
|
9
9
|
import { appendFileSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import { stat } from 'node:fs/promises'
|
|
11
|
+
import net from 'node:net'
|
|
11
12
|
import { homedir, hostname, networkInterfaces } from 'node:os'
|
|
12
13
|
import { dirname, extname, normalize, resolve } from 'node:path'
|
|
13
14
|
import { fileURLToPath } from 'node:url'
|
|
@@ -20,8 +21,24 @@ const PUBLIC_DIR = fileURLToPath(new URL('./public/', import.meta.url))
|
|
|
20
21
|
const INDEX_FILE = 'index.html'
|
|
21
22
|
const GATEWAY_SCRIPT = fileURLToPath(new URL('./gateway.cjs', import.meta.url))
|
|
22
23
|
const gatewayInstalled = existsSync(GATEWAY_SCRIPT)
|
|
23
|
-
// 本地网关管理 API 代理:
|
|
24
|
-
|
|
24
|
+
// 本地网关管理 API 代理: 让插件抽屉显示与网关管理页完全一致的数据。
|
|
25
|
+
// 端口读取优先级: DSH_REMOTE_GATEWAY_PORT > ~/.dsh-remote/gateway-port > 8787
|
|
26
|
+
function gatewayPortFile() { return `${homedir()}/.dsh-remote/gateway-port` }
|
|
27
|
+
|
|
28
|
+
function readGatewayPort() {
|
|
29
|
+
const valid = (v) => /^\d+$/.test(String(v)) && Number(v) >= 1 && Number(v) <= 65535
|
|
30
|
+
const envPort = process.env.DSH_REMOTE_GATEWAY_PORT
|
|
31
|
+
if (valid(envPort)) return String(Number(envPort))
|
|
32
|
+
try {
|
|
33
|
+
const filePort = readFileSync(gatewayPortFile(), 'utf8').trim()
|
|
34
|
+
if (valid(filePort)) return String(Number(filePort))
|
|
35
|
+
} catch {}
|
|
36
|
+
return '8787'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function gatewayBase() {
|
|
40
|
+
return (process.env.DSH_REMOTE_GATEWAY || `http://127.0.0.1:${readGatewayPort()}`).replace(/\/+$/, '')
|
|
41
|
+
}
|
|
25
42
|
|
|
26
43
|
function gatewayToken() {
|
|
27
44
|
if (process.env.DSH_REMOTE_TOKEN) return process.env.DSH_REMOTE_TOKEN
|
|
@@ -95,7 +112,7 @@ async function proxyGateway(path, method, body) {
|
|
|
95
112
|
const token = gatewayToken()
|
|
96
113
|
if (!token) return null
|
|
97
114
|
try {
|
|
98
|
-
const res = await fetch(`${
|
|
115
|
+
const res = await fetch(`${gatewayBase()}${path}`, {
|
|
99
116
|
method,
|
|
100
117
|
headers: {
|
|
101
118
|
authorization: `Bearer ${token}`,
|
|
@@ -126,9 +143,26 @@ function runExit(cmd, args) {
|
|
|
126
143
|
})
|
|
127
144
|
}
|
|
128
145
|
|
|
146
|
+
/** 127.0.0.1 端口占用预检: 能连上=被占用, 连接被拒/超时=可用。 */
|
|
147
|
+
function portInUse(port) {
|
|
148
|
+
return new Promise((resolvePromise) => {
|
|
149
|
+
const sock = net.connect({ host: '127.0.0.1', port: Number(port) })
|
|
150
|
+
let done = false
|
|
151
|
+
const finish = (used) => {
|
|
152
|
+
if (done) return
|
|
153
|
+
done = true
|
|
154
|
+
sock.destroy()
|
|
155
|
+
resolvePromise(used)
|
|
156
|
+
}
|
|
157
|
+
sock.once('connect', () => finish(true))
|
|
158
|
+
sock.once('error', () => finish(false))
|
|
159
|
+
sock.setTimeout(800, () => finish(false))
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
async function gatewayRunning() {
|
|
130
164
|
try {
|
|
131
|
-
const res = await fetch(`${
|
|
165
|
+
const res = await fetch(`${gatewayBase()}/health`, { signal: AbortSignal.timeout(3000) })
|
|
132
166
|
if (!res.ok) return { running: false }
|
|
133
167
|
const data = await res.json().catch(() => ({}))
|
|
134
168
|
return {
|
|
@@ -217,8 +251,12 @@ async function startGateway() {
|
|
|
217
251
|
if (!existsSync(script)) {
|
|
218
252
|
return { ok: false, running: false, error: '插件包缺少 gateway.cjs, 请升级插件' }
|
|
219
253
|
}
|
|
220
|
-
const port =
|
|
221
|
-
|
|
254
|
+
const port = readGatewayPort()
|
|
255
|
+
if (await portInUse(port)) {
|
|
256
|
+
logGateway(`端口 ${port} 已被占用, 拒绝启动`)
|
|
257
|
+
return { ok: false, running: false, error: `端口 ${port} 已被占用,请在插件页修改网关端口后重试` }
|
|
258
|
+
}
|
|
259
|
+
logGateway('启动网关, 端口: ' + port + ', 上游: ' + upstream)
|
|
222
260
|
|
|
223
261
|
// 首选 systemd-run: 网关成为独立 user 单元, DSH 重启/升级不会连带杀掉它
|
|
224
262
|
let sysd = false
|
|
@@ -302,7 +340,7 @@ async function stopGateway() {
|
|
|
302
340
|
const token = gatewayToken()
|
|
303
341
|
if (!token) return { ok: false, running: false, error: '找不到 ~/.dsh-remote/token, 无法认证网关' }
|
|
304
342
|
try {
|
|
305
|
-
const res = await fetch(`${
|
|
343
|
+
const res = await fetch(`${gatewayBase()}/admin/api/shutdown`, {
|
|
306
344
|
method: 'POST',
|
|
307
345
|
headers: { authorization: `Bearer ${token}`, 'x-dsh-remote-client': 'admin' },
|
|
308
346
|
signal: AbortSignal.timeout(2000),
|
|
@@ -348,7 +386,7 @@ function statsSend(session, event) {
|
|
|
348
386
|
const prev = statsQueues.get(session.id) || Promise.resolve()
|
|
349
387
|
const next = prev.then(async () => {
|
|
350
388
|
try {
|
|
351
|
-
await fetch(`${
|
|
389
|
+
await fetch(`${gatewayBase()}/stats/ingest`, {
|
|
352
390
|
method: 'POST',
|
|
353
391
|
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
354
392
|
body: JSON.stringify(payload),
|
|
@@ -434,7 +472,7 @@ async function serveStatic(req, res, ctx) {
|
|
|
434
472
|
if (proxied !== null) {
|
|
435
473
|
sendJson(res, proxied.status, proxied.json)
|
|
436
474
|
} else {
|
|
437
|
-
sendJson(res, 502, { ok: false, error: '本地网关不可用, Token
|
|
475
|
+
sendJson(res, 502, { ok: false, error: '本地网关不可用, Token 统计需要网关运行' })
|
|
438
476
|
}
|
|
439
477
|
return
|
|
440
478
|
}
|
|
@@ -486,11 +524,65 @@ async function serveStatic(req, res, ctx) {
|
|
|
486
524
|
if (proxied !== null) {
|
|
487
525
|
sendJson(res, proxied.status, proxied.json)
|
|
488
526
|
} else {
|
|
489
|
-
sendJson(res, 502, { ok: false, error: '本地网关不可用,
|
|
527
|
+
sendJson(res, 502, { ok: false, error: '本地网关不可用, 设备管理需在网关模式操作' })
|
|
490
528
|
}
|
|
491
529
|
return
|
|
492
530
|
}
|
|
493
531
|
|
|
532
|
+
// 网关端口配置(仅插件内嵌页使用): GET 当前生效端口 / PUT 修改端口
|
|
533
|
+
if (pathname === `${MOUNT}/admin/api/config`) {
|
|
534
|
+
if (req.method === 'GET') {
|
|
535
|
+
const h = await gatewayRunning()
|
|
536
|
+
sendJson(res, 200, {
|
|
537
|
+
ok: true,
|
|
538
|
+
port: Number(readGatewayPort()),
|
|
539
|
+
running: h.running,
|
|
540
|
+
source: process.env.DSH_REMOTE_GATEWAY_PORT ? 'env' : existsSync(gatewayPortFile()) ? 'file' : 'default',
|
|
541
|
+
})
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
if (req.method === 'PUT') {
|
|
545
|
+
let body = {}
|
|
546
|
+
try {
|
|
547
|
+
body = JSON.parse((await readBody(req, 4096)) || '{}')
|
|
548
|
+
} catch {}
|
|
549
|
+
const port = Number(body.port)
|
|
550
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
551
|
+
sendJson(res, 400, { ok: false, error: '端口必须是 1-65535 的整数' })
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
const oldPort = Number(readGatewayPort())
|
|
555
|
+
try {
|
|
556
|
+
mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
|
|
557
|
+
writeFileSync(gatewayPortFile(), String(port) + '\n')
|
|
558
|
+
} catch (e) {
|
|
559
|
+
sendJson(res, 500, { ok: false, error: '写入端口配置失败: ' + (e?.message || String(e)) })
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
const effectivePort = Number(readGatewayPort())
|
|
563
|
+
if (effectivePort !== oldPort) {
|
|
564
|
+
const h = await gatewayRunning()
|
|
565
|
+
if (h.running) {
|
|
566
|
+
await killGateway(h)
|
|
567
|
+
for (let i = 0; i < 10; i++) {
|
|
568
|
+
if (!(await gatewayRunning()).running) break
|
|
569
|
+
await sleep(200)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
let running = (await gatewayRunning()).running
|
|
574
|
+
if (gatewayAutostart()) {
|
|
575
|
+
const startOut = await startGateway()
|
|
576
|
+
running = !!startOut.running || (await gatewayRunning()).running
|
|
577
|
+
}
|
|
578
|
+
sendJson(res, 200, { ok: true, port: Number(port), effectivePort, running })
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
res.writeHead(405, { allow: 'GET, PUT' })
|
|
582
|
+
res.end()
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
494
586
|
// 本地网关开关(仅插件内嵌页使用): GET 状态 / POST {action:'start'|'stop'}
|
|
495
587
|
if (pathname === `${MOUNT}/admin/api/gateway`) {
|
|
496
588
|
if (req.method === 'GET') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/public/admin.html
CHANGED
|
@@ -79,6 +79,11 @@
|
|
|
79
79
|
.token-row code { flex: none; width: 100%; background: var(--dsr-bg); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 8px 10px; font-size: 12px; white-space: nowrap; overflow-x: auto; }
|
|
80
80
|
.token-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
81
81
|
.token-actions .mini-btn { flex: 1 1 auto; text-align: center; padding: 6px 8px; font-size: 12.5px; }
|
|
82
|
+
.gateway-port-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 14px; padding: 10px 12px; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); }
|
|
83
|
+
.gateway-port-row label { font-size: 12px; color: var(--dsr-muted); flex: none; }
|
|
84
|
+
.gateway-port-row input { width: 92px; background: var(--dsr-bg); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 6px 8px; font: inherit; font-size: 13px; outline: none; }
|
|
85
|
+
.gateway-port-row .muted { font-size: 12px; }
|
|
86
|
+
.gateway-port-row .mini-btn { margin-left: auto; }
|
|
82
87
|
.conn-badge { white-space: nowrap; }
|
|
83
88
|
#btn-close-drawer { white-space: nowrap; }
|
|
84
89
|
@media (max-width: 720px) {
|
|
@@ -229,6 +234,13 @@
|
|
|
229
234
|
</div>
|
|
230
235
|
</div>
|
|
231
236
|
|
|
237
|
+
<div id="gateway-port-row" class="gateway-port-row hidden">
|
|
238
|
+
<label data-i18n="gatewayPort">网关端口</label>
|
|
239
|
+
<input id="gateway-port-input" type="number" min="1" max="65535" inputmode="numeric" placeholder="8787">
|
|
240
|
+
<span id="gateway-port-current" class="muted">—</span>
|
|
241
|
+
<button id="btn-save-port" class="mini-btn" data-i18n="savePort">保存</button>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
232
244
|
<div id="pair-box" class="pair-box hidden">
|
|
233
245
|
<div class="pair-title" data-i18n="pairTitle">手机 App 扫码配对</div>
|
|
234
246
|
<div class="pair-qr" id="pair-qr"></div>
|
|
@@ -307,6 +319,14 @@
|
|
|
307
319
|
'stopGateway': '停止网关',
|
|
308
320
|
'starting': '启动中…',
|
|
309
321
|
'stopping': '停止中…',
|
|
322
|
+
'gatewayPort': '网关端口',
|
|
323
|
+
'gatewayPort.current': '当前 {port}',
|
|
324
|
+
'savePort': '保存',
|
|
325
|
+
'toast.portInvalid': '端口必须是 1-65535 的整数',
|
|
326
|
+
'toast.portSaved': '端口已更新,网关已切换至 {port}',
|
|
327
|
+
'toast.portSavedIdle': '端口已保存,网关未运行,下次启动生效',
|
|
328
|
+
'toast.portEnv': '环境变量优先,当前仍使用 {port}',
|
|
329
|
+
'toast.portFailedMsg': '端口保存失败:{msg}',
|
|
310
330
|
'qrCode': '二维码',
|
|
311
331
|
'rotateToken': '轮换令牌',
|
|
312
332
|
'copyToken': '复制令牌',
|
|
@@ -324,7 +344,7 @@
|
|
|
324
344
|
'stat.updateAvailable': '{version} 可用', 'stat.currentV': '当前 v{version}',
|
|
325
345
|
'stat.download': '去下载', 'stat.embedded': 'DSH 内嵌 · 免网关',
|
|
326
346
|
'stat.updateCheck': '更新检查: {error}', 'stat.latest': '已是最新(来源检查)', 'stat.notChecked': '未检查更新',
|
|
327
|
-
'stat.hostIP': '主机 IP · {hostname}', 'stat.ipSep': '、', 'stat.phoneGateway': ' (手机连
|
|
347
|
+
'stat.hostIP': '主机 IP · {hostname}', 'stat.ipSep': '、', 'stat.phoneGateway': ' (手机连 {port} 网关)', 'stat.phoneThis': ' (手机连这个地址)',
|
|
328
348
|
'stat.reachable': '可达', 'stat.unreachable': '不可达', 'stat.dshUpstream': 'DSH 上游 {url}',
|
|
329
349
|
'stat.devicesOnline': '设备在线 / 累计', 'stat.totalRequests': '总请求数', 'stat.authFailures': '认证失败',
|
|
330
350
|
'stat.uptime': '运行时长 · {host}:{port}',
|
|
@@ -338,7 +358,7 @@
|
|
|
338
358
|
'stats.output': '输出',
|
|
339
359
|
'stats.peak': '高峰', 'stats.off': '空闲',
|
|
340
360
|
'stats.days': '近 {n} 天',
|
|
341
|
-
'stats.gatewayDown': '
|
|
361
|
+
'stats.gatewayDown': '统计需要网关运行', 'stats.empty': '暂无统计,产生会话后自动聚合',
|
|
342
362
|
'stats.note': '注:本数据仅在使用 DeepSeek 官方 API 时估算;基于 token 计算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
343
363
|
'unit.sec': ' 秒', 'unit.min': ' 分钟', 'unit.hour': ' 小时 ', 'unit.minShort': ' 分', 'unit.day': ' 天 ',
|
|
344
364
|
'device.installedNotRunning': '网关已安装 · 当前未运行', 'device.noGatewayBinary': '未检测到网关程序',
|
|
@@ -382,6 +402,14 @@
|
|
|
382
402
|
'stopGateway': 'Stop gateway',
|
|
383
403
|
'starting': 'Starting…',
|
|
384
404
|
'stopping': 'Stopping…',
|
|
405
|
+
'gatewayPort': 'Gateway port',
|
|
406
|
+
'gatewayPort.current': 'Current {port}',
|
|
407
|
+
'savePort': 'Save',
|
|
408
|
+
'toast.portInvalid': 'Port must be an integer from 1 to 65535',
|
|
409
|
+
'toast.portSaved': 'Port updated, gateway switched to {port}',
|
|
410
|
+
'toast.portSavedIdle': 'Port saved; gateway not running, takes effect on next start',
|
|
411
|
+
'toast.portEnv': 'Environment variable takes priority, current port remains {port}',
|
|
412
|
+
'toast.portFailedMsg': 'Failed to save port: {msg}',
|
|
385
413
|
'qrCode': 'QR code',
|
|
386
414
|
'rotateToken': 'Rotate token',
|
|
387
415
|
'copyToken': 'Copy token',
|
|
@@ -399,7 +427,7 @@
|
|
|
399
427
|
'stat.updateAvailable': 'v{version} available', 'stat.currentV': 'Current v{version}',
|
|
400
428
|
'stat.download': 'Download', 'stat.embedded': 'Embedded in DSH · no gateway',
|
|
401
429
|
'stat.updateCheck': 'Update check: {error}', 'stat.latest': 'Up to date (source check)', 'stat.notChecked': 'Not checked',
|
|
402
|
-
'stat.hostIP': 'Host IP · {hostname}', 'stat.ipSep': ', ', 'stat.phoneGateway': ' (phone connects to gateway
|
|
430
|
+
'stat.hostIP': 'Host IP · {hostname}', 'stat.ipSep': ', ', 'stat.phoneGateway': ' (phone connects to gateway {port})', 'stat.phoneThis': ' (phone connects to this address)',
|
|
403
431
|
'stat.reachable': 'Reachable', 'stat.unreachable': 'Unreachable', 'stat.dshUpstream': 'DSH upstream {url}',
|
|
404
432
|
'stat.devicesOnline': 'Devices online / total', 'stat.totalRequests': 'Total requests', 'stat.authFailures': 'Auth failures',
|
|
405
433
|
'stat.uptime': 'Uptime · {host}:{port}',
|
|
@@ -413,7 +441,7 @@
|
|
|
413
441
|
'stats.output': 'Output',
|
|
414
442
|
'stats.peak': 'Peak', 'stats.off': 'Off-peak',
|
|
415
443
|
'stats.days': 'Last {n} days',
|
|
416
|
-
'stats.gatewayDown': 'Stats require the gateway
|
|
444
|
+
'stats.gatewayDown': 'Stats require the gateway to be running', 'stats.empty': 'No stats yet — they aggregate as sessions happen',
|
|
417
445
|
'stats.note': 'Note: estimates assume the official DeepSeek API. Token-based calculation may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
|
|
418
446
|
'unit.sec': 's', 'unit.min': 'min', 'unit.hour': 'h ', 'unit.minShort': 'm', 'unit.day': 'd ',
|
|
419
447
|
'device.installedNotRunning': 'Gateway installed · not running', 'device.noGatewayBinary': 'Gateway binary not found',
|
package/public/admin.js
CHANGED
|
@@ -24,6 +24,8 @@ let gatewayBusy = false
|
|
|
24
24
|
let shownToken = token
|
|
25
25
|
let lastState = null
|
|
26
26
|
let qrShown = false
|
|
27
|
+
let gatewayPort = 8787
|
|
28
|
+
let gatewayPortLoaded = false
|
|
27
29
|
|
|
28
30
|
const STATS_API = pluginMode ? API + '/stats' : '/stats'
|
|
29
31
|
let statsTimer = null
|
|
@@ -64,6 +66,26 @@ async function loadStats() {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
async function loadGatewayConfig() {
|
|
70
|
+
if (!pluginMode) return
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`${API}/config`, {
|
|
73
|
+
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'admin' }
|
|
74
|
+
})
|
|
75
|
+
const out = await res.json().catch(() => ({}))
|
|
76
|
+
if (out.ok) {
|
|
77
|
+
gatewayPort = Number(out.port) || 8787
|
|
78
|
+
gatewayPortLoaded = true
|
|
79
|
+
const row = $('gateway-port-row')
|
|
80
|
+
const input = $('gateway-port-input')
|
|
81
|
+
if (row) row.classList.toggle('hidden', !pluginMode)
|
|
82
|
+
if (input && document.activeElement !== input) input.value = gatewayPort
|
|
83
|
+
const cur = $('gateway-port-current')
|
|
84
|
+
if (cur) cur.textContent = t('gatewayPort.current', { port: gatewayPort })
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function renderStats(days) {
|
|
68
90
|
if (!days.length) {
|
|
69
91
|
$('stats-cards').innerHTML = ''
|
|
@@ -191,6 +213,12 @@ function render(st) {
|
|
|
191
213
|
? t(gatewayRunning ? 'stopping' : 'starting')
|
|
192
214
|
: t(gatewayRunning ? 'stopGateway' : 'startGateway')
|
|
193
215
|
$('btn-gateway').disabled = gatewayBusy
|
|
216
|
+
// 网关端口配置: 仅插件内嵌页提供
|
|
217
|
+
$('gateway-port-row').classList.toggle('hidden', !pluginMode || !gatewayPortLoaded)
|
|
218
|
+
if (pluginMode) {
|
|
219
|
+
const cur = $('gateway-port-current')
|
|
220
|
+
if (cur) cur.textContent = t('gatewayPort.current', { port: gatewayPort })
|
|
221
|
+
}
|
|
194
222
|
const upOk = st.upstream.reachable
|
|
195
223
|
const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
|
|
196
224
|
const latestHtml = st.latest?.newer
|
|
@@ -199,7 +227,7 @@ function render(st) {
|
|
|
199
227
|
$('stats').innerHTML = `
|
|
200
228
|
<div class="stat-card"><div class="v">v${st.version}</div><div class="k">${t(isPlugin ? 'stat.pluginVersion' : 'stat.gatewayVersion')}</div></div>
|
|
201
229
|
<div class="stat-card ${st.latest?.newer ? 'warn' : 'ok'}">${latestHtml}</div>
|
|
202
|
-
<div class="stat-card ok"><div class="v" style="font-size:13px">${hostIPs}</div><div class="k">${t('stat.hostIP', { hostname: st.hostname })}${isPlugin ? t('stat.phoneGateway') : t('stat.phoneThis')}</div></div>
|
|
230
|
+
<div class="stat-card ok"><div class="v" style="font-size:13px">${hostIPs}</div><div class="k">${t('stat.hostIP', { hostname: st.hostname })}${isPlugin ? t('stat.phoneGateway', { port: gatewayPort }) : t('stat.phoneThis')}</div></div>
|
|
203
231
|
<div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${t(upOk ? 'stat.reachable' : 'stat.unreachable')}</div><div class="k">${t('stat.dshUpstream', { url: st.upstream.url })}</div></div>
|
|
204
232
|
<div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">${t('stat.devicesOnline')}</div></div>
|
|
205
233
|
<div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">${t('stat.totalRequests')}</div></div>
|
|
@@ -434,6 +462,45 @@ $('btn-gateway').addEventListener('click', async () => {
|
|
|
434
462
|
setTimeout(loadState, 700)
|
|
435
463
|
})
|
|
436
464
|
|
|
465
|
+
$('btn-save-port').addEventListener('click', async () => {
|
|
466
|
+
const input = $('gateway-port-input')
|
|
467
|
+
const raw = input.value.trim()
|
|
468
|
+
const port = Number(raw)
|
|
469
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
470
|
+
toast(t('toast.portInvalid'), 'err')
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
const btn = $('btn-save-port')
|
|
474
|
+
const wasRunning = gatewayRunning
|
|
475
|
+
btn.disabled = true
|
|
476
|
+
try {
|
|
477
|
+
const res = await fetch(`${API}/config`, {
|
|
478
|
+
method: 'PUT',
|
|
479
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'admin' },
|
|
480
|
+
body: JSON.stringify({ port })
|
|
481
|
+
})
|
|
482
|
+
const out = await res.json().catch(() => ({}))
|
|
483
|
+
if (out.ok) {
|
|
484
|
+
const saved = Number(out.port) || port
|
|
485
|
+
const effective = Number(out.effectivePort || out.port) || saved
|
|
486
|
+
if (out.effectivePort && effective !== saved) {
|
|
487
|
+
toast(t('toast.portEnv', { port: effective }), 'ok')
|
|
488
|
+
} else if (wasRunning) {
|
|
489
|
+
toast(t('toast.portSaved', { port: effective }), 'ok')
|
|
490
|
+
} else {
|
|
491
|
+
toast(t('toast.portSavedIdle', { port: effective }), 'ok')
|
|
492
|
+
}
|
|
493
|
+
loadGatewayConfig()
|
|
494
|
+
setTimeout(loadState, 800)
|
|
495
|
+
} else {
|
|
496
|
+
toast(out.error || t('toast.portFailedMsg', { msg: res.status }), 'err')
|
|
497
|
+
}
|
|
498
|
+
} catch (e) {
|
|
499
|
+
toast(t('toast.portFailedMsg', { msg: e.message || e }), 'err')
|
|
500
|
+
}
|
|
501
|
+
btn.disabled = false
|
|
502
|
+
})
|
|
503
|
+
|
|
437
504
|
function renderLangBtn() {
|
|
438
505
|
const btn = $('btn-lang')
|
|
439
506
|
if (btn) btn.textContent = I18N.lang === 'zh' ? 'EN' : '中文'
|
|
@@ -521,6 +588,7 @@ function start(showLogin) {
|
|
|
521
588
|
}
|
|
522
589
|
showMain()
|
|
523
590
|
loadState()
|
|
591
|
+
loadGatewayConfig()
|
|
524
592
|
loadStats()
|
|
525
593
|
timer = setInterval(loadState, 5000)
|
|
526
594
|
if (statsTimer) clearInterval(statsTimer)
|