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,742 @@
import { state } from "./state.js";
import { t, tf, applyI18nStatic, toggleLang } from "./i18n/index.js";
const SESSION_MONITOR_ROLE_FILTER_KEY = "ops_session_monitor_role_filter";
const AUTH_TOKEN_KEY = "ops_admin_token";
const AUTH_SESSION_KEY = "ops_admin_session";
const CHAT_MEMORY_MODE_KEY = "ops_chat_memory_mode";
/** 与 localStorage 不同:sessionStorage 按标签页隔离,避免「管理员页 + 普通用户控制台」互相覆盖 token。 */
function authStoreGet(key) {
try {
return sessionStorage.getItem(key);
} catch (_) {
return null;
}
}
function authStoreSet(key, value) {
try {
sessionStorage.setItem(key, value);
} catch (_) {}
}
function authStoreRemove(key) {
try {
sessionStorage.removeItem(key);
} catch (_) {}
}
/** 历史版本把 token 放在 localStorage,多标签会串号;启动时清掉以免误读。 */
try {
localStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.removeItem(AUTH_SESSION_KEY);
} catch (_) {}
/**
* /chat 与 /admin 历史上使用不同 key;当用户在同一窗口从 /chat 跳到 /admin 时,
* 没有 opener 可用,需要从 chat 存储主动迁移一次以复用登录态。
*/
(function seedAdminAuthFromChatStorageIfNeeded() {
try {
if (authStoreGet(AUTH_TOKEN_KEY)) return;
const pairs = [
[sessionStorage, "ops_chat_token", "ops_chat_session"],
[localStorage, "ops_chat_token", "ops_chat_session"],
];
for (const [store, kTok, kSess] of pairs) {
let tok = null;
let sess = null;
try {
tok = store.getItem(kTok);
sess = store.getItem(kSess);
} catch (_) {
continue;
}
const t = String(tok || "").trim();
const s = String(sess || "").trim();
if (t && s) {
authStoreSet(AUTH_TOKEN_KEY, t);
authStoreSet(AUTH_SESSION_KEY, s);
break;
}
}
} catch (_) {}
})();
/** 从聊天页新开 /admin(审计、设置等)时,复用 opener 同一登录态(chat 或 admin token)。勿对 window.open 使用 noopener,否则读不到 opener。 */
(function seedAdminAuthFromOpenerIfNeeded() {
try {
if (authStoreGet(AUTH_TOKEN_KEY)) return;
const op = window.opener;
if (!op || op === window) return;
if (String(location.origin) !== String(op.location.origin)) return;
const pairs = [
["ops_admin_token", "ops_admin_session"],
["ops_chat_token", "ops_chat_session"],
];
for (const [kTok, kSess] of pairs) {
let tok = null;
let sess = null;
try {
tok = op.sessionStorage.getItem(kTok) || op.localStorage.getItem(kTok);
sess = op.sessionStorage.getItem(kSess) || op.localStorage.getItem(kSess);
} catch (_) {
continue;
}
const t = String(tok || "").trim();
const s = String(sess || "").trim();
if (t && s) {
authStoreSet(AUTH_TOKEN_KEY, t);
authStoreSet(AUTH_SESSION_KEY, s);
break;
}
}
} catch (_) {}
})();
/** 合并展示名与登录名(列表/团队页共用) */
function formatUserLabel(u) {
const un = String(u.username ?? "").trim();
const dn = String(u.display_name ?? "").trim();
if (dn && un && dn.toLowerCase() !== un.toLowerCase()) {
return `${dn} (${un})`;
}
return dn || un || "—";
}
function formatAuditActor(r) {
const un = String(r.actor_username ?? "").trim();
const dn = String(r.actor_display_name ?? "").trim();
const id = String(r.actor_user_id ?? "").trim();
if (dn && un && dn.toLowerCase() !== un.toLowerCase()) {
return `${dn} (${un})`;
}
if (dn || un) return dn || un;
return id ? id.slice(0, 8) + (id.length > 8 ? "…" : "") : "—";
}
function getRoute() {
const raw = (location.hash || "#/stack").replace(/^#\//, "");
const [pageRaw, queryRaw] = raw.split("?", 2);
let page = (pageRaw || "stack").trim();
if (page === "channels") page = "stack";
if (page === "tenants") page = "users";
const params = new URLSearchParams(queryRaw || "");
return { page, params };
}
/** 当控制台挂在子路径(如 /gw/admin)时,把 /admin/api/... 解析为 /gw/admin/api/... */
function resolveAdminApiUrl(path) {
const p = String(path || "");
if (!p.startsWith("/admin/")) return p;
const pathname = (location.pathname || "").replace(/\/+$/, "") || "/";
const marker = "/admin";
const pos = pathname.lastIndexOf(marker);
if (pos <= 0) return p;
return pathname.slice(0, pos) + p;
}
function resolveChatUrl() {
const pathname = (location.pathname || "").replace(/\/+$/, "") || "/";
const marker = "/admin";
if (pathname.endsWith(marker)) {
return `${pathname.slice(0, -marker.length) || ""}/chat`;
}
return "/chat";
}
function getStoredAuthToken() {
return String(authStoreGet(AUTH_TOKEN_KEY) || "").trim();
}
/** 会话在服务端失效或本地缺 token 时清理,并下一帧回到登录(避免在 router 内部 await router 盖住登录页) */
function scheduleReauthAfter401(requestUrl) {
const u = String(requestUrl || "");
if (u.includes("/admin/api/auth/login") || u.includes("/admin/api/auth/bootstrap")) return;
authStoreRemove(AUTH_TOKEN_KEY);
authStoreRemove(AUTH_SESSION_KEY);
state.authSession = null;
setTimeout(() => {
const fn = state.reauthHandler;
if (typeof fn === "function") fn();
}, 0);
}
function _haltAfter401() {
// 401 means "login required". We re-route to login; callers should not show a "request failed" popup.
// Returning a never-resolving promise avoids bubbling errors into UI code that would flash an alert.
return new Promise(() => {});
}
async function apiGet(path) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = { "accept": "application/json" };
if (token) headers["authorization"] = `Bearer ${token}`;
const res = await fetch(url, { headers });
if (res.status === 401) {
scheduleReauthAfter401(url);
return await _haltAfter401();
}
if (!res.ok) throw new Error(`GET ${url} ${res.status}`);
return await res.json();
}
async function apiGetOptional(path) {
try {
return await apiGet(path);
} catch (err) {
console.warn(`apiGetOptional failed path=${String(path || "")} err=${String(err)}`);
return null;
}
}
async function apiGetNoHang(path) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = { "accept": "application/json" };
if (token) headers["authorization"] = `Bearer ${token}`;
const res = await fetch(url, { headers });
if (res.status === 401) {
scheduleReauthAfter401(url);
// Returning null avoids Promise.all / await from hanging forever.
return null;
}
if (!res.ok) throw new Error(`GET ${url} ${res.status}`);
return await res.json();
}
async function apiPost(path, body) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = { "content-type": "application/json", "accept": "application/json" };
if (token) headers["authorization"] = `Bearer ${token}`;
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body ?? {}),
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch (_) {
data = null;
}
if (res.status === 401) {
scheduleReauthAfter401(url);
return await _haltAfter401();
}
if (!res.ok) {
const d = data && typeof data === "object" ? data : {};
const detail = (d).detail != null ? String((d).detail) : "";
const errKey = (d).error != null ? String((d).error) : "";
const msg = detail || errKey || `POST ${url} ${res.status}`;
throw new Error(msg);
}
return data ?? {};
}
/** Skills endpoints often return HTTP 200 with `{ ok: false, result: {...} }` on failure — treat as error for UX. */
function assertSkillMutationOk(r, fallbackMessage) {
if (!r || r.ok !== false) return;
const res = r.result && typeof r.result === "object" ? r.result : {};
const code = res.error_code != null ? String(res.error_code).trim() : "";
const detail = res.detail != null ? String(res.detail).trim() : "";
const msg = [code, detail].filter(Boolean).join(": ") || String(fallbackMessage || "skill operation failed");
throw new Error(msg);
}
async function apiRequest(method, path, body) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = { "accept": "application/json" };
if (token) headers["authorization"] = `Bearer ${token}`;
if (body !== undefined && method !== "GET" && method !== "HEAD") {
headers["content-type"] = "application/json";
}
const res = await fetch(url, {
method,
headers,
body: body !== undefined && method !== "GET" && method !== "HEAD" ? JSON.stringify(body ?? {}) : undefined,
});
if (res.status === 401) {
scheduleReauthAfter401(url);
return await _haltAfter401();
}
if (!res.ok) {
let msg = `${method} ${url} ${res.status}`;
try {
const j = await res.json();
if (j && j.detail !== undefined) {
msg = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
}
} catch (_) {}
throw new Error(msg);
}
const ct = res.headers.get("content-type") || "";
if (!ct.includes("application/json")) return {};
return await res.json();
}
async function apiPostFormData(path, formData) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = {};
if (token) headers.authorization = `Bearer ${token}`;
const res = await fetch(url, { method: "POST", headers, body: formData });
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch (_) {
data = null;
}
if (res.status === 401) {
scheduleReauthAfter401(url);
return await _haltAfter401();
}
if (!res.ok) {
const d = data && typeof data === "object" ? data : {};
const detail = d.detail != null ? String(d.detail) : "";
const errKey = d.error != null ? String(d.error) : "";
throw new Error(detail || errKey || `POST ${url} ${res.status}`);
}
return data ?? {};
}
async function apiDeleteJson(path) {
const url = resolveAdminApiUrl(path);
const token = getStoredAuthToken();
const headers = { accept: "application/json" };
if (token) headers.authorization = `Bearer ${token}`;
const res = await fetch(url, { method: "DELETE", headers });
if (res.status === 401) {
scheduleReauthAfter401(url);
return await _haltAfter401();
}
if (!res.ok) {
let msg = `DELETE ${url} ${res.status}`;
try {
const j = await res.json();
if (j && j.detail !== undefined) msg = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
} catch (_) {}
throw new Error(msg);
}
try {
return await res.json();
} catch (_) {
return {};
}
}
function _activeCopyCell() {
return document.querySelector(".cell-copyable.cell-selected");
}
function _selectCopyCell(cell) {
const prev = _activeCopyCell();
if (prev && prev !== cell) prev.classList.remove("cell-selected");
if (cell) cell.classList.add("cell-selected");
}
function _shouldIgnoreCopyShortcut(ev) {
const t = ev && ev.target ? ev.target : null;
if (!t || !(t instanceof HTMLElement)) return false;
const tag = String(t.tagName || "").toLowerCase();
if (t.isContentEditable) return true;
return tag === "input" || tag === "textarea" || tag === "select";
}
function _installCellCopyKeyboardShortcut() {
if (window.__adminCellCopyShortcutInstalled) return;
window.__adminCellCopyShortcutInstalled = true;
document.addEventListener("keydown", async (ev) => {
if (_shouldIgnoreCopyShortcut(ev)) return;
if (!(ev.ctrlKey || ev.metaKey) || String(ev.key || "").toLowerCase() !== "c") return;
const active = _activeCopyCell();
if (!active) return;
try {
const txt = String(active.getAttribute("data-copy-text") || active.getAttribute("title") || active.textContent || "");
if (!txt.trim()) return;
ev.preventDefault();
await navigator.clipboard.writeText(txt);
active.classList.add("cell-copied");
setTimeout(() => active.classList.remove("cell-copied"), 2600);
} catch (_) {
// no-op
}
});
}
function attachCellCopyBehavior(cell, getCopyText) {
if (!cell || cell.dataset.copyBound === "1") return;
_installCellCopyKeyboardShortcut();
cell.dataset.copyBound = "1";
cell.classList.add("cell-copyable");
cell.addEventListener("click", () => {
_selectCopyCell(cell);
});
cell.addEventListener("dblclick", async () => {
try {
const txt = String(typeof getCopyText === "function" ? getCopyText() : (cell.textContent || ""));
if (!txt) return;
cell.setAttribute("data-copy-text", txt);
await navigator.clipboard.writeText(txt);
cell.classList.add("cell-copied");
setTimeout(() => cell.classList.remove("cell-copied"), 2600);
} catch (_) {
// keep tooltip fallback when clipboard API unavailable
}
});
}
function el(tag, attrs = {}, children = []) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (v === undefined || v === null) continue;
if (k === "class") e.className = v;
else if (k === "text") e.textContent = v;
else if (k.startsWith("on") && typeof v === "function") e.addEventListener(k.slice(2), v);
else if (k === "disabled" && v === false) {
e.removeAttribute("disabled");
} else e.setAttribute(k, v);
}
for (const c of children) e.appendChild(c);
if (String(tag || "").toLowerCase() === "td" && String(e.getAttribute("data-copy-disabled") || "") !== "1") {
const txt = String(e.textContent || "").trim();
if (txt) {
if (!e.getAttribute("title")) e.setAttribute("title", txt);
attachCellCopyBehavior(e, () => String(e.getAttribute("title") || e.textContent || ""));
}
}
return e;
}
function tdCell(value, maxLen = 120) {
const full = String(value ?? "");
const shown = formatSystemLocalDateTime(full);
const title = shown === full ? full : `${shown}\n${full}`;
const td = el("td", { text: shortText(shown, maxLen), title });
td.setAttribute("data-copy-text", shown);
attachCellCopyBehavior(td, () => shown);
return td;
}
function enableTableColumnResize(tableEl, columnIndexes = []) {
if (!tableEl) return;
const heads = Array.from(tableEl.querySelectorAll("thead th"));
if (!heads.length) return;
tableEl.classList.add("table--resizable");
heads.forEach((th) => th.querySelectorAll(".table-col-resizer").forEach((x) => x.remove()));
const enabled = new Set(Array.isArray(columnIndexes) ? columnIndexes : []);
heads.forEach((th, idx) => {
if (enabled.size && !enabled.has(idx)) return;
const handle = document.createElement("span");
handle.className = "table-col-resizer";
handle.addEventListener("mousedown", (ev) => {
ev.preventDefault();
ev.stopPropagation();
const startX = ev.clientX;
const startW = th.getBoundingClientRect().width;
document.body.classList.add("col-resize-active");
const onMove = (mv) => {
const next = Math.max(72, Math.round(startW + (mv.clientX - startX)));
th.style.width = `${next}px`;
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
document.body.classList.remove("col-resize-active");
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
th.appendChild(handle);
});
}
function setActive(page) {
document.querySelectorAll(".nav__item").forEach((a) => {
a.classList.toggle("nav__item--active", a.dataset.page === page);
});
document.getElementById("topTitle").textContent = ({
stack: t("title.stack"),
"scheduled-jobs": t("title.scheduledJobs"),
users: t("title.users"),
memory: t("title.memory"),
models: t("title.models"),
"api-grants": t("title.apiGrants"),
audit: t("title.audit"),
"session-monitor": t("title.sessionMonitor"),
"admin-audit": t("title.adminAudit"),
plugins: t("title.plugins"),
skills: t("title.skills"),
attachments: t("title.attachments"),
"workspace-paths": t("title.workspacePaths"),
profile: t("title.profile"),
}[page] || page);
}
function mount(node) {
const c = document.getElementById("content");
c.innerHTML = "";
c.appendChild(node);
}
function renderPageShell(opts = {}, children = []) {
const title = String(opts.title || "").trim();
const subtitle = String(opts.subtitle || "").trim();
const actions = Array.isArray(opts.actions) ? opts.actions.filter(Boolean) : [];
const sections = Array.isArray(opts.sections) ? opts.sections.filter((x) => x && x.id && x.label) : [];
const head = [];
if (title || subtitle || actions.length) {
const left = [];
if (title) left.push(el("h1", { class: "page-shell__title", text: title }));
if (subtitle) left.push(el("div", { class: "page-shell__subtitle muted", text: subtitle }));
head.push(
el("div", { class: "page-shell__header" }, [
el("div", { class: "page-shell__headline" }, left),
el("div", { class: "page-shell__actions row" }, actions),
]),
);
if (sections.length > 1) {
head.push(
el(
"div",
{ class: "page-shell__toc", role: "navigation", "aria-label": "page sections" },
sections.map((s) =>
el("button", {
class: "page-shell__tocItem",
type: "button",
text: String(s.label),
onclick: () => {
const target = document.getElementById(String(s.id));
if (!target) return;
target.scrollIntoView({ behavior: "smooth", block: "start" });
},
}),
),
),
);
}
}
return el("section", { class: "page-shell" }, [...head, ...children.filter(Boolean)]);
}
function renderSectionCard(title, subtitle, bodyNodes = [], opts = {}) {
const headlineKids = [el("div", { class: "card__title", text: title })];
if (subtitle) headlineKids.push(el("div", { class: "card__subtitle muted", text: subtitle }));
const headKids = [el("div", { class: "card__headline" }, headlineKids)];
if (Array.isArray(opts.actions) && opts.actions.length) {
headKids.push(el("div", { class: "card__actions row" }, opts.actions));
}
const nodes = [el("div", { class: "card__head" }, headKids)];
bodyNodes.filter(Boolean).forEach((node) => nodes.push(node));
const attrs = { class: "card section-card" };
if (opts.id) attrs.id = String(opts.id);
return el("div", attrs, nodes);
}
function renderMetaChips(entries) {
const items = [];
const src = entries && typeof entries === "object" ? entries : {};
Object.keys(src).forEach((k) => {
const v = src[k];
if (v == null || v === "") return;
items.push(el("span", { class: "meta-chip", text: `${k}=${v}` }));
});
return el("div", { class: "meta-line" }, items.length ? items : [el("span", { class: "muted", text: "—" })]);
}
function shortText(v, maxLen = 200) {
const s = String(v ?? "");
if (s.length <= maxLen) return s;
return s.slice(0, Math.max(0, maxLen - 3)) + "...";
}
function formatSystemLocalDateTime(value) {
const raw = String(value ?? "").trim();
if (!raw) return "";
// Keep non-datetime strings unchanged.
if (!/^\d{4}-\d{2}-\d{2}T/.test(raw)) return raw;
const d = new Date(raw);
if (Number.isNaN(d.getTime())) return raw;
const locale = state.currentLang === "zh" ? "zh-CN" : "en-US";
try {
return d.toLocaleString(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
} catch (_) {
return raw;
}
}
function formatIds(v) {
if (!Array.isArray(v)) return "";
const vals = v.map((x) => String(x ?? "").trim()).filter(Boolean);
if (!vals.length) return "";
if (vals.length > 4) return vals.slice(0, 4).join(", ") + ` (+${vals.length - 4})`;
return vals.join(", ");
}
function yesNo(v) {
return v ? "yes" : "no";
}
let runtimePrewarmReminder = "";
function markPrewarmReminder(reason) {
const why = String(reason || "").trim();
const base = t("stack.configChangedPrewarm");
runtimePrewarmReminder = why ? `${base} [${why}]` : base;
}
/**
* Compact row action menu (button + fixed popover).
* Must NOT use nested &lt;details&gt; inside plugins-fold details — clicks get swallowed.
* items: [{ label, onClick, danger?, disabled? }]
*/
function rowActions(label, items) {
let menuEl = null;
let onDoc = null;
const close = () => {
if (onDoc) {
document.removeEventListener("click", onDoc, true);
onDoc = null;
}
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
menuEl = null;
};
const btn = el("button", {
class: "chat-sess-more row-actions__btn",
type: "button",
text: String(label || "⋯"),
title: "actions",
"aria-label": "actions",
onclick: (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (menuEl) {
close();
try {
btn.blur();
} catch (_) {}
return;
}
const menu = el("div", { class: "chat-sess-menu-pop row-actions__menu u-overlay-fixed" });
menu.style.visibility = "hidden";
(Array.isArray(items) ? items : []).forEach((it) => {
if (!it || !it.label) return;
menu.appendChild(
el("button", {
class: "chat-sess-menu-item" + (it.danger ? " row-actions__item--danger" : ""),
type: "button",
text: String(it.label),
disabled: !!it.disabled,
onclick: async (e2) => {
e2.preventDefault();
e2.stopPropagation();
close();
try {
btn.blur();
} catch (_) {}
if (typeof it.onClick !== "function") return;
try {
await it.onClick();
} catch (err) {
console.warn("row action failed", err);
window.alert(String((err && err.message) || err || "action failed"));
}
},
}),
);
});
document.body.appendChild(menu);
const rect = ev.currentTarget.getBoundingClientRect();
const pad = 8;
const mrect = menu.getBoundingClientRect();
// Prefer opening to the left of the ⋯ (actions column is trailing).
let left = rect.right - mrect.width;
let top = rect.bottom + 4;
if (left < pad) {
left = Math.min(rect.left, window.innerWidth - pad - mrect.width);
}
if (top + mrect.height > window.innerHeight - pad) {
top = Math.max(pad, rect.top - 4 - mrect.height);
}
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
menu.style.visibility = "visible";
menuEl = menu;
try {
btn.blur();
} catch (_) {}
onDoc = (e3) => {
if (menu.contains(e3.target) || btn.contains(e3.target)) return;
close();
};
setTimeout(() => document.addEventListener("click", onDoc, true), 0);
},
});
return btn;
}
/** Navigate/reload admin SPA without importing router (avoids cycles). */
function navigateAdmin() {
const fn = state.navigate;
if (typeof fn === "function") return fn();
return Promise.resolve();
}
export {
SESSION_MONITOR_ROLE_FILTER_KEY,
AUTH_TOKEN_KEY,
AUTH_SESSION_KEY,
CHAT_MEMORY_MODE_KEY,
authStoreGet,
authStoreSet,
authStoreRemove,
formatUserLabel,
formatAuditActor,
getRoute,
resolveAdminApiUrl,
resolveChatUrl,
getStoredAuthToken,
scheduleReauthAfter401,
_haltAfter401,
apiGet,
apiGetOptional,
apiGetNoHang,
apiPost,
assertSkillMutationOk,
apiRequest,
apiPostFormData,
apiDeleteJson,
_activeCopyCell,
_selectCopyCell,
_shouldIgnoreCopyShortcut,
_installCellCopyKeyboardShortcut,
attachCellCopyBehavior,
el,
tdCell,
enableTableColumnResize,
setActive,
mount,
renderPageShell,
renderSectionCard,
renderMetaChips,
shortText,
formatSystemLocalDateTime,
formatIds,
yesNo,
runtimePrewarmReminder,
markPrewarmReminder,
rowActions,
navigateAdmin,
state,
t,
tf,
applyI18nStatic,
toggleLang,
};