Ship Cursor-only MCP admin and modular Admin/Chat UI.

Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 19:38:26 +08:00
parent 2aff012b48
commit f6f129931f
61 changed files with 18057 additions and 16327 deletions

View file

@ -0,0 +1,833 @@
import { state, t, el, tdCell, apiGet, apiPost, apiRequest, resolveAdminApiUrl, getStoredAuthToken, renderPageShell, renderSectionCard, markPrewarmReminder } from "../core.js";
import { hasPermission } from "./authz.js";
async function renderModels() {
const canPickActive = hasPermission("admin:read");
const canConfigureBindings = canPickActive;
const canConfigureChatUi = hasPermission("admin:tenant:write");
const status = el("div", { class: "muted", text: "" });
const isAdminPool = String((state.authSession && state.authSession.username) || "").trim().toLowerCase() === "administrator";
// 与后端 _require_models_mutate 对齐:administrator 写全局池需 tenant:write;普通用户写自己的配置只需 admin:read
const canMutateProfiles = isAdminPool ? hasPermission("admin:tenant:write") : canPickActive;
const readonlyHint = el("div", {
class: "muted",
text: isAdminPool && !canMutateProfiles ? t("models.readonly") : "",
});
const activeSelect = el("select", { class: "input", disabled: !canPickActive });
const chatModelSelectorVisibleCb = el("input", { type: "checkbox", disabled: !canConfigureChatUi });
const bindingWrap = el("div", {});
const opsAiSpecialistSelect = el("select", { class: "input", disabled: !canConfigureBindings });
const opsAiProfileSelect = el("select", { class: "input", disabled: !canConfigureBindings });
const modelsGrantsLinkRow = el("div", { class: "muted u-hidden" });
const newName = el("input", { class: "input", placeholder: t("models.createNamePlaceholder") });
const newMode = el("select", { class: "input", disabled: !canMutateProfiles }, [
el("option", { value: "openai", text: t("models.mode.openai") }),
el("option", { value: "anthropic", text: t("models.mode.anthropic") }),
el("option", { value: "google", text: t("models.mode.google") }),
el("option", { value: "ollama", text: t("models.mode.ollama") }),
el("option", { value: "rule", text: t("models.mode.rule") }),
]);
const profName = el("input", { class: "input" });
const modeSel = el("select", { class: "input" });
const modelInp = el("input", { class: "input" });
const baseInp = el("input", { class: "input", placeholder: t("models.baseUrlPlaceholder") });
const thinkingModeCb = el("input", { type: "checkbox" });
const reasoningEffortSel = el("select", { class: "input", style: "max-width:180px" }, [
el("option", { value: "", text: t("models.reasoningEffortDefault") }),
el("option", { value: "low", text: t("models.reasoningEffortLow") }),
el("option", { value: "medium", text: t("models.reasoningEffortMedium") }),
el("option", { value: "high", text: t("models.reasoningEffortHigh") }),
]);
const keyInp = el("input", { class: "input", type: "password", autocomplete: "off" });
const rememberCb = el("input", { type: "checkbox" });
const readonlyProfileHint = el("div", { class: "muted", text: "" });
const builtinCap = el("div", { class: "muted", text: "" });
const warnKey = el("div", { class: "muted", text: "" });
const openaiHint = el("div", { class: "muted", text: "" });
const ollamaHint = el("div", { class: "muted", text: "" });
const evalMetrics = el("div", { class: "row" });
const evalTableWrap = el("div");
const evalDetails = el("details", { class: "details" });
const evalSummary = el("summary", { text: t("models.evalToggle") });
const evalInner = el("div", {});
const expertsStatus = el("div", { class: "muted", text: "" });
const expertsSelect = el("select", { class: "input" });
const expertNewId = el("input", { class: "input", placeholder: "new expert id, e.g. qa" });
const expertNewNameEn = el("input", { class: "input", placeholder: "English name (required)" });
const expertNewNameZh = el("input", { class: "input", placeholder: t("models.phNameZh") });
const expertRoleSel = el("select", { class: "input" }, [
el("option", { value: "expert", text: "expert" }),
el("option", { value: "system", text: "system" }),
]);
const expertNameEn = el("input", { class: "input", placeholder: "English name (required)" });
const expertNameZh = el("input", { class: "input", placeholder: t("models.phNameZh") });
const expertSoul = el("textarea", { class: "input", rows: "5", placeholder: "SOUL.md (optional)" });
const expertRoleSystem = el("textarea", { class: "input", rows: "5", placeholder: "ROLE_SYSTEM.md (optional)" });
async function downloadEvalExport(format) {
const fmt = format === "json" ? "json" : "csv";
const url = resolveAdminApiUrl(`/admin/api/models/eval/export?format=${encodeURIComponent(fmt)}`);
const token = getStoredAuthToken();
const res = await fetch(url, {
headers: {
accept: fmt === "json" ? "application/json" : "text/csv",
...(token ? { authorization: `Bearer ${token}` } : {}),
},
});
if (!res.ok) throw new Error(`export ${res.status}`);
const blob = await res.blob();
const a = document.createElement("a");
const name = fmt === "json" ? "agent_eval_logs.json" : "agent_eval_logs.csv";
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
URL.revokeObjectURL(a.href);
}
const btnEvalCsv = el("button", {
class: "btn",
text: t("models.evalDownloadCsv"),
onclick: async () => {
try {
await downloadEvalExport("csv");
} catch (e) {
status.textContent = String(e.message || e);
}
},
});
const btnEvalJson = el("button", {
class: "btn",
text: t("models.evalDownloadJson"),
onclick: async () => {
try {
await downloadEvalExport("json");
} catch (e) {
status.textContent = String(e.message || e);
}
},
});
const evalActionRow = el("div", { class: "row" }, [
el("span", { class: "muted", text: t("models.evalLogsPreview") }),
btnEvalCsv,
btnEvalJson,
]);
evalInner.appendChild(evalMetrics);
evalInner.appendChild(evalActionRow);
evalInner.appendChild(evalTableWrap);
evalDetails.appendChild(evalSummary);
evalDetails.appendChild(evalInner);
let state = null;
let evalPack = null;
let secretsStatus = null;
let expertsState = { items: [] };
const secretsStatusMsg = el("div", { class: "muted", text: "" });
const secretsMigrateBtn = el("button", { class: "btn", text: t("secrets.migrateBtn") });
const secretsCard = el("div", { class: "card u-hidden" }, [
el("div", { class: "card__title", text: t("secrets.migrateTitle") }),
el("div", { class: "muted", text: t("secrets.migrateHint") }),
el("div", { class: "row" }, [secretsMigrateBtn]),
secretsStatusMsg,
]);
function fmtSecretsDone(s, p) {
return t("secrets.migrateDone").replace("{s}", String(s)).replace("{p}", String(p));
}
function paintSecretsCard() {
secretsStatusMsg.textContent = "";
if (!secretsStatus || secretsStatus.ok !== true) {
secretsCard.style.display = "none";
return;
}
const n1 = intOr0(secretsStatus.legacy_b64_app_settings);
const n2 = intOr0(secretsStatus.legacy_b64_llm_profiles);
const total = n1 + n2;
const canMigrate = hasPermission("admin:tenant:write");
if (total <= 0) {
secretsCard.style.display = "none";
return;
}
secretsCard.style.display = "block";
if (!canMigrate) {
secretsMigrateBtn.disabled = true;
secretsStatusMsg.textContent = t("secrets.migrateForbidden");
return;
}
if (!secretsStatus.has_master_key) {
secretsMigrateBtn.disabled = true;
secretsStatusMsg.textContent = t("secrets.migrateNeedKey");
return;
}
secretsMigrateBtn.disabled = false;
secretsStatusMsg.textContent = `legacy: settings=${n1}, profiles=${n2}`;
}
const VIS_KEYS = {
builtin: "models.vis.builtin",
owned: "models.vis.owned",
grant_user: "models.vis.grantUser",
grant_tenant: "models.vis.grantTenant",
global: "models.vis.global",
other_user: "models.vis.otherUser",
};
function visibilityTag(vr) {
const k = VIS_KEYS[String(vr || "")];
return k ? t(k) : "";
}
function labelFor(pid) {
const profiles = (state && state.profiles) || [];
const p = profiles.find((x) => String(x.id) === String(pid));
const base = (p && String(p.name || "").trim()) || pid;
const vr = p && p.visibility_reason;
if (!vr) return base;
const tag = visibilityTag(vr);
return tag ? `${base} (${tag})` : base;
}
function fillModeSelect(sel, modes, current) {
sel.innerHTML = "";
modes.forEach((m) => {
const label =
m === "openai"
? t("models.mode.openai")
: m === "openai_responses"
? t("models.mode.openai_responses")
: m === "anthropic"
? t("models.mode.anthropic")
: m === "google"
? t("models.mode.google")
: m === "ollama"
? t("models.mode.ollama")
: t("models.mode.rule");
sel.appendChild(el("option", {
value: m,
text: label,
}));
});
if (modes.includes(current)) sel.value = current;
else sel.value = modes[0];
}
async function refresh() {
try {
const data = await apiGet("/admin/api/models");
state = data;
status.textContent = "";
paint();
try {
secretsStatus = await apiGet("/admin/api/secrets/status");
} catch (_) {
secretsStatus = null;
}
paintSecretsCard();
try {
evalPack = await apiGet("/admin/api/models/eval?limit_logs=100");
paintEval();
} catch (ee) {
evalPack = { ok: false, summary: {}, logs: [] };
paintEval();
status.textContent = [status.textContent, `${t("models.sectionEval")}: ${String(ee.message || ee)}`].filter(Boolean).join(" | ");
}
try {
expertsState = await apiGet("/admin/api/experts");
} catch (_) {
expertsState = { ok: false, items: [] };
}
paintExperts();
} catch (e) {
state = null;
evalPack = null;
secretsStatus = null;
expertsState = { ok: false, items: [] };
status.textContent = String(e.message || e);
}
}
function currentExpertItem() {
const items = Array.isArray(expertsState && expertsState.items) ? expertsState.items : [];
const id = String(expertsSelect.value || "");
return items.find((x) => String(x.id || "") === id) || null;
}
function fillExpertFields(item) {
const files = (item && item.files && typeof item.files === "object") ? item.files : {};
expertNameEn.value = String((item && item.display_name_en) || "");
expertNameZh.value = String((item && item.display_name_zh) || "");
expertRoleSel.value = String((item && item.role) || "expert");
expertSoul.value = String(files["SOUL.md"] || "");
expertRoleSystem.value = String(files["ROLE_SYSTEM.md"] || "");
const isSystem = !!(item && (item.builtin || String(item.role || "") === "system"));
expertRoleSel.disabled = isSystem;
btnExpertDelete.disabled = isSystem;
}
function paintExperts() {
const items = Array.isArray(expertsState && expertsState.items) ? expertsState.items : [];
const prev = String(expertsSelect.value || "");
expertsSelect.innerHTML = "";
items.forEach((x) => {
const id = String(x.id || "");
if (!id) return;
const isSystem = !!(x && (x.builtin || String(x.role || "") === "system"));
const tail = isSystem ? " (system)" : "";
const name = String(x.display_name_en || "").trim() || id;
expertsSelect.appendChild(el("option", { value: id, text: `${name} [${id}]${tail}` }));
});
if (items.some((x) => String(x.id || "") === prev)) expertsSelect.value = prev;
else if (items.length) expertsSelect.value = String(items[0].id || "");
fillExpertFields(currentExpertItem());
}
secretsMigrateBtn.addEventListener("click", async () => {
if (!hasPermission("admin:tenant:write")) {
secretsStatusMsg.textContent = t("secrets.migrateForbidden");
return;
}
secretsMigrateBtn.disabled = true;
secretsStatusMsg.textContent = "";
try {
const res = await apiPost("/admin/api/secrets/migrate", {});
if (res && res.ok) {
const s = intOr0(res.migrated_app_settings);
const p = intOr0(res.migrated_llm_profiles);
secretsStatusMsg.textContent = (s + p) > 0 ? fmtSecretsDone(s, p) : t("secrets.migrateNoop");
} else {
secretsStatusMsg.textContent = String((res && (res.error || res.detail)) || "migrate_failed");
}
try {
secretsStatus = await apiGet("/admin/api/secrets/status");
} catch (_) {
secretsStatus = null;
}
paintSecretsCard();
} catch (e) {
secretsStatusMsg.textContent = String(e.message || e);
paintSecretsCard();
} finally {
secretsMigrateBtn.disabled = false;
}
});
function paintBindings() {
bindingWrap.innerHTML = "";
if (!state || !state.bindings) return;
const profiles = state.profiles || [];
const profileIds = profiles.map((p) => String(p.id));
const roleIds = Array.isArray(state.role_ids) ? state.role_ids : [];
roleIds.forEach((rid) => {
const sel = el("select", { class: "input", disabled: !canConfigureBindings });
sel.appendChild(el("option", { value: "", text: t("models.useGlobal") }));
profiles.forEach((p) => {
sel.appendChild(el("option", { value: String(p.id), text: labelFor(p.id) }));
});
const v = String(state.bindings[rid] || "").trim();
sel.value = profileIds.includes(v) ? v : "";
sel.addEventListener("change", async () => {
if (!canConfigureBindings) return;
const next = Object.assign({}, state.bindings);
next[rid] = String(sel.value || "");
try {
await apiPost("/admin/api/models/bindings", { bindings: next });
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
});
bindingWrap.appendChild(el("div", { class: "row" }, [
el("label", { text: t("models.role." + rid) + " " }),
sel,
]));
});
}
function paintOpsAiBindings() {
if (!state || !state.ops_ai_bindings) return;
const profiles = state.profiles || [];
const profileIds = profiles.map((p) => String(p.id));
const roleIds = (Array.isArray(state.role_ids) ? state.role_ids : []).filter((rid) => String(rid || "") !== "manager");
const curSpecialist = String(opsAiSpecialistSelect.value || "").trim();
opsAiSpecialistSelect.innerHTML = "";
roleIds.forEach((rid) => {
opsAiSpecialistSelect.appendChild(el("option", { value: String(rid), text: t("models.role." + rid) }));
});
if (roleIds.includes(curSpecialist)) opsAiSpecialistSelect.value = curSpecialist;
else if (roleIds.length) opsAiSpecialistSelect.value = roleIds[0];
const rid = String(opsAiSpecialistSelect.value || "").trim();
opsAiProfileSelect.innerHTML = "";
profiles.forEach((p) => {
opsAiProfileSelect.appendChild(el("option", { value: String(p.id), text: labelFor(p.id) }));
});
const v = String((state.ops_ai_bindings && state.ops_ai_bindings[rid]) || "").trim();
opsAiProfileSelect.value = profileIds.includes(v) ? v : "";
}
function paintEval() {
evalMetrics.innerHTML = "";
evalTableWrap.innerHTML = "";
const summary = (evalPack && evalPack.summary) || {};
const total = intOr0(summary.total);
const sr = Number(summary.success_rate || 0);
const p95 = intOr0(summary.p95_latency_ms);
evalMetrics.appendChild(el("span", { text: `${t("models.evalTotal")}: ${total} ` }));
evalMetrics.appendChild(el("span", { text: `${t("models.evalSuccess")}: ${(sr * 100).toFixed(1)}% ` }));
evalMetrics.appendChild(el("span", { text: `${t("models.evalP95")}: ${p95} ms` }));
const logs = Array.isArray(evalPack && evalPack.logs) ? evalPack.logs : [];
if (!logs.length) {
evalTableWrap.appendChild(el("div", { class: "muted", text: t("models.noEvalLogs") }));
return;
}
const tbody = el("tbody");
logs.forEach((r) => {
tbody.appendChild(el("tr", {}, [
tdCell(r.timestamp || "", 24),
tdCell(r.session_id || "", 20),
tdCell(r.specialist || "", 12),
tdCell(r.task_kind || "", 14),
tdCell(r.success ? "1" : "0", 4),
tdCell(String(r.latency_ms ?? ""), 8),
tdCell(String(r.notes || ""), 60),
]));
});
evalTableWrap.appendChild(el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
el("thead", {}, [el("tr", {}, [
el("th", { text: t("table.timestamp") }),
el("th", { text: "session_id" }),
el("th", { text: t("table.specialist") }),
el("th", { text: "task_kind" }),
el("th", { text: t("table.status") }),
el("th", { text: t("table.durationMs") }),
el("th", { text: "notes" }),
])]),
tbody,
])]));
}
function intOr0(x) {
const n = parseInt(String(x), 10);
return Number.isFinite(n) ? n : 0;
}
function paint() {
if (!state) return;
if (state.ok !== true) {
status.textContent = status.textContent || t("models.loadFailed");
return;
}
const profiles = Array.isArray(state.profiles) ? state.profiles : [];
const builtin = String(state.builtin_ollama_profile_id || "");
activeSelect.innerHTML = "";
profiles.forEach((p) => {
activeSelect.appendChild(el("option", { value: String(p.id), text: labelFor(p.id) }));
});
const aid = String(state.active_llm_profile_id || "");
if (profiles.some((p) => String(p.id) === aid)) activeSelect.value = aid;
activeSelect.disabled = !canPickActive;
chatModelSelectorVisibleCb.disabled = !canConfigureChatUi;
chatModelSelectorVisibleCb.checked = state.chat_model_selector_visible !== false;
opsAiSpecialistSelect.disabled = !canConfigureBindings;
opsAiProfileSelect.disabled = !canConfigureBindings;
paintBindings();
paintOpsAiBindings();
const selProf = profiles.find((p) => String(p.id) === aid) || profiles[0] || {};
const pid = String(selProf.id || "");
const rowMutable = selProf.mutable !== false;
const canEditFields = canMutateProfiles && rowMutable;
readonlyProfileHint.textContent = rowMutable ? "" : t("models.profileReadonlyHint");
const isBuiltin = pid === builtin;
profName.value = String(selProf.name || "");
const rawMode = String(selProf.mode || "openai").toLowerCase();
const baseAllowedModes = ["openai", "anthropic", "google", "ollama", "rule"];
const allowedModes = rawMode === "openai_responses" ? ["openai_responses", ...baseAllowedModes] : baseAllowedModes;
const modeVal = allowedModes.includes(rawMode) ? rawMode : "openai";
if (isBuiltin) {
fillModeSelect(modeSel, ["ollama"], "ollama");
modeSel.disabled = true;
builtinCap.textContent = t("models.builtinLocked");
} else {
modeSel.disabled = !canEditFields;
builtinCap.textContent = "";
fillModeSelect(modeSel, allowedModes, modeVal);
}
modelInp.value = String(selProf.model || "");
baseInp.value = String(selProf.base_url || "");
thinkingModeCb.checked = !!selProf.thinking_mode_enabled;
reasoningEffortSel.value = String(selProf.reasoning_effort || "");
keyInp.value = String(state.profile_secret || "");
rememberCb.checked = !!selProf.has_key;
profName.disabled = !canEditFields;
modelInp.disabled = !canEditFields;
baseInp.disabled = !canEditFields;
thinkingModeCb.disabled = !canEditFields;
reasoningEffortSel.disabled = !canEditFields;
keyInp.disabled = !canEditFields;
rememberCb.disabled = !canEditFields;
const m = isBuiltin ? "ollama" : modeVal;
warnKey.textContent = "";
openaiHint.textContent = "";
ollamaHint.textContent = "";
if (m === "openai" || m === "openai_responses") {
ollamaHint.textContent = "";
if (state.has_openai_api_key_env) openaiHint.textContent = t("models.openaiKeyHint");
if (selProf.has_key && !String(keyInp.value || "").trim()) {
warnKey.textContent = t("models.warnKeyInDb");
}
} else if (m === "google") {
openaiHint.textContent = "";
ollamaHint.textContent = "";
} else if (m === "ollama") {
ollamaHint.textContent = t("models.ollamaHint");
}
btnDelete.disabled = !canMutateProfiles || !rowMutable || pid === builtin;
modelsGrantsLinkRow.innerHTML = "";
if (state.can_manage_llm_grants) {
modelsGrantsLinkRow.style.display = "block";
modelsGrantsLinkRow.appendChild(el("span", { text: `${t("models.grantsNavHint")} ` }));
modelsGrantsLinkRow.appendChild(el("a", { href: "#/api-grants", text: t("models.linkApiGrants") }));
} else {
modelsGrantsLinkRow.style.display = "none";
}
paintEval();
}
activeSelect.addEventListener("change", async () => {
if (!canPickActive) return;
try {
await apiPost("/admin/api/models/active", { profile_id: activeSelect.value });
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
});
chatModelSelectorVisibleCb.addEventListener("change", async () => {
if (!canConfigureChatUi) return;
try {
await apiPost("/admin/api/models/chat-ui", {
chat_model_selector_visible: !!chatModelSelectorVisibleCb.checked,
});
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
});
opsAiSpecialistSelect.addEventListener("change", async () => {
paintOpsAiBindings();
});
opsAiProfileSelect.addEventListener("change", async () => {
if (!canConfigureBindings) return;
try {
const rid = String(opsAiSpecialistSelect.value || "").trim();
const next = Object.assign({}, (state && state.ops_ai_bindings) || {});
next[rid] = String(opsAiProfileSelect.value || "");
await apiPost("/admin/api/models/ops-ai/bindings", { bindings: next });
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
});
const btnCreate = el("button", { class: "btn btn--primary", text: t("models.createBtn"), onclick: async () => {
if (!canMutateProfiles) return;
try {
await apiPost("/admin/api/models/profiles", {
name: newName.value.trim() || t("models.newProfileDefault"),
mode: newMode.value,
});
newName.value = "";
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
}});
const btnSave = el("button", { class: "btn btn--primary", text: t("models.save"), onclick: async () => {
if (!canMutateProfiles) return;
const sel = (state.profiles || []).find((p) => String(p.id) === String(state.active_llm_profile_id || ""));
if (sel && sel.mutable === false) return;
const pid = String(state.active_llm_profile_id || "");
const builtin = String(state.builtin_ollama_profile_id || "");
try {
let modeSave = modeSel.value;
if (pid === builtin) modeSave = "ollama";
await apiRequest("PATCH", "/admin/api/models/profiles/" + encodeURIComponent(pid), {
name: profName.value.trim() || t("models.newProfileDefault"),
mode: modeSave,
model: modelInp.value.trim(),
base_url: baseInp.value.trim(),
thinking_mode_enabled: !!thinkingModeCb.checked,
reasoning_effort: String(reasoningEffortSel.value || ""),
});
await apiRequest("POST", "/admin/api/models/profiles/" + encodeURIComponent(pid) + "/secret", {
remember: rememberCb.checked,
secret: keyInp.value,
});
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
}});
const btnDelete = el("button", { class: "btn btn--danger", text: t("models.delete"), onclick: async () => {
if (!canMutateProfiles) return;
const pid = String(state.active_llm_profile_id || "");
const builtin = String(state.builtin_ollama_profile_id || "");
if (pid === builtin) {
status.textContent = t("models.cannotDeleteBuiltin");
return;
}
if (!globalThis.confirm(t("models.deleteConfirm"))) return;
try {
await apiRequest("DELETE", "/admin/api/models/profiles/" + encodeURIComponent(pid), {});
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
}});
const btnExpertCreate = el("button", {
class: "btn btn--primary",
text: "Create expert",
onclick: async () => {
if (!hasPermission("admin:tenant:write")) return;
const eid = String(expertNewId.value || "").trim();
if (!eid) {
expertsStatus.textContent = "expert id required";
return;
}
if (!String(expertNewNameEn.value || "").trim()) {
expertsStatus.textContent = "English name is required";
return;
}
if (!String(expertSoul.value || "").trim() && !String(expertRoleSystem.value || "").trim()) {
expertsStatus.textContent = "SOUL.md or ROLE_SYSTEM.md is required";
return;
}
try {
const res = await apiPost("/admin/api/experts", {
id: eid,
display_name_en: String(expertNewNameEn.value || "").trim(),
display_name_zh: String(expertNewNameZh.value || "").trim(),
role: "expert",
files: {
"SOUL.md": expertSoul.value,
"ROLE_SYSTEM.md": expertRoleSystem.value,
},
});
if (!res || res.ok !== true) throw new Error(String((res && res.error) || "create_failed"));
expertNewId.value = "";
expertNewNameEn.value = "";
expertNewNameZh.value = "";
expertsStatus.textContent = "expert created";
markPrewarmReminder("expert_created");
await refresh();
expertsSelect.value = String(res.created || "");
fillExpertFields(currentExpertItem());
} catch (e) {
expertsStatus.textContent = String((e && e.message) || e);
}
},
});
const btnExpertSave = el("button", {
class: "btn btn--primary",
text: "Save expert files",
onclick: async () => {
if (!hasPermission("admin:tenant:write")) return;
const item = currentExpertItem();
if (!item) return;
if (!String(expertNameEn.value || "").trim()) {
expertsStatus.textContent = "English name is required";
return;
}
if (!String(expertSoul.value || "").trim() && !String(expertRoleSystem.value || "").trim()) {
expertsStatus.textContent = "SOUL.md or ROLE_SYSTEM.md is required";
return;
}
try {
const eid = String(item.id || "");
const res = await apiRequest("PATCH", "/admin/api/experts/" + encodeURIComponent(eid), {
display_name_en: String(expertNameEn.value || "").trim(),
display_name_zh: String(expertNameZh.value || "").trim(),
role: String(expertRoleSel.value || "expert"),
files: {
"SOUL.md": expertSoul.value,
"ROLE_SYSTEM.md": expertRoleSystem.value,
},
});
if (!res || res.ok !== true) throw new Error(String((res && res.error) || "update_failed"));
expertsStatus.textContent = "expert saved";
markPrewarmReminder("expert_updated");
await refresh();
expertsSelect.value = eid;
fillExpertFields(currentExpertItem());
} catch (e) {
expertsStatus.textContent = String((e && e.message) || e);
}
},
});
const btnExpertDelete = el("button", {
class: "btn btn--danger",
text: "Delete expert",
onclick: async () => {
if (!hasPermission("admin:tenant:write")) return;
const item = currentExpertItem();
if (!item) return;
if (item.builtin || String(item.role || "") === "system") {
expertsStatus.textContent = "system expert cannot be deleted";
return;
}
const eid = String(item.id || "");
if (!globalThis.confirm(`Delete expert ${eid}?`)) return;
try {
const res = await apiRequest("DELETE", "/admin/api/experts/" + encodeURIComponent(eid), {});
if (!res || res.ok !== true) throw new Error(String((res && res.error) || "delete_failed"));
expertsStatus.textContent = "expert deleted";
markPrewarmReminder("expert_deleted");
await refresh();
} catch (e) {
expertsStatus.textContent = String((e && e.message) || e);
}
},
});
expertsSelect.addEventListener("change", () => {
fillExpertFields(currentExpertItem());
});
await refresh();
if (!state) {
return el("div", {}, [
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("title.models") }),
el("div", { class: "muted", text: t("models.loadFailed") }),
el("pre", { class: "pre", text: status.textContent || "" }),
]),
]);
}
if (state.ok === true && Array.isArray(state.profiles) && state.profiles.length === 0) {
const noProfBody = [
el("div", { class: "card__title", text: t("title.models") }),
el("div", { class: "muted", text: t("models.noProfiles") }),
];
if (state.db_path) {
noProfBody.push(el("div", { class: "muted", text: `${t("models.dbPath")}: ${state.db_path}` }));
}
noProfBody.push(status);
return el("div", {}, [el("div", { class: "card" }, noProfBody)]);
}
if (state.ok !== true) {
return el("div", {}, [
el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("title.models") }),
el("div", { class: "muted", text: t("models.loadFailed") }),
el("pre", { class: "pre", text: JSON.stringify(state, null, 2) }),
status,
]),
]);
}
const topBits = [readonlyHint, status, modelsGrantsLinkRow];
if (state.db_path) {
topBits.push(el("div", { class: "muted", text: `${t("models.dbPath")}: ${state.db_path}` }));
}
// Show secret migration helper when legacy secrets exist.
topBits.push(secretsCard);
return renderPageShell({
title: t("title.models"),
subtitle: t("models.subtitle"),
sections: [
{ id: "models-overview", label: t("models.toc.overview") },
{ id: "models-bindings", label: t("models.toc.bindings") },
{ id: "models-ops-ai", label: t("models.toc.opsAi") },
{ id: "models-api", label: t("models.toc.api") },
{ id: "models-experts", label: "Experts" },
{ id: "models-eval", label: t("models.toc.eval") },
],
}, [
...topBits,
el("div", { class: "page-grid page-grid--two" }, [
renderSectionCard(t("models.sectionActive"), "", [
el("div", { class: "row" }, [el("label", { text: t("models.pickModel") }), activeSelect]),
el("div", { class: "row" }, [el("label", { text: t("models.chatModelSelector") }), chatModelSelectorVisibleCb]),
el("div", { class: "muted", text: t("models.chatModelSelectorHint") }),
], { id: "models-overview" }),
renderSectionCard(t("models.sectionNew"), "", [
el("div", { class: "row" }, [el("label", { text: t("models.profileName") }), newName]),
el("div", { class: "row" }, [el("label", { text: t("models.mode") }), newMode]),
el("div", { class: "row" }, [btnCreate]),
]),
]),
renderSectionCard(t("models.sectionBindings"), t("models.agentBindingHelp"), [
el("div", { class: "muted", text: t("models.bindingsExtraHint") }),
el("div", { class: "muted", text: t("models.bindingsScopeHint") }),
bindingWrap,
], { id: "models-bindings" }),
renderSectionCard(t("models.opsAiTitle"), t("models.opsAiHint"), [
el("div", { class: "row" }, [el("label", { text: t("models.labelSpecialist") }), opsAiSpecialistSelect]),
el("div", { class: "row" }, [el("label", { text: t("models.labelApiProfile") }), opsAiProfileSelect]),
], { id: "models-ops-ai" }),
renderSectionCard(t("models.sectionApi"), "", [
builtinCap,
readonlyProfileHint,
el("div", { class: "row" }, [el("label", { text: t("models.profileName") }), profName]),
el("div", { class: "row" }, [el("label", { text: t("models.mode") }), modeSel]),
el("div", { class: "row" }, [el("label", { text: t("models.model") }), modelInp]),
el("div", { class: "row" }, [el("label", { text: t("models.baseUrl") }), baseInp]),
el("div", { class: "row" }, [el("label", { text: t("models.thinkMode") }), thinkingModeCb]),
el("div", { class: "muted", text: t("models.thinkModeHint") }),
el("div", { class: "row" }, [el("label", { text: t("models.reasoningEffort") }), reasoningEffortSel]),
warnKey,
openaiHint,
ollamaHint,
el("div", { class: "row" }, [el("label", { text: t("models.apiKey") }), keyInp]),
el("div", { class: "row" }, [el("label", { text: t("models.rememberKey") }), rememberCb]),
el("div", { class: "row" }, [btnSave]),
el("div", { class: "row" }, [btnDelete]),
], { id: "models-api" }),
renderSectionCard("Experts", "Runtime registry and workspace prompt files are split into two sections below.", [
el("div", { class: "card u-pad-block" }, [
el("div", { class: "card__title", text: "Runtime Expert Registry" }),
el("div", { class: "row" }, [el("label", { text: "Existing" }), expertsSelect]),
el("div", { class: "row" }, [btnExpertDelete]),
el("div", { class: "row" }, [el("label", { text: "Create ID" }), expertNewId]),
el("div", { class: "row" }, [el("label", { text: "Create Name(en)" }), expertNewNameEn]),
el("div", { class: "row" }, [el("label", { text: "Create Name(zh)" }), expertNewNameZh]),
el("div", { class: "row" }, [btnExpertCreate]),
el("div", { class: "row" }, [el("label", { text: "Name(en)" }), expertNameEn]),
el("div", { class: "row" }, [el("label", { text: "Name(zh)" }), expertNameZh]),
el("div", { class: "row" }, [el("label", { text: "Role" }), expertRoleSel]),
]),
el("div", { class: "card u-pad-block" }, [
el("div", { class: "card__title", text: "Workspace Prompt Files" }),
el("div", { class: "muted", text: "SOUL.md or ROLE_SYSTEM.md is required." }),
el("div", { class: "row" }, [el("label", { text: "SOUL.md" }), expertSoul]),
el("div", { class: "row" }, [el("label", { text: "ROLE_SYSTEM.md" }), expertRoleSystem]),
el("div", { class: "row" }, [btnExpertSave]),
]),
expertsStatus,
], { id: "models-experts" }),
el("div", { class: "card section-card u-hidden", id: "models-eval" }, [evalDetails]),
]);
}
const PLUGINS_PAGE_SIZE = 15;
export { renderModels };