dsh-router-core 0.1.0 → 0.3.0
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 +58 -13
- package/lib/client-registry.js +824 -153
- package/lib/client.js +824 -153
- package/lib/index.js +1606 -227
- package/lib/types/llm/adapter.d.ts +2 -0
- package/lib/types/router/account-pool.d.ts +41 -0
- package/lib/types/router/index.d.ts +72 -4
- package/lib/types/router/types.d.ts +12 -10
- package/lib/types/router/usage-store.d.ts +93 -0
- package/lib/types/router/usage-tokens.d.ts +118 -0
- package/lib/types/shared.d.ts +69 -0
- package/lib/types/supplier-config.d.ts +15 -4
- package/lib/types/suppliers/contract.d.ts +70 -21
- package/lib/types/suppliers/registry.d.ts +4 -3
- package/package.json +2 -1
- package/src/client/CombosTab.tsx +28 -16
- package/src/client/RouterView.tsx +15 -2
- package/src/client/StatsTab.tsx +521 -0
- package/src/client/SupplierDetail.tsx +58 -34
- package/src/client/router.css +406 -0
- package/src/client/workspace-mount.tsx +2 -9
- package/src/index.ts +52 -4
- package/src/llm/adapter.test.ts +108 -0
- package/src/llm/adapter.ts +18 -6
- package/src/router/account-pool.test.ts +120 -0
- package/src/router/account-pool.ts +161 -0
- package/src/router/index.ts +652 -26
- package/src/router/models-cache.test.ts +455 -0
- package/src/router/precise-dispatch.test.ts +198 -0
- package/src/router/types.ts +12 -7
- package/src/router/usage-store.ts +499 -0
- package/src/router/usage-tokens.ts +327 -0
- package/src/router/usage.test.ts +558 -0
- package/src/shared.ts +77 -0
- package/src/supplier-config.ts +31 -14
- package/src/suppliers/contract.ts +71 -8
- package/src/suppliers/loader.ts +21 -13
- package/src/suppliers/nvidia/plugin.ts +161 -119
- package/src/suppliers/opencode/plugin.ts +47 -95
- package/src/suppliers/openrouter/plugin.ts +67 -122
- package/src/suppliers/registry.test.ts +194 -0
- package/src/suppliers/registry.ts +79 -59
|
@@ -39,6 +39,7 @@ const I = {
|
|
|
39
39
|
edit: 'M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3zM13.5 6.5l3 3',
|
|
40
40
|
loading: 'M12 3a9 9 0 1 0 9 9M12 3a9 9 0 0 1 9 9',
|
|
41
41
|
search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3',
|
|
42
|
+
refresh: 'M20.5 12a8.5 8.5 0 1 1-2.6-6.1M20.5 4v5h-5',
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
interface SupplierDetailProps {
|
|
@@ -108,8 +109,8 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
108
109
|
const [removeModelTarget, setRemoveModelTarget] = useState<string | null>(null)
|
|
109
110
|
const [poolStrategy, setPoolStrategy] = useState<'fallback' | 'round-robin'>('fallback')
|
|
110
111
|
const [poolOrder, setPoolOrder] = useState<string[]>([])
|
|
111
|
-
const [checkinRule, setCheckinRule] = useState<'all' | 'first'>('all')
|
|
112
112
|
const [checkingIn, setCheckingIn] = useState(false)
|
|
113
|
+
const [refreshing, setRefreshing] = useState(false)
|
|
113
114
|
const [dragIndex, setDragIndex] = useState<number | null>(null)
|
|
114
115
|
const mounted = useRef(true)
|
|
115
116
|
|
|
@@ -144,20 +145,17 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
144
145
|
|
|
145
146
|
const loadPoolConfig = useCallback(async (): Promise<void> => {
|
|
146
147
|
try {
|
|
147
|
-
const [strategyRes, orderRes
|
|
148
|
+
const [strategyRes, orderRes] = await Promise.all([
|
|
148
149
|
fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, { cache: 'no-store' }),
|
|
149
150
|
fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/order`, { cache: 'no-store' }),
|
|
150
|
-
fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin/rule`, { cache: 'no-store' }),
|
|
151
151
|
])
|
|
152
|
-
const [strategyData, orderData
|
|
152
|
+
const [strategyData, orderData] = await Promise.all([strategyRes.json(), orderRes.json()]) as [
|
|
153
153
|
{ ok: boolean; strategy?: 'fallback' | 'round-robin' },
|
|
154
154
|
{ ok: boolean; order?: string[] },
|
|
155
|
-
{ ok: boolean; rule?: 'all' | 'first' },
|
|
156
155
|
]
|
|
157
156
|
if (mounted.current) {
|
|
158
157
|
if (strategyData.ok && strategyData.strategy) setPoolStrategy(strategyData.strategy)
|
|
159
158
|
if (orderData.ok && Array.isArray(orderData.order)) setPoolOrder(orderData.order)
|
|
160
|
-
if (ruleData.ok && ruleData.rule) setCheckinRule(ruleData.rule)
|
|
161
159
|
}
|
|
162
160
|
} catch {
|
|
163
161
|
// 连接池配置加载失败不阻断
|
|
@@ -209,6 +207,45 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
209
207
|
}
|
|
210
208
|
}
|
|
211
209
|
|
|
210
|
+
/** 刷新链接池:积分刷一遍 + 全供应商健康探测(最简会话,走真实 chatCompletions
|
|
211
|
+
* 并按账号池回退,所以「这个供应商还有没有活着的链接」能回答;不按链接细分 —— 那需要
|
|
212
|
+
* 按 uid 指定账号的管道,会给插件增负)。 */
|
|
213
|
+
const runRefresh = async (): Promise<void> => {
|
|
214
|
+
if (refreshing) return
|
|
215
|
+
setRefreshing(true)
|
|
216
|
+
let msg = ''
|
|
217
|
+
try {
|
|
218
|
+
const response = await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/links/refresh`, { method: 'POST', cache: 'no-store' })
|
|
219
|
+
const data = await response.json() as { ok: boolean; changed?: boolean; accounts?: unknown[]; error?: string }
|
|
220
|
+
if (!data.ok) {
|
|
221
|
+
showToast(data.error ?? '刷新失败')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
// 健康探测:拿一个启用模型跑最简会话;没有启用模型就退回任意第一个
|
|
225
|
+
const probe = models?.find(m => m.enabled) ?? models?.[0]
|
|
226
|
+
if (probe === undefined) {
|
|
227
|
+
msg = `已刷新 ${data.accounts?.length ?? 0} 链接 · 无模型可探测`
|
|
228
|
+
} else {
|
|
229
|
+
const probeRes = await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/test`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ id: probe.id }),
|
|
233
|
+
cache: 'no-store',
|
|
234
|
+
})
|
|
235
|
+
const probeData = await probeRes.json() as { ok: boolean; error?: string }
|
|
236
|
+
msg = probeData.ok
|
|
237
|
+
? `已刷新 ${data.accounts?.length ?? 0} 链接 · 健康`
|
|
238
|
+
: `已刷新 ${data.accounts?.length ?? 0} 链接 · 不可用:${probeData.error ?? '未知'}`
|
|
239
|
+
}
|
|
240
|
+
showToast(msg)
|
|
241
|
+
onRefresh()
|
|
242
|
+
} catch (err) {
|
|
243
|
+
showToast(`刷新失败:${(err as Error).message}`)
|
|
244
|
+
} finally {
|
|
245
|
+
setRefreshing(false)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
const startLogin = async (): Promise<void> => {
|
|
213
250
|
// API key 供应商:弹窗直接填名字+key,不走 URL 登录
|
|
214
251
|
if (canApiKey) {
|
|
@@ -238,15 +275,15 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
238
275
|
}
|
|
239
276
|
}
|
|
240
277
|
|
|
241
|
-
const completeLogin = async (): Promise<void> => {
|
|
242
|
-
if (callbackUrl.trim() === '') return
|
|
278
|
+
const completeLogin = async (poll = false): Promise<void> => {
|
|
279
|
+
if (!poll && callbackUrl.trim() === '') return
|
|
243
280
|
setBusy(true)
|
|
244
281
|
setError('')
|
|
245
282
|
try {
|
|
246
283
|
const response = await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/login/callback`, {
|
|
247
284
|
method: 'POST',
|
|
248
285
|
headers: { 'Content-Type': 'application/json' },
|
|
249
|
-
body: JSON.stringify({ callbackUrl: callbackUrl.trim() }),
|
|
286
|
+
body: JSON.stringify({ callbackUrl: poll ? '' : callbackUrl.trim() }),
|
|
250
287
|
cache: 'no-store',
|
|
251
288
|
})
|
|
252
289
|
const data = await response.json() as { ok: boolean; error?: string; account?: { uid: string; nickname: string } }
|
|
@@ -470,7 +507,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
470
507
|
const data = await response.json() as { ok: boolean; error?: string }
|
|
471
508
|
if (data.ok) {
|
|
472
509
|
setAlias(aliasDraft.trim())
|
|
473
|
-
// 同时保存连接池策略
|
|
510
|
+
// 同时保存连接池策略
|
|
474
511
|
try {
|
|
475
512
|
await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, {
|
|
476
513
|
method: 'POST',
|
|
@@ -481,16 +518,6 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
481
518
|
} catch {
|
|
482
519
|
// 策略保存失败不阻断前缀保存
|
|
483
520
|
}
|
|
484
|
-
try {
|
|
485
|
-
await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin/rule`, {
|
|
486
|
-
method: 'POST',
|
|
487
|
-
headers: { 'Content-Type': 'application/json' },
|
|
488
|
-
body: JSON.stringify({ rule: checkinRule }),
|
|
489
|
-
cache: 'no-store',
|
|
490
|
-
})
|
|
491
|
-
} catch {
|
|
492
|
-
// 签到规则保存失败不阻断前缀保存
|
|
493
|
-
}
|
|
494
521
|
setShowAlias(false)
|
|
495
522
|
} else {
|
|
496
523
|
setAliasError(data.error ?? '保存失败')
|
|
@@ -559,9 +586,16 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
559
586
|
<div className="dshr-cardHead">
|
|
560
587
|
<div className="dshr-cardTitle">链接池</div>
|
|
561
588
|
<div className="dshr-cardActions">
|
|
589
|
+
<button type="button" className="dshr-iconBtn dshr-iconBtn-sm" onClick={() => void runRefresh()} disabled={refreshing || supplierAccounts.length === 0} title="刷新积分并探测健康" aria-label="刷新积分并探测健康">
|
|
590
|
+
<span className={refreshing ? 'dshr-spin' : undefined}>
|
|
591
|
+
<Icon d={I.refresh} size={15} />
|
|
592
|
+
</span>
|
|
593
|
+
</button>
|
|
562
594
|
{canCheckin && (
|
|
563
|
-
<button type="button" className="dshr-
|
|
564
|
-
{checkingIn ? '
|
|
595
|
+
<button type="button" className="dshr-iconBtn dshr-iconBtn-sm" onClick={() => void runCheckin()} disabled={checkingIn || supplierAccounts.length === 0} title="签到所有链接" aria-label="签到所有链接">
|
|
596
|
+
<span className={checkingIn ? 'dshr-spin' : undefined}>
|
|
597
|
+
<Icon d={I.checkCircle} size={15} />
|
|
598
|
+
</span>
|
|
565
599
|
</button>
|
|
566
600
|
)}
|
|
567
601
|
{canLogin && (
|
|
@@ -792,7 +826,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
792
826
|
</Modal>
|
|
793
827
|
)}
|
|
794
828
|
|
|
795
|
-
{/* ---- 编辑供应商弹窗(前缀 +
|
|
829
|
+
{/* ---- 编辑供应商弹窗(前缀 + 连接池策略) ---- */}
|
|
796
830
|
{showAlias && (
|
|
797
831
|
<Modal title="编辑供应商" onClose={() => setShowAlias(false)}>
|
|
798
832
|
<div className="dshr-modalForm">
|
|
@@ -818,16 +852,6 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
818
852
|
<option value="round-robin">轮询</option>
|
|
819
853
|
</select>
|
|
820
854
|
</>
|
|
821
|
-
<label className="dshr-fieldLabel">签到规则</label>
|
|
822
|
-
<p className="dshr-muted">所有链接 = 签到每个链接;首个链接 = 只签到第一个链接。(dsh-router 通用策略)</p>
|
|
823
|
-
<select
|
|
824
|
-
className="dshr-select dshr-input"
|
|
825
|
-
value={checkinRule}
|
|
826
|
-
onChange={(e) => setCheckinRule(e.target.value as 'all' | 'first')}
|
|
827
|
-
>
|
|
828
|
-
<option value="all">所有链接</option>
|
|
829
|
-
<option value="first">首个链接</option>
|
|
830
|
-
</select>
|
|
831
855
|
{aliasError !== '' && <div className="dshr-alert"><strong>出错了</strong><span>{aliasError}</span></div>}
|
|
832
856
|
<div className="dshr-modalActions">
|
|
833
857
|
<button type="button" className="dshr-miniButton" onClick={() => setShowAlias(false)}>取消</button>
|
|
@@ -895,7 +919,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
895
919
|
<div className="dshr-loginStep"><span className="dshr-loginNum">2</span>登录成功后点「完成添加」,自动获取凭证</div>
|
|
896
920
|
{error !== '' && <div className="dshr-alert"><strong>出错了</strong><span>{error}</span></div>}
|
|
897
921
|
<div className="dshr-modalActions">
|
|
898
|
-
<button type="button" className="dshr-primaryButton" onClick={() => void completeLogin()} disabled={busy}>
|
|
922
|
+
<button type="button" className="dshr-primaryButton" onClick={() => void completeLogin(true)} disabled={busy}>
|
|
899
923
|
{busy ? '轮询中…' : '完成添加'}
|
|
900
924
|
</button>
|
|
901
925
|
</div>
|
package/src/client/router.css
CHANGED
|
@@ -430,14 +430,17 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-
|
|
|
430
430
|
|
|
431
431
|
.dshr-tabBody {
|
|
432
432
|
display: flex;
|
|
433
|
+
flex: 1 1 auto; /* 填满 content;自身滚动 */
|
|
433
434
|
min-width: 0;
|
|
434
435
|
min-height: 0;
|
|
435
436
|
flex-direction: column;
|
|
436
437
|
gap: 14px;
|
|
437
438
|
padding: 14px 18px;
|
|
439
|
+
overflow-y: auto;
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
.dshr-card {
|
|
443
|
+
flex: none; /* 不参与 flex 伸缩:高度=内容,避免被压缩裁切 */
|
|
441
444
|
min-width: 0;
|
|
442
445
|
border: 1px solid var(--rs-line);
|
|
443
446
|
border-radius: 10px;
|
|
@@ -1100,6 +1103,12 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-
|
|
|
1100
1103
|
|
|
1101
1104
|
.dshr-iconBtn-sm { width: 26px; height: 26px; }
|
|
1102
1105
|
|
|
1106
|
+
/* 禁用态必须看得出来:纯图标按钮没有文字可读(不像「刷新中…」),
|
|
1107
|
+
只靠 hover 反馈会让人以为还能点。 */
|
|
1108
|
+
.dshr-iconBtn:disabled { opacity: .4; cursor: default; }
|
|
1109
|
+
|
|
1110
|
+
.dshr-iconBtn:disabled:hover { color: var(--rs-muted); background: transparent; }
|
|
1111
|
+
|
|
1103
1112
|
/* Require API key 行 */
|
|
1104
1113
|
.dshr-requireRow {
|
|
1105
1114
|
display: flex;
|
|
@@ -2094,3 +2103,400 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-
|
|
|
2094
2103
|
width: 1px;
|
|
2095
2104
|
background: var(--rs-line);
|
|
2096
2105
|
}
|
|
2106
|
+
|
|
2107
|
+
/* ============================================================
|
|
2108
|
+
dsh-router — 概览(用量看板,9router Overview 风格)
|
|
2109
|
+
============================================================ */
|
|
2110
|
+
|
|
2111
|
+
/* 周期切换 + 操作条 */
|
|
2112
|
+
.dshr-statsBar {
|
|
2113
|
+
display: flex;
|
|
2114
|
+
flex: none;
|
|
2115
|
+
flex-wrap: wrap;
|
|
2116
|
+
align-items: center;
|
|
2117
|
+
gap: 10px;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
.dshr-periods {
|
|
2121
|
+
display: inline-flex;
|
|
2122
|
+
gap: 2px;
|
|
2123
|
+
padding: 2px;
|
|
2124
|
+
border: 1px solid var(--rs-line);
|
|
2125
|
+
border-radius: 8px;
|
|
2126
|
+
background: var(--rs-layer-2);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/* 周期按钮三态。特异性阶梯(类/伪类数 , 元素数):
|
|
2130
|
+
文件顶部有 `.dshr-shell button { color: inherit; font: inherit }` = (1,1),
|
|
2131
|
+
单类的 .dshr-period (1,0) 会被它打死(白字、600 字重全丢),所以下面每条
|
|
2132
|
+
都必须带 .dshr-shell 抬到 (2,0) 以上。
|
|
2133
|
+
两种 hover 用 :not() 做成互斥,谁赢跟源码顺序无关(免得以后有人挪块就翻车)。 */
|
|
2134
|
+
.dshr-shell .dshr-period {
|
|
2135
|
+
min-width: 46px;
|
|
2136
|
+
height: 26px;
|
|
2137
|
+
padding: 0 10px;
|
|
2138
|
+
border: none;
|
|
2139
|
+
border-radius: 6px;
|
|
2140
|
+
color: var(--rs-muted);
|
|
2141
|
+
background: transparent;
|
|
2142
|
+
font-size: 12px;
|
|
2143
|
+
font-weight: 500;
|
|
2144
|
+
cursor: pointer;
|
|
2145
|
+
transition: color .12s ease, background .12s ease, filter .12s ease;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/* 未激活悬停:跟 .dshr-miniButton / .dshr-iconButton 一个套路(主文字色 + --rs-hover 底) */
|
|
2149
|
+
.dshr-shell .dshr-period:not(.dshr-period-on):hover {
|
|
2150
|
+
color: var(--rs-text);
|
|
2151
|
+
background: var(--rs-hover);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/* 激活:强调色块 + 白字。底色用 layer-1 时跟卡片同色,根本看不出选中了哪个 */
|
|
2155
|
+
.dshr-shell .dshr-period-on {
|
|
2156
|
+
color: #fff;
|
|
2157
|
+
background: var(--rs-accent);
|
|
2158
|
+
font-weight: 600;
|
|
2159
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / .12);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/* 激活态被悬停:跟 .dshr-primaryButton 一样提亮,表示「它还是能点的」 */
|
|
2163
|
+
.dshr-shell .dshr-period-on:hover {
|
|
2164
|
+
color: #fff;
|
|
2165
|
+
background: var(--rs-accent);
|
|
2166
|
+
filter: brightness(1.08);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
.dshr-statsBarRight {
|
|
2170
|
+
display: flex;
|
|
2171
|
+
align-items: center;
|
|
2172
|
+
gap: 6px;
|
|
2173
|
+
margin-left: auto;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.dshr-lifetime {
|
|
2177
|
+
font-size: 11px;
|
|
2178
|
+
white-space: nowrap;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/* 汇总卡(照 9router OverviewCards 的自适应五宫格) */
|
|
2182
|
+
.dshr-statGrid {
|
|
2183
|
+
display: grid;
|
|
2184
|
+
flex: none;
|
|
2185
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
2186
|
+
gap: 10px;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
.dshr-statCard {
|
|
2190
|
+
display: flex;
|
|
2191
|
+
min-width: 0;
|
|
2192
|
+
flex-direction: column;
|
|
2193
|
+
gap: 3px;
|
|
2194
|
+
padding: 12px 14px;
|
|
2195
|
+
border: 1px solid var(--rs-line);
|
|
2196
|
+
border-radius: 10px;
|
|
2197
|
+
background: var(--rs-layer-1);
|
|
2198
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / .04);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.dshr-statLabel {
|
|
2202
|
+
overflow: hidden;
|
|
2203
|
+
color: var(--rs-faint);
|
|
2204
|
+
font-size: 11px;
|
|
2205
|
+
font-weight: 600;
|
|
2206
|
+
letter-spacing: .03em;
|
|
2207
|
+
text-overflow: ellipsis;
|
|
2208
|
+
text-transform: uppercase;
|
|
2209
|
+
white-space: nowrap;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
.dshr-statValue {
|
|
2213
|
+
overflow: hidden;
|
|
2214
|
+
font-size: 22px;
|
|
2215
|
+
font-weight: 700;
|
|
2216
|
+
letter-spacing: -.01em;
|
|
2217
|
+
line-height: 1.2;
|
|
2218
|
+
text-overflow: ellipsis;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
.dshr-statHint {
|
|
2222
|
+
color: var(--rs-faint);
|
|
2223
|
+
font-size: 11px;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.dshr-statCard-primary .dshr-statValue { color: var(--rs-accent); }
|
|
2227
|
+
.dshr-statCard-ok .dshr-statValue { color: var(--rs-success); }
|
|
2228
|
+
.dshr-statCard-warn .dshr-statValue { color: var(--rs-warn); }
|
|
2229
|
+
.dshr-statCard-danger .dshr-statValue { color: var(--rs-danger); }
|
|
2230
|
+
|
|
2231
|
+
.dshr-estimateNote {
|
|
2232
|
+
margin: 0;
|
|
2233
|
+
color: var(--rs-faint);
|
|
2234
|
+
font-size: 11px;
|
|
2235
|
+
line-height: 1.6;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
.dshr-estimateNote code {
|
|
2239
|
+
padding: 0 3px;
|
|
2240
|
+
border-radius: 3px;
|
|
2241
|
+
background: var(--rs-layer-2);
|
|
2242
|
+
font-family: var(--rs-mono);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/* 趋势折线图(纯 SVG + HTML 叠加层,不引图表库) */
|
|
2246
|
+
.dshr-trendWrap {
|
|
2247
|
+
position: relative;
|
|
2248
|
+
padding: 14px 14px 10px;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
.dshr-trendBusy {
|
|
2252
|
+
position: absolute;
|
|
2253
|
+
inset: 0;
|
|
2254
|
+
background: color-mix(in srgb, var(--rs-layer-1) 55%, transparent);
|
|
2255
|
+
pointer-events: none;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/* 绘图区:高度跟 SVG 视口一致,叠加层用百分比定位才对得上 */
|
|
2259
|
+
.dshr-linePlot {
|
|
2260
|
+
position: relative;
|
|
2261
|
+
height: 150px;
|
|
2262
|
+
cursor: crosshair;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/* 键盘可达(←/→ 逐格):聚焦环跟面板其它控件一致 */
|
|
2266
|
+
.dshr-linePlot:focus-visible {
|
|
2267
|
+
outline: 2px solid var(--rs-accent);
|
|
2268
|
+
outline-offset: 2px;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/* 峰值:给读数一个量级参照;半透明底是怕压在折线上看不清 */
|
|
2272
|
+
.dshr-linePeak {
|
|
2273
|
+
position: absolute;
|
|
2274
|
+
top: 0;
|
|
2275
|
+
right: 0;
|
|
2276
|
+
padding: 0 4px;
|
|
2277
|
+
border-radius: 3px;
|
|
2278
|
+
color: var(--rs-faint);
|
|
2279
|
+
background: color-mix(in srgb, var(--rs-layer-1) 72%, transparent);
|
|
2280
|
+
font-size: 9px;
|
|
2281
|
+
font-variant-numeric: tabular-nums;
|
|
2282
|
+
pointer-events: none;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
.dshr-lineSvg {
|
|
2286
|
+
position: absolute;
|
|
2287
|
+
display: block;
|
|
2288
|
+
width: 100%;
|
|
2289
|
+
height: 100%;
|
|
2290
|
+
inset: 0;
|
|
2291
|
+
overflow: visible;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
.dshr-lineGrid {
|
|
2295
|
+
stroke: var(--rs-line);
|
|
2296
|
+
stroke-dasharray: 3 3;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.dshr-lineArea {
|
|
2300
|
+
fill: color-mix(in srgb, var(--rs-accent) 14%, transparent);
|
|
2301
|
+
stroke: none;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
.dshr-linePath {
|
|
2305
|
+
fill: none;
|
|
2306
|
+
stroke: var(--rs-accent);
|
|
2307
|
+
stroke-width: 2;
|
|
2308
|
+
stroke-linejoin: round;
|
|
2309
|
+
stroke-linecap: round;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/* 数值点:空心小圆(SVG 会被横向拉伸,所以画在 HTML 层) */
|
|
2313
|
+
.dshr-linePoint {
|
|
2314
|
+
position: absolute;
|
|
2315
|
+
width: 5px;
|
|
2316
|
+
height: 5px;
|
|
2317
|
+
border: 1.5px solid var(--rs-accent);
|
|
2318
|
+
border-radius: 50%;
|
|
2319
|
+
background: var(--rs-layer-1);
|
|
2320
|
+
transform: translate(-50%, -50%);
|
|
2321
|
+
pointer-events: none;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
.dshr-lineGuide {
|
|
2325
|
+
position: absolute;
|
|
2326
|
+
top: 0;
|
|
2327
|
+
bottom: 0;
|
|
2328
|
+
width: 1px;
|
|
2329
|
+
background: var(--rs-line-strong);
|
|
2330
|
+
pointer-events: none;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
.dshr-lineDot {
|
|
2334
|
+
position: absolute;
|
|
2335
|
+
width: 9px;
|
|
2336
|
+
height: 9px;
|
|
2337
|
+
border: 2px solid var(--rs-layer-1);
|
|
2338
|
+
border-radius: 50%;
|
|
2339
|
+
background: var(--rs-accent);
|
|
2340
|
+
box-shadow: 0 0 0 1px var(--rs-accent);
|
|
2341
|
+
transform: translate(-50%, -50%);
|
|
2342
|
+
pointer-events: none;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.dshr-lineLabels {
|
|
2346
|
+
position: relative;
|
|
2347
|
+
height: 14px;
|
|
2348
|
+
margin-top: 5px;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
.dshr-lineLabel {
|
|
2352
|
+
position: absolute;
|
|
2353
|
+
top: 0;
|
|
2354
|
+
overflow: visible;
|
|
2355
|
+
color: var(--rs-faint);
|
|
2356
|
+
font-size: 9px;
|
|
2357
|
+
font-variant-numeric: tabular-nums;
|
|
2358
|
+
white-space: nowrap;
|
|
2359
|
+
transform: translateX(-50%);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/* 选中的时段:标签跟着高亮,不用抬头对读数才知道看的是哪一段 */
|
|
2363
|
+
.dshr-lineLabel-on {
|
|
2364
|
+
color: var(--rs-accent);
|
|
2365
|
+
font-weight: 700;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
.dshr-trendReadout {
|
|
2369
|
+
display: flex;
|
|
2370
|
+
align-items: center;
|
|
2371
|
+
gap: 2px;
|
|
2372
|
+
min-height: 18px;
|
|
2373
|
+
padding-top: 6px;
|
|
2374
|
+
font-size: 11px;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
.dshr-trendEmpty {
|
|
2378
|
+
display: flex;
|
|
2379
|
+
height: 150px;
|
|
2380
|
+
flex-direction: column;
|
|
2381
|
+
align-items: center;
|
|
2382
|
+
justify-content: center;
|
|
2383
|
+
gap: 6px;
|
|
2384
|
+
color: var(--rs-faint);
|
|
2385
|
+
font-size: 12px;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
.dshr-trendEmptyIcon {
|
|
2389
|
+
display: inline-flex;
|
|
2390
|
+
align-items: center;
|
|
2391
|
+
justify-content: center;
|
|
2392
|
+
width: 44px;
|
|
2393
|
+
height: 44px;
|
|
2394
|
+
border-radius: 50%;
|
|
2395
|
+
color: var(--rs-accent);
|
|
2396
|
+
background: color-mix(in srgb, var(--rs-accent) 10%, transparent);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/* Top 榜(供应商 / 模型并排,窄屏堆叠) */
|
|
2400
|
+
.dshr-rankPair {
|
|
2401
|
+
display: grid;
|
|
2402
|
+
flex: none;
|
|
2403
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
2404
|
+
gap: 12px;
|
|
2405
|
+
align-items: start;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
.dshr-rankList,
|
|
2409
|
+
.dshr-recentList {
|
|
2410
|
+
display: flex;
|
|
2411
|
+
flex-direction: column;
|
|
2412
|
+
padding: 6px 14px 12px;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.dshr-rankRow,
|
|
2416
|
+
.dshr-recentRow {
|
|
2417
|
+
display: flex;
|
|
2418
|
+
align-items: center;
|
|
2419
|
+
gap: 10px;
|
|
2420
|
+
min-width: 0;
|
|
2421
|
+
padding: 7px 0;
|
|
2422
|
+
border-bottom: 1px solid var(--rs-line);
|
|
2423
|
+
font-size: 12px;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
.dshr-rankRow:last-child,
|
|
2427
|
+
.dshr-recentRow:last-child { border-bottom: none; }
|
|
2428
|
+
|
|
2429
|
+
/* 表头:不参与 hover,且不画分隔线(视觉上属于卡片头) */
|
|
2430
|
+
.dshr-rankHead {
|
|
2431
|
+
border-bottom: 1px solid var(--rs-line);
|
|
2432
|
+
color: var(--rs-faint);
|
|
2433
|
+
font-size: 11px;
|
|
2434
|
+
font-weight: 600;
|
|
2435
|
+
letter-spacing: .03em;
|
|
2436
|
+
text-transform: uppercase;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
.dshr-rankRow:not(.dshr-rankHead):hover { background: var(--rs-hover); }
|
|
2440
|
+
|
|
2441
|
+
.dshr-rankName {
|
|
2442
|
+
min-width: 0;
|
|
2443
|
+
flex: 1 1 auto;
|
|
2444
|
+
overflow: hidden;
|
|
2445
|
+
text-overflow: ellipsis;
|
|
2446
|
+
white-space: nowrap;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
.dshr-rankNum {
|
|
2450
|
+
flex: none;
|
|
2451
|
+
min-width: 52px;
|
|
2452
|
+
text-align: right;
|
|
2453
|
+
font-variant-numeric: tabular-nums;
|
|
2454
|
+
white-space: nowrap;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
.dshr-rankFail { color: var(--rs-danger); }
|
|
2458
|
+
|
|
2459
|
+
.dshr-tokenIn { color: var(--rs-accent); }
|
|
2460
|
+
.dshr-tokenOut { color: var(--rs-success); }
|
|
2461
|
+
|
|
2462
|
+
/* 最近请求列宽(比 Top 榜宽,要放得下模型名) */
|
|
2463
|
+
.dshr-recentTime {
|
|
2464
|
+
display: flex;
|
|
2465
|
+
flex: none;
|
|
2466
|
+
align-items: center;
|
|
2467
|
+
gap: 5px;
|
|
2468
|
+
width: 78px;
|
|
2469
|
+
color: var(--rs-muted);
|
|
2470
|
+
font-variant-numeric: tabular-nums;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
.dshr-recentModel {
|
|
2474
|
+
min-width: 0;
|
|
2475
|
+
flex: 1 1 auto;
|
|
2476
|
+
overflow: hidden;
|
|
2477
|
+
text-overflow: ellipsis;
|
|
2478
|
+
white-space: nowrap;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
.dshr-recentSupplier {
|
|
2482
|
+
flex: none;
|
|
2483
|
+
width: 92px;
|
|
2484
|
+
overflow: hidden;
|
|
2485
|
+
color: var(--rs-muted);
|
|
2486
|
+
text-overflow: ellipsis;
|
|
2487
|
+
white-space: nowrap;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
.dshr-recentRow-fail .dshr-recentModel { color: var(--rs-danger); }
|
|
2491
|
+
|
|
2492
|
+
/* 状态点 */
|
|
2493
|
+
.dshr-dot {
|
|
2494
|
+
display: inline-block;
|
|
2495
|
+
flex: none;
|
|
2496
|
+
width: 6px;
|
|
2497
|
+
height: 6px;
|
|
2498
|
+
border-radius: 50%;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
.dshr-dot-ok { background: var(--rs-success); }
|
|
2502
|
+
.dshr-dot-fail { background: var(--rs-danger); }
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* other sidebar panels (taskboard / ssh / mnemon) through the shared
|
|
6
6
|
* `dsh-panel-activate` event.
|
|
7
7
|
*/
|
|
8
|
-
import { useEffect } from 'react'
|
|
9
8
|
import { createRoot, type Root } from 'react-dom/client'
|
|
10
|
-
import { ROUTER_ACTIVE_ATTR
|
|
9
|
+
import { ROUTER_ACTIVE_ATTR } from '../shared.ts'
|
|
11
10
|
import { RouterView } from './RouterView.tsx'
|
|
12
11
|
import { mountRouterSidebarEntry } from './sidebar-entry.ts'
|
|
13
12
|
import { RouterWorkspaceController } from './workspace-controller.ts'
|
|
@@ -24,13 +23,7 @@ function conversationColumn(): HTMLElement | undefined {
|
|
|
24
23
|
|
|
25
24
|
function mountPanel(controller: RouterWorkspaceController): () => void {
|
|
26
25
|
function Panel(): JSX.Element {
|
|
27
|
-
|
|
28
|
-
// Keep the panel title in the browser tab for orientation, like the
|
|
29
|
-
// official panels do.
|
|
30
|
-
const original = document.title
|
|
31
|
-
document.title = ROUTER_TITLE
|
|
32
|
-
return () => { document.title = original }
|
|
33
|
-
}, [])
|
|
26
|
+
// 不改 document.title:这是宿主页面(会话)的标题,插件不该动它。
|
|
34
27
|
return <RouterView onBack={() => controller.close()} />
|
|
35
28
|
}
|
|
36
29
|
|