dsh-remote-plugin 0.5.7-rc.1 → 0.5.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 +1 -0
- package/README.md +1 -0
- package/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +26 -12
- package/package.json +1 -1
- package/public/admin.html +15 -1
- package/public/admin.js +18 -6
- package/public/app.js +461 -42
- package/public/desktop/desktop.css +207 -0
- package/public/desktop/desktop.html +247 -0
- package/public/desktop/desktop.js +917 -0
- package/public/desktop/i18n.js +53 -0
- package/public/index.html +77 -2
- package/public/styles.css +54 -351
- package/public/theme-vars.css +351 -0
- package/public/update.json +3 -3
- package/public/version.json +1 -1
package/README.en.md
CHANGED
|
@@ -37,6 +37,7 @@ The app is embedded in the plugin package (`apk/dsh-remote.apk`) — install the
|
|
|
37
37
|
- Tap **QR code** in the desktop drawer → scan it with the phone to open the admin page → download the app from that page
|
|
38
38
|
- Or open `http://<gateway-IP>:8787` in a browser to download
|
|
39
39
|
- App updates are pushed by the gateway too (`update.json` relative path), never touching GitHub
|
|
40
|
+
- A desktop browser opening `http://<gateway-IP>:8787` auto-enters the desktop WebUI (sidebar sessions + files + settings + stats + approval notification stack)
|
|
40
41
|
|
|
41
42
|
The app has built-in multi-server latency switching and offline chat history caching; downloaded files are stored in the system `Download/dsh-remote` folder.
|
|
42
43
|
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ App 已随插件包内嵌(`apk/dsh-remote.apk`),装好插件即可获取
|
|
|
37
37
|
- 桌面抽屉点「二维码」→ 手机扫码打开管理页 → 页面提供 App 下载
|
|
38
38
|
- 或浏览器直接访问 `http://<网关IP>:8787` 下载
|
|
39
39
|
- App 更新同样走网关自动推送(`update.json` 相对路径),全程不绕 GitHub
|
|
40
|
+
- 电脑浏览器打开 `http://<网关IP>:8787` 自动进入桌面端 WebUI(侧栏会话 + 文件 + 设置 + 统计 + 审批通知卡片栈)
|
|
40
41
|
|
|
41
42
|
App 内置多服务器测速切换与聊天记录离线缓存,下载文件统一放系统「下载/dsh-remote」。
|
|
42
43
|
|
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway-stats.cjs
CHANGED
|
@@ -47,6 +47,8 @@ const BUCKET_PRICE_KEY = {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const BJ_OFFSET_MS = 8 * 3600 * 1000
|
|
50
|
+
// 定价生效日(北京时间): 2026-08-17 零点更新, 此前的 token 不计入费用统计
|
|
51
|
+
const PRICING_START_DATE = '2026-08-17'
|
|
50
52
|
const DAYS_DIR = 'days'
|
|
51
53
|
const CURSORS_FILE = 'cursors.json'
|
|
52
54
|
|
|
@@ -245,6 +247,7 @@ class StatsStore {
|
|
|
245
247
|
|
|
246
248
|
const model = eventModel(event) || fallbackModel || 'unknown'
|
|
247
249
|
const { date, hour, period } = eventKey(time)
|
|
250
|
+
if (date < PRICING_START_DATE) return { processed: false, gap: false, skip: true }
|
|
248
251
|
const day = this._loadDay(date)
|
|
249
252
|
const hourBucket = day.hours[hour] || (day.hours[hour] = {})
|
|
250
253
|
const modelBucket = hourBucket[model] || (hourBucket[model] = emptyBucket())
|
|
@@ -313,12 +316,14 @@ class StatsStore {
|
|
|
313
316
|
if (usage) {
|
|
314
317
|
const model = eventModel(event) || currentModel || 'unknown'
|
|
315
318
|
const { date, hour, period } = eventKey(event.time)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
if (date >= PRICING_START_DATE) {
|
|
320
|
+
const day = this._loadDay(date)
|
|
321
|
+
const hourBucket = day.hours[hour] || (day.hours[hour] = {})
|
|
322
|
+
const modelBucket = hourBucket[model] || (hourBucket[model] = emptyBucket())
|
|
323
|
+
addUsage(modelBucket, model, period, usage)
|
|
324
|
+
this._saveDay(day)
|
|
325
|
+
processed++
|
|
326
|
+
}
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
lastSeq = event.seq
|
|
@@ -362,10 +367,16 @@ class StatsStore {
|
|
|
362
367
|
summary(days) {
|
|
363
368
|
const n = Math.max(1, Math.min(Number(days) || 7, 90))
|
|
364
369
|
const out = []
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
// 查询窗口起点: 不早于定价生效日(生效日之前不统计)
|
|
371
|
+
const today = beijingDate(Date.now())
|
|
372
|
+
const startD = new Date(Date.now() + BJ_OFFSET_MS)
|
|
373
|
+
startD.setUTCDate(startD.getUTCDate() - (n - 1))
|
|
374
|
+
const windowStart = `${startD.getUTCFullYear()}-${pad2(startD.getUTCMonth() + 1)}-${pad2(startD.getUTCDate())}`
|
|
375
|
+
const first = windowStart > PRICING_START_DATE ? windowStart : PRICING_START_DATE
|
|
376
|
+
const [fy, fm, fd] = first.split('-').map(Number)
|
|
377
|
+
for (let cur = Date.UTC(fy, fm - 1, fd); ; cur += 86400_000) {
|
|
378
|
+
const date = beijingDate(cur)
|
|
379
|
+
if (date > today) break
|
|
369
380
|
const day = this._loadDay(date)
|
|
370
381
|
const s = dayTotals(day)
|
|
371
382
|
const byModel = {}
|
|
@@ -383,7 +394,9 @@ class StatsStore {
|
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
detail(date) {
|
|
386
|
-
|
|
397
|
+
// 生效日前不返回统计(页面会展示空态与生效日提示)
|
|
398
|
+
const effective = date >= PRICING_START_DATE ? date : PRICING_START_DATE
|
|
399
|
+
const day = this._loadDay(effective)
|
|
387
400
|
const hours = []
|
|
388
401
|
for (let hour = 0; hour < 24; hour++) {
|
|
389
402
|
const models = day.hours[hour] || {}
|
|
@@ -391,12 +404,13 @@ class StatsStore {
|
|
|
391
404
|
for (const bucket of Object.values(models)) mergeBucket(total, bucket)
|
|
392
405
|
hours.push({ hour, period: periodOfHour(hour), models, total })
|
|
393
406
|
}
|
|
394
|
-
return { date, hours }
|
|
407
|
+
return { date: effective, pricingStart: PRICING_START_DATE, hours }
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
|
|
398
411
|
module.exports = {
|
|
399
412
|
BJ_OFFSET_MS,
|
|
413
|
+
PRICING_START_DATE,
|
|
400
414
|
PEAK_HOURS,
|
|
401
415
|
PRICES,
|
|
402
416
|
beijingHour,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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
|
@@ -20,7 +20,16 @@
|
|
|
20
20
|
.stat-card.ok .v { color: var(--dsr-success); } .stat-card.warn .v { color: var(--dsr-warning); }
|
|
21
21
|
.stats-dash { margin-bottom: 14px; }
|
|
22
22
|
.stats-dash .stat-grid { margin-bottom: 8px; }
|
|
23
|
-
.stats-chart { display: flex; align-items: flex-end; gap: 8px; height:
|
|
23
|
+
.stats-chart { display: flex; align-items: flex-end; gap: 8px; height: 180px; padding: 12px 14px 10px; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); overflow-x: auto; overflow-y: hidden; }
|
|
24
|
+
.stats-legend { display: flex; gap: 12px; font-size: 11px; color: var(--dsr-muted); margin: 6px 2px 0; }
|
|
25
|
+
.stats-legend .lg { display: inline-flex; align-items: center; gap: 4px; }
|
|
26
|
+
.stats-legend .sw { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
|
27
|
+
.stats-legend .sw.peak { background: var(--dsr-accent-strong); }
|
|
28
|
+
.stats-legend .sw.off { background: var(--dsr-accent-2); }
|
|
29
|
+
.bucket-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3px 12px; margin-top: 6px; }
|
|
30
|
+
.bucket-grid .b { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 11px; min-width: 0; }
|
|
31
|
+
.bucket-grid .b .n { color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
32
|
+
.bucket-grid .b .t { font-weight: 600; white-space: nowrap; }
|
|
24
33
|
.stats-bar { flex: 1; min-width: 38px; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 5px; }
|
|
25
34
|
.stats-bar .bars { width: 100%; max-width: 46px; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; border-radius: 6px 6px 0 0; overflow: hidden; background: var(--dsr-accent-soft); }
|
|
26
35
|
.stats-bar .seg { width: 100%; }
|
|
@@ -29,6 +38,7 @@
|
|
|
29
38
|
.stats-bar .lbl { font-size: 10px; color: var(--dsr-muted); white-space: nowrap; }
|
|
30
39
|
.stats-bar .val { font-size: 10px; color: var(--dsr-text); white-space: nowrap; }
|
|
31
40
|
.stats-empty { color: var(--dsr-muted); text-align: center; padding: 20px 0; font-size: 12px; }
|
|
41
|
+
.stats-note { color: var(--dsr-muted); font-size: 11px; line-height: 1.6; margin-top: 8px; }
|
|
32
42
|
.table-wrap { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); overflow-x: auto; }
|
|
33
43
|
table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 0; }
|
|
34
44
|
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--dsr-line); vertical-align: top; }
|
|
@@ -158,7 +168,9 @@
|
|
|
158
168
|
<div class="stats-dash">
|
|
159
169
|
<div class="section-head"><span data-i18n="stats.title">Token 统计</span><span id="stats-sub" class="muted"></span></div>
|
|
160
170
|
<div class="stat-grid" id="stats-cards"></div>
|
|
171
|
+
<div id="stats-legend" class="stats-legend"></div>
|
|
161
172
|
<div id="stats-chart" class="stats-chart"></div>
|
|
173
|
+
<div id="stats-note" class="stats-note"></div>
|
|
162
174
|
</div>
|
|
163
175
|
|
|
164
176
|
<div class="section-head"><span data-i18n="devices">已连接设备</span><span id="device-summary" class="muted"></span></div>
|
|
@@ -237,6 +249,7 @@
|
|
|
237
249
|
'stats.peak': '高峰', 'stats.off': '空闲',
|
|
238
250
|
'stats.days': '近 {n} 天',
|
|
239
251
|
'stats.gatewayDown': '统计需要 8787 网关运行', 'stats.empty': '暂无统计,产生会话后自动聚合',
|
|
252
|
+
'stats.note': '注:本数据仅在使用 DeepSeek 官方 API 时估算;基于 token 计算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
240
253
|
'unit.sec': ' 秒', 'unit.min': ' 分钟', 'unit.hour': ' 小时 ', 'unit.minShort': ' 分', 'unit.day': ' 天 ',
|
|
241
254
|
'device.installedNotRunning': '网关已安装 · 当前未运行', 'device.noGatewayBinary': '未检测到网关程序',
|
|
242
255
|
'device.ipRefresh': '{n} 个 IP · 每 5 秒刷新',
|
|
@@ -308,6 +321,7 @@
|
|
|
308
321
|
'stats.peak': 'Peak', 'stats.off': 'Off-peak',
|
|
309
322
|
'stats.days': 'Last {n} days',
|
|
310
323
|
'stats.gatewayDown': 'Stats require the gateway on 8787', 'stats.empty': 'No stats yet — they aggregate as sessions happen',
|
|
324
|
+
'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.',
|
|
311
325
|
'unit.sec': 's', 'unit.min': 'min', 'unit.hour': 'h ', 'unit.minShort': 'm', 'unit.day': 'd ',
|
|
312
326
|
'device.installedNotRunning': 'Gateway installed · not running', 'device.noGatewayBinary': 'Gateway binary not found',
|
|
313
327
|
'device.ipRefresh': '{n} IPs · refreshed every 5s',
|
package/public/admin.js
CHANGED
|
@@ -59,6 +59,8 @@ async function loadStats() {
|
|
|
59
59
|
$('stats-cards').innerHTML = ''
|
|
60
60
|
$('stats-chart').innerHTML = `<div class="stats-empty">${t('stats.gatewayDown')}</div>`
|
|
61
61
|
$('stats-sub').textContent = ''
|
|
62
|
+
$('stats-note').textContent = ''
|
|
63
|
+
$('stats-legend').innerHTML = ''
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -67,6 +69,8 @@ function renderStats(days) {
|
|
|
67
69
|
$('stats-cards').innerHTML = ''
|
|
68
70
|
$('stats-chart').innerHTML = `<div class="stats-empty">${t('stats.empty')}</div>`
|
|
69
71
|
$('stats-sub').textContent = ''
|
|
72
|
+
$('stats-note').textContent = t('stats.note')
|
|
73
|
+
$('stats-legend').innerHTML = ''
|
|
70
74
|
return
|
|
71
75
|
}
|
|
72
76
|
const today = days[days.length - 1]
|
|
@@ -76,21 +80,29 @@ function renderStats(days) {
|
|
|
76
80
|
const totalCost = peakCost + offCost
|
|
77
81
|
const peakShare = totalCost > 0 ? Math.round(peakCost / totalCost * 100) : 0
|
|
78
82
|
$('stats-cards').innerHTML = `
|
|
79
|
-
<div class="stat-card"><div class="v">${fmtTokens(totalTokens)}
|
|
83
|
+
<div class="stat-card"><div class="v">${fmtTokens(totalTokens)} <span style="font-size:12px;font-weight:500;color:var(--dsr-muted)">${t('stats.todayTokens')}</span></div>
|
|
84
|
+
<div class="bucket-grid">
|
|
85
|
+
<div class="b"><span class="n">${t('stats.input')}</span><span class="t">${fmtTokens(today.total.input)}</span></div>
|
|
86
|
+
<div class="b"><span class="n">${t('stats.cacheRead')}</span><span class="t">${fmtTokens(today.total.cacheRead)}</span></div>
|
|
87
|
+
<div class="b"><span class="n">${t('stats.cacheWrite')}</span><span class="t">${fmtTokens(today.total.cacheWrite)}</span></div>
|
|
88
|
+
<div class="b"><span class="n">${t('stats.output')}</span><span class="t">${fmtTokens(today.total.output)}</span></div>
|
|
89
|
+
</div></div>
|
|
80
90
|
<div class="stat-card"><div class="v">${fmtCost(totalCost)}</div><div class="k">${t('stats.todayCost')} · ${t('stats.peak')} ${fmtCost(peakCost)} / ${t('stats.off')} ${fmtCost(offCost)}</div></div>
|
|
81
91
|
<div class="stat-card ${peakShare >= 50 ? 'warn' : 'ok'}"><div class="v">${peakShare}%</div><div class="k">${t('stats.peakShare')} · ${t('stats.days', { n: days.length })}</div></div>`
|
|
82
92
|
$('stats-sub').textContent = today.date
|
|
93
|
+
$('stats-note').textContent = t('stats.note')
|
|
94
|
+
$('stats-legend').innerHTML = `<span class="lg"><span class="sw peak"></span>${t('stats.peak')}</span><span class="lg"><span class="sw off"></span>${t('stats.off')}</span>`
|
|
83
95
|
|
|
84
|
-
// 近 7 日柱状图:
|
|
96
|
+
// 近 7 日柱状图: 柱总高按当日费用相对窗口最大值, 柱内峰/谷按当日实际占比堆叠
|
|
85
97
|
const maxCost = Math.max(...days.map(d => (d.total.cost || 0)), 0.0001)
|
|
86
98
|
$('stats-chart').innerHTML = days.map(d => {
|
|
87
99
|
const cost = d.total.cost || 0
|
|
88
|
-
const peakH =
|
|
89
|
-
const offH = Math.max(0,
|
|
90
|
-
const
|
|
100
|
+
const peakH = cost > 0 ? Math.round((d.peak.cost || 0) / cost * 100) : 0
|
|
101
|
+
const offH = cost > 0 ? Math.max(0, 100 - peakH) : 0
|
|
102
|
+
const totalH = cost > 0 ? Math.max(3, Math.round(cost / maxCost * 100)) : 0
|
|
91
103
|
const label = d.date.slice(5)
|
|
92
104
|
return `<div class="stats-bar" title="${d.date} · ${t('stats.peak')} ${fmtCost(d.peak.cost)} · ${t('stats.off')} ${fmtCost(d.off.cost)} · tokens ${fmtTokens(bucketTokens(d.total))}">
|
|
93
|
-
<div class="bars" style="height:${
|
|
105
|
+
<div class="bars" style="height:${totalH}%">
|
|
94
106
|
<div class="seg peak" style="height:${peakH}%"></div>
|
|
95
107
|
<div class="seg off" style="height:${offH}%"></div>
|
|
96
108
|
</div>
|