dsh-remote-plugin 0.6.7 → 0.6.9

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.
@@ -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 => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[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)
package/public/styles.css CHANGED
@@ -20,7 +20,7 @@ html, body {
20
20
  overscroll-behavior-y: none;
21
21
  }
22
22
 
23
- body { padding-bottom: calc(64px + var(--dsr-safe-b)); }
23
+ body { padding-bottom: calc(92px + var(--dsr-safe-b)); }
24
24
 
25
25
  .hidden { display: none !important; }
26
26
  .muted { color: var(--dsr-muted); font-size: 12px; }
@@ -55,9 +55,10 @@ button {
55
55
  }
56
56
  .icon-btn {
57
57
  width: 34px; height: 34px; border-radius: 10px;
58
- background: var(--dsr-panel); border: 1px solid var(--dsr-line);
58
+ background: var(--dsr-panel); border: 1px solid var(--dsr-line); color: var(--dsr-accent-strong);
59
59
  font-size: 17px; display: grid; place-items: center;
60
60
  }
61
+ .icon-btn svg { width: 19px; height: 19px; display: block; }
61
62
  .icon-btn:active { background: var(--dsr-panel-2); }
62
63
  /* 顶栏按钮统一: 反馈/刷新/连接徽章同高同基线 */
63
64
  .topbar-btn {
@@ -66,6 +67,9 @@ button {
66
67
  line-height: 1; vertical-align: middle; flex-shrink: 0;
67
68
  }
68
69
  .topbar-right .topbar-btn.icon-btn { width: 44px; padding: 0; font-size: 19px; border-radius: 12px; }
70
+ .topbar-right #btn-donate { color: var(--dsr-accent-2); }
71
+ .topbar-right #btn-feedback { color: var(--dsr-info); }
72
+ .topbar-right #btn-refresh { color: var(--dsr-success); }
69
73
  .topbar-right .topbar-btn.conn-badge { padding: 0 14px; border-radius: 999px; font-size: 12.5px; }
70
74
  .mini-btn {
71
75
  padding: 5px 11px; border-radius: 9px; font-size: 13px;
@@ -199,6 +203,9 @@ a.mini-btn { text-decoration: none; display: inline-flex; align-items: center; }
199
203
  .session-swipe { position: relative; overflow: hidden; border-radius: var(--dsr-radius); background: var(--dsr-error); touch-action: pan-y; }
200
204
  .session-swipe .session-card { position: relative; z-index: 1; transform: translateX(var(--swipe-x, 0)); transition: transform .18s ease; }
201
205
  .session-swipe.revealed .session-card { transform: translateX(-92px); }
206
+ .session-swipe .wb-session { position: relative; z-index: 1; transform: translateX(var(--swipe-x, 0)); transition: transform .18s ease; }
207
+ .session-swipe.revealed .wb-session { transform: translateX(-92px); }
208
+ .wb-sessions .session-swipe { border-radius: 8px; }
202
209
  .sc-archive-btn { position: absolute; inset: 0 0 0 auto; z-index: 0; width: 92px; display: grid; place-items: center; padding: 0; background: var(--dsr-error); color: #fff; font-size: 13px; font-weight: 700; }
203
210
  .sc-archive-btn:active { filter: brightness(.9); }
204
211
  .empty { text-align: center; color: var(--dsr-muted); padding: 34px 0; }
@@ -236,8 +243,9 @@ body.in-session .main {
236
243
  display: flex;
237
244
  flex-direction: column;
238
245
  overflow: hidden;
239
- padding: 12px 12px 76px;
246
+ padding: 12px 12px 0;
240
247
  }
248
+ body.in-session .bottom-nav { display: none; }
241
249
  body.in-session .view {
242
250
  flex: 1;
243
251
  min-height: 0;
@@ -273,6 +281,8 @@ body.in-session .view {
273
281
  }
274
282
  .session-head .session-title { flex-shrink: 1; }
275
283
  .session-head-meta { flex: 1; min-width: 0; }
284
+ .session-send-btn { flex: 0 0 auto; min-height: 38px; padding: 0 13px; border-radius: 12px; border: 1px solid var(--dsr-accent-line); background: var(--dsr-accent-soft); color: var(--dsr-accent-strong); font: inherit; font-size: 13px; font-weight: 700; }
285
+ .session-send-btn:active { filter: brightness(.92); }
276
286
  .session-title {
277
287
  font-size: 16px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
278
288
  }
@@ -314,6 +324,8 @@ body.in-session .view {
314
324
  padding-right: 16px;
315
325
  scrollbar-width: none;
316
326
  }
327
+ body.in-session .history { padding-bottom: calc(76px + var(--dsr-safe-b)); }
328
+ body.in-session.has-composer-images .history { padding-bottom: calc(146px + var(--dsr-safe-b)); }
317
329
  .history::-webkit-scrollbar { display: none; }
318
330
  .history-more { text-align: center; margin: 4px 0; }
319
331
 
@@ -363,6 +375,16 @@ body.in-session .view {
363
375
  padding: 5px 12px; border-radius: 999px; max-width: 92%;
364
376
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
365
377
  }
378
+ .event.event-detail {
379
+ align-self: stretch; max-width: 96%; white-space: normal; overflow: visible;
380
+ border-radius: 12px; padding: 8px 10px; line-height: 1.45;
381
+ }
382
+ .event.event-detail summary { white-space: normal; overflow-wrap: anywhere; cursor: pointer; }
383
+ .event.event-detail pre {
384
+ margin: 8px 0 0; max-height: 280px; overflow: auto; white-space: pre-wrap;
385
+ overflow-wrap: anywhere; word-break: break-word; font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
386
+ background: var(--dsr-code-bg); border-radius: 8px; padding: 9px 10px;
387
+ }
366
388
  .tool {
367
389
  align-self: flex-start; max-width: 96%; background: var(--dsr-panel);
368
390
  border: 1px solid var(--dsr-line); border-left: 3px solid var(--dsr-accent-2);
@@ -391,10 +413,26 @@ body.in-session .view {
391
413
  background: var(--dsr-composer-bg); backdrop-filter: blur(10px);
392
414
  border-top: 1px solid var(--dsr-line);
393
415
  }
416
+ .composer-attachments {
417
+ display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px 0;
418
+ scrollbar-width: none;
419
+ }
420
+ .composer-attachments::-webkit-scrollbar { display: none; }
421
+ .composer-attachment { position: relative; flex: 0 0 58px; width: 58px; height: 58px; border-radius: 11px; overflow: hidden; border: 1px solid var(--dsr-line); background: var(--dsr-panel); }
422
+ .composer-attachment img { width: 100%; height: 100%; object-fit: cover; display: block; }
423
+ .composer-attachment-remove { position: absolute; top: 3px; right: 3px; width: 19px; height: 19px; border-radius: 50%; border: 1px solid rgba(0,0,0,.18); background: rgba(0,0,0,.62); color: #fff; font-size: 14px; line-height: 16px; padding: 0; }
424
+ .composer-image-btn { flex: 0 0 42px; width: 42px; height: 42px; display: grid; place-items: center; border-radius: 14px; color: var(--dsr-info); background: var(--dsr-panel); border: 1px solid var(--dsr-line); }
425
+ .composer-image-btn svg { width: 20px; height: 20px; }
426
+ .composer-image-btn:active, .composer-image-btn.active { color: var(--dsr-accent-strong); background: var(--dsr-accent-soft); border-color: var(--dsr-accent-line); }
427
+ .composer-image-menu { position: absolute; right: 12px; bottom: 58px; z-index: 4; display: flex; gap: 7px; padding: 7px; border: 1px solid var(--dsr-line); border-radius: 14px; background: var(--dsr-bg-2); box-shadow: 0 12px 30px var(--dsr-shadow); }
428
+ .composer-image-option { display: inline-flex; align-items: center; gap: 5px; min-height: 34px; padding: 6px 10px; border: 1px solid var(--dsr-line); border-radius: 10px; background: var(--dsr-panel); color: var(--dsr-text); font: inherit; font-size: 12px; }
429
+ .composer-image-option svg { width: 16px; height: 16px; color: var(--dsr-info); }
394
430
  .composer {
395
431
  display: flex; gap: 8px; align-items: flex-end;
396
432
  padding: 8px 12px calc(8px + var(--dsr-safe-b));
397
433
  }
434
+ .composer-input-wrap { flex: 1; min-width: 0; position: relative; display: flex; align-items: flex-end; }
435
+ .composer-input-wrap textarea { width: 100%; min-width: 0; }
398
436
  .composer textarea {
399
437
  flex: 1; resize: none; background: var(--dsr-panel); color: var(--dsr-text);
400
438
  border: 1px solid var(--dsr-line); border-radius: 16px;
@@ -402,6 +440,23 @@ body.in-session .view {
402
440
  max-height: 120px; outline: none;
403
441
  }
404
442
  .composer textarea:focus { border-color: var(--dsr-accent-line); }
443
+ .composer-fs-btn { position: absolute; top: 5px; right: 6px; z-index: 2; width: 27px; height: 27px; display: grid; place-items: center; border-radius: 8px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); color: var(--dsr-muted); }
444
+ .composer-fs-btn:active { color: var(--dsr-accent-strong); border-color: var(--dsr-accent-line); }
445
+ .composer-fs-btn svg { width: 15px; height: 15px; }
446
+ .composer-input-wrap.has-fs-btn textarea { padding-right: 38px; }
447
+ .composer-fs-handle { display: none; }
448
+ .composer-wrap.fs { position: fixed; top: calc(54px + var(--dsr-safe-top)); left: 0; right: 0; bottom: 0; max-width: none; height: auto; z-index: 35; display: flex; flex-direction: column; background: var(--dsr-composer-bg); }
449
+ .composer-wrap.fs .composer-menu { display: none; }
450
+ .composer-wrap.fs .composer-fs-handle { display: flex; align-items: center; justify-content: center; flex: none; height: 28px; touch-action: pan-y; }
451
+ .composer-fs-handle span { width: 38px; height: 4px; border-radius: 999px; background: var(--dsr-line); }
452
+ .composer-wrap.fs .composer { flex: 1; align-items: stretch; padding-bottom: calc(8px + var(--dsr-safe-b)); }
453
+ .composer-wrap.fs .composer-input-wrap { align-items: stretch; }
454
+ .composer-wrap.fs .composer-input-wrap textarea { height: 100%; max-height: none; resize: none; }
455
+ .composer-wrap.fs .composer-fs-btn { top: 7px; right: 7px; width: 30px; height: 30px; }
456
+ .composer-wrap.fs .plus-btn { display: none; }
457
+ .composer-wrap.fs .composer-image-btn, .composer-wrap.fs .composer-image-menu, .composer-wrap.fs .send-btn { display: none; }
458
+ .composer-wrap.fs .composer-image-menu { bottom: 58px; }
459
+ body.composer-fullscreen { overflow: hidden; }
405
460
  .plus-btn {
406
461
  flex-shrink: 0; width: 42px; height: 42px; border-radius: 14px;
407
462
  background: var(--dsr-panel); color: var(--dsr-info); border: 1px solid var(--dsr-line);
@@ -439,7 +494,65 @@ body.in-session .view {
439
494
  }
440
495
  .model-chip.current { color: var(--dsr-info); border-color: var(--dsr-info-line); background: var(--dsr-info-soft); }
441
496
  .model-provider { font-size: 11px; color: var(--dsr-muted); margin: 7px 0 5px; }
442
- body.in-session .main { padding-bottom: 84px; }
497
+ body.in-session .main { padding-bottom: 0; }
498
+
499
+ /* ---------- 系统总览 · A 方案 ---------- */
500
+ .overview-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin:2px 0 16px; }
501
+ .overview-eyebrow { color:var(--dsr-accent-strong); font-size:10px; font-weight:800; letter-spacing:1.7px; line-height:1.3; }
502
+ .overview-title { margin:3px 0 2px; font-size:25px; line-height:1.18; letter-spacing:-.4px; }
503
+ .overview-subtitle { margin:0; color:var(--dsr-muted); font-size:12px; }
504
+ .overview-refresh { width:34px; height:34px; padding:0; border-radius:11px; font-size:18px; line-height:1; }
505
+ .overview-refresh { color: var(--dsr-accent-strong); }
506
+ .overview-refresh svg { width:17px; height:17px; display:block; }
507
+ .overview-pulse-card { position:relative; overflow:hidden; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px 12px; align-items:center; padding:14px; margin-bottom:14px; border:1px solid var(--dsr-accent-line); border-radius:16px; background:linear-gradient(135deg,var(--dsr-panel),var(--dsr-bg-2)); box-shadow:0 10px 26px var(--dsr-shadow); }
508
+ .overview-pulse-card::after { content:""; position:absolute; width:110px; height:110px; right:-45px; top:-55px; border-radius:50%; background:var(--dsr-accent-soft); filter:blur(3px); pointer-events:none; }
509
+ .overview-pulse-copy { min-width:0; align-self:start; }
510
+ .overview-status { margin-top:6px; font-size:20px; font-weight:780; letter-spacing:-.3px; }
511
+ .overview-status-desc { margin-top:4px; color:var(--dsr-muted); font-size:11px; }
512
+ .overview-primary-action { display:inline-flex; align-items:center; justify-content:center; min-height:34px; margin-top:13px; padding:7px 13px; border:1px solid var(--dsr-accent-strong); border-radius:10px; background:var(--dsr-accent-strong); color:var(--dsr-on-accent); font:inherit; font-size:12px; font-weight:750; cursor:pointer; transition:transform .18s ease, filter .18s ease; }
513
+ .overview-primary-action:hover, .overview-primary-action:focus-visible { filter:brightness(1.08); transform:translateY(-1px); }
514
+ .overview-primary-action:disabled { opacity:.65; cursor:wait; transform:none; }
515
+ .overview-pulse-card.status-degraded { border-color:var(--dsr-warning-line); }
516
+ .overview-pulse-card.status-degraded .overview-primary-action { border-color:var(--dsr-warning); background:var(--dsr-warning); color:var(--dsr-on-warning); }
517
+ .overview-pulse-card.status-offline { border-color:var(--dsr-error-line); }
518
+ .overview-pulse-card.status-offline .overview-primary-action { border-color:var(--dsr-error); background:var(--dsr-error); color:var(--dsr-neutral-9); }
519
+ .system-pulse-ring { --pulse-pct:0%; position:relative; z-index:1; width:92px; height:92px; padding:7px; border-radius:50%; background:conic-gradient(var(--dsr-accent) 0 var(--pulse-pct),var(--dsr-line) var(--pulse-pct) 100%); box-shadow:0 0 20px var(--dsr-glow); }
520
+ .system-pulse-ring::before { content:""; position:absolute; inset:6px; border-radius:50%; border:1px solid var(--dsr-accent-line); opacity:.7; }
521
+ .system-pulse-core { position:relative; display:flex; flex-direction:column; align-items:center; justify-content:center; width:100%; height:100%; border-radius:50%; background:var(--dsr-bg-2); color:var(--dsr-muted); }
522
+ .system-pulse-core strong { max-width:100%; color:var(--dsr-text); font-size:14px; line-height:1.05; letter-spacing:.2px; text-align:center; white-space:nowrap; }
523
+ .system-pulse-core span { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:4px; font-size:8px; text-align:center; }
524
+ .overview-links { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:6px; padding-top:8px; border-top:1px solid var(--dsr-line); }
525
+ .overview-link { min-width:0; display:grid; grid-template-columns:auto 1fr; align-items:center; gap:4px 6px; padding-top:9px; color:var(--dsr-muted); font-size:10px; }
526
+ .overview-link i { width:6px; height:6px; border-radius:50%; background:var(--dsr-muted); box-shadow:0 0 0 3px var(--dsr-line); }
527
+ .overview-link b { grid-column:2; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dsr-text); font-size:10px; font-weight:650; }
528
+ .overview-link.ok i { background:var(--dsr-success); box-shadow:0 0 0 3px var(--dsr-success-soft); }
529
+ .overview-link.warn i { background:var(--dsr-warning); box-shadow:0 0 0 3px var(--dsr-warning-soft); }
530
+ .overview-link.off i { background:var(--dsr-error); box-shadow:0 0 0 3px var(--dsr-error-soft); }
531
+ .overview-section-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin:17px 1px 8px; font-size:12px; font-weight:750; letter-spacing:.3px; }
532
+ .overview-section-meta { color:var(--dsr-muted); font-size:11px; font-weight:500; }
533
+ .overview-attention-list, .overview-session-list { display:flex; flex-direction:column; gap:7px; }
534
+ .overview-attention-item, .overview-session-item { width:100%; display:flex; align-items:center; gap:10px; min-width:0; padding:11px 12px; border:1px solid var(--dsr-line); border-radius:13px; background:var(--dsr-panel); color:var(--dsr-text); text-align:left; }
535
+ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
536
+ .overview-attention-item { border-left:3px solid var(--dsr-warning); }
537
+ .overview-attention-item.question { border-left-color:var(--dsr-accent-2); }
538
+ .overview-item-mark { flex:0 0 auto; width:25px; height:25px; display:grid; place-items:center; border-radius:8px; background:var(--dsr-warning-soft); color:var(--dsr-warning); font-size:12px; }
539
+ .overview-attention-item.question .overview-item-mark { background:var(--dsr-accent-2-soft); color:var(--dsr-accent-2); }
540
+ .overview-item-copy { min-width:0; flex:1; }
541
+ .overview-item-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; font-weight:650; }
542
+ .overview-item-desc { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px; color:var(--dsr-muted); font-size:11px; }
543
+ .overview-item-arrow { color:var(--dsr-muted); font-size:18px; }
544
+ .overview-item-actions { flex:0 0 auto; display:flex; gap:4px; }
545
+ .overview-item-actions .mini-btn { padding:4px 7px; font-size:10px; }
546
+ .overview-metric-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
547
+ .overview-metric { min-width:0; padding:11px 12px; border:1px solid var(--dsr-line); border-radius:13px; background:var(--dsr-panel); }
548
+ .overview-metric span { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dsr-muted); font-size:10.5px; }
549
+ .overview-metric strong { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:5px; color:var(--dsr-text); font-size:15px; font-weight:700; }
550
+ .overview-session-item { cursor:pointer; }
551
+ .overview-session-item .overview-item-mark { background:var(--dsr-info-soft); color:var(--dsr-info); }
552
+ .overview-session-item.running .overview-item-mark { background:var(--dsr-success-soft); color:var(--dsr-success); }
553
+ .overview-session-item .overview-item-desc { font-variant-numeric:tabular-nums; }
554
+ .overview-empty { padding:10px 12px; border:1px dashed var(--dsr-line); border-radius:13px; color:var(--dsr-muted); font-size:12px; text-align:center; }
555
+ .overview-legacy { display:none; }
443
556
 
444
557
  /* ---------- 待办 ---------- */
445
558
  .pending-list, .jobs-list { display: flex; flex-direction: column; gap: 8px; }
@@ -489,7 +602,8 @@ body.in-session .main { padding-bottom: 84px; }
489
602
  padding: 11px 13px; min-width: 0;
490
603
  }
491
604
  .fs-row:active { background: var(--dsr-panel-2); }
492
- .fs-ico { flex-shrink: 0; font-size: 22px; }
605
+ .fs-ico { flex-shrink: 0; width: 22px; height: 22px; display:grid; place-items:center; color:var(--dsr-info); }
606
+ .fs-ico svg { width:20px; height:20px; display:block; }
493
607
  .fs-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; }
494
608
  .fs-name {
495
609
  font-size: 14.5px; font-weight: 600; white-space: nowrap;
@@ -586,18 +700,41 @@ body.in-session .main { padding-bottom: 84px; }
586
700
  /* ---------- 底部导航 ---------- */
587
701
  .bottom-nav {
588
702
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 25;
589
- display: flex; background: var(--dsr-nav-bg); backdrop-filter: blur(12px);
590
- border-top: 1px solid var(--dsr-line);
591
- padding-bottom: var(--dsr-safe-b);
703
+ display: flex; align-items: flex-end; min-height: 88px; gap: 3px; isolation: isolate;
704
+ padding: 0 8px var(--dsr-safe-b); background: transparent; border-top: none;
592
705
  }
593
- .nav-btn {
594
- flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
595
- padding: 8px 0 7px; color: var(--dsr-nav-muted); font-size: 11px; position: relative;
706
+ .nav-wave { position: absolute; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; filter: drop-shadow(0 -8px 20px var(--dsr-shadow)); }
707
+ .nav-wave svg { width: 100%; height: 100%; display: block; overflow: visible; }
708
+ .nav-wave-fill { fill: var(--dsr-nav-bg); }
709
+ .nav-wave-line { fill: none; stroke: var(--dsr-accent-line); stroke-width: .55; vector-effect: non-scaling-stroke; }
710
+ .nav-wave::after { content: ''; position: absolute; left: 10%; right: 10%; top: 28%; height: 1px; background: linear-gradient(90deg, transparent, var(--dsr-accent-2-line), var(--dsr-accent-line), transparent); opacity: .45;
596
711
  }
597
- .nav-btn .nav-ico { font-size: 19px; line-height: 1; }
598
- .nav-btn.active { color: var(--dsr-nav-active); }
712
+ .nav-btn {
713
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 4px;
714
+ min-width: 0; min-height: 69px; padding: 7px 2px 5px; color: var(--dsr-nav-muted); font-size: 11px; position: relative;
715
+ border-radius: 14px 14px 0 0; touch-action: manipulation;
716
+ }
717
+ .nav-btn .nav-ico { width: 22px; height: 22px; display: grid; place-items: center; line-height: 1; transition: transform .18s ease, color .18s ease; }
718
+ .nav-btn .nav-ico svg { width: 21px; height: 21px; display: block; }
719
+ .nav-btn:nth-of-type(1) { --nav-tint: var(--dsr-info); }
720
+ .nav-btn:nth-of-type(2) { --nav-tint: var(--dsr-success); }
721
+ .nav-btn:nth-of-type(4) { --nav-tint: var(--dsr-warning); }
722
+ .nav-btn:nth-of-type(5) { --nav-tint: var(--dsr-accent-2); }
723
+ .nav-btn:not(.nav-home) .nav-ico { color: var(--nav-tint, var(--dsr-nav-muted)); opacity: .82; }
724
+ .nav-btn:not(.nav-home) > span:last-of-type { color: var(--dsr-nav-muted); }
725
+ .nav-btn.active:not(.nav-home) { color: var(--dsr-nav-active); }
726
+ .nav-btn.active:not(.nav-home) .nav-ico { transform: translateY(-1px); opacity: 1; }
727
+ .nav-btn.active:not(.nav-home)::after { content: ''; position: absolute; bottom: 3px; width: 4px; height: 4px; border-radius: 50%; background: var(--dsr-nav-active); box-shadow: 0 0 10px var(--dsr-glow); }
728
+ .nav-home { z-index: 2; transform: translateY(-13px); color: var(--dsr-nav-muted); }
729
+ .nav-home-orb { width: 56px; height: 56px; display: grid; place-items: center; border-radius: 50%; background: var(--dsr-bg-2); border: 1px solid var(--dsr-accent-line); box-shadow: 0 5px 18px var(--dsr-shadow); transition: transform .18s ease, background .18s ease, box-shadow .18s ease; }
730
+ .nav-home .nav-ico { width: 30px; height: 30px; color: var(--dsr-accent-strong); }
731
+ .nav-home .nav-ico svg { width: 29px; height: 29px; }
732
+ .nav-home.active { color: var(--dsr-nav-active); }
733
+ .nav-home.active .nav-home-orb { background: radial-gradient(circle at 34% 27%, var(--dsr-info), var(--dsr-accent) 42%, var(--dsr-accent-2)); border-color: var(--dsr-accent); color: var(--dsr-on-accent); box-shadow: 0 7px 24px var(--dsr-glow), inset 0 0 0 1px rgba(255,255,255,.32); }
734
+ .nav-home.active .nav-ico { color: var(--dsr-on-accent); transform: none; }
735
+ .nav-home:active .nav-home-orb { transform: scale(.96); }
599
736
  .nav-badge {
600
- position: absolute; top: 4px; left: calc(50% + 10px);
737
+ position: absolute; top: 1px; left: calc(50% + 19px); z-index: 3;
601
738
  background: var(--dsr-nav-badge-bg); color: var(--dsr-nav-badge-text); border-radius: 999px;
602
739
  font-size: 10px; font-weight: 800; padding: 0 6px; line-height: 16px;
603
740
  }
@@ -623,7 +760,7 @@ body.in-session .main { padding-bottom: 84px; }
623
760
  padding: 9px 10px; border-radius: 13px; border: none; background: transparent; color: var(--dsr-text);
624
761
  font: inherit; text-align: left; cursor: pointer; text-decoration: none;
625
762
  }
626
- .sheet-item:hover, .sheet-item:focus-visible { background: var(--dsr-bg); outline: none; }
763
+ .sheet-item:hover, .sheet-item:focus-visible { background: var(--dsr-bg); }
627
764
  .sheet-item.primary { background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line); }
628
765
  .sheet-item.primary:hover { background: var(--dsr-accent-soft); filter: brightness(1.03); }
629
766
  .sheet-ico {
@@ -692,6 +829,13 @@ body.in-session .main { padding-bottom: 84px; }
692
829
  .announcement-content { line-height: 1.7; overflow-wrap: anywhere; word-break: break-word; }
693
830
  .announcement-action { display: inline-block; margin-top: 12px; color: var(--dsr-accent-strong); font-size: 13px; font-weight: 600; text-decoration: none; }
694
831
  .announcement-action:hover { text-decoration: underline; }
832
+ .announcement-history-card { max-width: 520px; }
833
+ .announcement-history-list { display: flex; flex-direction: column; gap: 8px; }
834
+ .announcement-history-item { border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); padding: 10px 12px; }
835
+ .announcement-history-item summary { cursor: pointer; display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-weight: 700; overflow-wrap: anywhere; }
836
+ .announcement-history-item summary small { flex: 0 0 auto; color: var(--dsr-muted); font-size: 11px; font-weight: 400; }
837
+ .announcement-history-item .announcement-history-content { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dsr-line); line-height: 1.65; overflow-wrap: anywhere; word-break: break-word; }
838
+ .announcement-history-item .announcement-action { margin-top: 8px; }
695
839
  .kv { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; padding: 3px 0; }
696
840
  .kv .k { color: var(--dsr-muted); } .kv .v { word-break: break-all; text-align: right; }
697
841
 
@@ -744,7 +888,24 @@ body.in-session .main { padding-bottom: 84px; }
744
888
  .toast.err { border-color: var(--dsr-error-line); color: var(--dsr-error); }
745
889
  .toast.ok { border-color: var(--dsr-success-line); color: var(--dsr-success); }
746
890
 
891
+ /* 键盘焦点:保证深浅皮肤和移动端外接键盘都有清晰反馈 */
892
+ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
893
+ outline: 2px solid var(--dsr-accent);
894
+ outline-offset: 2px;
895
+ }
896
+
897
+ @media (prefers-reduced-motion: reduce) {
898
+ *, *::before, *::after {
899
+ animation-duration: .01ms !important;
900
+ animation-iteration-count: 1 !important;
901
+ transition-duration: .01ms !important;
902
+ scroll-behavior: auto !important;
903
+ }
904
+ }
905
+
747
906
  @media (min-width: 560px) {
748
907
  .stat-strip { grid-template-columns: repeat(3, 1fr); }
749
908
  .msg { max-width: 78%; }
909
+ .overview-pulse-card { grid-template-columns:1fr auto; }
910
+ .overview-metric-grid { grid-template-columns:repeat(4,minmax(0,1fr)); }
750
911
  }
@@ -9,6 +9,15 @@
9
9
  --dsr-safe-b: max(env(safe-area-inset-bottom, 0px), var(--native-bottom, 0px));
10
10
  }
11
11
 
12
+ html,
13
+ html[data-theme="default"],
14
+ html[data-theme="dark"] { color-scheme: dark; }
15
+ html[data-theme="light"],
16
+ html[data-theme="neutral"] { color-scheme: light; }
17
+ @media (prefers-color-scheme: light) {
18
+ html:not([data-theme]) { color-scheme: light; }
19
+ }
20
+
12
21
  /* ============ 深空「默认」: 经典深蓝紫(保留原配色) ============ */
13
22
  :root,
14
23
  html[data-theme="default"] {
@@ -1,10 +1,18 @@
1
1
  {
2
- "version": "0.6.7",
2
+ "version": "0.6.9",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "c2cb5b68c8fef1eabe4e1e669f03d41815b923d5568e325bb04d5d7a1fbd122a",
5
- "releasedAt": "2026-08-21T10:34:07.760Z",
6
- "notes": "修复手机端左滑归档松手后按钮不固定;新增云端公告弹窗,支持按 App 版本和有效期定向发布;新增工作台绑定与项目级会话;新增手机端左滑归档和二次确认;新增手机回车行为设置;新增归档会话折叠和 /permission 参数选择。",
4
+ "sha256": "b8d869a6a6840261bc511005e4c86e3a2853df15f29ee33406e5c9e1f21f5796",
5
+ "releasedAt": "2026-08-21T19:36:51.824Z",
6
+ "notes": "0.6.9 正式版:全面优化网络连接稳定性,重构网关实时链路与连接管理;修复 downlink-only 心跳断联、网关重启风暴和 WebSocket 状态竞态;新增 VPN 友好 Ping/Pong、集中 mux/host collector、短时 WebSocket ticket、CORS 收紧与统计旁路优化。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.9",
10
+ "notes": "0.6.9 正式版:全面优化网络连接稳定性,重构网关实时链路与连接管理;修复 downlink-only 心跳断联、网关重启风暴和 WebSocket 状态竞态;新增 VPN 友好 Ping/Pong、集中 mux/host collector、短时 WebSocket ticket、CORS 收紧与统计旁路优化。"
11
+ },
12
+ {
13
+ "version": "0.6.8",
14
+ "notes": "0.6.8 正式版:重构手机端、桌面端和插件管理界面;新增工作台、图片附件、历史公告和全屏输入;修复会话布局、系统提示裁切、主题图标对比度与全屏输入高度问题。"
15
+ },
8
16
  {
9
17
  "version": "0.6.7",
10
18
  "notes": "修复手机端左滑归档松手后按钮不固定;新增云端公告弹窗,支持按 App 版本和有效期定向发布;新增工作台绑定与项目级会话;新增手机端左滑归档和二次确认;新增手机回车行为设置;新增归档会话折叠和 /permission 参数选择。"
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.7"
2
+ "version": "0.6.9"
3
3
  }