dsh-model-health 0.2.0 → 0.2.2
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/dist/client.js +142 -73
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -17,7 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
var exports = module.exports;
|
|
18
18
|
|
|
19
19
|
const React = require("react");
|
|
20
|
-
const { useState, useEffect, useCallback } = React;
|
|
20
|
+
const { useState, useEffect, useCallback, useRef } = React;
|
|
21
21
|
|
|
22
22
|
// ── 多语言词典(locale 服务,语言跟随 harness)─────────────────────────
|
|
23
23
|
const NS = "dsh-model-health";
|
|
@@ -48,6 +48,8 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
statusOk: "可用",
|
|
49
49
|
statusFail: "不可用",
|
|
50
50
|
statusSkip: "跳过",
|
|
51
|
+
testOne: "测试",
|
|
52
|
+
testOneTip: "点击测试此模型",
|
|
51
53
|
};
|
|
52
54
|
const en = {
|
|
53
55
|
tab: "Model Health",
|
|
@@ -76,6 +78,8 @@ window.__ModuleLoader__.load({
|
|
|
76
78
|
statusOk: "Available",
|
|
77
79
|
statusFail: "Unavailable",
|
|
78
80
|
statusSkip: "Skipped",
|
|
81
|
+
testOne: "Test",
|
|
82
|
+
testOneTip: "Click to test this model",
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
// 内联样式(适配 DSH 明暗主题的 CSS 变量)
|
|
@@ -139,6 +143,20 @@ window.__ModuleLoader__.load({
|
|
|
139
143
|
opacity: 0.5,
|
|
140
144
|
cursor: "not-allowed",
|
|
141
145
|
},
|
|
146
|
+
// 状态列里的"测试"小按钮(未测试的模型显示它代替"未测试"文字)
|
|
147
|
+
btnTestSmall: {
|
|
148
|
+
boxSizing: "border-box",
|
|
149
|
+
height: 22,
|
|
150
|
+
padding: "0 12px",
|
|
151
|
+
fontSize: 12,
|
|
152
|
+
lineHeight: "16px",
|
|
153
|
+
cursor: "pointer",
|
|
154
|
+
border: "1px solid var(--dsw-alias-button-primary-fill, #4f46e5)",
|
|
155
|
+
borderRadius: 11,
|
|
156
|
+
background: "transparent",
|
|
157
|
+
color: "var(--dsw-alias-button-primary-fill, #4f46e5)",
|
|
158
|
+
whiteSpace: "nowrap",
|
|
159
|
+
},
|
|
142
160
|
progress: {
|
|
143
161
|
fontSize: 12,
|
|
144
162
|
color: "var(--dsw-alias-label-tertiary, #888)",
|
|
@@ -179,33 +197,49 @@ window.__ModuleLoader__.load({
|
|
|
179
197
|
fontSize: 12,
|
|
180
198
|
fontFamily: "var(--ds-font-family-code, monospace)",
|
|
181
199
|
},
|
|
182
|
-
|
|
183
|
-
display: "inline-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
statusWrap: {
|
|
201
|
+
display: "inline-flex",
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
gap: 6,
|
|
186
204
|
fontSize: 12,
|
|
187
|
-
fontWeight: 500,
|
|
188
205
|
whiteSpace: "nowrap",
|
|
189
206
|
},
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
statusTextIdle: {
|
|
208
|
+
color: "var(--dsw-alias-label-tertiary, #999)",
|
|
209
|
+
},
|
|
210
|
+
statusTextTesting: {
|
|
211
|
+
color: "var(--dsw-alias-state-info-primary, #1967d2)",
|
|
212
|
+
},
|
|
213
|
+
statusTextOk: {
|
|
192
214
|
color: "var(--dsw-alias-state-success-primary, #1e8e3e)",
|
|
193
215
|
},
|
|
194
|
-
|
|
195
|
-
background: "var(--dsw-alias-state-error-bg, #fce8e6)",
|
|
216
|
+
statusTextFail: {
|
|
196
217
|
color: "var(--dsw-alias-state-error-primary, #d32f2f)",
|
|
197
218
|
},
|
|
198
|
-
|
|
199
|
-
background: "var(--dsw-alias-state-info-bg, #e8f0fe)",
|
|
200
|
-
color: "var(--dsw-alias-state-info-primary, #1967d2)",
|
|
201
|
-
},
|
|
202
|
-
badgeSkip: {
|
|
203
|
-
background: "var(--dsw-alias-bg-layer-2, #f0f0f0)",
|
|
219
|
+
statusTextSkip: {
|
|
204
220
|
color: "var(--dsw-alias-label-tertiary, #888)",
|
|
205
221
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
dot: {
|
|
223
|
+
width: 6,
|
|
224
|
+
height: 6,
|
|
225
|
+
borderRadius: "50%",
|
|
226
|
+
flex: "none",
|
|
227
|
+
},
|
|
228
|
+
dotIdle: {
|
|
229
|
+
background: "var(--dsw-alias-label-dimmed, #bbb)",
|
|
230
|
+
},
|
|
231
|
+
dotTesting: {
|
|
232
|
+
background: "var(--dsw-alias-state-info-primary, #1967d2)",
|
|
233
|
+
animation: "dsmh-pulse 1s ease-in-out infinite",
|
|
234
|
+
},
|
|
235
|
+
dotOk: {
|
|
236
|
+
background: "var(--dsw-alias-state-success-primary, #1e8e3e)",
|
|
237
|
+
},
|
|
238
|
+
dotFail: {
|
|
239
|
+
background: "var(--dsw-alias-state-error-primary, #d32f2f)",
|
|
240
|
+
},
|
|
241
|
+
dotSkip: {
|
|
242
|
+
background: "var(--dsw-alias-label-dimmed, #bbb)",
|
|
209
243
|
},
|
|
210
244
|
center: {
|
|
211
245
|
textAlign: "center",
|
|
@@ -265,13 +299,31 @@ window.__ModuleLoader__.load({
|
|
|
265
299
|
fail: "statusFail",
|
|
266
300
|
skip: "statusSkip",
|
|
267
301
|
};
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
302
|
+
// 状态 → 圆点颜色 / 文字颜色映射(圆点 + 彩色文字;测试中圆点带脉冲动画)
|
|
303
|
+
const STATUS_DOT_STYLE = {
|
|
304
|
+
idle: STYLES.dotIdle,
|
|
305
|
+
testing: STYLES.dotTesting,
|
|
306
|
+
ok: STYLES.dotOk,
|
|
307
|
+
fail: STYLES.dotFail,
|
|
308
|
+
skip: STYLES.dotSkip,
|
|
274
309
|
};
|
|
310
|
+
const STATUS_TEXT_STYLE = {
|
|
311
|
+
idle: STYLES.statusTextIdle,
|
|
312
|
+
testing: STYLES.statusTextTesting,
|
|
313
|
+
ok: STYLES.statusTextOk,
|
|
314
|
+
fail: STYLES.statusTextFail,
|
|
315
|
+
skip: STYLES.statusTextSkip,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** 注入测试中圆点的脉冲动画 keyframes(内联样式无法定义 @keyframes)。 */
|
|
319
|
+
function ensureStatusCss() {
|
|
320
|
+
const id = "dsh-model-health-status-style";
|
|
321
|
+
if (document.getElementById(id)) return;
|
|
322
|
+
const el = document.createElement("style");
|
|
323
|
+
el.id = id;
|
|
324
|
+
el.textContent = "@keyframes dsmh-pulse { 0%,100% { opacity:1; } 50% { opacity:.25; } }";
|
|
325
|
+
document.head.appendChild(el);
|
|
326
|
+
}
|
|
275
327
|
|
|
276
328
|
/**
|
|
277
329
|
* 模型列表 section 组件。
|
|
@@ -321,6 +373,9 @@ window.__ModuleLoader__.load({
|
|
|
321
373
|
// 初始值从 localStorage 读取,实现跨刷新持久化
|
|
322
374
|
const [testResults, setTestResults] = useState(loadStoredResults);
|
|
323
375
|
const [testing, setTesting] = useState(false);
|
|
376
|
+
// 请求序号:同一模型重复触发测试(单测 + 测试全部并发)时,
|
|
377
|
+
// 只有最新一次请求的结果允许写入,避免旧响应覆盖新状态
|
|
378
|
+
const seqRef = useRef({});
|
|
324
379
|
const [progress, setProgress] = useState({ done: 0, total: 0 });
|
|
325
380
|
// 可选列显示状态:{ [colKey]: boolean },默认 false
|
|
326
381
|
const [optionalCols, setOptionalCols] = useState({});
|
|
@@ -352,6 +407,42 @@ window.__ModuleLoader__.load({
|
|
|
352
407
|
loadData();
|
|
353
408
|
}, [loadData]);
|
|
354
409
|
|
|
410
|
+
// 测试单个模型:该行置为 testing → POST /api/model-health/test → 写回结果
|
|
411
|
+
// ("测试全部"与状态列的"测试"按钮共用此逻辑)
|
|
412
|
+
const testModel = useCallback(async (key) => {
|
|
413
|
+
const seq = (seqRef.current[key] || 0) + 1;
|
|
414
|
+
seqRef.current[key] = seq;
|
|
415
|
+
setTestResults((s) => ({ ...s, [key]: { status: "testing" } }));
|
|
416
|
+
|
|
417
|
+
let result;
|
|
418
|
+
try {
|
|
419
|
+
const resp = await fetch("/api/model-health/test", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { "Content-Type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ key }),
|
|
423
|
+
cache: "no-store",
|
|
424
|
+
});
|
|
425
|
+
const json = await resp.json();
|
|
426
|
+
result = !json.ok
|
|
427
|
+
? { status: "fail", error: json.error }
|
|
428
|
+
: {
|
|
429
|
+
status: json.status || "fail",
|
|
430
|
+
latency: json.latency,
|
|
431
|
+
error: json.error,
|
|
432
|
+
};
|
|
433
|
+
} catch (e) {
|
|
434
|
+
result = { status: "fail", error: e.message };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 期间若同一模型又发起了更新的测试,丢弃这份过期结果
|
|
438
|
+
if (seqRef.current[key] !== seq) return;
|
|
439
|
+
setTestResults((s) => {
|
|
440
|
+
const next = { ...s, [key]: result };
|
|
441
|
+
saveStoredResults(next);
|
|
442
|
+
return next;
|
|
443
|
+
});
|
|
444
|
+
}, []);
|
|
445
|
+
|
|
355
446
|
// 测试全部:并发发起所有测试请求,每个完成即更新对应行状态
|
|
356
447
|
const testAll = useCallback(async () => {
|
|
357
448
|
const data = state.data;
|
|
@@ -359,59 +450,20 @@ window.__ModuleLoader__.load({
|
|
|
359
450
|
const total = data.models.length;
|
|
360
451
|
setTesting(true);
|
|
361
452
|
setProgress({ done: 0, total });
|
|
362
|
-
// 初始化所有为 testing(按 provider/modelId key)
|
|
363
|
-
const init = {};
|
|
364
|
-
for (const m of data.models) init[m.key] = { status: "testing" };
|
|
365
|
-
setTestResults(init);
|
|
366
|
-
saveStoredResults(init);
|
|
367
453
|
|
|
368
454
|
let done = 0;
|
|
369
455
|
// 并发测试,但限制并发数避免压垮 host/网络
|
|
370
456
|
const CONCURRENCY = 6;
|
|
371
457
|
const queue = data.models.map((m) => m.key);
|
|
372
458
|
|
|
373
|
-
// 更新单个结果并同步到 localStorage
|
|
374
|
-
function updateResult(key, result) {
|
|
375
|
-
setTestResults((s) => {
|
|
376
|
-
const next = { ...s, [key]: result };
|
|
377
|
-
saveStoredResults(next);
|
|
378
|
-
return next;
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function runOne(key) {
|
|
383
|
-
try {
|
|
384
|
-
const resp = await fetch("/api/model-health/test", {
|
|
385
|
-
method: "POST",
|
|
386
|
-
headers: { "Content-Type": "application/json" },
|
|
387
|
-
body: JSON.stringify({ key }),
|
|
388
|
-
cache: "no-store",
|
|
389
|
-
});
|
|
390
|
-
const json = await resp.json();
|
|
391
|
-
if (!json.ok) {
|
|
392
|
-
updateResult(key, { status: "fail", error: json.error });
|
|
393
|
-
} else {
|
|
394
|
-
updateResult(key, {
|
|
395
|
-
status: json.status || "fail",
|
|
396
|
-
latency: json.latency,
|
|
397
|
-
error: json.error,
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
} catch (e) {
|
|
401
|
-
updateResult(key, { status: "fail", error: e.message });
|
|
402
|
-
} finally {
|
|
403
|
-
done++;
|
|
404
|
-
setProgress({ done, total });
|
|
405
|
-
if (done >= total) setTesting(false);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// 限制并发的 worker pool
|
|
410
459
|
async function worker() {
|
|
411
460
|
while (queue.length > 0) {
|
|
412
461
|
const key = queue.shift();
|
|
413
462
|
if (key === undefined) break;
|
|
414
|
-
await
|
|
463
|
+
await testModel(key);
|
|
464
|
+
done++;
|
|
465
|
+
setProgress({ done, total });
|
|
466
|
+
if (done >= total) setTesting(false);
|
|
415
467
|
}
|
|
416
468
|
}
|
|
417
469
|
const workers = Array.from(
|
|
@@ -419,7 +471,7 @@ window.__ModuleLoader__.load({
|
|
|
419
471
|
() => worker()
|
|
420
472
|
);
|
|
421
473
|
await Promise.all(workers);
|
|
422
|
-
}, [state.data]);
|
|
474
|
+
}, [state.data, testModel]);
|
|
423
475
|
|
|
424
476
|
const h = React.createElement;
|
|
425
477
|
|
|
@@ -562,7 +614,8 @@ window.__ModuleLoader__.load({
|
|
|
562
614
|
const tr = testResults[row.key] || { status: "idle" };
|
|
563
615
|
const labelKey = STATUS_LABEL[tr.status];
|
|
564
616
|
const statusLabel = labelKey ? t(labelKey) : tr.status;
|
|
565
|
-
const
|
|
617
|
+
const dotStyle = STATUS_DOT_STYLE[tr.status] || STYLES.dotIdle;
|
|
618
|
+
const textStyle = STATUS_TEXT_STYLE[tr.status] || STYLES.statusTextIdle;
|
|
566
619
|
const latencyText = tr.latency != null ? `${tr.latency}ms` : "-";
|
|
567
620
|
return h("tr", { key: (row.modelId || "") + i },
|
|
568
621
|
visibleCols.map((col) => {
|
|
@@ -576,6 +629,19 @@ window.__ModuleLoader__.load({
|
|
|
576
629
|
}, row.provider);
|
|
577
630
|
}
|
|
578
631
|
if (col.key === "_status") {
|
|
632
|
+
// 未测试:显示可点击的"测试"按钮,点击即测单个模型
|
|
633
|
+
if (tr.status === "idle") {
|
|
634
|
+
return h("td", {
|
|
635
|
+
key: col.key,
|
|
636
|
+
style: STYLES.td,
|
|
637
|
+
},
|
|
638
|
+
h("button", {
|
|
639
|
+
style: STYLES.btnTestSmall,
|
|
640
|
+
title: t("testOneTip"),
|
|
641
|
+
onClick: () => testModel(row.key),
|
|
642
|
+
}, t("testOne"))
|
|
643
|
+
);
|
|
644
|
+
}
|
|
579
645
|
const hasError = tr.error && tr.status === "fail";
|
|
580
646
|
return h("td", {
|
|
581
647
|
key: col.key,
|
|
@@ -583,8 +649,8 @@ window.__ModuleLoader__.load({
|
|
|
583
649
|
},
|
|
584
650
|
h("span", {
|
|
585
651
|
style: {
|
|
586
|
-
...STYLES.
|
|
587
|
-
...
|
|
652
|
+
...STYLES.statusWrap,
|
|
653
|
+
...textStyle,
|
|
588
654
|
cursor: hasError ? "help" : "default",
|
|
589
655
|
},
|
|
590
656
|
onMouseEnter: hasError
|
|
@@ -607,7 +673,9 @@ window.__ModuleLoader__.load({
|
|
|
607
673
|
onMouseLeave: hasError
|
|
608
674
|
? () => setTooltip(null)
|
|
609
675
|
: undefined,
|
|
610
|
-
},
|
|
676
|
+
},
|
|
677
|
+
h("span", { style: { ...STYLES.dot, ...dotStyle } }),
|
|
678
|
+
statusLabel)
|
|
611
679
|
);
|
|
612
680
|
}
|
|
613
681
|
if (col.key === "_latency") {
|
|
@@ -662,6 +730,7 @@ window.__ModuleLoader__.load({
|
|
|
662
730
|
* @param {import('@deepseek-ai/dsh-client-runtime/client').ClientContext} ctx
|
|
663
731
|
*/
|
|
664
732
|
function apply(ctx) {
|
|
733
|
+
ensureStatusCss();
|
|
665
734
|
// 注册 zh/en 词典;t 每次调用读取当前激活语言
|
|
666
735
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-model-health: dictionaries");
|
|
667
736
|
const t = ctx.locale.bind(NS);
|
package/package.json
CHANGED