2026-06-16 16:25:20 +08:00
|
|
|
|
// profiles.js — LLM 配置管理页面逻辑
|
|
|
|
|
|
|
|
|
|
|
|
const Profiles = {
|
|
|
|
|
|
_data: [],
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化:绑定按钮事件
|
|
|
|
|
|
init() {
|
|
|
|
|
|
document.getElementById("add-profile-btn").addEventListener("click", () => Profiles.showForm());
|
|
|
|
|
|
document.getElementById("save-profile-btn").addEventListener("click", () => Profiles.save());
|
|
|
|
|
|
document.getElementById("cancel-profile-btn").addEventListener("click", () => Profiles.hideForm());
|
2026-06-23 13:58:43 +08:00
|
|
|
|
document.getElementById("test-profile-btn").addEventListener("click", () => Profiles.testForm());
|
2026-06-16 16:25:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 加载并渲染 Profile 列表
|
|
|
|
|
|
async load() {
|
|
|
|
|
|
const grid = document.getElementById("profile-cards");
|
|
|
|
|
|
const empty = document.getElementById("profiles-empty");
|
|
|
|
|
|
grid.innerHTML = '<p class="muted">加载中…</p>';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await API.profiles();
|
|
|
|
|
|
Profiles._data = data.profiles || [];
|
|
|
|
|
|
grid.innerHTML = "";
|
|
|
|
|
|
if (Profiles._data.length === 0) {
|
|
|
|
|
|
empty.hidden = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
empty.hidden = true;
|
|
|
|
|
|
Profiles._data.forEach(p => grid.appendChild(Profiles.renderCard(p)));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
grid.innerHTML = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染单个 Profile 卡片
|
|
|
|
|
|
renderCard(p) {
|
|
|
|
|
|
const card = document.createElement("div");
|
|
|
|
|
|
card.className = "profile-card";
|
|
|
|
|
|
card.dataset.id = p.profile_id;
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
|
<div class="profile-card-head">
|
|
|
|
|
|
<div class="profile-card-name">${App.escape(p.name)}</div>
|
|
|
|
|
|
<div class="profile-card-actions">
|
2026-06-23 13:58:43 +08:00
|
|
|
|
<button class="btn btn-sm btn-test" data-action="test">测试</button>
|
2026-06-16 16:25:20 +08:00
|
|
|
|
<button class="btn btn-sm" data-action="edit">编辑</button>
|
|
|
|
|
|
<button class="btn btn-sm btn-danger" data-action="delete">删除</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="profile-card-field"><span class="field-label">模型</span> <code>${App.escape(p.model)}</code></div>
|
|
|
|
|
|
<div class="profile-card-field"><span class="field-label">Base URL</span> <code>${App.escape(p.base_url)}</code></div>
|
|
|
|
|
|
<div class="profile-card-field"><span class="field-label">超时</span> ${p.timeout_seconds}s</div>
|
2026-06-23 13:58:43 +08:00
|
|
|
|
<div class="profile-test-result" data-result hidden></div>
|
2026-06-16 16:25:20 +08:00
|
|
|
|
`;
|
2026-06-23 13:58:43 +08:00
|
|
|
|
card.querySelector("[data-action=test]").addEventListener("click", () => Profiles.testCard(p, card));
|
2026-06-16 16:25:20 +08:00
|
|
|
|
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;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-23 13:58:43 +08:00
|
|
|
|
// 测试已保存的 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}`;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-16 16:25:20 +08:00
|
|
|
|
// 显示新建或编辑表单
|
|
|
|
|
|
showForm(profile = null) {
|
|
|
|
|
|
const panel = document.getElementById("profile-form-panel");
|
|
|
|
|
|
const title = document.getElementById("profile-form-title");
|
|
|
|
|
|
panel.hidden = false;
|
|
|
|
|
|
title.textContent = profile ? "编辑 LLM 配置" : "新建 LLM 配置";
|
|
|
|
|
|
document.getElementById("edit-profile-id").value = profile ? profile.profile_id : "";
|
|
|
|
|
|
document.getElementById("pf-name").value = profile ? profile.name : "";
|
|
|
|
|
|
document.getElementById("pf-model").value = profile ? profile.model : "";
|
|
|
|
|
|
document.getElementById("pf-base-url").value = profile ? profile.base_url : "";
|
|
|
|
|
|
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 = "";
|
2026-06-23 13:58:43 +08:00
|
|
|
|
const resultEl = document.getElementById("profile-form-test-result");
|
|
|
|
|
|
resultEl.hidden = true;
|
|
|
|
|
|
resultEl.className = "profile-test-result";
|
2026-06-16 16:25:20 +08:00
|
|
|
|
panel.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hideForm() {
|
|
|
|
|
|
document.getElementById("profile-form-panel").hidden = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 保存(新建 or 更新)
|
|
|
|
|
|
async save() {
|
|
|
|
|
|
const id = document.getElementById("edit-profile-id").value;
|
|
|
|
|
|
const body = {
|
|
|
|
|
|
name: document.getElementById("pf-name").value.trim(),
|
|
|
|
|
|
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.name || !body.model || !body.base_url || !body.api_key) {
|
|
|
|
|
|
errEl.textContent = "请填写所有必填字段(名称、模型、Base URL、API Key)";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (id) {
|
|
|
|
|
|
await API.updateProfile(id, body);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await API.createProfile(body);
|
|
|
|
|
|
}
|
|
|
|
|
|
Profiles.hideForm();
|
|
|
|
|
|
await Profiles.load();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
errEl.textContent = `保存失败:${err.message}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 删除 Profile
|
|
|
|
|
|
async remove(profileId, name) {
|
|
|
|
|
|
if (!confirm(`确认删除配置「${name}」?`)) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await API.deleteProfile(profileId);
|
|
|
|
|
|
await Profiles.load();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert(`删除失败:${err.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前已加载的 profiles(供 runner.js 使用)
|
|
|
|
|
|
getAll() {
|
|
|
|
|
|
return Profiles._data;
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|