diff --git a/webapp/static/css/app.css b/webapp/static/css/app.css
index 645b1dd..1499d0e 100644
--- a/webapp/static/css/app.css
+++ b/webapp/static/css/app.css
@@ -294,6 +294,21 @@ table.group-table td { border-bottom: 1px solid #f1f5f9; font-variant-numeric: t
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-danger { color: var(--bad); border-color: var(--bad); }
.btn-danger:hover { background: #fee2e2; }
+.btn-test { color: #0369a1; border-color: #0369a1; }
+.btn-test:hover { background: #e0f2fe; }
+
+/* LLM 连通性测试结果 */
+.profile-test-result {
+ margin-top: 8px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ display: none;
+}
+.profile-test-result:not([hidden]) { display: block; }
+.profile-test-result.ok { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
+.profile-test-result.fail { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; word-break: break-all; }
/* 选中态 run 卡片 */
.run-card.selected {
diff --git a/webapp/static/index.html b/webapp/static/index.html
index d7a2d57..a262c55 100644
--- a/webapp/static/index.html
+++ b/webapp/static/index.html
@@ -219,9 +219,11 @@
${App.escape(p.name)}
+
@@ -46,12 +48,72 @@ const Profiles = {
模型 ${App.escape(p.model)}
Base URL ${App.escape(p.base_url)}
超时 ${p.timeout_seconds}s
+
`;
+ card.querySelector("[data-action=test]").addEventListener("click", () => Profiles.testCard(p, card));
card.querySelector("[data-action=edit]").addEventListener("click", () => Profiles.showForm(p));
card.querySelector("[data-action=delete]").addEventListener("click", () => Profiles.remove(p.profile_id, p.name));
return card;
},
+ // 测试已保存的 profile(卡片上的测试按钮)
+ async testCard(p, card) {
+ const btn = card.querySelector("[data-action=test]");
+ const resultEl = card.querySelector("[data-result]");
+ btn.disabled = true;
+ btn.textContent = "测试中…";
+ resultEl.hidden = true;
+ resultEl.className = "profile-test-result";
+ try {
+ const res = await API.testProfile(p.profile_id);
+ Profiles._showTestResult(resultEl, res);
+ } catch (err) {
+ Profiles._showTestResult(resultEl, { ok: false, message: err.message });
+ } finally {
+ btn.disabled = false;
+ btn.textContent = "测试";
+ }
+ },
+
+ // 测试表单中当前填写的参数(保存前即可测试)
+ async testForm() {
+ const body = {
+ model: document.getElementById("pf-model").value.trim(),
+ base_url: document.getElementById("pf-base-url").value.trim(),
+ api_key: document.getElementById("pf-api-key").value.trim(),
+ timeout_seconds: parseInt(document.getElementById("pf-timeout").value, 10) || 30,
+ };
+ const errEl = document.getElementById("profile-form-error");
+ if (!body.model || !body.base_url || !body.api_key) {
+ errEl.textContent = "请先填写模型名称、Base URL 和 API Key";
+ return;
+ }
+ errEl.textContent = "";
+ const testBtn = document.getElementById("test-profile-btn");
+ const resultEl = document.getElementById("profile-form-test-result");
+ testBtn.disabled = true;
+ testBtn.textContent = "测试中…";
+ resultEl.hidden = true;
+ resultEl.className = "profile-test-result";
+ try {
+ const res = await API.probeConnectivity(body);
+ Profiles._showTestResult(resultEl, res);
+ } catch (err) {
+ Profiles._showTestResult(resultEl, { ok: false, message: err.message });
+ } finally {
+ testBtn.disabled = false;
+ testBtn.textContent = "测试连通性";
+ }
+ },
+
+ // 渲染测试结果到指定元素
+ _showTestResult(el, res) {
+ el.hidden = false;
+ el.classList.add(res.ok ? "ok" : "fail");
+ const latency = res.latency_ms != null ? ` (${res.latency_ms}ms)` : "";
+ el.textContent = res.ok ? `✓ 连接成功${latency}` : `✗ ${res.message}`;
+ },
+
// 显示新建或编辑表单
showForm(profile = null) {
const panel = document.getElementById("profile-form-panel");
@@ -65,6 +127,9 @@ const Profiles = {
document.getElementById("pf-api-key").value = profile ? profile.api_key : "";
document.getElementById("pf-timeout").value = profile ? profile.timeout_seconds : 30;
document.getElementById("profile-form-error").textContent = "";
+ const resultEl = document.getElementById("profile-form-test-result");
+ resultEl.hidden = true;
+ resultEl.className = "profile-test-result";
panel.scrollIntoView({ behavior: "smooth", block: "start" });
},