dsh-model-health 0.2.4 → 0.2.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 +2 -1
- package/dist/client.js +70 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ dsh --profile web --dump-config | grep dsh-model-health # 配置层含本行
|
|
|
47
47
|
| # | 形式 | 入口 | 说明 |
|
|
48
48
|
|---|------|------|------|
|
|
49
49
|
| 1 | 「模型健康」面板 | Web UI → 设置 → 模型健康 | React 表格:Provider / 模型 ID / 名称 / 上下文窗口 / 最大输出 / 输入模态 / API 协议 / BaseURL,可选列可切换显示 |
|
|
50
|
-
| 2 | 测试全部 | 面板内「测试全部」按钮 | 并发(上限 6)对每个模型发一次 `max_tokens=1` 的最小 chat completions 请求,10s 超时;逐行显示 可用 / 不可用 / 跳过 / 延迟,失败原因悬停查看,结果持久化到 localStorage |
|
|
50
|
+
| 2 | 测试全部 | 面板内「测试全部」按钮 | 并发(上限 6)对每个模型发一次 `max_tokens=1` 的最小 chat completions 请求,10s 超时;逐行显示 可用 / 不可用 / 跳过 / 延迟,失败原因悬停查看,结果持久化到 localStorage。已出结果的徽章可点击,单独重测该模型(悬停显示下划线) |
|
|
51
51
|
| 3 | Tool 插件 `list_models` | 对话中调用 | 返回 Markdown 表格,供模型在对话中查看模型清单 |
|
|
52
52
|
|
|
53
53
|
**特性一览:**
|
|
@@ -56,6 +56,7 @@ dsh --profile web --dump-config | grep dsh-model-health # 配置层含本行
|
|
|
56
56
|
- 可用性测试支持 `openai-completions` 与 `deepseek` 协议,其余协议自动标记「跳过」
|
|
57
57
|
- API Key 通过 DSH credential service(`ctx.credentials.resolve`)解析,密钥不会输出到浏览器
|
|
58
58
|
- 测试结果(状态/延迟/错误)持久化到 localStorage,刷新页面后仍可见
|
|
59
|
+
- 状态列的 可用 / 不可用 / 跳过 徽章随时可点击,对单个模型重新测试,原地刷新状态与延迟
|
|
59
60
|
|
|
60
61
|
## 实现方式 / How it works
|
|
61
62
|
|
package/dist/client.js
CHANGED
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
statusFail: "不可用",
|
|
44
44
|
statusSkip: "跳过",
|
|
45
45
|
testOne: "测试",
|
|
46
|
-
testOneTip: "点击测试此模型"
|
|
46
|
+
testOneTip: "点击测试此模型",
|
|
47
|
+
retestTip: "点击重新测试此模型"
|
|
47
48
|
};
|
|
48
49
|
const en = {
|
|
49
50
|
tab: "Model Health",
|
|
@@ -73,7 +74,8 @@
|
|
|
73
74
|
statusFail: "Unavailable",
|
|
74
75
|
statusSkip: "Skipped",
|
|
75
76
|
testOne: "Test",
|
|
76
|
-
testOneTip: "Click to test this model"
|
|
77
|
+
testOneTip: "Click to test this model",
|
|
78
|
+
retestTip: "Click to re-test this model"
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
//#endregion
|
|
@@ -172,19 +174,31 @@
|
|
|
172
174
|
opacity: .5,
|
|
173
175
|
cursor: "not-allowed"
|
|
174
176
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
padding:
|
|
177
|
+
testLink: {
|
|
178
|
+
border: "none",
|
|
179
|
+
background: "transparent",
|
|
180
|
+
padding: 0,
|
|
181
|
+
margin: 0,
|
|
182
|
+
font: "inherit",
|
|
179
183
|
fontSize: 12,
|
|
180
184
|
lineHeight: "16px",
|
|
185
|
+
textAlign: "left",
|
|
186
|
+
color: "var(--dsw-alias-label-secondary, #555)",
|
|
187
|
+
textDecoration: "underline",
|
|
188
|
+
textUnderlineOffset: "3px",
|
|
181
189
|
cursor: "pointer",
|
|
182
|
-
border: "1px solid var(--dsw-alias-button-primary-fill, #4f46e5)",
|
|
183
|
-
borderRadius: 11,
|
|
184
|
-
background: "transparent",
|
|
185
|
-
color: "var(--dsw-alias-button-primary-fill, #4f46e5)",
|
|
186
190
|
whiteSpace: "nowrap"
|
|
187
191
|
},
|
|
192
|
+
retestBtn: {
|
|
193
|
+
border: "none",
|
|
194
|
+
background: "transparent",
|
|
195
|
+
padding: 0,
|
|
196
|
+
margin: 0,
|
|
197
|
+
font: "inherit",
|
|
198
|
+
fontSize: 12,
|
|
199
|
+
lineHeight: "16px",
|
|
200
|
+
textAlign: "left"
|
|
201
|
+
},
|
|
188
202
|
progress: {
|
|
189
203
|
fontSize: 12,
|
|
190
204
|
color: "var(--dsw-alias-label-tertiary, #888)"
|
|
@@ -476,33 +490,51 @@
|
|
|
476
490
|
const { modelKey, result: tr, t, onTest, onErrorTip, onClearTip } = props;
|
|
477
491
|
if (tr.status === "idle") return /* @__PURE__ */ React.createElement("td", { style: STYLES.td }, /* @__PURE__ */ React.createElement("button", {
|
|
478
492
|
type: "button",
|
|
479
|
-
|
|
493
|
+
className: "dsmh-test-link",
|
|
494
|
+
style: STYLES.testLink,
|
|
480
495
|
title: t("testOneTip"),
|
|
481
496
|
onClick: () => onTest(modelKey)
|
|
482
497
|
}, t("testOne")));
|
|
483
498
|
const hasError = Boolean(tr.error) && tr.status === "fail";
|
|
484
499
|
const labelKey = STATUS_LABEL[tr.status];
|
|
485
500
|
const statusLabel = labelKey ? t(labelKey) : tr.status;
|
|
501
|
+
const showTip = hasError ? (e) => {
|
|
502
|
+
const rowEl = e.currentTarget.closest("tr");
|
|
503
|
+
const tableEl = e.currentTarget.closest("table");
|
|
504
|
+
if (rowEl && tableEl) {
|
|
505
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
506
|
+
const tableRect = tableEl.getBoundingClientRect();
|
|
507
|
+
onErrorTip({
|
|
508
|
+
left: tableRect.left,
|
|
509
|
+
width: tableRect.width,
|
|
510
|
+
rowTop: rowRect.top,
|
|
511
|
+
error: tr.error || ""
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
} : void 0;
|
|
515
|
+
if (tr.status !== "testing") return /* @__PURE__ */ React.createElement("td", { style: STYLES.td }, /* @__PURE__ */ React.createElement("button", {
|
|
516
|
+
type: "button",
|
|
517
|
+
className: "dsmh-retest",
|
|
518
|
+
title: t("retestTip"),
|
|
519
|
+
style: {
|
|
520
|
+
...STYLES.statusWrap,
|
|
521
|
+
...STYLES.retestBtn,
|
|
522
|
+
...STATUS_TEXT_STYLE[tr.status]
|
|
523
|
+
},
|
|
524
|
+
onClick: () => onTest(modelKey),
|
|
525
|
+
onMouseEnter: showTip,
|
|
526
|
+
onMouseLeave: hasError ? onClearTip : void 0
|
|
527
|
+
}, /* @__PURE__ */ React.createElement("span", { style: {
|
|
528
|
+
...STYLES.dot,
|
|
529
|
+
...STATUS_DOT_STYLE[tr.status]
|
|
530
|
+
} }), /* @__PURE__ */ React.createElement("span", { className: "dsmh-status-label" }, statusLabel)));
|
|
486
531
|
return /* @__PURE__ */ React.createElement("td", { style: STYLES.td }, /* @__PURE__ */ React.createElement("span", {
|
|
487
532
|
style: {
|
|
488
533
|
...STYLES.statusWrap,
|
|
489
534
|
...STATUS_TEXT_STYLE[tr.status],
|
|
490
535
|
cursor: hasError ? "help" : "default"
|
|
491
536
|
},
|
|
492
|
-
onMouseEnter:
|
|
493
|
-
const rowEl = e.currentTarget.closest("tr");
|
|
494
|
-
const tableEl = e.currentTarget.closest("table");
|
|
495
|
-
if (rowEl && tableEl) {
|
|
496
|
-
const rowRect = rowEl.getBoundingClientRect();
|
|
497
|
-
const tableRect = tableEl.getBoundingClientRect();
|
|
498
|
-
onErrorTip({
|
|
499
|
-
left: tableRect.left,
|
|
500
|
-
width: tableRect.width,
|
|
501
|
-
rowTop: rowRect.top,
|
|
502
|
-
error: tr.error || ""
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
} : void 0,
|
|
537
|
+
onMouseEnter: showTip,
|
|
506
538
|
onMouseLeave: hasError ? onClearTip : void 0
|
|
507
539
|
}, /* @__PURE__ */ React.createElement("span", { style: {
|
|
508
540
|
...STYLES.dot,
|
|
@@ -671,13 +703,24 @@
|
|
|
671
703
|
//#region src/client/apply.tsx
|
|
672
704
|
/** client 侧依赖:slots(注册 UI slot 的服务)+ locale(多语言词典) */
|
|
673
705
|
const inject = ["slots", "locale"];
|
|
674
|
-
/**
|
|
706
|
+
/** 注入内联样式无法表达的规则:@keyframes 脉冲动画 + 可重测状态徽章的 hover 反馈。 */
|
|
675
707
|
function ensureStatusCss() {
|
|
676
708
|
const id = "dsh-model-health-status-style";
|
|
677
709
|
if (document.getElementById(id)) return;
|
|
678
710
|
const el = document.createElement("style");
|
|
679
711
|
el.id = id;
|
|
680
|
-
el.textContent =
|
|
712
|
+
el.textContent = `
|
|
713
|
+
@keyframes dsmh-pulse { 0%,100% { opacity:1; } 50% { opacity:.25; } }
|
|
714
|
+
/* 可点击重测的状态徽章:hover/聚焦 下划线提示可点 */
|
|
715
|
+
.dsmh-retest { cursor: pointer; }
|
|
716
|
+
.dsmh-retest:hover .dsmh-status-label,
|
|
717
|
+
.dsmh-retest:focus-visible .dsmh-status-label { text-decoration: underline; text-underline-offset: 3px; }
|
|
718
|
+
.dsmh-retest:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; border-radius: 2px; }
|
|
719
|
+
/* 未测试行的"测试"文字链:hover 提亮为主题色 */
|
|
720
|
+
.dsmh-test-link { transition: color .15s ease; }
|
|
721
|
+
.dsmh-test-link:hover,
|
|
722
|
+
.dsmh-test-link:focus-visible { color: var(--dsw-alias-button-primary-fill, #4f46e5); }
|
|
723
|
+
`;
|
|
681
724
|
document.head.appendChild(el);
|
|
682
725
|
}
|
|
683
726
|
function apply(ctx) {
|
package/package.json
CHANGED