Localize Plugins actions and parallelize Plugins/Skills boot fetches.

Cut time-to-interactive on those admin pages and align button/table copy with the existing zh/en i18n switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 19:44:07 +08:00
parent f6f129931f
commit f92cb85a4a
6 changed files with 141 additions and 84 deletions

View file

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>oliver</title>
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-17" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-17" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-17" />
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-18" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-18" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-18" />
<style>
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
.admin-boot-splash {
@ -129,33 +129,33 @@
<script type="importmap">
{
"imports": {
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-17",
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-17",
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-17",
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-17",
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-17",
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-17",
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-17",
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-17",
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-17",
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-17",
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-17",
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-17",
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-17",
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-17",
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-17",
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-17",
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-17",
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-17",
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-17",
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-17",
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-17",
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-17",
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-17",
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-17"
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-18",
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-18",
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-18",
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-18",
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-18",
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-18",
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-18",
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-18",
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-18",
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-18",
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-18",
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-18",
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-18",
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-18",
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-18",
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-18",
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-18",
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-18",
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-18",
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-18",
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-18",
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-18",
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-18",
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-18"
}
}
</script>
<script type="module" src="/admin/assets/app.js?v=20260812-17" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
<script type="module" src="/admin/assets/app.js?v=20260812-18" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
</body>
</html>

View file

@ -3,4 +3,4 @@
* - interfaces/admin/static/index.html importmap + CSS/JS ?v=
* - interfaces/admin/static/app.js entry query (optional if importmap covers it)
*/
export const ADMIN_ASSET_V = "20260812-17";
export const ADMIN_ASSET_V = "20260812-18";

View file

@ -276,6 +276,38 @@ export default {
"plugins.toc.install": "Install",
"plugins.toc.instances": "Instances",
"plugins.toc.policy": "Policy",
"plugins.action.cancel": "Cancel",
"plugins.action.clear": "Clear",
"plugins.action.delete": "Delete",
"plugins.action.disable": "Disable",
"plugins.action.edit": "Edit",
"plugins.action.enable": "Enable",
"plugins.action.exportJson": "Export JSON",
"plugins.action.exportJsonTitle": "Download Cursor mcpServers snapshot for reinstall / migration",
"plugins.action.health": "Health",
"plugins.action.install": "Install",
"plugins.action.locate": "Locate",
"plugins.action.save": "Save",
"plugins.action.saveBinding": "Save Binding",
"plugins.action.saveToolPolicy": "Save Tool Policy",
"plugins.action.selectAll": "Select All",
"plugins.action.syncTools": "Sync Tools",
"plugins.binding.empty": "No MCP servers installed yet.",
"plugins.binding.hint": "Bind MCP servers to specialists (many-to-many).",
"plugins.binding.reverse": "Reverse view: server -> specialists",
"plugins.binding.specialist": "Specialist",
"plugins.col.action": "action",
"plugins.col.boundSpecialists": "bound specialists",
"plugins.col.commandUrl": "command / url",
"plugins.col.enabled": "enabled",
"plugins.col.health": "health",
"plugins.col.serverId": "server_id",
"plugins.col.tools": "tools",
"plugins.confirm.delete": "Delete {sid} from MCP registry?",
"plugins.edit.configHint": "Cursor server config (single mcpServers value)",
"plugins.edit.enabled": "Enabled",
"plugins.edit.title": "Edit MCP server",
"plugins.edit.titleNamed": "Edit MCP: {sid}",
"profile.avatar": "Avatar",
"profile.avatarHint": "PNG, JPEG, WebP, or GIF, up to 2 MB.",
"profile.chatPrefs": "Chat Preferences",

View file

@ -276,6 +276,38 @@ export default {
"plugins.toc.install": "安装",
"plugins.toc.instances": "实例",
"plugins.toc.policy": "策略",
"plugins.action.cancel": "取消",
"plugins.action.clear": "清空",
"plugins.action.delete": "删除",
"plugins.action.disable": "禁用",
"plugins.action.edit": "编辑",
"plugins.action.enable": "启用",
"plugins.action.exportJson": "导出 JSON",
"plugins.action.exportJsonTitle": "下载 Cursor mcpServers 快照,便于重装/迁移",
"plugins.action.health": "健康检查",
"plugins.action.install": "安装",
"plugins.action.locate": "定位",
"plugins.action.save": "保存",
"plugins.action.saveBinding": "保存绑定",
"plugins.action.saveToolPolicy": "保存工具策略",
"plugins.action.selectAll": "全选",
"plugins.action.syncTools": "同步 Tools",
"plugins.binding.empty": "尚未安装 MCP 服务。",
"plugins.binding.hint": "将 MCP 服务绑定到专家(多对多)。",
"plugins.binding.reverse": "反向视图:server → specialists",
"plugins.binding.specialist": "专家",
"plugins.col.action": "操作",
"plugins.col.boundSpecialists": "已绑定专家",
"plugins.col.commandUrl": "command / url",
"plugins.col.enabled": "启用",
"plugins.col.health": "健康",
"plugins.col.serverId": "server_id",
"plugins.col.tools": "tools",
"plugins.confirm.delete": "从 MCP 注册表删除 {sid}?",
"plugins.edit.configHint": "Cursor 服务配置(单个 mcpServers 值)",
"plugins.edit.enabled": "启用",
"plugins.edit.title": "编辑 MCP 服务",
"plugins.edit.titleNamed": "编辑 MCP:{sid}",
"profile.avatar": "头像",
"profile.avatarHint": "支持 PNG、JPEG、WebP、GIF,最大 2MB。",
"profile.chatPrefs": "对话偏好",

View file

@ -75,8 +75,7 @@ function cursorCommandSummary(cursor) {
async function renderPlugins() {
document.querySelectorAll("[data-mcp-edit-modal]").forEach((n) => n.remove());
document.querySelectorAll("body > .row-actions__menu, body > .chat-sess-menu-pop.row-actions__menu").forEach((n) => n.remove());
const p = await apiGet("/admin/api/plugins");
let toolPolicy = {
const defaultToolPolicy = {
disable_tool_confirm: false,
enforced_retry_mode: "first_round_only",
tool_loop_state_machine: true,
@ -100,17 +99,17 @@ async function renderPlugins() {
wecom_longconn_workers: 2,
wecom_longconn_inbound_queue_maxsize: 200,
};
let mcp = { servers: [] };
let mcpBinding = { available_specialists: ["generalist"], servers: [], mapping: {} };
try {
mcp = await apiGet("/admin/api/mcp/servers");
} catch (_) {}
try {
mcpBinding = await apiGet("/admin/api/mcp/binding");
} catch (_) {}
try {
toolPolicy = await apiGet("/admin/api/tool-policy");
} catch (_) {}
const [p, mcp, mcpBinding, toolPolicyRaw] = await Promise.all([
apiGet("/admin/api/plugins"),
apiGet("/admin/api/mcp/servers").catch(() => ({ servers: [] })),
apiGet("/admin/api/mcp/binding").catch(() => ({
available_specialists: ["generalist"],
servers: [],
mapping: {},
})),
apiGet("/admin/api/tool-policy").catch(() => null),
]);
let toolPolicy = toolPolicyRaw && typeof toolPolicyRaw === "object" ? toolPolicyRaw : defaultToolPolicy;
const pluginCatalog = Array.isArray(p.plugins) ? p.plugins : [];
const pluginPageRef = { value: 1 };
@ -222,7 +221,7 @@ async function renderPlugins() {
});
const saveToolPolicyBtn = el("button", {
class: "btn",
text: "Save Tool Policy",
text: t("plugins.action.saveToolPolicy"),
onclick: async () => {
const r = await apiPost("/admin/api/tool-policy", {
turn_max_tool_workers: Number(turnMaxWorkersInput.value || 8),
@ -312,7 +311,7 @@ async function renderPlugins() {
});
const locateBtn = el("button", {
class: "btn btn--small",
text: "Locate",
text: t("plugins.action.locate"),
onclick: () => flashInstalledRow(sid),
});
return el("tr", {}, [
@ -327,9 +326,9 @@ async function renderPlugins() {
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: "server_id" }),
el("th", { text: "bound specialists" }),
el("th", { text: "action" }),
el("th", { text: t("plugins.col.serverId") }),
el("th", { text: t("plugins.col.boundSpecialists") }),
el("th", { text: t("plugins.col.action") }),
]),
]),
el("tbody", {}, rows.length ? rows : [el("tr", {}, [el("td", { text: "-", colspan: "3" })])]),
@ -350,7 +349,7 @@ async function renderPlugins() {
const slice = bindingServers.slice(start, start + PLUGINS_PAGE_SIZE);
bindingListRowsMount.innerHTML = "";
if (!bindingServers.length) {
bindingListRowsMount.appendChild(el("div", { class: "muted", text: "No MCP servers installed yet." }));
bindingListRowsMount.appendChild(el("div", { class: "muted", text: t("plugins.binding.empty") }));
bindingListPagerMount.innerHTML = "";
const blBar0 = pluginsPagerBar(bindingListTotalHolder, bindingListPageRef, renderBindingList);
bindingListPagerMount.appendChild(blBar0.wrap);
@ -386,7 +385,7 @@ async function renderPlugins() {
});
const selectAllBindingBtn = el("button", {
class: "btn",
text: "Select All",
text: t("plugins.action.selectAll"),
onclick: () => {
const current = String(specialistSelect.value || "");
bindingDraft[current] = bindingServers.map((x) => String(x.server_id || "")).filter((x) => x);
@ -396,7 +395,7 @@ async function renderPlugins() {
});
const clearBindingBtn = el("button", {
class: "btn",
text: "Clear",
text: t("plugins.action.clear"),
onclick: () => {
const current = String(specialistSelect.value || "");
bindingDraft[current] = [];
@ -406,7 +405,7 @@ async function renderPlugins() {
});
const saveBindingBtn = el("button", {
class: "btn",
text: "Save Binding",
text: t("plugins.action.saveBinding"),
onclick: async () => {
const r = await apiPost("/admin/api/mcp/binding", { mapping: bindingDraft });
bindingStatus.textContent = `[binding] ` + JSON.stringify(r);
@ -435,7 +434,7 @@ async function renderPlugins() {
});
const installBtn = el("button", {
class: "btn",
text: "Install",
text: t("plugins.action.install"),
onclick: async () => {
const raw = String(jsonInstallInput.value || "").trim();
if (!raw) {
@ -472,8 +471,8 @@ async function renderPlugins() {
});
const mcpExportJsonBtn = el("button", {
class: "btn",
text: "Export JSON",
title: "Download Cursor mcpServers snapshot for reinstall / migration",
text: t("plugins.action.exportJson"),
title: t("plugins.action.exportJsonTitle"),
onclick: async () => {
let r;
try {
@ -525,7 +524,7 @@ async function renderPlugins() {
let editingServer = null;
const editModal = el("div", { class: "session-monitor-modal u-hidden", "data-mcp-edit-modal": "1" });
const editTitle = el("div", { class: "card__title", text: "Edit MCP server" });
const editTitle = el("div", { class: "card__title", text: t("plugins.edit.title") });
const editServerIdLab = el("div", { class: "muted", text: "" });
const editCursorInput = el("textarea", {
class: "input",
@ -551,8 +550,8 @@ async function renderPlugins() {
if (!row) return;
editingServer = row;
const sid = String(row.server_id || "");
editTitle.textContent = `Edit MCP: ${sid}`;
editServerIdLab.textContent = `server_id: ${sid}`;
editTitle.textContent = tf("plugins.edit.titleNamed", { sid });
editServerIdLab.textContent = `${t("plugins.col.serverId")}: ${sid}`;
const cursor = row.cursor && typeof row.cursor === "object" ? row.cursor : {};
editCursorInput.value = JSON.stringify(cursor, null, 2);
editEnabledCb.checked = !!row.enabled;
@ -568,7 +567,7 @@ async function renderPlugins() {
});
const editSaveBtn = el("button", {
class: "btn btn--primary",
text: "Save",
text: t("plugins.action.save"),
onclick: async () => {
if (!editingServer) return;
const sid = String(editingServer.server_id || "").trim();
@ -608,16 +607,16 @@ async function renderPlugins() {
el("div", { class: "card session-monitor-modal__card", style: "width:min(720px,96vw);" }, [
editTitle,
editServerIdLab,
el("div", { class: "muted", text: "Cursor server config (single mcpServers value)" }),
el("div", { class: "muted", text: t("plugins.edit.configHint") }),
editCursorInput,
el("div", { class: "row", style: "align-items:center;gap:10px;flex-wrap:wrap;" }, [
el("label", { class: "kv" }, [editEnabledCb, document.createTextNode(" Enabled")]),
el("label", { class: "kv" }, [editEnabledCb, document.createTextNode(` ${t("plugins.edit.enabled")}`)]),
el("label", { text: "timeout_s" }),
editTimeoutInput,
]),
editStatus,
el("div", { class: "row u-row-end", style: "gap:8px;margin-top:10px;" }, [
el("button", { class: "btn", text: "Cancel", onclick: closeEditModal }),
el("button", { class: "btn", text: t("plugins.action.cancel"), onclick: closeEditModal }),
editSaveBtn,
]),
]),
@ -645,11 +644,11 @@ async function renderPlugins() {
const cursor = x.cursor && typeof x.cursor === "object" ? x.cursor : {};
const actions = rowActions("⋯", [
{
label: "Edit",
label: t("plugins.action.edit"),
onClick: () => openEditModal(x),
},
{
label: x.enabled ? "Disable" : "Enable",
label: x.enabled ? t("plugins.action.disable") : t("plugins.action.enable"),
onClick: async () => {
await apiPost("/admin/api/mcp/toggle", { server_id: sid, enabled: !x.enabled });
markPrewarmReminder("mcp_toggled");
@ -658,7 +657,7 @@ async function renderPlugins() {
},
},
{
label: "Health",
label: t("plugins.action.health"),
onClick: async () => {
const r = await apiPost("/admin/api/mcp/healthcheck", { server_id: sid });
installStatus.textContent = `[health:${sid}] ` + JSON.stringify(r);
@ -666,7 +665,7 @@ async function renderPlugins() {
},
},
{
label: "Sync Tools",
label: t("plugins.action.syncTools"),
onClick: async () => {
const r = await apiPost("/admin/api/mcp/tools/sync", { server_id: sid });
installStatus.textContent = `[sync:${sid}] ` + JSON.stringify(r);
@ -674,10 +673,10 @@ async function renderPlugins() {
},
},
{
label: "Delete",
label: t("plugins.action.delete"),
danger: true,
onClick: async () => {
if (!window.confirm(`Delete ${sid} from MCP registry?`)) return;
if (!window.confirm(tf("plugins.confirm.delete", { sid }))) return;
const r = await apiPost("/admin/api/mcp/delete", { server_id: sid });
installStatus.textContent = `[delete:${sid}] ` + JSON.stringify(r);
markPrewarmReminder("mcp_deleted");
@ -809,11 +808,11 @@ async function renderPlugins() {
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: "server_id" }),
el("th", { text: "command / url" }),
el("th", { text: "tools" }),
el("th", { text: "enabled" }),
el("th", { text: "health" }),
el("th", { text: t("plugins.col.serverId") }),
el("th", { text: t("plugins.col.commandUrl") }),
el("th", { text: t("plugins.col.tools") }),
el("th", { text: t("plugins.col.enabled") }),
el("th", { text: t("plugins.col.health") }),
el("th", { class: "table__cell--actions", text: t("tenants.rowActions") }),
]),
]),
@ -845,16 +844,16 @@ async function renderPlugins() {
]);
const foldMcpBinding = pluginsFold(t("plugins.fold.bindingEdit"), [
el("div", { class: "muted", text: "Bind MCP servers to specialists (many-to-many)." }),
el("div", { class: "muted", text: t("plugins.binding.hint") }),
el("div", { class: "row" }, [
el("label", { text: "Specialist" }),
el("label", { text: t("plugins.binding.specialist") }),
specialistSelect,
selectAllBindingBtn,
clearBindingBtn,
saveBindingBtn,
]),
bindingListWrap,
el("div", { class: "muted", text: "Reverse view: server -> specialists" }),
el("div", { class: "muted", text: t("plugins.binding.reverse") }),
bindingReverseWrap,
bindingStatus,
]);

View file

@ -1220,18 +1220,13 @@ async function renderSkills() {
};
const refreshSkillsState = async () => {
await loadRows();
await loadAudits();
await loadSkillBinding();
await Promise.all([loadRows(), loadAudits(), loadSkillBinding()]);
repaint();
renderSkillBindingList();
};
try {
await loadRows();
await loadAudits();
await loadSkillBinding();
await loadSkillMode();
await Promise.all([loadRows(), loadAudits(), loadSkillBinding(), loadSkillMode()]);
const rolesForPreview = (
Array.isArray(skillBindingState.roles) && skillBindingState.roles.length
? skillBindingState.roles
@ -1378,8 +1373,7 @@ async function renderSkills() {
llmToolsBox.addEventListener("toggle", async () => {
if (!llmToolsBox.open || lazyLoadState.llmLoaded) return;
lazyLoadState.llmLoaded = true;
await loadExposureTraceSetting();
await loadLLMToolsPreview(llmRoleSelect.value);
await Promise.all([loadExposureTraceSetting(), loadLLMToolsPreview(llmRoleSelect.value)]);
});
selfCheckBox.addEventListener("toggle", async () => {
if (!selfCheckBox.open || lazyLoadState.selfCheckLoaded) return;