dsh-remote-plugin 0.6.6 → 0.6.8
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/README.en.md +31 -25
- package/README.md +30 -24
- package/apk/dsh-remote.apk +0 -0
- package/client.js +11 -9
- package/gateway.cjs +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/admin.html +103 -7
- package/public/admin.js +24 -0
- package/public/announcements.json +12 -0
- package/public/app.js +850 -57
- package/public/desktop/desktop.css +170 -20
- package/public/desktop/desktop.html +102 -13
- package/public/desktop/desktop.js +422 -26
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +200 -40
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +220 -15
- package/public/theme-vars.css +9 -0
- package/public/update.json +12 -4
- package/public/version.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>DSH Remote</title>
|
|
7
|
+
<script>/* 插件抽屉首帧沿用当前主题 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
|
|
8
|
+
<link rel="stylesheet" href="theme-vars.css">
|
|
9
|
+
<style>
|
|
10
|
+
:root { color-scheme: dark; }
|
|
11
|
+
html[data-theme="light"], html[data-theme="neutral"] { color-scheme: light; }
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
body {
|
|
14
|
+
margin: 0; min-width: 280px; color: var(--dsr-text); background:
|
|
15
|
+
radial-gradient(420px 240px at 100% 0, var(--dsr-accent-soft), transparent 72%),
|
|
16
|
+
var(--dsr-bg); font: 13px/1.45 system-ui, -apple-system, "PingFang SC", "Noto Sans CJK SC", sans-serif;
|
|
17
|
+
}
|
|
18
|
+
button, a { font: inherit; }
|
|
19
|
+
button { cursor: pointer; }
|
|
20
|
+
.plugin-shell { min-height: 100vh; padding: 18px 16px 16px; display: flex; flex-direction: column; gap: 14px; }
|
|
21
|
+
.plugin-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
22
|
+
.plugin-brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
|
23
|
+
.plugin-brand-mark { width: 11px; height: 11px; flex: none; border-radius: 50%; background: var(--dsr-accent); box-shadow: 0 0 0 5px var(--dsr-accent-soft), 0 0 18px var(--dsr-glow); }
|
|
24
|
+
.plugin-brand-copy { min-width: 0; display: flex; flex-direction: column; }
|
|
25
|
+
.plugin-brand-name { font-size: 15px; font-weight: 750; letter-spacing: -.1px; }
|
|
26
|
+
.plugin-brand-sub { color: var(--dsr-muted); font-size: 10px; }
|
|
27
|
+
.plugin-close { width: 32px; height: 32px; display: grid; place-items: center; flex: none; border: 1px solid var(--dsr-line); border-radius: 10px; background: var(--dsr-panel); color: var(--dsr-muted); }
|
|
28
|
+
.plugin-close:hover, .plugin-close:focus-visible { color: var(--dsr-text); border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
|
|
29
|
+
.plugin-close svg { width: 16px; height: 16px; }
|
|
30
|
+
.plugin-hero { position: relative; overflow: hidden; padding: 18px; border: 1px solid var(--dsr-line); border-radius: 20px; background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); box-shadow: 0 16px 32px var(--dsr-shadow); }
|
|
31
|
+
.plugin-hero::after { content: ""; position: absolute; width: 150px; height: 150px; right: -76px; top: -76px; border-radius: 50%; background: var(--dsr-accent-soft); pointer-events: none; }
|
|
32
|
+
.plugin-eyebrow { color: var(--dsr-accent-strong); font-size: 10px; font-weight: 800; letter-spacing: 1.4px; }
|
|
33
|
+
.plugin-status { position: relative; z-index: 1; margin: 7px 0 4px; font-size: 24px; line-height: 1.15; font-weight: 780; letter-spacing: -.5px; }
|
|
34
|
+
.plugin-status-desc { position: relative; z-index: 1; min-height: 20px; color: var(--dsr-muted); font-size: 12px; }
|
|
35
|
+
.plugin-status-line { position: relative; z-index: 1; display: flex; align-items: center; gap: 7px; margin-top: 13px; color: var(--dsr-muted); font-size: 11px; }
|
|
36
|
+
.plugin-status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsr-muted); box-shadow: 0 0 0 4px var(--dsr-line); }
|
|
37
|
+
.plugin-hero.ok .plugin-status-dot { background: var(--dsr-success); box-shadow: 0 0 0 4px var(--dsr-success-soft); }
|
|
38
|
+
.plugin-hero.warn .plugin-status-dot { background: var(--dsr-warning); box-shadow: 0 0 0 4px var(--dsr-warning-soft); }
|
|
39
|
+
.plugin-hero.off .plugin-status-dot { background: var(--dsr-error); box-shadow: 0 0 0 4px var(--dsr-error-soft); }
|
|
40
|
+
.plugin-primary { position: relative; z-index: 1; width: 100%; min-height: 42px; margin-top: 16px; border: 1px solid var(--dsr-accent-strong); border-radius: 12px; background: var(--dsr-accent-strong); color: var(--dsr-on-accent); font-weight: 750; transition: transform .18s ease, filter .18s ease; }
|
|
41
|
+
.plugin-primary:hover, .plugin-primary:focus-visible { filter: brightness(1.08); transform: translateY(-1px); }
|
|
42
|
+
.plugin-primary:disabled { cursor: wait; opacity: .65; transform: none; }
|
|
43
|
+
.plugin-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
44
|
+
.plugin-action { min-height: 42px; padding: 7px 5px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); color: var(--dsr-text); font-size: 11px; }
|
|
45
|
+
.plugin-action:hover, .plugin-action:focus-visible { border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
|
|
46
|
+
.plugin-action svg { display: block; width: 17px; height: 17px; margin: 0 auto 3px; color: var(--dsr-accent-strong); }
|
|
47
|
+
.plugin-usage { padding: 14px; border: 1px solid var(--dsr-line); border-radius: 16px; background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); }
|
|
48
|
+
.plugin-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--dsr-text); font-size: 12px; font-weight: 750; }
|
|
49
|
+
.plugin-section-meta { color: var(--dsr-muted); font-size: 10px; font-weight: 500; }
|
|
50
|
+
.plugin-usage-grid { display: grid; grid-template-columns: minmax(0, 1fr) 82px; gap: 12px; align-items: center; margin-top: 11px; }
|
|
51
|
+
.plugin-usage-total { display: flex; align-items: baseline; gap: 6px; }
|
|
52
|
+
.plugin-usage-total strong { font-size: 25px; line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -.6px; }
|
|
53
|
+
.plugin-usage-total span { color: var(--dsr-muted); font-size: 11px; }
|
|
54
|
+
.plugin-usage-track { display: flex; height: 8px; gap: 2px; margin-top: 13px; overflow: hidden; border-radius: 999px; background: var(--dsr-line); }
|
|
55
|
+
.plugin-usage-track i { display: block; min-width: 2px; height: 100%; }
|
|
56
|
+
.plugin-usage-track .input { background: var(--dsr-accent); }
|
|
57
|
+
.plugin-usage-track .cache { background: var(--dsr-info); }
|
|
58
|
+
.plugin-usage-track .output { background: var(--dsr-accent-2); }
|
|
59
|
+
.plugin-usage-legend { display: flex; flex-wrap: wrap; gap: 4px 9px; margin-top: 7px; color: var(--dsr-muted); font-size: 9px; }
|
|
60
|
+
.plugin-usage-legend span { display: inline-flex; align-items: center; gap: 4px; }
|
|
61
|
+
.plugin-usage-legend i { width: 6px; height: 6px; border-radius: 50%; background: var(--dsr-line); }
|
|
62
|
+
.plugin-usage-legend .input i { background: var(--dsr-accent); }
|
|
63
|
+
.plugin-usage-legend .cache i { background: var(--dsr-info); }
|
|
64
|
+
.plugin-usage-legend .output i { background: var(--dsr-accent-2); }
|
|
65
|
+
.plugin-usage-ring { --plugin-peak: 0%; width: 76px; height: 76px; padding: 6px; display: grid; place-items: center; border-radius: 50%; background: conic-gradient(var(--dsr-accent-strong) 0 var(--plugin-peak), var(--dsr-line) var(--plugin-peak) 100%); box-shadow: 0 0 18px var(--dsr-glow); }
|
|
66
|
+
.plugin-usage-ring > div { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 50%; background: var(--dsr-bg-2); }
|
|
67
|
+
.plugin-usage-ring strong { font-size: 15px; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
68
|
+
.plugin-usage-ring span { margin-top: 4px; color: var(--dsr-muted); font-size: 9px; }
|
|
69
|
+
.plugin-usage-cost { display: flex; align-items: baseline; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--dsr-divider); }
|
|
70
|
+
.plugin-usage-cost span { color: var(--dsr-muted); font-size: 10px; }
|
|
71
|
+
.plugin-usage-cost strong { color: var(--dsr-accent-strong); font-size: 17px; font-variant-numeric: tabular-nums; }
|
|
72
|
+
.plugin-usage-cost small { margin-left: auto; color: var(--dsr-muted); font-size: 9px; }
|
|
73
|
+
.plugin-usage.empty { color: var(--dsr-muted); }
|
|
74
|
+
.plugin-usage-empty { padding: 12px 0 2px; font-size: 11px; }
|
|
75
|
+
.plugin-details { border-top: 1px solid var(--dsr-divider); padding-top: 12px; }
|
|
76
|
+
.plugin-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
|
|
77
|
+
.plugin-detail { min-width: 0; padding: 9px 10px; border: 1px solid var(--dsr-line); border-radius: 11px; background: var(--dsr-panel); }
|
|
78
|
+
.plugin-detail-label { color: var(--dsr-muted); font-size: 10px; }
|
|
79
|
+
.plugin-detail-value { margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 650; }
|
|
80
|
+
.plugin-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; color: var(--dsr-muted); font-size: 10px; }
|
|
81
|
+
.plugin-foot a { color: var(--dsr-accent-strong); text-decoration: none; }
|
|
82
|
+
.plugin-foot a:hover, .plugin-foot a:focus-visible { text-decoration: underline; }
|
|
83
|
+
.plugin-error { padding: 10px 12px; border: 1px solid var(--dsr-error-line); border-radius: 11px; background: var(--dsr-error-soft); color: var(--dsr-error); font-size: 11px; }
|
|
84
|
+
.hidden { display: none !important; }
|
|
85
|
+
:focus-visible { outline: 2px solid var(--dsr-accent); outline-offset: 2px; }
|
|
86
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<main class="plugin-shell">
|
|
91
|
+
<header class="plugin-head">
|
|
92
|
+
<div class="plugin-brand">
|
|
93
|
+
<span class="plugin-brand-mark" aria-hidden="true"></span>
|
|
94
|
+
<span class="plugin-brand-copy"><span class="plugin-brand-name">DSH Remote</span><span class="plugin-brand-sub">本地控制器</span></span>
|
|
95
|
+
</div>
|
|
96
|
+
<button id="plugin-close" class="plugin-close" type="button" aria-label="关闭面板" title="关闭面板">
|
|
97
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg>
|
|
98
|
+
</button>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
<section id="plugin-hero" class="plugin-hero" aria-live="polite">
|
|
102
|
+
<div class="plugin-eyebrow">REMOTE CONTROL</div>
|
|
103
|
+
<div id="plugin-status" class="plugin-status">检查中…</div>
|
|
104
|
+
<div id="plugin-status-desc" class="plugin-status-desc">正在读取本地服务状态</div>
|
|
105
|
+
<div class="plugin-status-line"><span class="plugin-status-dot" aria-hidden="true"></span><span id="plugin-status-meta">—</span></div>
|
|
106
|
+
<button id="plugin-primary" class="plugin-primary" type="button">打开控制台</button>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<div id="plugin-error" class="plugin-error hidden" role="alert"></div>
|
|
110
|
+
|
|
111
|
+
<section class="plugin-actions" aria-label="快速操作">
|
|
112
|
+
<button id="plugin-console" class="plugin-action" type="button">
|
|
113
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="m8 10 2 2-2 2M12 15h4"/></svg>控制台
|
|
114
|
+
</button>
|
|
115
|
+
<button id="plugin-copy" class="plugin-action" type="button">
|
|
116
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>复制令牌
|
|
117
|
+
</button>
|
|
118
|
+
<button id="plugin-toggle" class="plugin-action" type="button">
|
|
119
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v9M7.8 5.4a7 7 0 1 0 8.4 0"/></svg><span id="plugin-toggle-label">启动网关</span>
|
|
120
|
+
</button>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<section id="plugin-usage" class="plugin-usage" aria-labelledby="plugin-usage-title">
|
|
124
|
+
<div class="plugin-section-head"><span id="plugin-usage-title">用量概览</span><span id="plugin-usage-date" class="plugin-section-meta">—</span></div>
|
|
125
|
+
<div class="plugin-usage-grid">
|
|
126
|
+
<div>
|
|
127
|
+
<div class="plugin-usage-total"><strong id="plugin-token-total">—</strong><span>今日 Token</span></div>
|
|
128
|
+
<div id="plugin-token-track" class="plugin-usage-track" aria-label="Token 分布"></div>
|
|
129
|
+
<div class="plugin-usage-legend"><span class="input"><i></i>输入</span><span class="cache"><i></i>缓存</span><span class="output"><i></i>输出</span></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div id="plugin-usage-ring" class="plugin-usage-ring" style="--plugin-peak:0%"><div><strong id="plugin-peak-share">0%</strong><span>高峰占比</span></div></div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="plugin-usage-cost"><span>今日费用</span><strong id="plugin-cost">¥0.00</strong><small id="plugin-cost-split">等待网关统计</small></div>
|
|
134
|
+
<div id="plugin-usage-empty" class="plugin-usage-empty hidden">网关运行后显示 Token 与费用统计</div>
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<section class="plugin-details" aria-labelledby="plugin-details-title">
|
|
138
|
+
<div class="plugin-section-head"><span id="plugin-details-title">运行详情</span><span class="plugin-section-meta">实时</span></div>
|
|
139
|
+
<div class="plugin-detail-grid">
|
|
140
|
+
<div class="plugin-detail"><div class="plugin-detail-label">插件版本</div><div id="plugin-version" class="plugin-detail-value">—</div></div>
|
|
141
|
+
<div class="plugin-detail"><div class="plugin-detail-label">网关版本</div><div id="plugin-gateway-version" class="plugin-detail-value">—</div></div>
|
|
142
|
+
<div class="plugin-detail"><div class="plugin-detail-label">设备在线</div><div id="plugin-devices" class="plugin-detail-value">—</div></div>
|
|
143
|
+
<div class="plugin-detail"><div class="plugin-detail-label">本地地址</div><div id="plugin-host" class="plugin-detail-value">—</div></div>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
|
|
147
|
+
<footer class="plugin-foot"><span>状态面板 · 需要深入管理时进入控制台</span><a id="plugin-about" href="/remote/admin/" target="_blank" rel="noopener">关于与支持</a></footer>
|
|
148
|
+
</main>
|
|
149
|
+
<script src="plugin.js"></script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
package/public/plugin.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/* DSH Remote 插件端快速状态面板 · 零依赖 */
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const $ = (id) => document.getElementById(id)
|
|
5
|
+
const API = '/remote/admin/api'
|
|
6
|
+
let latest = null
|
|
7
|
+
let busy = false
|
|
8
|
+
let usageBusy = false
|
|
9
|
+
let usageLoadedAt = 0
|
|
10
|
+
|
|
11
|
+
function text(id, value) { const el = $(id); if (el) el.textContent = value == null ? '—' : String(value) }
|
|
12
|
+
function escapeText(value) { return String(value ?? '').replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])) }
|
|
13
|
+
function openConsole() { window.open('/remote/admin/', '_blank', 'noopener') }
|
|
14
|
+
function closePanel() { window.parent.postMessage({ source: 'dsh-remote-plugin', type: 'close' }, location.origin) }
|
|
15
|
+
function fmtTokens(value) {
|
|
16
|
+
const n = Number(value) || 0
|
|
17
|
+
if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + 'M'
|
|
18
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e5 ? 0 : 1) + 'K'
|
|
19
|
+
return String(Math.round(n))
|
|
20
|
+
}
|
|
21
|
+
function fmtCost(value) { return '¥' + (Number(value) || 0).toFixed(2) }
|
|
22
|
+
function renderUsage(days) {
|
|
23
|
+
const section = $('plugin-usage')
|
|
24
|
+
const empty = $('plugin-usage-empty')
|
|
25
|
+
if (!section) return
|
|
26
|
+
if (!Array.isArray(days) || !days.length) {
|
|
27
|
+
section.classList.add('empty')
|
|
28
|
+
empty?.classList.remove('hidden')
|
|
29
|
+
text('plugin-usage-date', '—')
|
|
30
|
+
text('plugin-token-total', '—')
|
|
31
|
+
text('plugin-peak-share', '0%')
|
|
32
|
+
text('plugin-cost', '—')
|
|
33
|
+
text('plugin-cost-split', '等待网关统计')
|
|
34
|
+
if ($('plugin-token-track')) $('plugin-token-track').innerHTML = ''
|
|
35
|
+
$('plugin-usage-ring')?.style.setProperty('--plugin-peak', '0%')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const today = days[days.length - 1] || {}
|
|
39
|
+
const total = today.total || {}
|
|
40
|
+
const input = Number(total.input) || 0
|
|
41
|
+
const cache = (Number(total.cacheRead) || 0) + (Number(total.cacheWrite) || 0)
|
|
42
|
+
const output = Number(total.output) || 0
|
|
43
|
+
const totalTokens = input + cache + output
|
|
44
|
+
const peakCost = Number(today.peak?.cost) || 0
|
|
45
|
+
const offCost = Number(today.off?.cost) || 0
|
|
46
|
+
const totalCost = peakCost + offCost
|
|
47
|
+
const peakShare = totalCost > 0 ? Math.round(peakCost / totalCost * 100) : 0
|
|
48
|
+
const share = (n) => totalTokens ? Math.max(0, n / totalTokens * 100) : 0
|
|
49
|
+
section.classList.remove('empty')
|
|
50
|
+
empty?.classList.add('hidden')
|
|
51
|
+
text('plugin-usage-date', today.date || '—')
|
|
52
|
+
text('plugin-token-total', fmtTokens(totalTokens))
|
|
53
|
+
text('plugin-peak-share', peakShare + '%')
|
|
54
|
+
text('plugin-cost', fmtCost(totalCost))
|
|
55
|
+
text('plugin-cost-split', `高峰 ${fmtCost(peakCost)} · 空闲 ${fmtCost(offCost)}`)
|
|
56
|
+
const track = $('plugin-token-track')
|
|
57
|
+
if (track) track.innerHTML = `<i class="input" style="width:${share(input)}%"></i><i class="cache" style="width:${share(cache)}%"></i><i class="output" style="width:${share(output)}%"></i>`
|
|
58
|
+
$('plugin-usage-ring')?.style.setProperty('--plugin-peak', peakShare + '%')
|
|
59
|
+
}
|
|
60
|
+
async function loadUsage() {
|
|
61
|
+
if (usageBusy) return
|
|
62
|
+
usageBusy = true
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${API}/stats/summary?days=7`, { headers: { 'x-dsh-remote-client': 'plugin' } })
|
|
65
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
66
|
+
const out = await res.json()
|
|
67
|
+
renderUsage(out.days || [])
|
|
68
|
+
} catch {
|
|
69
|
+
renderUsage([])
|
|
70
|
+
} finally {
|
|
71
|
+
usageLoadedAt = Date.now()
|
|
72
|
+
usageBusy = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setBusy(value) {
|
|
77
|
+
busy = value
|
|
78
|
+
$('plugin-primary').disabled = value
|
|
79
|
+
$('plugin-toggle').disabled = value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function render(st) {
|
|
83
|
+
latest = st
|
|
84
|
+
const gateway = st.mode === 'gateway'
|
|
85
|
+
const healthy = gateway && st.upstream?.reachable !== false
|
|
86
|
+
const installed = st.gatewayInstalled !== false
|
|
87
|
+
const hero = $('plugin-hero')
|
|
88
|
+
hero.classList.toggle('ok', healthy)
|
|
89
|
+
hero.classList.toggle('warn', gateway && !healthy)
|
|
90
|
+
hero.classList.toggle('off', !gateway)
|
|
91
|
+
|
|
92
|
+
text('plugin-status', healthy ? '系统运行正常' : gateway ? '网关需要关注' : installed ? '网关待启动' : 'DSH 已连接')
|
|
93
|
+
text('plugin-status-desc', healthy
|
|
94
|
+
? `${st.onlineCount || 0} 台设备在线 · 最近请求 ${st.totalRequests || 0} 次`
|
|
95
|
+
: gateway ? 'DSH 上游暂时不可达,请打开控制台诊断' : installed ? '本地网关尚未运行,启动后即可远程连接' : '插件已连接 DSH,但未检测到网关程序')
|
|
96
|
+
text('plugin-status-meta', healthy ? '本地服务可用' : gateway ? '需要查看诊断' : installed ? '本地服务未启动' : '仅 DSH 内嵌模式')
|
|
97
|
+
|
|
98
|
+
const primary = $('plugin-primary')
|
|
99
|
+
primary.textContent = healthy ? '打开控制台' : installed ? '启动网关' : '查看控制台'
|
|
100
|
+
primary.dataset.action = healthy ? 'console' : installed ? 'start' : 'console'
|
|
101
|
+
text('plugin-toggle-label', gateway ? '停止网关' : '启动网关')
|
|
102
|
+
$('plugin-toggle').classList.toggle('hidden', !installed)
|
|
103
|
+
|
|
104
|
+
text('plugin-version', st.version ? 'v' + st.version : '—')
|
|
105
|
+
text('plugin-gateway-version', gateway ? (st.version ? 'v' + st.version : '运行中') : '未运行')
|
|
106
|
+
text('plugin-devices', `${st.onlineCount || 0} / ${st.deviceCount || 0}`)
|
|
107
|
+
text('plugin-host', (st.lanIPs || []).find(x => x && x !== '127.0.0.1') || st.host || '127.0.0.1')
|
|
108
|
+
if (!gateway) {
|
|
109
|
+
usageLoadedAt = Date.now()
|
|
110
|
+
renderUsage([])
|
|
111
|
+
} else if (Date.now() - usageLoadedAt > 30000) {
|
|
112
|
+
loadUsage()
|
|
113
|
+
}
|
|
114
|
+
$('plugin-error').classList.add('hidden')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function load() {
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(`${API}/state`, { headers: { 'x-dsh-remote-client': 'plugin' } })
|
|
120
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
121
|
+
render(await res.json())
|
|
122
|
+
} catch (e) {
|
|
123
|
+
$('plugin-hero').classList.remove('ok', 'warn')
|
|
124
|
+
$('plugin-hero').classList.add('off')
|
|
125
|
+
text('plugin-status', '无法读取状态')
|
|
126
|
+
text('plugin-status-desc', 'DSH Remote 插件暂时无法访问本地服务')
|
|
127
|
+
text('plugin-status-meta', '请稍后重试')
|
|
128
|
+
$('plugin-error').textContent = '状态读取失败:' + escapeText(e.message || e)
|
|
129
|
+
$('plugin-error').classList.remove('hidden')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function toggleGateway() {
|
|
134
|
+
if (busy) return
|
|
135
|
+
setBusy(true)
|
|
136
|
+
const action = latest?.mode === 'gateway' ? 'stop' : 'start'
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${API}/gateway`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json', 'x-dsh-remote-client': 'plugin' },
|
|
141
|
+
body: JSON.stringify({ action }),
|
|
142
|
+
})
|
|
143
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 650))
|
|
145
|
+
await load()
|
|
146
|
+
} catch (e) {
|
|
147
|
+
$('plugin-error').textContent = '网关操作失败:' + (e.message || e)
|
|
148
|
+
$('plugin-error').classList.remove('hidden')
|
|
149
|
+
} finally { setBusy(false) }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function copyToken() {
|
|
153
|
+
const token = latest?.token || ''
|
|
154
|
+
if (!token) return
|
|
155
|
+
try {
|
|
156
|
+
await navigator.clipboard.writeText(token)
|
|
157
|
+
const button = $('plugin-copy')
|
|
158
|
+
const old = button.lastChild
|
|
159
|
+
if (old) old.textContent = '已复制'
|
|
160
|
+
setTimeout(() => { if (old) old.textContent = '复制令牌' }, 1400)
|
|
161
|
+
} catch {
|
|
162
|
+
$('plugin-error').textContent = '复制失败,请从控制台手动复制'
|
|
163
|
+
$('plugin-error').classList.remove('hidden')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
$('plugin-close').addEventListener('click', closePanel)
|
|
168
|
+
$('plugin-console').addEventListener('click', openConsole)
|
|
169
|
+
$('plugin-toggle').addEventListener('click', toggleGateway)
|
|
170
|
+
$('plugin-copy').addEventListener('click', copyToken)
|
|
171
|
+
$('plugin-primary').addEventListener('click', () => {
|
|
172
|
+
if ($('plugin-primary').dataset.action === 'start') toggleGateway()
|
|
173
|
+
else openConsole()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
load()
|
|
177
|
+
loadUsage()
|
|
178
|
+
setInterval(load, 5000)
|
|
179
|
+
setInterval(loadUsage, 30000)
|