dsh-dock 0.11.3 → 0.11.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/README.md +14 -1
- package/client.js +426 -92
- package/features/mobile-relay/host.js +55 -3
- package/features/mobile-relay/view.jsx +103 -23
- package/features/tokenlog/view.jsx +252 -0
- package/package.json +1 -1
- package/screenshots/dock-tokenlog-charts-2.png +0 -0
- package/screenshots/dock-tokenlog-charts-3.png +0 -0
- package/screenshots/dock-tokenlog-charts.png +0 -0
|
@@ -154,6 +154,24 @@ const MOBILE_LAYOUT_CSS = [
|
|
|
154
154
|
' .dsh-mobile-drawer-btn:active{transform:scale(.94)}',
|
|
155
155
|
' .dsh-mobile-drawer-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}',
|
|
156
156
|
' .dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}',
|
|
157
|
+
// ---- 仿 ZCode 手机端:底部 Tab 栏 + 输入区吸附底部 ----
|
|
158
|
+
// Tab 栏 z 序 65:低于遮罩 75 / 抽屉 80 / overlayLayer 90 / 功能坞面板 200,
|
|
159
|
+
// 这些浮层打开时自然盖住 Tab 栏。功能坞面板打开时另由行为脚本整体隐藏(防露边)。
|
|
160
|
+
' .dsh-mobile-tabbar{position:fixed;left:0;right:0;bottom:0;z-index:65;display:flex;align-items:stretch;justify-content:space-around;padding:6px 8px calc(8px + env(safe-area-inset-bottom,0px));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 92%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.25));touch-action:manipulation}',
|
|
161
|
+
' .dsh-mobile-tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:48px;padding:4px 0;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#8b95ab);font-size:11px;font-weight:600;line-height:1.2;font-family:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;border-radius:10px}',
|
|
162
|
+
' .dsh-mobile-tab svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}',
|
|
163
|
+
' .dsh-mobile-tab.on{color:var(--dsw-alias-label-primary,#e6eaf2)}',
|
|
164
|
+
' .dsh-mobile-tab.on svg{color:#4d9fff}',
|
|
165
|
+
' .dsh-mobile-tab:active{transform:scale(.94)}',
|
|
166
|
+
// 输入区吸附到 Tab 栏上方:宿主把欢迎内容 justify-content:center 垂直居中,
|
|
167
|
+
// 手机视口下输入卡片悬在中部、下方留出大片空白。改为 flex-end 贴底,并给
|
|
168
|
+
// 滚动体留出 Tab 栏高度的内边距,避免内容被 Tab 栏遮挡。
|
|
169
|
+
' [class*="scrollBody"]{justify-content:flex-end!important;padding-bottom:calc(72px + env(safe-area-inset-bottom,0px))!important}',
|
|
170
|
+
' [class*="scrollBody"]>[class*="composerSeat"]{padding-bottom:6px}',
|
|
171
|
+
// 收起的侧栏轨道(collapsed rail)在窄屏只剩 1px 占位,但轨道里的按钮
|
|
172
|
+
// 仍会溢出贴在左边缘(功能坞/进化/设置三枚露出半边的圆钮)。隐藏整列,
|
|
173
|
+
// 这些入口已由底部 Tab 栏接管;展开抽屉(z 80)不受影响。
|
|
174
|
+
' [class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}',
|
|
157
175
|
'}',
|
|
158
176
|
].join('\n')
|
|
159
177
|
const MOBILE_LAYOUT_MARKER = 'data-dsh-mobile-layout'
|
|
@@ -180,10 +198,39 @@ const MOBILE_BEHAVIOR_JS = [
|
|
|
180
198
|
'function collapse(){var b=document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');if(b)b.click()}',
|
|
181
199
|
'function expand(){var b=document.querySelector(\'button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]\');if(b)b.click()}',
|
|
182
200
|
'function closeDetails(){var d=document.querySelector(\'[class*="detailsCol"]\');if(!d||!d.getBoundingClientRect().width)return;var c=d.querySelector(\'button[aria-label="关闭详情"]\');if(c)c.click()}',
|
|
183
|
-
'var fab=null,scrim=null;',
|
|
201
|
+
'var fab=null,scrim=null,tabbar=null;',
|
|
202
|
+
'var TAB_DEFS=[',
|
|
203
|
+
' {id:"sessions",label:"会话",icon:\'<svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg>\',act:function(){expand()}},',
|
|
204
|
+
' {id:"dock",label:"功能坞",icon:\'<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>\',act:function(){var b=document.querySelector("button.docke2-rail,button[class*=\'docke2-btn\'][class*=\'docke2-rail\']");if(b){var col=b.closest(\'[class*="sidebarCol"]\');if(col)col.style.visibility="";b.click();if(col)setTimeout(function(){col.style.visibility=""},0)}}},',
|
|
205
|
+
' {id:"settings",label:"设置",icon:\'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.23.6.86 1 1.51 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>\',act:function(){var bs=document.querySelectorAll("button");for(var i=0;i<bs.length;i++){if(bs[i].getAttribute("aria-label")==="设置"){bs[i].click();return}}}}',
|
|
206
|
+
'];',
|
|
207
|
+
'function syncTabs(){',
|
|
208
|
+
' if(!tabbar)return;',
|
|
209
|
+
' var expanded=!sidebarCollapsed();',
|
|
210
|
+
' var settingsOpen=dialogOpen();',
|
|
211
|
+
' var dockOpen=!!document.querySelector(".dockm-backdrop");',
|
|
212
|
+
' var bs=tabbar.querySelectorAll(".dsh-mobile-tab");',
|
|
213
|
+
' for(var i=0;i<bs.length;i++){',
|
|
214
|
+
' var id=bs[i].dataset.tab;',
|
|
215
|
+
' var on=(id==="sessions"&&expanded)||(id==="dock"&&dockOpen)||(id==="settings"&&settingsOpen);',
|
|
216
|
+
' if(on)bs[i].classList.add("on");else bs[i].classList.remove("on");',
|
|
217
|
+
' }',
|
|
218
|
+
'}',
|
|
184
219
|
'function ensureChrome(){',
|
|
185
220
|
' if(!fab){fab=document.createElement("button");fab.type="button";fab.className="dsh-mobile-drawer-btn";fab.setAttribute("aria-label","打开会话列表");fab.innerHTML=\'<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>\';fab.addEventListener("click",function(){expand()});document.body.appendChild(fab)}',
|
|
186
221
|
' if(!scrim){scrim=document.createElement("div");scrim.className="dsh-mobile-scrim";document.body.appendChild(scrim)}',
|
|
222
|
+
' if(!tabbar){',
|
|
223
|
+
' tabbar=document.createElement("nav");tabbar.className="dsh-mobile-tabbar";tabbar.setAttribute("aria-label","底部导航");',
|
|
224
|
+
' for(var i=0;i<TAB_DEFS.length;i++){',
|
|
225
|
+
' (function(def){',
|
|
226
|
+
' var b=document.createElement("button");b.type="button";b.className="dsh-mobile-tab";b.dataset.tab=def.id;',
|
|
227
|
+
' b.innerHTML=def.icon+"<span>"+def.label+"</span>";',
|
|
228
|
+
' b.addEventListener("click",function(){def.act()});',
|
|
229
|
+
' tabbar.appendChild(b);',
|
|
230
|
+
' })(TAB_DEFS[i]);',
|
|
231
|
+
' }',
|
|
232
|
+
' document.body.appendChild(tabbar);',
|
|
233
|
+
' }',
|
|
187
234
|
'}',
|
|
188
235
|
// 把手定位:贴着趣味游戏浮标(.dgfab,可拖拽)正上方;浮标太靠上时改放它下面
|
|
189
236
|
// (避开顶栏标题),被拖走/隐藏/不存在时兜底左侧中部(38% 视高)。
|
|
@@ -206,10 +253,15 @@ const MOBILE_BEHAVIOR_JS = [
|
|
|
206
253
|
' if(!fab||!scrim)return;',
|
|
207
254
|
' var on=narrow()&&!!frameEl()&&!dialogOpen();',
|
|
208
255
|
' var col=sidebarCollapsed();',
|
|
209
|
-
' var
|
|
256
|
+
' var dockOpen=!!document.querySelector(".dockm-backdrop");',
|
|
257
|
+
' var showFab=on&&col&&!dockOpen;',
|
|
210
258
|
' fab.style.display=showFab?"":"none";',
|
|
211
259
|
' if(showFab)place();',
|
|
212
260
|
' scrim.style.display=on&&!col?"":"none";',
|
|
261
|
+
// Tab 栏:窄屏且页面骨架就绪后常驻;功能坞面板打开时整体隐藏(面板全屏
|
|
262
|
+
// 模态,Tab 栏 z 序低于它仍可能从边缘露出);宿主设置弹窗 z 序更高,自然盖住。
|
|
263
|
+
' if(tabbar)tabbar.style.display=narrow()&&!!frameEl()&&!dockOpen?"":"none";',
|
|
264
|
+
' syncTabs();',
|
|
213
265
|
'}',
|
|
214
266
|
'function boot(){',
|
|
215
267
|
' ensureChrome();syncChrome();',
|
|
@@ -224,7 +276,7 @@ const MOBILE_BEHAVIOR_JS = [
|
|
|
224
276
|
' if(!narrow())return;',
|
|
225
277
|
' var t=e.target;',
|
|
226
278
|
' if(!t||!t.closest)return;',
|
|
227
|
-
' if(t.closest(".dsh-mobile-drawer-btn"))return;',
|
|
279
|
+
' if(t.closest(".dsh-mobile-drawer-btn")||t.closest(".dsh-mobile-tabbar"))return;',
|
|
228
280
|
' var expanded=!!document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');',
|
|
229
281
|
' var col=document.querySelector(\'[class*="sidebarCol"]\');',
|
|
230
282
|
' var inSidebar=col&&col.contains(t);',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 访问地址二维码;远程设备上经 window.__DSH_REMOTE__ 显示退出登录。
|
|
5
5
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
6
6
|
import QRCode from 'qrcode'
|
|
7
|
+
import { openPanel, setPanelOpen, subscribePanel, panelNav } from '../../src/shared.js'
|
|
7
8
|
|
|
8
9
|
function rpc(method, payload) {
|
|
9
10
|
return fetch('/dsh-dock/mobile-relay/' + method, {
|
|
@@ -171,10 +172,12 @@ export function MobileRelayView() {
|
|
|
171
172
|
export function MobileRelayOverlay() {
|
|
172
173
|
useEffect(() => {
|
|
173
174
|
if (typeof window === 'undefined' || typeof document === 'undefined') return undefined
|
|
174
|
-
|
|
175
|
+
// 宿主版(<head> 注入,重启后接管)就位时,抽屉把手/遮罩/收起逻辑由宿主负责,
|
|
176
|
+
// 本组件只补宿主旧版没有的底部 Tab 栏;宿主未就位(旧版无抽屉 DOM)时全套自建。
|
|
177
|
+
const hostPresent = !!(window.__dshDockMobileDrawer && document.querySelector('.dsh-mobile-drawer-btn'))
|
|
175
178
|
const mq = window.matchMedia ? window.matchMedia('(max-width:700px)') : null
|
|
176
179
|
if (!mq) return undefined
|
|
177
|
-
window.__dshDockMobileDrawer = 'client'
|
|
180
|
+
if (!hostPresent) window.__dshDockMobileDrawer = 'client'
|
|
178
181
|
|
|
179
182
|
const narrow = () => mq.matches
|
|
180
183
|
const frameEl = () => document.querySelector('[class$="frame"]')
|
|
@@ -192,21 +195,72 @@ export function MobileRelayOverlay() {
|
|
|
192
195
|
// 抽屉按钮:窄屏 + 侧栏收起 + 无设置弹窗时才显示(见 sync)。
|
|
193
196
|
// 视觉是贴左边缘的「抽屉把手」(与趣味游戏浮标 dgfab 同款边缘吸附语言),
|
|
194
197
|
// 位置动态放在趣味游戏浮标正上方(place()),游戏浮标不在时兜底左侧中部。
|
|
195
|
-
|
|
196
|
-
fab
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
// 宿主就位时把手/遮罩已由宿主创建,这里置 null 走纯 Tab 栏模式。
|
|
199
|
+
let fab = null
|
|
200
|
+
let scrim = null
|
|
201
|
+
if (!hostPresent) {
|
|
202
|
+
fab = document.createElement('button')
|
|
203
|
+
fab.type = 'button'
|
|
204
|
+
fab.className = 'dsh-mobile-drawer-btn'
|
|
205
|
+
fab.setAttribute('aria-label', '打开会话列表')
|
|
206
|
+
fab.innerHTML = '<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>'
|
|
207
|
+
fab.addEventListener('click', expand)
|
|
208
|
+
scrim = document.createElement('div')
|
|
209
|
+
scrim.className = 'dsh-mobile-scrim'
|
|
210
|
+
document.body.appendChild(fab)
|
|
211
|
+
document.body.appendChild(scrim)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- 仿 ZCode 手机端底部 Tab 栏:会话 / 功能坞 / 设置 ----
|
|
215
|
+
// 挂 body(与抽屉把手同理,避开 overlayLayer 堆叠上下文),z 序 65:
|
|
216
|
+
// 低于遮罩 75 / 侧栏抽屉 80 / 功能坞面板 200,这些打开时自然盖住 Tab 栏。
|
|
217
|
+
const tabbar = document.createElement('nav')
|
|
218
|
+
tabbar.className = 'dsh-mobile-tabbar'
|
|
219
|
+
tabbar.setAttribute('aria-label', '底部导航')
|
|
220
|
+
const tabDefs = [
|
|
221
|
+
{
|
|
222
|
+
id: 'sessions', label: '会话',
|
|
223
|
+
icon: '<svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg>',
|
|
224
|
+
act: () => expand(),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'dock', label: '功能坞',
|
|
228
|
+
icon: '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>',
|
|
229
|
+
act: () => { if (panelNav.open) setPanelOpen(false); else openPanel('home') },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'settings', label: '设置',
|
|
233
|
+
icon: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.23.6.86 1 1.51 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>',
|
|
234
|
+
act: () => { const b = [...document.querySelectorAll('button')].find((x) => (x.getAttribute('aria-label') || '') === '设置' || (x.textContent || '').trim() === '设置'); if (b) b.click() },
|
|
235
|
+
},
|
|
236
|
+
]
|
|
237
|
+
const tabButtons = tabDefs.map((def) => {
|
|
238
|
+
const b = document.createElement('button')
|
|
239
|
+
b.type = 'button'
|
|
240
|
+
b.className = 'dsh-mobile-tab'
|
|
241
|
+
b.dataset.tab = def.id
|
|
242
|
+
b.innerHTML = def.icon + '<span>' + def.label + '</span>'
|
|
243
|
+
b.addEventListener('click', () => def.act())
|
|
244
|
+
tabbar.appendChild(b)
|
|
245
|
+
return b
|
|
246
|
+
})
|
|
247
|
+
document.body.appendChild(tabbar)
|
|
248
|
+
const syncTabs = () => {
|
|
249
|
+
const expanded = !sidebarCollapsed()
|
|
250
|
+
const settingsOpen = dialogOpen()
|
|
251
|
+
for (const b of tabButtons) {
|
|
252
|
+
const id = b.dataset.tab
|
|
253
|
+
const on = (id === 'sessions' && expanded) || (id === 'dock' && panelNav.open) || (id === 'settings' && settingsOpen)
|
|
254
|
+
b.classList.toggle('on', on)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const unsubPanel = subscribePanel(syncTabs)
|
|
205
258
|
|
|
206
259
|
// 把手定位:贴着趣味游戏浮标(.dgfab,可拖拽、位置内联在 style 上)正上方;
|
|
207
260
|
// 浮标被拖到顶/隐藏/不存在时兜底左侧中部(38% 视高)。sync 每 1.5s 复测,
|
|
208
261
|
// 拖拽浮标后把手最迟一个周期跟上;resize 立即复测。
|
|
209
262
|
const place = () => {
|
|
263
|
+
if (!fab) return
|
|
210
264
|
const vh = window.innerHeight
|
|
211
265
|
let top = null
|
|
212
266
|
const g = document.querySelector('.dgfab')
|
|
@@ -225,10 +279,15 @@ export function MobileRelayOverlay() {
|
|
|
225
279
|
const sync = () => {
|
|
226
280
|
const on = narrow() && !!frameEl() && !dialogOpen()
|
|
227
281
|
const collapsed = sidebarCollapsed()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
282
|
+
if (fab) {
|
|
283
|
+
const showFab = on && collapsed && !panelNav.open
|
|
284
|
+
fab.style.display = showFab ? '' : 'none'
|
|
285
|
+
if (showFab) place()
|
|
286
|
+
}
|
|
287
|
+
if (scrim) scrim.style.display = on && !collapsed ? '' : 'none'
|
|
288
|
+
// Tab 栏:窄屏且页面骨架就绪后常驻;功能坞面板/宿主设置弹窗打开时隐藏(它们的遮罩更高,避免露边)
|
|
289
|
+
tabbar.style.display = narrow() && !!frameEl() && !panelNav.open ? '' : 'none'
|
|
290
|
+
syncTabs()
|
|
232
291
|
}
|
|
233
292
|
sync()
|
|
234
293
|
// MutationObserver 跟属性翻转即时同步;1.5s 轮询覆盖路由重渲等一切边角
|
|
@@ -240,13 +299,14 @@ export function MobileRelayOverlay() {
|
|
|
240
299
|
else if (mq.addListener) mq.addListener(onMq)
|
|
241
300
|
window.addEventListener('resize', place)
|
|
242
301
|
|
|
243
|
-
//
|
|
302
|
+
// 捕获阶段统一处理(仅自建模式;宿主就位时宿主已有同款全局捕获):
|
|
303
|
+
// 抽屉按钮自身排除(否则展开动作会被"点外部收起"当场撤销);
|
|
244
304
|
// 遮罩不挂自己的 click——它的点击也走这里 collapse(),单一路径防止同一事件两次 toggle。
|
|
245
305
|
const onClick = (e) => {
|
|
246
306
|
if (!narrow()) return
|
|
247
307
|
const t = e.target
|
|
248
308
|
if (!t || !t.closest) return
|
|
249
|
-
if (t.closest('.dsh-mobile-drawer-btn')) return
|
|
309
|
+
if (t.closest('.dsh-mobile-drawer-btn') || t.closest('.dsh-mobile-tabbar')) return
|
|
250
310
|
const expanded = !!document.querySelector('button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]')
|
|
251
311
|
const col = document.querySelector('[class*="sidebarCol"]')
|
|
252
312
|
const inSidebar = col && col.contains(t)
|
|
@@ -261,17 +321,19 @@ export function MobileRelayOverlay() {
|
|
|
261
321
|
if (t.closest('[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[class*="detailsCol"]')) return
|
|
262
322
|
collapse()
|
|
263
323
|
}
|
|
264
|
-
document.addEventListener('click', onClick, true)
|
|
324
|
+
if (!hostPresent) document.addEventListener('click', onClick, true)
|
|
265
325
|
|
|
266
326
|
return () => {
|
|
267
|
-
document.removeEventListener('click', onClick, true)
|
|
327
|
+
if (!hostPresent) document.removeEventListener('click', onClick, true)
|
|
268
328
|
if (observer) observer.disconnect()
|
|
269
329
|
clearInterval(timer)
|
|
330
|
+
unsubPanel()
|
|
270
331
|
if (mq.removeEventListener) mq.removeEventListener('change', onMq)
|
|
271
332
|
else if (mq.removeListener) mq.removeListener(onMq)
|
|
272
333
|
window.removeEventListener('resize', place)
|
|
273
|
-
fab.remove()
|
|
274
|
-
scrim.remove()
|
|
334
|
+
if (fab) fab.remove()
|
|
335
|
+
if (scrim) scrim.remove()
|
|
336
|
+
tabbar.remove()
|
|
275
337
|
if (window.__dshDockMobileDrawer === 'client') delete window.__dshDockMobileDrawer
|
|
276
338
|
}
|
|
277
339
|
}, [])
|
|
@@ -295,6 +357,24 @@ export const feature = {
|
|
|
295
357
|
.dmr{--dmr-accent:var(--dk-accent,#2f6fed);--dmr-accent-soft:color-mix(in srgb,var(--dmr-accent) 13%,transparent);display:flex;flex-direction:column;gap:16px;max-width:720px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}.dmr h3,.dmr h4,.dmr p{margin:0}.dmr h3{font-size:17px;line-height:1.25;letter-spacing:-.01em}.dmr h4{font-size:13px}.dmr-hero,.dmr-status-head{display:flex;align-items:flex-start;gap:12px}.dmr-hero>div,.dmr-status-head>div{min-width:0;display:flex;flex-direction:column;gap:4px}.dmr-hero p,.dmr-status-head p,.dmr-field small,.dmr-share small,.dmr-overview-card small,.dmr-task small,.dmr-note p{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}.dmr-hero-icon{width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;flex:none;border-radius:14px;background:var(--dmr-accent-soft);color:var(--dmr-accent);border:1px solid color-mix(in srgb,var(--dmr-accent) 35%,var(--dsw-alias-border-l1))}.dmr-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none}.dmr-steps li{display:flex;gap:9px;padding:11px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px}.dmr-steps li>span{width:21px;height:21px;display:inline-flex;align-items:center;justify-content:center;flex:none;border-radius:50%;font-size:11px;font-weight:700;color:var(--dmr-accent);background:var(--dmr-accent-soft)}.dmr-steps div{display:flex;flex-direction:column;gap:3px;min-width:0}.dmr-steps strong{font-size:12px}.dmr-steps small{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dmr-field{display:flex;flex-direction:column;gap:6px;font-weight:600}.dmr-field input,.dmr-field select,.dmr-note textarea{box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:10px;padding:10px 12px;min-height:44px;font:inherit;outline:none;transition:border-color .18s ease,box-shadow .18s ease}.dmr-field input:focus,.dmr-field select:focus,.dmr-note textarea:focus{border-color:var(--dmr-accent);box-shadow:0 0 0 3px var(--dmr-accent-soft)}.dmr-field small{font-weight:400}.dmr-network-grid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(150px,.6fr);gap:12px}.dmr-security{padding:12px 13px;border:1px solid color-mix(in srgb,var(--dk-warn) 48%,var(--dsw-alias-border-l1));border-radius:12px;background:color-mix(in srgb,var(--dk-warn) 9%,transparent)}.dmr-security strong{display:block;margin-bottom:4px;color:var(--dk-warn);font-size:12px}.dmr-security p{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.55}.dmr-primary,.dmr-secondary,.dmr-text-button,.dmr-icon-button{font:inherit;touch-action:manipulation;cursor:pointer;transition:transform .15s ease,background .18s ease,border-color .18s ease,opacity .18s ease}.dmr-primary,.dmr-secondary{min-height:44px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:10px;padding:9px 13px;font-weight:600}.dmr-primary{align-self:flex-start;border:1px solid var(--dmr-accent);background:var(--dmr-accent);color:#fff}.dmr-primary:hover{filter:brightness(1.05)}.dmr-secondary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dmr-secondary:hover{border-color:var(--dmr-accent);background:var(--dmr-accent-soft)}.dmr-primary:active,.dmr-secondary:active,.dmr-icon-button:active{transform:scale(.98)}.dmr-primary:disabled,.dmr-secondary:disabled,.dmr-text-button:disabled,.dmr-icon-button:disabled{cursor:not-allowed;opacity:.5}.dmr-message{display:flex;align-items:flex-start;gap:7px;padding:10px 12px;border-radius:10px;font-size:12px}.dmr-message.success{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}.dmr-message.error{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}.dmr-status-head{justify-content:space-between}.dmr-eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:var(--dmr-accent)}.dmr-eyebrow i{width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px var(--dmr-accent-soft)}.dmr-icon-button{display:inline-flex;align-items:center;justify-content:center;flex:none;width:44px;height:44px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary)}.dmr-icon-button:hover{color:var(--dsw-alias-state-error-primary);border-color:currentColor}.dmr-share{display:flex;flex-direction:column;gap:9px;padding:13px;border:1px solid color-mix(in srgb,var(--dmr-accent) 32%,var(--dsw-alias-border-l1));border-radius:13px;background:var(--dmr-accent-soft)}.dmr-share-layout{display:grid;grid-template-columns:196px minmax(0,1fr);gap:16px;align-items:center}.dmr-qr-card{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px;border-radius:12px;background:#fff;color:#111827;text-align:center}.dmr-qr-card img{display:block;width:176px;height:176px;max-width:100%;object-fit:contain}.dmr-qr-card strong{font-size:12px}.dmr-qr-card small{color:#4b5563;font-size:10px}.dmr-qr-loading{display:grid;place-items:center;width:176px;height:176px;color:#64748b;font-size:12px}.dmr-share-detail{min-width:0;display:flex;flex-direction:column;gap:10px}.dmr-share-detail>div:first-child{display:flex;flex-direction:column;gap:4px}.dmr-link{padding:10px 11px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all;color:var(--dsw-alias-label-secondary)}.dmr-share-actions{display:flex;gap:8px;flex-wrap:wrap}.dmr-share-actions .dmr-primary{align-self:auto}.dmr-overview{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.dmr-overview-card{display:flex;flex-direction:column;gap:3px;padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dmr-overview-card>span{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dmr-overview-card strong{font-size:16px;letter-spacing:-.01em}.dmr-section{display:flex;flex-direction:column;gap:9px}.dmr-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmr-section-head small{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dmr-text-button{border:0;background:transparent;color:var(--dmr-accent);padding:8px;min-height:36px;font-weight:600}.dmr-task-list{display:flex;flex-direction:column;gap:8px}.dmr-task{display:flex;align-items:flex-start;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dmr-task>div{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}.dmr-task strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmr-phase{flex:none;border-radius:999px;padding:3px 8px;font-size:11px;background:var(--dmr-accent-soft);color:var(--dmr-accent)}.dmr-phase.write{color:var(--dsw-alias-state-success-primary)}.dmr-phase.code{color:var(--dk-warn)}.dmr-phase.search{color:#0d9488}.dmr-empty{padding:18px 12px;text-align:center;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-label-secondary);font-size:12px}.dmr-note{display:flex;flex-direction:column;gap:9px;padding-top:2px}.dmr-note blockquote{margin:0;padding:10px 12px;border-left:3px solid var(--dmr-accent);border-radius:0 9px 9px 0;background:var(--dmr-accent-soft);white-space:pre-wrap;font-size:12px}.dmr-note textarea{min-height:88px;resize:vertical;line-height:1.5}.dmr-note .dmr-secondary{align-self:flex-start}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media (max-width:680px){.dmr{gap:14px;font-size:14px}.dmr h3{font-size:18px}.dmr-steps{grid-template-columns:1fr;gap:8px}.dmr-steps li{padding:10px}.dmr-steps small,.dmr-hero p,.dmr-status-head p,.dmr-field small,.dmr-share small,.dmr-overview-card small,.dmr-task small,.dmr-note p{font-size:12px}.dmr-network-grid{grid-template-columns:1fr;gap:10px}.dmr-field input,.dmr-field select{font-size:16px}.dmr-primary,.dmr-secondary{width:100%;font-size:14px}.dmr-share-layout{grid-template-columns:1fr;gap:12px}.dmr-qr-card{width:min(240px,100%);margin:0 auto}.dmr-qr-card img,.dmr-qr-loading{width:210px;height:210px}.dmr-share-actions{flex-direction:column}.dmr-share-actions .dmr-primary{width:100%}.dmr-overview{gap:8px}.dmr-overview-card{padding:11px}.dmr-status-head{gap:8px}.dmr-link{font-size:11px}.dmr-note textarea{font-size:16px;min-height:104px}.dmr-note .dmr-secondary{align-self:stretch}.dmr-compact .dmr-hero-icon{width:40px;height:40px;border-radius:13px}}@media (prefers-reduced-motion:reduce){.dmr-primary,.dmr-secondary,.dmr-text-button,.dmr-icon-button,.dmr-field input,.dmr-field select,.dmr-note textarea{transition:none}}.dmr-lan{flex:none;margin-top:6px;padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1)}.dmr-lan-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.dmr-lan-title{min-width:0;display:flex;flex-direction:column;gap:4px}.dmr-lan-title p{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}.dmr-lan-badge{flex:none;display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:5px 10px;font-size:11px;font-weight:700;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}.dmr-lan-badge.on{border-color:color-mix(in srgb,var(--dmr-accent) 45%,transparent);background:var(--dmr-accent-soft);color:var(--dmr-accent)}.dmr-lan-active,.dmr-lan-idle{display:flex;flex-direction:column;gap:12px}.dmr-lan-active .dmr-share-detail small strong{color:var(--dsw-alias-state-error-primary)}.dmr-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,var(--dsw-alias-border-l2));color:var(--dsw-alias-state-error-primary)}.dmr-danger:hover{border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
|
|
296
358
|
/* 抽屉把手:贴左边缘(同趣味游戏浮标的边缘吸附语言),top 由 place() 动态设定;
|
|
297
359
|
* z 序 70 < 遮罩 75 < 侧栏 80 < overlayLayer 90,面板/弹窗打开时自然被盖。 */
|
|
298
|
-
@media (max-width:700px){.dsh-mobile-drawer-btn{position:fixed;left:0;top:38vh;z-index:70;box-sizing:border-box;width:30px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.35));border-left:none;border-radius:0 12px 12px 0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 88%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary,#e6eaf2);box-shadow:4px 0 18px rgba(0,0,0,.28);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}.dsh-mobile-drawer-btn:active{transform:scale(.94)}.dsh-mobile-drawer-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}
|
|
360
|
+
@media (max-width:700px){.dsh-mobile-drawer-btn{position:fixed;left:0;top:38vh;z-index:70;box-sizing:border-box;width:30px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.35));border-left:none;border-radius:0 12px 12px 0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 88%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary,#e6eaf2);box-shadow:4px 0 18px rgba(0,0,0,.28);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}.dsh-mobile-drawer-btn:active{transform:scale(.94)}.dsh-mobile-drawer-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}
|
|
361
|
+
/* ---- 仿 ZCode 手机端:底部 Tab 栏 + 输入区吸附底部 ---- */
|
|
362
|
+
.dsh-mobile-tabbar{position:fixed;left:0;right:0;bottom:0;z-index:65;display:flex;align-items:stretch;justify-content:space-around;padding:6px 8px calc(8px + env(safe-area-inset-bottom,0px));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 92%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.25));touch-action:manipulation}
|
|
363
|
+
.dsh-mobile-tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:48px;padding:4px 0;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#8b95ab);font:600 11px/1.2 inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;border-radius:10px}
|
|
364
|
+
.dsh-mobile-tab svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
365
|
+
.dsh-mobile-tab.on{color:var(--dsw-alias-label-primary,#e6eaf2)}
|
|
366
|
+
.dsh-mobile-tab.on svg{color:var(--dk-accent,#4d9fff)}
|
|
367
|
+
.dsh-mobile-tab:active{transform:scale(.94)}
|
|
368
|
+
/* 输入区吸附到 Tab 栏上方:宿主把欢迎内容 justify-content:center 垂直居中,
|
|
369
|
+
* 手机视口下输入卡片悬在中部、下方留出大片空白。改为 flex-end 贴底,并给
|
|
370
|
+
* 滚动体留出 Tab 栏高度的内边距,避免内容被 Tab 栏遮挡。 */
|
|
371
|
+
[class*="scrollBody"]{justify-content:flex-end !important;padding-bottom:calc(72px + env(safe-area-inset-bottom,0px)) !important}
|
|
372
|
+
[class*="scrollBody"]>[class*="composerSeat"]{padding-bottom:6px}
|
|
373
|
+
/* 收起的侧栏轨道(collapsed rail)在窄屏只剩 1px 占位,但轨道里的按钮
|
|
374
|
+
* 仍会溢出贴在左边缘(功能坞/进化/设置三枚露出半边的圆钮)。隐藏整列,
|
|
375
|
+
* 这些入口已由底部 Tab 栏接管;展开抽屉(z 80)不受影响。 */
|
|
376
|
+
[class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}
|
|
377
|
+
/* 抽屉把手从屏幕中部挪到 Tab 栏上方一点,免得孤零零悬在半空 */
|
|
378
|
+
.dsh-mobile-drawer-btn{top:auto !important;bottom:calc(96px + env(safe-area-inset-bottom,0px))}}`,
|
|
299
379
|
View: MobileRelayView, HomeStat: MobileRelayHomeStat, Overlay: MobileRelayOverlay,
|
|
300
380
|
}
|
|
@@ -64,6 +64,19 @@ function fmtCostCnyExact(usd, rate) {
|
|
|
64
64
|
const v = (Number(usd) || 0) * (Number(rate) > 0 ? Number(rate) : 7.2);
|
|
65
65
|
return "¥" + v.toFixed(4);
|
|
66
66
|
}
|
|
67
|
+
function fmtDurCompact(ms) {
|
|
68
|
+
if (ms === null || ms === undefined || isNaN(ms)) return "—";
|
|
69
|
+
if (ms < 1000) return Math.round(ms) + "ms";
|
|
70
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
71
|
+
if (ms < 3600000) return Math.round(ms / 60000) + "m";
|
|
72
|
+
return (ms / 3600000).toFixed(1) + "h";
|
|
73
|
+
}
|
|
74
|
+
function startOfToday() {
|
|
75
|
+
const d = new Date();
|
|
76
|
+
d.setHours(0, 0, 0, 0);
|
|
77
|
+
return d.getTime();
|
|
78
|
+
}
|
|
79
|
+
function hourOf(ts) { return new Date(ts).getHours(); }
|
|
67
80
|
|
|
68
81
|
// ---------- CSV 导出 ----------
|
|
69
82
|
// 表头即「调用明细」表的中文列名, 列序/取值格式与页面完全一致(日期时间、千分位、命中%、¥xx.xxxx、
|
|
@@ -152,6 +165,201 @@ function statusInfo(r) {
|
|
|
152
165
|
return { code: 0, label: "…", cls: "pend", title: "进行中" };
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
// ---------- 图表统计(纯 CSS/SVG,无第三方图表库:视图由外壳直接渲染,依赖越少越稳) ----------
|
|
169
|
+
// 调色板按索引取色,循环使用;按值降序分配,颜色与排名绑定而不是与名字绑定。
|
|
170
|
+
const CHART_COLORS = ["#60a5fa", "#fbbf24", "#4ade80", "#f472b6", "#a78bfa", "#fb923c", "#2dd4bf", "#e879f9", "#94a3b8", "#f87171"];
|
|
171
|
+
function chartColor(i) { return CHART_COLORS[i % CHART_COLORS.length]; }
|
|
172
|
+
const CHART_METRICS = [
|
|
173
|
+
["totalTokens", "总 Token", (v) => fmtCompact(v)],
|
|
174
|
+
["calls", "调用次数", (v) => fmtNum(v)],
|
|
175
|
+
["cost", "金额", (v, rate) => fmtCostCny(v, rate)],
|
|
176
|
+
["outputTokens", "输出 Token", (v) => fmtCompact(v)],
|
|
177
|
+
];
|
|
178
|
+
const CHART_DIMS = [
|
|
179
|
+
["model", "模型"],
|
|
180
|
+
["provider", "提供商"],
|
|
181
|
+
["status", "状态"],
|
|
182
|
+
["effort", "推理强度"],
|
|
183
|
+
];
|
|
184
|
+
function emptyAgg() {
|
|
185
|
+
return { calls: 0, totalTokens: 0, outputTokens: 0, inputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, cost: 0, llmMs: 0, timed: 0 };
|
|
186
|
+
}
|
|
187
|
+
function addToAgg(a, r) {
|
|
188
|
+
a.calls += 1;
|
|
189
|
+
a.totalTokens += Number(r.totalTokens) || 0;
|
|
190
|
+
a.outputTokens += Number(r.outputTokens) || 0;
|
|
191
|
+
a.inputTokens += Number(r.inputTokens) || 0;
|
|
192
|
+
a.cacheReadTokens += Number(r.cacheReadTokens) || 0;
|
|
193
|
+
a.reasoningTokens += Number(r.reasoningTokens) || 0;
|
|
194
|
+
a.cost += Number(r.cost) || 0;
|
|
195
|
+
if (typeof r.llmMs === "number") { a.llmMs += r.llmMs; a.timed += 1; }
|
|
196
|
+
}
|
|
197
|
+
/** records → [{key, ...agg}],按 metric 降序。 */
|
|
198
|
+
function groupByKey(records, keyFn, metric) {
|
|
199
|
+
const map = new Map();
|
|
200
|
+
for (const r of records) {
|
|
201
|
+
const k = keyFn(r);
|
|
202
|
+
let a = map.get(k);
|
|
203
|
+
if (!a) { a = emptyAgg(); a.key = k; map.set(k, a); }
|
|
204
|
+
addToAgg(a, r);
|
|
205
|
+
}
|
|
206
|
+
return [...map.values()].sort((a, b) => (b[metric] || 0) - (a[metric] || 0));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 环形占比图:SVG conic 扇区 + 中央合计 + 图例(最多 8 项,其余并入「其他」)。 */
|
|
210
|
+
function DonutChart({ items, metric, metricFmt, rate, centerLabel, emptyText }) {
|
|
211
|
+
const total = items.reduce((s, it) => s + (Number(it[metric]) || 0), 0);
|
|
212
|
+
if (!items.length || total <= 0) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
213
|
+
const shown = items.slice(0, 8);
|
|
214
|
+
const restSum = items.slice(8).reduce((s, it) => s + (Number(it[metric]) || 0), 0);
|
|
215
|
+
if (restSum > 0) {
|
|
216
|
+
const restAgg = Object.assign(emptyAgg(), { key: "其他(" + (items.length - 8) + "项)" });
|
|
217
|
+
restAgg[metric] = restSum;
|
|
218
|
+
shown.push(restAgg);
|
|
219
|
+
}
|
|
220
|
+
const R = 15.9155; // 周长 100 的半径
|
|
221
|
+
let acc = 0;
|
|
222
|
+
const segs = shown.map((it, i) => {
|
|
223
|
+
const v = Number(it[metric]) || 0;
|
|
224
|
+
const frac = v / total;
|
|
225
|
+
const seg = (
|
|
226
|
+
<circle key={it.key} cx="21" cy="21" r={R} fill="none"
|
|
227
|
+
stroke={chartColor(i)} strokeWidth={frac > 0.02 ? 7 : 5}
|
|
228
|
+
strokeDasharray={(frac * 100) + " " + (100 - frac * 100)}
|
|
229
|
+
strokeDashoffset={String(25 - acc * 100)}>
|
|
230
|
+
<title>{it.key + ":" + metricFmt(v, rate) + "(" + (frac * 100).toFixed(1) + "%)"}</title>
|
|
231
|
+
</circle>
|
|
232
|
+
);
|
|
233
|
+
acc += frac;
|
|
234
|
+
return seg;
|
|
235
|
+
});
|
|
236
|
+
return (
|
|
237
|
+
<div className="dtok-donut-wrap">
|
|
238
|
+
<svg className="dtok-donut" viewBox="0 0 42 42" role="img">
|
|
239
|
+
<circle cx="21" cy="21" r={R} fill="none" stroke="var(--dsw-alias-border-l1)" strokeWidth="7" />
|
|
240
|
+
{segs}
|
|
241
|
+
<text x="21" y="19.5" textAnchor="middle" className="dtok-donut-v">{metricFmt(total, rate)}</text>
|
|
242
|
+
<text x="21" y="26" textAnchor="middle" className="dtok-donut-l">{centerLabel}</text>
|
|
243
|
+
</svg>
|
|
244
|
+
<div className="dtok-legend">
|
|
245
|
+
{shown.map((it, i) => {
|
|
246
|
+
const v = Number(it[metric]) || 0;
|
|
247
|
+
return (
|
|
248
|
+
<div className="dtok-legend-row" key={it.key} title={it.key}>
|
|
249
|
+
<span className="dtok-dot" style={{ background: chartColor(i) }} />
|
|
250
|
+
<span className="dtok-legend-k">{it.key}</span>
|
|
251
|
+
<span className="dtok-legend-v">{metricFmt(v, rate)} · {(v / total * 100).toFixed(1)}%</span>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 横向条形图:行 = 分组(默认模型),条长 = 指标值(默认耗时)。 */
|
|
261
|
+
function HBarChart({ items, metric, metricFmt, rate, maxRows, emptyText }) {
|
|
262
|
+
if (!items.length) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
263
|
+
const shown = items.slice(0, maxRows || 12);
|
|
264
|
+
const max = Math.max(...shown.map((it) => Number(it[metric]) || 0), 1e-9);
|
|
265
|
+
return (
|
|
266
|
+
<div className="dtok-hbars">
|
|
267
|
+
{shown.map((it, i) => {
|
|
268
|
+
const v = Number(it[metric]) || 0;
|
|
269
|
+
const pct = Math.max(1.5, (v / max) * 100);
|
|
270
|
+
return (
|
|
271
|
+
<div className="dtok-hbar-row" key={it.key} title={it.key + ":" + metricFmt(v, rate)}>
|
|
272
|
+
<span className="dtok-hbar-k">{it.key}</span>
|
|
273
|
+
<span className="dtok-hbar-track">
|
|
274
|
+
<span className="dtok-hbar-fill" style={{ width: pct + "%", background: chartColor(i) }} />
|
|
275
|
+
</span>
|
|
276
|
+
<span className="dtok-hbar-v">{metricFmt(v, rate)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 24 小时调用分布柱状图:柱高 = 指标值,hover 显示具体数值。 */
|
|
285
|
+
function HourChart({ hours, metric, metricFmt, rate, emptyText }) {
|
|
286
|
+
const total = hours.reduce((s, h) => s + (Number(h[metric]) || 0), 0);
|
|
287
|
+
if (total <= 0) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
288
|
+
const max = Math.max(...hours.map((h) => Number(h[metric]) || 0), 1e-9);
|
|
289
|
+
return (
|
|
290
|
+
<div className="dtok-hours" role="img">
|
|
291
|
+
{hours.map((h) => {
|
|
292
|
+
const v = Number(h[metric]) || 0;
|
|
293
|
+
const pct = v > 0 ? Math.max(2, (v / max) * 100) : 0;
|
|
294
|
+
return (
|
|
295
|
+
<span key={h.hour} className="dtok-hour-col" title={h.hour + " 时:" + metricFmt(v, rate)}>
|
|
296
|
+
{pct > 0 ? <span className="dtok-hour-bar" style={{ height: pct + "%" }} /> : null}
|
|
297
|
+
</span>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 图表统计区:所有图都基于「当前筛选结果 records」客户端聚合,筛选一变图表即时联动。
|
|
305
|
+
* scope = "range"(整个时间范围)| "today"(records 里属于今天的部分)。 */
|
|
306
|
+
function ChartsPanel({ records, rate }) {
|
|
307
|
+
const [metric, setMetric] = useState("totalTokens");
|
|
308
|
+
const [dim, setDimLocal] = useState("model");
|
|
309
|
+
const metricDef = CHART_METRICS.find((m) => m[0] === metric) || CHART_METRICS[0];
|
|
310
|
+
const metricFmt = metricDef[2];
|
|
311
|
+
const todayRecs = useMemo(() => {
|
|
312
|
+
const t0 = startOfToday();
|
|
313
|
+
return records.filter((r) => Number(r.time) >= t0);
|
|
314
|
+
}, [records]);
|
|
315
|
+
const todayByModel = useMemo(() => groupByKey(todayRecs, (r) => r.model || "(未知)", metric), [todayRecs, metric]);
|
|
316
|
+
const byDim = useMemo(() => groupByKey(records, (r) => r[dim] || "(空)", metric), [records, dim, metric]);
|
|
317
|
+
const modelTime = useMemo(() => groupByKey(records, (r) => r.model || "(未知)", "llmMs"), [records]);
|
|
318
|
+
const timedCalls = useMemo(() => records.reduce((s, r) => s + (typeof r.llmMs === "number" ? 1 : 0), 0), [records]);
|
|
319
|
+
const hours = useMemo(() => {
|
|
320
|
+
const arr = [];
|
|
321
|
+
for (let h = 0; h < 24; h++) { const a = emptyAgg(); a.hour = h; arr.push(a); }
|
|
322
|
+
for (const r of records) addToAgg(arr[hourOf(r.time)], r);
|
|
323
|
+
return arr;
|
|
324
|
+
}, [records]);
|
|
325
|
+
return (
|
|
326
|
+
<div className="dtok-charts">
|
|
327
|
+
<div className="dtok-chart-toolbar">
|
|
328
|
+
<span className="dtok-chart-toolbar-label">指标</span>
|
|
329
|
+
{CHART_METRICS.map(([id, label]) => (
|
|
330
|
+
<button key={id} className={"dtok-btn tiny" + (metric === id ? " primary" : "")} onClick={() => setMetric(id)}>{label}</button>
|
|
331
|
+
))}
|
|
332
|
+
<span className="dtok-chart-toolbar-note">图表跟随上方筛选条件(时间 / 会话 / 提供商 / 模型 / 状态 / 强度)实时联动</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div className="dtok-chart-grid">
|
|
335
|
+
<div className="dtok-chart-card">
|
|
336
|
+
<div className="dtok-chart-title">今日模型用量占比<span className="dtok-chart-sub">{metricDef[1]} · {todayRecs.length} 次调用</span></div>
|
|
337
|
+
<DonutChart items={todayByModel} metric={metric} metricFmt={metricFmt} rate={rate} centerLabel={"今日" + metricDef[1]} emptyText="今日暂无调用" />
|
|
338
|
+
</div>
|
|
339
|
+
<div className="dtok-chart-card">
|
|
340
|
+
<div className="dtok-chart-title">24 小时调用分布<span className="dtok-chart-sub">按小时聚合 · {metricDef[1]}</span></div>
|
|
341
|
+
<HourChart hours={hours} metric={metric} metricFmt={metricFmt} rate={rate} />
|
|
342
|
+
</div>
|
|
343
|
+
<div className="dtok-chart-card">
|
|
344
|
+
<div className="dtok-chart-title">模型耗时排行<span className="dtok-chart-sub">累计 LLM 耗时 · 已计时 {timedCalls} 次</span></div>
|
|
345
|
+
<HBarChart items={modelTime} metric="llmMs" metricFmt={(v) => fmtDurCompact(v)} rate={rate} emptyText="暂无计时数据" />
|
|
346
|
+
</div>
|
|
347
|
+
<div className="dtok-chart-card">
|
|
348
|
+
<div className="dtok-chart-title">
|
|
349
|
+
分布统计
|
|
350
|
+
<span className="dtok-chart-sub">
|
|
351
|
+
{CHART_DIMS.map(([id, label]) => (
|
|
352
|
+
<button key={id} className={"dtok-btn tiny" + (dim === id ? " primary" : "")} style={{ marginLeft: 4 }} onClick={() => setDimLocal(id)}>{label}</button>
|
|
353
|
+
))}
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
<HBarChart items={byDim} metric={metric} metricFmt={metricFmt} rate={rate} />
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
155
363
|
// ---------- 样式(dtok- 前缀,dock 面板内自适应:宽度铺满、表格横向滚动) ----------
|
|
156
364
|
const css = `
|
|
157
365
|
.dtok-root{display:flex;flex-direction:column;gap:8px;width:100%;min-width:0;}
|
|
@@ -283,6 +491,35 @@ const css = `
|
|
|
283
491
|
.dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
284
492
|
.dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
285
493
|
.dtok-src.fallback{color:var(--dk-warn);}
|
|
494
|
+
/* ---- 图表统计(纯 CSS/SVG) ---- */
|
|
495
|
+
.dtok-charts{display:flex;flex-direction:column;gap:10px;min-width:0;}
|
|
496
|
+
.dtok-chart-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);}
|
|
497
|
+
.dtok-chart-toolbar-label{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none;}
|
|
498
|
+
.dtok-chart-toolbar-note{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);margin-left:auto;}
|
|
499
|
+
.dtok-chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:10px;}
|
|
500
|
+
.dtok-chart-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;min-width:0;display:flex;flex-direction:column;gap:8px;}
|
|
501
|
+
.dtok-chart-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
502
|
+
.dtok-chart-sub{font-size:11px;font-weight:400;color:var(--dsw-alias-label-secondary);}
|
|
503
|
+
.dtok-chart-empty{text-align:center;color:var(--dsw-alias-label-secondary);padding:28px 0;font-size:12px;}
|
|
504
|
+
.dtok-donut-wrap{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
|
|
505
|
+
.dtok-donut{width:150px;height:150px;flex:none;}
|
|
506
|
+
.dtok-donut-v{font-size:6.5px;font-weight:700;fill:var(--dsw-alias-label-primary);}
|
|
507
|
+
.dtok-donut-l{font-size:3.6px;fill:var(--dsw-alias-label-secondary);}
|
|
508
|
+
.dtok-legend{flex:1;min-width:150px;display:flex;flex-direction:column;gap:3px;max-height:170px;overflow:auto;}
|
|
509
|
+
.dtok-legend-row{display:flex;align-items:center;gap:6px;font-size:11px;min-width:0;}
|
|
510
|
+
.dtok-dot{width:8px;height:8px;border-radius:50%;flex:none;}
|
|
511
|
+
.dtok-legend-k{color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
512
|
+
.dtok-legend-v{color:var(--dsw-alias-label-secondary);margin-left:auto;flex:none;}
|
|
513
|
+
.dtok-hbars{display:flex;flex-direction:column;gap:4px;max-height:220px;overflow:auto;}
|
|
514
|
+
.dtok-hbar-row{display:flex;align-items:center;gap:8px;font-size:11px;min-width:0;}
|
|
515
|
+
.dtok-hbar-k{flex:0 1 34%;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
516
|
+
.dtok-hbar-track{flex:1;height:12px;border-radius:4px;background:rgb(127 127 127 / .16);overflow:hidden;min-width:40px;}
|
|
517
|
+
.dtok-hbar-fill{display:block;height:100%;border-radius:4px;transition:width .3s ease;}
|
|
518
|
+
.dtok-hbar-v{flex:none;color:var(--dsw-alias-label-secondary);min-width:52px;text-align:right;}
|
|
519
|
+
.dtok-hours{display:flex;align-items:flex-end;gap:3px;height:130px;padding-top:4px;}
|
|
520
|
+
.dtok-hour-col{flex:1;display:flex;align-items:flex-end;height:100%;min-width:0;}
|
|
521
|
+
.dtok-hour-bar{display:block;width:100%;border-radius:3px 3px 0 0;background:linear-gradient(180deg,var(--dk-accent),rgb(127 127 127 / .25));min-height:2px;transition:height .3s ease;}
|
|
522
|
+
.dtok-hour-col:hover .dtok-hour-bar{filter:brightness(1.3);}
|
|
286
523
|
`;
|
|
287
524
|
|
|
288
525
|
// ---------- 详情弹窗 ----------
|
|
@@ -702,6 +939,9 @@ export function TokenLogView(props) {
|
|
|
702
939
|
// 「单价设置」折叠面板:默认收起(费用不准确时才需要展开调整)
|
|
703
940
|
// 「单价设置」子弹窗:默认收起;支持经 params.openPricing 直接打开(深链/测试用)
|
|
704
941
|
const [showPricing, setShowPricing] = useState(() => !!(props && props.params && props.params.openPricing));
|
|
942
|
+
// 明细 | 图表 页签:图表全部基于当前筛选结果客户端聚合,不额外发请求。
|
|
943
|
+
// params.charts 支持深链/测试直接落在图表页签。
|
|
944
|
+
const [tab, setTab] = useState(() => (props && props.params && props.params.charts) ? "charts" : "detail");
|
|
705
945
|
// 查询完成信号:loading 由 true→false 时记一次时间戳,用于「✓ 数据已更新」提示与 KPI 卡回弹。
|
|
706
946
|
// 5 秒静默自动刷新不置 loading,所以轮询不会每 5 秒闪一下。
|
|
707
947
|
const [fetchedAt, setFetchedAt] = useState(0);
|
|
@@ -929,6 +1169,17 @@ export function TokenLogView(props) {
|
|
|
929
1169
|
|
|
930
1170
|
const bodyNodes = [];
|
|
931
1171
|
if (err) bodyNodes.push(<div key="err" className="dtok-err">错误: {err}</div>);
|
|
1172
|
+
bodyNodes.push(
|
|
1173
|
+
<div key="tabs" className="dtok-filter" style={{ padding: "2px 0 0", border: "none", background: "transparent", gap: 4 }}>
|
|
1174
|
+
<button className={"dtok-btn" + (tab === "detail" ? " primary" : "")} onClick={() => setTab("detail")}>明细</button>
|
|
1175
|
+
<button className={"dtok-btn" + (tab === "charts" ? " primary" : "")} onClick={() => setTab("charts")}>图表统计</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
);
|
|
1178
|
+
if (tab === "charts") {
|
|
1179
|
+
bodyNodes.push(
|
|
1180
|
+
<ChartsPanel key={"charts-" + fetchedAt} records={records} rate={rateCny} />
|
|
1181
|
+
);
|
|
1182
|
+
} else {
|
|
932
1183
|
if (cards.length) bodyNodes.push(
|
|
933
1184
|
// key 带 fetchedAt:每次「手动查询 / 首次加载」拿到新数据时重建一次,KPI 卡重播回弹动画
|
|
934
1185
|
<div className={"dtok-cards" + (fetchedAt ? " dtok-pop" : "")} key={"cards-" + fetchedAt}>
|
|
@@ -982,6 +1233,7 @@ export function TokenLogView(props) {
|
|
|
982
1233
|
);
|
|
983
1234
|
bodyNodes.push(pager("pager-bottom", "bottom"));
|
|
984
1235
|
}
|
|
1236
|
+
}
|
|
985
1237
|
|
|
986
1238
|
return (
|
|
987
1239
|
<div className="dtok-root" onClick={(e) => e.stopPropagation()}>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价与多段分时价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|