Files
dsh_shenxian/poc/business-plugins/lib/client.js
T
admin ac67db7702 fix(ui): 「系统管理」分区改为仅 admin 注册(普通用户不再看到该栏)
- apply() 先异步判 /api/auth/me 角色,非 admin 不注入该 section(fail-closed)
- 依据:官方 settings 外壳按 ctx.slots.getVersion('settings.section') 重算导航
  (dsh-client-ui-settings/lib/client.js:560)⇒ 晚注册会被正确反映
- 验收脚本补「非 admin 只注册 1 个分区」断言 + 等一拍异步注册;npm run verify 全绿
- 版本 0.3.1 → 0.3.2,已铺发
2026-09-13 20:19:01 +08:00

1237 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @dsh-local/business-plugins — client half (dsh 0.1.2-rc.1, v0.2.4)
//
// dsh client bundles 以普通脚本形式执行,向 window.__ModuleLoader__ 注册 factory;
// 首次 import 时惰性物化 factory(require) -> exports。require 解析平台 seed 词
// (react/jsx-runtime)与图行(其他 dsh.client 包)。**严格镜像官方 client bundle
// 的 CJS-style factory 形态,只导出 apply + inject,绝不 exports.default**(R3:
// exports.default 会让 loader 的 ESM/CJS interop 把 bare apply FUNCTION 当作插件体,
// 无 inject 声明点 → ctx.slots 抛 cannot get property "slots" without inject)。
//
// v0.2.0(档案 37 · 批次 3):
// · 配色全部改用官方 design token `--dsw-*`(原来硬编码深色)→ **自动跟随 dsh 主题**
// (亮/暗/第三方主题都适配),不再需要自己实现主题切换。
// · 文案走官方 locale:ctx.locale.register(NS,{zh,en}) + ctx.locale.bind(NS) → 中英双语。
// · 交互:搜索过滤 + 每行状态徽章(已启用/未启用)+ **被自动禁用插件的行内提示与重试**
// + 空态/加载态。
// · 应用成功且实例重启后,**自动探活并刷新页面**(重启会换端口,刷新即可接上新实例)。
//
// v0.2.4(档案 67 · 对齐 06-工作台UI规范.md):
// · **信息层次反转**:主视觉改为插件的**用途说明**(候选池入库时已优先取官方目录的中文),
// 插件名 / 版本退为副行小字 —— 与门户 plugins 页同一决策(「一眼知道这插件干什么」)。
// · 字号对齐规范 §2.2 阶梯(section 语境取 14 / 13 / 12,不照搬页面级 16px 基准);
// 徽章改 `2px 8px` r10、按钮改 `.btn-sm` 量级、行距与内边距按规范 §2.4 序列。
// · 空态/加载态按规范 §4.9 改为「标题 + 说明」层次,不再是一行灰字。
// · 新增注入式 CSS 提供行 hover 反馈与按钮 hover(规范 §4.3 / §5);disposer 随 fiber 移除。
//
// v0.2.5(档案 67 补 · 按 `impeccable` 的 craft-floor 复核后修正;检测器未随技能包提供,人工过检):
// · **去掉 rejected 提示的彩色 `border-left`** —— craft-floor 明令:卡片/列表项/提示上
// 不得出现 >1px 的彩色 border-left/right。改为整块浅底 + 圆角,危险语义由标题文字色承担。
// · **对比度**:所有必读文字从 `--dsw-alias-label-tertiary` 提到 `secondary`
// (tertiary 在亮色下约 3.5:1,低于 craft-floor 的 4.5:1 底线)。
// · **层次**:主视觉加 `fontWeight: 500`,与 12px 副行拉开 weight 台阶(原先只差 2px 字号,
// 属于 craft-floor 点名的「scale and weight steps 不明显」)。
// · **动效**:transition 由 `.15s ease` 改为 `.18s cubic-bezier(.16,1,.3,1)`(exponential ease-out)。
//
// v0.2.7(档案 75 维度 B 落地 · 2026-09-13 用户要求):
// · **卡片加内存预估**:每张卡片多一行「预估内存 ≈ N MiB」小徽章,卡片加高(minHeight 112)
// 让「用途说明 / 包名 / 预估内存 / 状态徽章」四层排得开。
// · **列表上方新增「内存预估」状态条**:实心段 = 当前已启用插件的预估占用,
// 半透明段 = 本次勾选带来的增量;超过单实例上限(384 MiB)时整条转红并提示。
// · **口径**:`MEM_BASE_MIB`(-dsh 本体+官方插件基线) + `MEM_TABLE`(逐插件实测加载成本);
// **是预估值,不是实时读数**(客户端拿不到 cgroup 实测值,需平台加只读路由才能做实时 —— 见档案 75)。
//
// 功能:在 dsh 设置面板注册「功能管理」section —— 列出候选池 + 每个插件启用状态,
// 批量勾选后点「应用更改」,确认弹窗(提示重启会中断会话)→ POST 门户
// /api/plugins/mine/apply(跨子域 fetch,credentials include;门户已加 CORS)。
// 门户端启用=复制 bundle 到该用户 profile 并加入 bundles,禁用=从 bundles 移除,
// 然后重启该用户实例生效;**启用会让实例起不来的插件会被门户单独摘掉并标记**。
window.__ModuleLoader__.load({
id: "@dsh-local/business-plugins",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
var jsxRuntime = require("react/jsx-runtime");
var React = require("react");
// 门户主域:去掉实例子域 label(admin.alotbuy.com -> alotbuy.com)。
function portalHost() {
try {
var labels = window.location.hostname.split(".");
if (labels.length > 2) {
return window.location.protocol + "//" + labels.slice(1).join(".");
}
} catch (e) {}
return window.location.origin;
}
// 官方 design token(dsh 主题变量)。刻意**不硬编码颜色**:dsw-* 由主题提供,
// 亮/暗/第三方主题自动适配;fallback 仅在该 token 缺失时兜底。
// 注:06-工作台UI规范 §2.1 给的是亮色基线色值,本 section 嵌在 dsh 面板内,
// 跟随宿主主题优先,故取 token + 规范色值作 fallback。
var T = {
text: "var(--dsw-alias-label-primary, #24292f)",
sub: "var(--dsw-alias-label-secondary, #4e5969)",
dim: "var(--dsw-alias-label-tertiary, #6b7280)",
border: "var(--dsw-alias-border-l2, #e3e6ea)",
field: "var(--dsw-alias-bg-layer-3, #f5f6f8)",
primary: "var(--dsw-alias-brand-primary, #2f6fed)",
danger: "var(--dsw-alias-state-error-primary, #d93026)",
warn: "var(--dsw-alias-state-warning-primary, #bf8700)",
success: "var(--dsw-alias-state-success-primary, #1a7f37)"
};
// ── 内存预估模型(档案 75「维度 B 资源成本」的前端落地)────────────────────
// 口径:**预估值** = 空载基线 + Σ(已启用插件的加载成本)。客户端拿不到 cgroup 实测值,
// 因此这里是估算而非实时读数;数值来源 = 2026-09-13 隔离 cgroup 实测
//(`node --expose-gc` 逐项 import 的 rss 增量,见 `.workbuddy/memory/2026-09-13.md`)。
/** 单实例 cgroup 上限(MiB)—— 档案 58 定档;改它必须同时改编排器的 `MemoryMax`。 */
var MEM_LIMIT_MIB = 384;
/** 空载基线(MiB):dsh 本体 + 148 个官方插件 + 平台自研 ×3(smaps 实测 ≈ 285)。 */
var MEM_BASE_MIB = 285;
/** 逐插件加载成本(MiB,实测 rss 增量)。不在此表的按 MEM_FALLBACK_MIB 计。 */
var MEM_TABLE = {
"dsh-univer-office": 64,
"dsh-plugin-mcn-suite": 39
};
/** 未实测插件的兜底值 —— 平台自研轻量插件实测合计仅 1.7 MiB。 */
var MEM_FALLBACK_MIB = 2;
/** 越过该比例即提前警示,给页缓存与用户自己的任务留余量。 */
var MEM_TIGHT_RATIO = 0.85;
function memMiB(p) {
if (p && p.name && Object.prototype.hasOwnProperty.call(MEM_TABLE, p.name)) {
return MEM_TABLE[p.name];
}
return MEM_FALLBACK_MIB;
}
/** 把一组插件里 `key` 为真的那些的预估内存求和。 */
function sumMiB(list, key) {
var total = 0;
for (var i = 0; i < list.length; i++) {
if (list[i][key]) total += memMiB(list[i]);
}
return total;
}
/** 本插件的 locale 命名空间(官方 i18n)。 */
var NS = "business-plugins";
/** 简体中文字典(key 集的事实来源)。 */
var zh = {
"section.label": "功能管理",
"loading": "加载中…",
"loadingDesc": "正在从门户读取候选池…",
"emptyTitle": "暂无功能插件",
"empty": "需管理员先在门户「插件管理」里投放,之后可在此启用",
"noMatch": "没有匹配的插件",
"search": "搜索插件名或用途",
"selectAll": "全选",
"clear": "清空",
"enabled": "已启用",
"disabled": "未启用",
"selected": "已选",
"apply": "应用更改",
"applying": "应用中…",
"confirm.title": "确认应用更改?",
"confirm": "将重启当前 dsh 实例以生效。重启会中断当前会话(正在进行的对话会断开)。",
"confirm.cancel": "取消",
"confirm.ok": "确认应用",
"confirm.overTitle": "⚠ 本次勾选将超出单实例内存上限",
"nochange": "没有需要更改的插件",
"queued": "排队中…",
"installing": "正在安装",
"configuring": "正在配置",
"restarting": "正在重启实例",
"probing": "正在检查实例",
"isolating": "正在定位问题插件",
"applied": "✓ 已应用",
"reloading": "实例重启中,页面将自动刷新…",
"rejected.title": "以下插件与实例不兼容,已自动禁用",
"retry": "重试",
"requestFailed": "请求失败",
"loadFailed": "加载失败",
"mem.title": "内存预估",
"mem.est": "预估(实测基准,非实时读数)",
"mem.now": "当前",
"mem.planned": "勾选后",
"mem.limit": "上限",
"mem.left": "剩余",
"mem.over": "将超出上限",
"mem.tight": "接近上限",
"mem.safe": "余量充足",
"mem.overWarn": "勾选后将超出单实例内存上限,实例可能起不来。建议先停用部分插件再应用。",
"mem.perCard": "预估内存",
"pa.label": "系统管理",
"pa.readonly": "只读",
"pa.loading": "加载中…",
"pa.loadingDesc": "正在读取平台状态…",
"pa.notAdminTitle": "系统管理仅对管理员显示",
"pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。",
"pa.loadFailed": "平台状态读取失败",
"pa.retry": "重试",
"pa.openPortal": "打开完整管理台",
"pa.readonlyHint": "点开每一项,在弹窗内直接管理(无需跳转门户)。",
"pa.inst": "当前实例",
"pa.running": "运行中",
"pa.stopped": "未运行",
"pa.port": "端口",
"pa.restarts": "自动重启",
"pa.breaker": "熔断",
"pa.breakerOpen": "已熔断(冷却中)",
"pa.breakerOk": "正常",
"pa.users": "用户",
"pa.total": "合计",
"pa.pending": "待审核",
"pa.disabled": "已禁用",
"pa.storage": "存储",
"pa.storageUsers": "有用量的用户",
"pa.storageNote": "报告每小时刷新",
"pa.pool": "候选池",
"pa.poolCount": "已投放插件",
"pa.runtime": "运行时",
"pa.items": "条目",
"pa.usersTitle": "用户管理",
"pa.colUser": "用户名",
"pa.colRole": "角色",
"pa.colOp": "操作",
"pa.roleAdmin": "管理员",
"pa.roleActive": "已启用",
"pa.rolePending": "待审核",
"pa.roleDisabled": "已禁用",
"pa.opApprove": "通过",
"pa.opDisable": "禁用",
"pa.opEnable": "启用",
"pa.opRemove": "删除",
"pa.confirmRemove": "确认删除该用户?此操作不可撤销。",
"pa.done": "已更新",
"pa.failed": "操作失败",
"pa.noUsers": "暂无用户",
"pa.working": "处理中…"
};
/** English dictionary, key-set complete against zh. */
var en = {
"section.label": "Feature management",
"loading": "Loading…",
"loadingDesc": "Reading the candidate pool from the portal…",
"emptyTitle": "No feature plugins yet",
"empty": "An admin must publish them in the portal's plugin management first",
"noMatch": "No matching plugins",
"search": "Search by name or purpose",
"selectAll": "Select all",
"clear": "Clear",
"enabled": "Enabled",
"disabled": "Disabled",
"selected": "selected",
"apply": "Apply changes",
"applying": "Applying…",
"confirm.title": "Apply the changes?",
"confirm": "The dsh instance will restart, which interrupts the current session (any running conversation is disconnected).",
"confirm.cancel": "Cancel",
"confirm.ok": "Apply",
"confirm.overTitle": "⚠ This selection exceeds the per-instance memory limit",
"nochange": "No changes to apply",
"queued": "Queued…",
"installing": "Installing",
"configuring": "Configuring",
"restarting": "Restarting instance",
"probing": "Checking instance health",
"isolating": "Locating the failing plugin",
"applied": "✓ Applied",
"reloading": "Instance is restarting — the page will refresh automatically…",
"rejected.title": "These plugins are incompatible with the instance and were disabled automatically",
"retry": "Retry",
"requestFailed": "Request failed",
"loadFailed": "Failed to load",
"mem.title": "Memory estimate",
"mem.est": "estimate from measured baselines, not a live reading",
"mem.now": "now",
"mem.planned": "after selection",
"mem.limit": "limit",
"mem.left": "left",
"mem.over": "over the limit",
"mem.tight": "close to the limit",
"mem.safe": "headroom is fine",
"mem.overWarn": "The selection exceeds the per-instance memory limit and the instance may fail to start. Disable some plugins first.",
"mem.perCard": "est. memory",
"pa.label": "System management",
"pa.readonly": "read-only",
"pa.loading": "Loading…",
"pa.loadingDesc": "Reading platform status…",
"pa.notAdminTitle": "System management is admin-only",
"pa.notAdminDesc": "This account is not an admin, so platform info is hidden here.",
"pa.loadFailed": "Failed to read platform status",
"pa.retry": "Retry",
"pa.openPortal": "Open full admin console",
"pa.readonlyHint": "Open any item to manage it in a popup — no portal jump.",
"pa.inst": "Current instance",
"pa.running": "Running",
"pa.stopped": "Stopped",
"pa.port": "Port",
"pa.restarts": "Auto-restarts",
"pa.breaker": "Circuit breaker",
"pa.breakerOpen": "Open (cooling down)",
"pa.breakerOk": "OK",
"pa.users": "Users",
"pa.total": "Total",
"pa.pending": "Pending",
"pa.disabled": "Disabled",
"pa.storage": "Storage",
"pa.storageUsers": "Users with usage",
"pa.storageNote": "report refreshes hourly",
"pa.pool": "Candidate pool",
"pa.poolCount": "Published plugins",
"pa.runtime": "Runtime",
"pa.items": "items",
"pa.usersTitle": "User management",
"pa.colUser": "Username",
"pa.colRole": "Role",
"pa.colOp": "Actions",
"pa.roleAdmin": "Admin",
"pa.roleActive": "Active",
"pa.rolePending": "Pending",
"pa.roleDisabled": "Disabled",
"pa.opApprove": "Approve",
"pa.opDisable": "Disable",
"pa.opEnable": "Enable",
"pa.opRemove": "Delete",
"pa.confirmRemove": "Delete this user? This cannot be undone.",
"pa.done": "Updated",
"pa.failed": "Action failed",
"pa.noUsers": "No users",
"pa.working": "Working…"
};
/**
* Required services (cordis fiber inject). `locale` is required for the
* dictionaries; the slot registry for the settings section.
*/
var inject = ["slots", "locale"];
/** 应用成功后探活并刷新(重启会换端口;探通了再 reload,避免白屏)。 */
function waitAndReload(attempts) {
if (attempts <= 0) {
window.location.reload();
return;
}
fetch(window.location.origin + "/", { cache: "no-store" })
.then(function (r) {
if (r.ok) window.location.reload();
else setTimeout(function () { waitAndReload(attempts - 1); }, 1500);
})
.catch(function () { setTimeout(function () { waitAndReload(attempts - 1); }, 1500); });
}
function apply(ctx) {
ctx.effect(function () {
return ctx.locale.register(NS, { zh: zh, en: en });
}, "business-plugins: dictionaries");
// 内联样式表达不了 `:hover`,而 06-工作台UI规范要求「列表行 hover 反馈」(§4.3)
// 与「hover 才提示可点」(§5)。这里注入一小段 CSS,disposer 随 fiber 移除 ——
// 不引外部样式表,也不污染宿主全局(选择器统一带 bp- 前缀)。
ctx.effect(function () {
var el = document.createElement("style");
el.setAttribute("data-bp-style", "1");
el.textContent = [
".bp-row{transition:background .18s cubic-bezier(.16,1,.3,1),border-color .18s cubic-bezier(.16,1,.3,1),box-shadow .18s cubic-bezier(.16,1,.3,1)}",
".bp-row:hover{border-color:var(--dsw-alias-brand-primary, #2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.15)}",
".bp-btn{transition:border-color .18s cubic-bezier(.16,1,.3,1),color .18s cubic-bezier(.16,1,.3,1)}",
".bp-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary, #2f6fed);color:var(--dsw-alias-brand-primary, #2f6fed)}",
".bp-row input[type=checkbox]{cursor:pointer;margin:0}",
// 2026-09-13 ·「系统管理」视觉层(对齐 06-工作台UI规范:§4.5 卡片 hover 上浮 / §4.7 弹窗 / §4.1 按钮)
".pa-card{transition:border-color .15s cubic-bezier(.16,1,.3,1),box-shadow .15s cubic-bezier(.16,1,.3,1),transform .15s cubic-bezier(.16,1,.3,1)}",
".pa-card:hover{border-color:var(--dsw-alias-brand-primary,#2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.15);transform:translateY(-2px)}",
".pa-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}",
".pa-card:hover .pa-go{color:var(--dsw-alias-brand-primary,#2f6fed);opacity:1}",
".pa-go{opacity:.45;transition:color .15s,opacity .15s}",
".pa-btn{transition:border-color .15s cubic-bezier(.16,1,.3,1),color .15s cubic-bezier(.16,1,.3,1),background .15s cubic-bezier(.16,1,.3,1)}",
".pa-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed);background:rgba(47,111,237,.06)}",
".pa-row{transition:background .15s}",
".pa-row:hover{background:rgba(47,111,237,.06)}",
".pa-modal{box-shadow:0 8px 30px rgba(0,0,0,.28);animation:paIn .18s cubic-bezier(.16,1,.3,1)}",
"@keyframes paIn{from{opacity:0;transform:translateY(6px) scale(.985)}to{opacity:1;transform:none}}",
"@media (prefers-reduced-motion: reduce){.pa-card,.pa-modal{animation:none;transition:none}.pa-card:hover{transform:none}}"
].join("");
document.head.appendChild(el);
return function () { el.remove(); };
}, "business-plugins: styles");
var t = ctx.locale.bind(NS);
function BusinessPluginsSection() {
var useState = React.useState;
var useEffect = React.useEffect;
var pluginsState = useState([]);
var plugins = pluginsState[0];
var setPlugins = pluginsState[1];
var loadingState = useState(true);
var loading = loadingState[0];
var setLoading = loadingState[1];
var busyState = useState(false);
var busy = busyState[0];
var setBusy = busyState[1];
var msgState = useState("");
var msg = msgState[0];
var setMsg = msgState[1];
var queryState = useState("");
var query = queryState[0];
var setQuery = queryState[1];
var rejectedState = useState([]);
var rejected = rejectedState[0];
var setRejected = rejectedState[1];
// 「应用更改」的页内确认弹窗开关(替代 window.confirm)。
var confirmState = useState(false);
var confirmOpen = confirmState[0];
var setConfirmOpen = confirmState[1];
// 「服务端当前已启用」的 id 快照 —— 用于把「当前占用」与「勾选后会有多少」分开算。
// 没有它就没法在用户勾选时显示「增量」(p.enabled 会被 toggle 就地改掉)。
var savedState = useState([]);
var savedIds = savedState[0];
var setSavedIds = savedState[1];
function load() {
setLoading(true);
fetch(portalHost() + "/api/plugins/mine", { credentials: "include" })
.then(function (r) { return r.json(); })
.then(function (d) {
var list = d.plugins || [];
setPlugins(list);
// 记下「服务端认为已启用」的那批 → 内存条据此区分「当前」与「勾选后」。
setSavedIds(list.filter(function (p) { return p.enabled; }).map(function (p) { return p.id; }));
setLoading(false);
})
.catch(function () { setLoading(false); setMsg(t("loadFailed")); });
}
useEffect(function () { load(); }, []);
function toggle(id) {
setPlugins(plugins.map(function (p) {
if (p.id === id) return Object.assign({}, p, { enabled: !p.enabled });
return p;
}));
}
/**
* 打开**页内确认弹窗**(不再用 `window.confirm`)。
*
* 2026-09-13 用户要求:所有弹窗改成页面弹窗,视觉参考 MCN 工作台
* (`dsh-plugin-mcn` 的 `fixed inset:0` 遮罩 + 居中面板 + 点遮罩关闭 +
* `stopPropagation` + ✕,见其 `modalTitle/modalText/modalBtns` 三段结构)。
*/
function askApply() {
setConfirmOpen(true);
}
/** 用户已在弹窗里点「确认应用」:真正的提交(原 `applyChanges` 主体)。 */
function doApply() {
setConfirmOpen(false);
setBusy(true);
setMsg(t("queued"));
setRejected([]);
fetch(portalHost() + "/api/plugins/mine/apply", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ plugins: plugins.map(function (p) { return { id: p.id, enabled: p.enabled }; }) })
})
.then(function (r) { return r.json().catch(function () { return {}; }); })
.then(function (d) {
if (d.error) { setBusy(false); setMsg(t("requestFailed") + ":" + d.error); return; }
if (d.noop) { setBusy(false); setMsg(t("nochange")); return; }
pollTask(d.taskId);
})
.catch(function () { setBusy(false); setMsg(t("requestFailed")); });
}
// 异步任务:轮询「阶段 + 已用秒数」(B 方案,不滚日志)。
function pollTask(taskId) {
var start = Date.now();
var timer = setInterval(function () {
fetch(portalHost() + "/api/plugins/mine/task/" + taskId, { credentials: "include" })
.then(function (r) { return r.json().catch(function () { return {}; }); })
.then(function (task) {
var secs = Math.floor((Date.now() - start) / 1000);
var stage = task.stage || t("configuring");
if (task.status === "pending" || task.status === "running") {
setMsg("⏳ " + stage + "(" + secs + "s)");
} else if (task.status === "success") {
clearInterval(timer);
var bad = task.rejected || [];
setRejected(bad);
setBusy(false);
if (bad.length > 0) {
setMsg(t("applied") + " — " + t("rejected.title"));
load();
} else {
setMsg(t("applied") + " · " + t("reloading"));
waitAndReload(3);
}
} else {
clearInterval(timer);
setBusy(false);
setMsg("✗ " + (task.error || t("requestFailed")) + "(已回滚到改动前的状态)");
// 2026-09-12 修复:失败后必须重新拉取真实状态。否则本地 plugins 仍停留在
// 用户勾选后的样子 → 徽章显示「已启用」,与「安装失败」自相矛盾(用户实测报障)。
// 后端失败分支已 restoreProfile() 回滚,load() 即可取回正确状态。
load();
}
})
.catch(function () { /* 网络抖动忽略,继续下一轮 */ });
}, 1500);
}
// 06-工作台UI规范 §2.2 字号阶梯:section 语境取「正文 14 / 次要 13 / 辅助 12」三档
//(不照搬页面级 16px 基准 —— 本 section 嵌在 dsh 设置面板内,非独立页)。
var wrap = {
display: "flex",
flexDirection: "column",
gap: "10px",
padding: "4px 2px",
color: T.text,
fontSize: "14px"
};
/** 规范 §4.9 空态/加载态:应有「标题 + 说明」层次,而非一行灰字。 */
function StateBlock(props) {
return jsxRuntime.jsxs("div", {
style: {
padding: "28px 20px", display: "flex", flexDirection: "column",
alignItems: "center", gap: "6px", textAlign: "center"
},
children: [
jsxRuntime.jsx("div", {
key: "t",
style: { fontSize: "15px", fontWeight: 500, color: T.text },
children: props.title
}),
jsxRuntime.jsx("div", {
key: "d",
style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 },
children: props.desc
})
]
});
}
/**
* 内存预估状态条(置于列表上方 —— 2026-09-13 用户要求)。
*
* 三段视觉:**实心** = 「服务端当前已启用」那批的预估占用;**半透明** = 本次勾选
* 带来的增量;超过单实例上限时整条转红并给出文字警告。全程只读本地数据、不发请求。
*/
function MemBar(props) {
var limit = props.limit;
var nowMiB = props.nowMiB;
var plannedMiB = props.plannedMiB;
var over = plannedMiB > limit;
var tight = !over && plannedMiB > limit * MEM_TIGHT_RATIO;
var accent = over ? T.danger : (tight ? T.warn : T.success);
var nowPct = Math.max(0, Math.min(100, (nowMiB / limit) * 100));
var planPct = Math.max(0, Math.min(100, (plannedMiB / limit) * 100));
var delta = plannedMiB - nowMiB;
var stateText = over ? t("mem.over") : (tight ? t("mem.tight") : t("mem.safe"));
var line = t("mem.now") + " " + nowMiB + " MiB";
if (delta !== 0) {
line += " → " + t("mem.planned") + " " + plannedMiB + " MiB";
} else {
line += " / " + t("mem.limit") + " " + limit + " MiB";
}
return jsxRuntime.jsxs("div", {
style: {
display: "flex", flexDirection: "column", gap: "6px", padding: "10px 12px",
borderRadius: "10px", background: T.field,
border: "1px solid " + (over ? T.danger : T.border)
},
children: [
jsxRuntime.jsxs("div", {
key: "h",
style: { display: "flex", alignItems: "baseline", gap: "8px", flexWrap: "wrap" },
children: [
jsxRuntime.jsx("span", {
key: "t",
style: { fontSize: "13px", fontWeight: 500, color: T.text },
children: t("mem.title")
}),
jsxRuntime.jsx("span", {
key: "n",
style: { fontSize: "13px", color: T.sub, fontVariantNumeric: "tabular-nums" },
children: line
}),
jsxRuntime.jsx("span", {
key: "s",
style: { fontSize: "12px", color: accent, marginLeft: "auto", flexShrink: 0 },
children: (over ? "⚠ " : "") + stateText
}),
jsxRuntime.jsx("span", {
key: "e",
style: { fontSize: "12px", color: T.dim, flexBasis: "100%" },
children: t("mem.est")
})
]
}),
jsxRuntime.jsxs("div", {
key: "bar",
style: {
position: "relative", height: "8px", borderRadius: "4px",
background: T.border, overflow: "hidden"
},
children: [
jsxRuntime.jsx("div", {
key: "plan",
style: {
position: "absolute", left: 0, top: 0, bottom: 0,
width: planPct + "%", background: accent, opacity: 0.28
}
}),
jsxRuntime.jsx("div", {
key: "now",
style: {
position: "absolute", left: 0, top: 0, bottom: 0,
width: nowPct + "%", background: accent, opacity: 0.85
}
})
]
}),
over
? jsxRuntime.jsx("div", {
key: "w",
style: { fontSize: "12px", color: T.danger, lineHeight: 1.6 },
children: t("mem.overWarn")
})
: null
]
});
}
if (loading) {
return jsxRuntime.jsx("div", {
style: wrap,
children: jsxRuntime.jsx(StateBlock, { title: t("loading"), desc: t("loadingDesc") })
});
}
if (plugins.length === 0) {
return jsxRuntime.jsx("div", {
style: wrap,
children: jsxRuntime.jsx(StateBlock, { title: t("emptyTitle"), desc: t("empty") })
});
}
var kw = query.trim().toLowerCase();
var shown = kw === ""
? plugins
: plugins.filter(function (p) {
return (p.name || "").toLowerCase().indexOf(kw) >= 0 || (p.description || "").toLowerCase().indexOf(kw) >= 0;
});
var selected = plugins.filter(function (p) { return p.enabled; }).length;
// 内存预估:基线 + Σ 插件成本。
// · nowMiB = 服务端快照里「已启用」的那批(savedIds)→ 代表**当前**占用;
// · plannedMiB = 当前勾选状态(用户点一下就变)→ 代表**应用后会是多少**。
var nowMiB = MEM_BASE_MIB + plugins.reduce(function (sum, p) {
return sum + (savedIds.indexOf(p.id) >= 0 ? memMiB(p) : 0);
}, 0);
var plannedMiB = MEM_BASE_MIB + sumMiB(plugins, "enabled");
var memOver = plannedMiB > MEM_LIMIT_MIB;
var inputStyle = {
flex: "1",
minWidth: "180px",
padding: "6px 10px",
borderRadius: "8px",
border: "1px solid " + T.border,
background: T.field,
color: T.text,
fontSize: "14px",
outline: "none"
};
// 规范 §4.1:次要动作用白底 + border(`.btn` / `.btn-sm` 量级),非主色实心。
var ghostBtnStyle = {
padding: "5px 12px", borderRadius: "8px", cursor: "pointer",
border: "1px solid " + T.border, background: "transparent", color: T.sub, fontSize: "13px"
};
var rows = [];
var cards = [];
// 内存预估状态条:**卡片列表上方**(2026-09-13 用户要求)——
// 当前占用 / 勾选后预估 / 是否超上限,让用户在点「应用」之前就知道后果。
rows.push(jsxRuntime.jsx(MemBar, {
key: "__membar",
limit: MEM_LIMIT_MIB,
nowMiB: nowMiB,
plannedMiB: plannedMiB
}));
// 工具行:搜索 + 全选/清空 + 计数(规范 §2.4:工具条内部 gap 8~10px)
rows.push(jsxRuntime.jsxs("div", {
key: "__toolbar",
style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
children: [
jsxRuntime.jsx("input", {
key: "q",
value: query,
placeholder: t("search"),
onChange: function (e) { setQuery(e.target.value); },
style: inputStyle
}),
jsxRuntime.jsx("button", {
key: "all",
type: "button",
className: "bp-btn",
onClick: function () {
setPlugins(plugins.map(function (p) { return Object.assign({}, p, { enabled: true }); }));
},
style: ghostBtnStyle,
children: t("selectAll")
}),
jsxRuntime.jsx("button", {
key: "none",
type: "button",
className: "bp-btn",
onClick: function () {
setPlugins(plugins.map(function (p) { return Object.assign({}, p, { enabled: false }); }));
},
style: ghostBtnStyle,
children: t("clear")
}),
jsxRuntime.jsx("span", {
key: "cnt",
style: { color: T.sub, fontSize: "13px", marginLeft: "auto" },
children: selected + " / " + plugins.length + " " + t("selected")
})
]
}));
if (shown.length === 0) {
rows.push(jsxRuntime.jsx("div", {
key: "__nomatch",
style: { padding: "20px 0", textAlign: "center", color: T.sub, fontSize: "13px" },
children: t("noMatch")
}));
}
for (var i = 0; i < shown.length; i++) {
(function (p) {
var bad = rejected.filter(function (r) { return r.id === p.id; })[0];
var desc = p.description && String(p.description).trim() !== "" ? String(p.description).trim() : "";
// 主视觉:用途说明(候选池入库时已优先取官方目录中文);无说明时回退为包名。
var primary = desc !== "" ? desc : p.name + (p.version ? " v" + p.version : "");
// 副行:包名 + 版本(有说明时才渲染,避免与主视觉重复)。
var meta = p.name + (p.version ? " v" + p.version : "");
cards.push(jsxRuntime.jsxs("label", {
key: p.id,
className: "bp-row",
style: {
display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer",
padding: "14px 16px", borderRadius: "12px", minWidth: 0, minHeight: "112px",
background: "var(--dsw-alias-bg-layer-1, #ffffff)",
border: "1px solid " + (bad ? T.danger : T.border)
},
children: [
jsxRuntime.jsx("input", {
key: "cb",
type: "checkbox",
checked: !!p.enabled,
onChange: function () { toggle(p.id); }
}),
jsxRuntime.jsxs("span", {
key: "nm",
style: { flex: "1", minWidth: "0", display: "flex", flexDirection: "column", gap: "2px" },
children: [
// 主视觉 = 用途说明(规范 §5「信息层次」;与门户 plugins 页同一决策)
jsxRuntime.jsx("span", {
key: "p1",
style: {
fontSize: "14px", fontWeight: 500, color: T.text, lineHeight: 1.45,
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"
},
title: primary,
children: primary
}),
// 副行 = 包名 + 版本(长文本截断三件套 + title,规范 §5)
desc !== ""
? jsxRuntime.jsx("span", {
key: "p2",
style: {
fontSize: "12px", color: T.sub,
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"
},
title: meta,
children: meta
})
: null,
// 预估内存(2026-09-13 用户要求):数值越大越"贵",用文字色分级
// (≥60 MiB 红 / ≥30 MiB 黄 / 其余次要色),一眼能看出谁是大头。
jsxRuntime.jsx("span", {
key: "mem",
style: { marginTop: "4px" },
children: jsxRuntime.jsx("span", {
style: {
padding: "1px 8px", borderRadius: "10px",
border: "1px solid " + T.border, background: T.field,
color: memMiB(p) >= 60 ? T.danger : (memMiB(p) >= 30 ? T.warn : T.sub)
},
children: t("mem.perCard") + " ≈ " + memMiB(p) + " MiB"
})
})
]
}),
jsxRuntime.jsx("span", {
key: "bd",
style: {
fontSize: "12px", padding: "2px 8px", borderRadius: "10px", flexShrink: 0,
color: p.enabled ? T.success : T.dim,
border: "1px solid " + (p.enabled ? T.success : T.border)
},
children: p.enabled ? t("enabled") : t("disabled")
}),
bad
? jsxRuntime.jsx("span", {
key: "bad",
style: { fontSize: "12px", color: T.danger, flexShrink: 0 },
title: String(bad.reason || ""),
children: "⚠"
})
: null
]
}));
})(shown[i]);
}
// 卡片网格:一行 2 个(用户 2026-09-12 要求「插件列表改为卡片形式,一行放 2 个卡片」)
// 单独包一层 grid —— 不能直接改外层 wrap(它同时装标题/搜索/按钮/提示等纵向块)。
rows.push(jsxRuntime.jsx("div", {
key: "__grid",
style: { display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: "10px" },
children: cards
}));
rows.push(jsxRuntime.jsxs("div", {
key: "__actions",
style: { display: "flex", alignItems: "center", gap: "10px", marginTop: "4px" },
children: [
// 规范 §4.1:主行动用 primary 实心(`.btn` 量级,section 内 14px)
jsxRuntime.jsx("button", {
key: "apply",
type: "button",
onClick: askApply,
disabled: busy,
style: {
display: "inline-flex", alignItems: "center", justifyContent: "center",
padding: "7px 16px", borderRadius: "8px", border: "1px solid " + T.primary,
background: T.primary, color: "#ffffff", cursor: busy ? "default" : "pointer",
opacity: busy ? 0.5 : 1, fontSize: "14px"
},
children: busy ? t("applying") : t("apply")
}),
rejected.length > 0
? jsxRuntime.jsx("button", {
key: "retry",
type: "button",
className: "bp-btn",
onClick: askApply,
disabled: busy,
style: Object.assign({}, ghostBtnStyle, { opacity: busy ? 0.5 : 1 }),
children: t("retry")
})
: null
]
}));
if (rejected.length > 0) {
// impeccable craft-floor 明令禁止「卡片/列表项/提示上 >1px 的彩色 border-left/right」
// —— 改为整块浅底 + 圆角,危险语义由标题文字色承担(不靠侧边色条)。
rows.push(jsxRuntime.jsxs("div", {
key: "__rejected",
style: {
padding: "10px 12px", borderRadius: "8px",
color: T.sub, fontSize: "13px", lineHeight: "1.6", background: T.field
},
children: [
jsxRuntime.jsxs("div", {
key: "h",
style: { fontWeight: 500, color: T.danger, marginBottom: "2px" },
children: ["⚠ ", t("rejected.title")]
}),
jsxRuntime.jsx("div", { key: "b", children: rejected.map(function (r) { return r.id; }).join("、") })
]
}));
}
if (msg) {
rows.push(jsxRuntime.jsx("div", { key: "__msg", style: { color: T.sub, fontSize: "13px" }, children: msg }));
}
// ── 页内确认弹窗(替代 window.confirm;视觉对齐 MCN 工作台弹窗)──────────
// 结构照 MCN:遮罩(fixed inset:0 + rgba(0,0,0,.35) + zIndex 2000 + 居中)→ 面板
// (radius 12 / padding 18·22 / maxHeight 82vh / 阴影)→ 标题行(✕ 关闭)→ 正文 →
// 内存块(**超限时红底 + 警告**,用户 2026-09-13 明确要求)→ 按钮行(取消 / 确认)。
// 点遮罩关闭、点面板 stopPropagation(与 MCN 同款交互)。
if (confirmOpen) {
var overNow = plannedMiB > MEM_LIMIT_MIB;
rows.push(jsxRuntime.jsxs("div", {
key: "__confirm",
style: {
position: "fixed", inset: 0, background: "rgba(0,0,0,.35)", zIndex: 2000,
display: "flex", alignItems: "center", justifyContent: "center", padding: "24px"
},
onClick: function () { setConfirmOpen(false); },
children: jsxRuntime.jsxs("div", {
style: {
background: "var(--dsw-alias-bg-layer-1, #ffffff)",
borderRadius: "12px", maxWidth: "480px", width: "100%", maxHeight: "82vh",
overflow: "auto", padding: "18px 22px",
boxShadow: "0 8px 30px rgba(0,0,0,.18)",
display: "flex", flexDirection: "column", gap: "10px"
},
onClick: function (e) { e.stopPropagation(); },
children: [
jsxRuntime.jsxs("div", {
key: "t",
style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" },
children: [
jsxRuntime.jsx("span", {
key: "s",
style: { fontSize: "15px", fontWeight: 500, color: T.text },
children: t("confirm.title")
}),
jsxRuntime.jsx("button", {
key: "x",
type: "button",
title: t("confirm.cancel"),
onClick: function () { setConfirmOpen(false); },
style: {
border: "none", background: "none", fontSize: "18px", lineHeight: 1,
cursor: "pointer", color: T.dim, padding: "2px 4px"
},
children: "✕"
})
]
}),
jsxRuntime.jsx("div", {
key: "b",
style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 },
children: t("confirm")
}),
jsxRuntime.jsxs("div", {
key: "m",
style: {
padding: "10px 12px", borderRadius: "10px",
background: overNow ? "rgba(217,48,38,.06)" : T.field,
border: "1px solid " + (overNow ? T.danger : T.border),
display: "flex", flexDirection: "column", gap: "4px"
},
children: [
jsxRuntime.jsx("div", {
key: "h",
style: { fontSize: "13px", fontWeight: 500, color: overNow ? T.danger : T.text },
children: overNow ? t("confirm.overTitle") : t("mem.title")
}),
jsxRuntime.jsx("div", {
key: "n",
style: { fontSize: "13px", color: T.sub, fontVariantNumeric: "tabular-nums" },
children: t("mem.now") + " " + nowMiB + " MiB → " + t("mem.planned") + " " + plannedMiB
+ " MiB / " + t("mem.limit") + " " + MEM_LIMIT_MIB + " MiB"
}),
overNow
? jsxRuntime.jsx("div", {
key: "w",
style: { fontSize: "12px", color: T.danger, lineHeight: 1.6 },
children: t("mem.overWarn")
})
: null,
jsxRuntime.jsx("div", {
key: "e",
style: { fontSize: "12px", color: T.dim },
children: t("mem.est")
})
]
}),
jsxRuntime.jsxs("div", {
key: "f",
style: {
display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "8px",
paddingTop: "10px", borderTop: "1px solid " + T.border
},
children: [
jsxRuntime.jsx("button", {
key: "c",
type: "button",
className: "bp-btn",
onClick: function () { setConfirmOpen(false); },
style: ghostBtnStyle,
children: t("confirm.cancel")
}),
jsxRuntime.jsx("button", {
key: "k",
type: "button",
onClick: doApply,
disabled: busy,
style: {
padding: "7px 16px", borderRadius: "8px", cursor: busy ? "default" : "pointer",
border: "1px solid " + (overNow ? T.danger : T.primary),
background: overNow ? T.danger : T.primary, color: "#ffffff",
fontSize: "14px", opacity: busy ? 0.5 : 1
},
children: busy ? t("applying") : t("confirm.ok")
})
]
})
]
})
}));
}
return jsxRuntime.jsx("div", { style: wrap, children: rows });
}
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(
{
name: "settings.section",
id: "business-plugins",
order: 101,
label: function () { return t("section.label"); }
},
BusinessPluginsSection
);
});
// ── R2 · 平台管理(只读就地化;档案 81 §9.2b「原生弹窗」形态)────────────────
// 形态:**原生渲染**(非 iframe)——复用本插件的 token 与 06-工作台UI规范 字号阶梯。
// 数据:直接调平台只读 API(跨子域 fetch + credentials,门户已加 CORS 白名单)。
// 范围:**只读子集**(实例 / 用户 / 存储 / 候选池 / 运行时);
// **写操作一律回跳平台管理台** —— 不复制门户的写 UI(否则双源维护 + 扩大执行面)。
// 可见性:**仅 admin**(先 /api/auth/me 判角色;非 admin 只渲染一行说明)。
function PaCard(props) {
return jsxRuntime.jsx("div", {
style: {
border: "1px solid " + T.border, borderRadius: "10px", background: T.field,
padding: "12px 14px", display: "flex", flexDirection: "column", gap: "4px",
minWidth: "0", flex: "1 1 150px"
},
children: [
jsxRuntime.jsx("div", { key: "t", style: { fontSize: "12px", color: T.dim }, children: props.title }),
jsxRuntime.jsx("div", { key: "v", style: { fontSize: "20px", fontWeight: 500, color: props.color || T.text, fontVariantNumeric: "tabular-nums" }, children: props.value }),
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "12px", color: T.dim, lineHeight: 1.6 }, children: props.desc })
]
});
}
function PlatformAdminSection() {
var useState = React.useState;
var useEffect = React.useEffect;
var st = useState({ loading: true, err: "", me: null, status: null, users: null, storage: null, pool: null, runtime: null });
var s = st[0];
var setS = st[1];
var ul = useState([]);
var userList = ul[0];
var setUserList = ul[1];
var mv = useState(null);
var modal = mv[0];
var setModal = mv[1];
var bs = useState(false);
var busy = bs[0];
var setBusy = bs[1];
var nt = useState("");
var note = nt[0];
var setNote = nt[1];
var ne = useState(false);
var noteErr = ne[0];
var setNoteErr = ne[1];
function req(path, method) {
return fetch(portalHost() + path, { credentials: "include", method: method || "GET" }).then(function (r) {
return r.ok ? r.json().catch(function () { return {}; }) : null;
}).catch(function () { return null; });
}
function loadUsers() { return req("/api/admin/users").then(function (d) { setUserList((d && d.users) || []); }); }
function load() {
setS(function (old) { return Object.assign({}, old, { loading: true, err: "" }); });
req("/api/auth/me").then(function (me) {
var role = me && me.user ? me.user.role : null;
if (role !== "admin") { setS(function (old) { return Object.assign({}, old, { loading: false, me: me }); }); return; }
Promise.all([
req("/api/dsh/status"), req("/api/admin/users"),
req("/api/admin/storage"), req("/api/plugins/business"), req("/api/admin/runtime")
]).then(function (r) {
setUserList((r[1] && r[1].users) || []);
setS({ loading: false, err: "", me: me, status: r[0], users: r[1], storage: r[2], pool: r[3], runtime: r[4] });
});
}).catch(function () {
setS(function (old) { return Object.assign({}, old, { loading: false, err: t("pa.loadFailed") }); });
});
}
useEffect(function () { load(); }, []);
// 管理动作:就地调用平台 admin API(不跳门户)
function doUserAction(u, url, method, label) {
if (method === "DELETE" && !window.confirm(t("pa.confirmRemove"))) return;
setBusy(true); setNote("");
req(url, method).then(function (r) {
setBusy(false);
setNoteErr(!r);
setNote(r ? t("pa.done") : t("pa.failed"));
return loadUsers();
}).then(function () { return req("/api/admin/users"); }).then(function (d) { if (d && d.users) setUserList(d.users); });
}
var wrap = { padding: "4px 2px", fontSize: "13px", color: T.text, display: "flex", flexDirection: "column", gap: "12px" };
if (s.loading) return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.loading") }),
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub }, children: t("pa.loadingDesc") }) ] });
if (s.err) return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500, color: T.danger }, children: s.err }),
jsxRuntime.jsx("button", { key: "b", type: "button", onClick: load, style: { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.text, fontSize: "13px" }, children: t("pa.retry") }) ] });
if (!s.me || !s.me.user || s.me.user.role !== "admin") return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.notAdminTitle") }),
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("pa.notAdminDesc") }) ] });
var inst = s.status && s.status.running ? t("pa.running") : t("pa.stopped");
var brk = s.status && s.status.breaker;
var nPending = userList.filter(function (u) { return u.role === "pending"; }).length;
var nDisabled = userList.filter(function (u) { return u.role === "disabled"; }).length;
var stUsers = (s.storage && s.storage.users) || [];
var pool = (s.pool && (s.pool.plugins || s.pool.items)) || [];
var rt = (s.runtime && (s.runtime.items || s.runtime.groups)) || [];
// ── 管理项清单:每项点开都是「弹窗内管理」(用户 2026-09-13 要求)
var ITEMS = [
{ id: "users", icon: "👥", title: t("pa.usersTitle"), desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled, badge: userList.length },
{ id: "plugins", icon: "🧩", title: t("pa.pool"), desc: t("pa.poolCount"), badge: pool.length },
{ id: "runtime", icon: "📦", title: t("pa.runtime"), desc: t("pa.items"), badge: rt.length },
{ id: "storage", icon: "💾", title: t("pa.storage"), desc: t("pa.storageUsers"), badge: stUsers.length },
{ id: "instance", icon: "🖥️", title: t("pa.inst"), desc: inst + (brk ? " · " + t("pa.breakerOpen") : ""), badge: s.status && s.status.instance ? s.status.instance.port : "—" },
];
// 卡片(06-工作台UI规范 §4.5 .data-card:r12 / 16px 18px / shadow / hover 上浮 + 蓝边 + 蓝影)
function itemCard(it) {
return jsxRuntime.jsx("div", {
key: it.id, role: "button", tabIndex: 0, className: "pa-card",
onClick: function () { setNote(""); setModal(it.id); },
onKeyDown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setModal(it.id); } },
style: {
border: "1px solid " + T.border, borderRadius: "12px", background: T.field,
padding: "16px 18px", cursor: "pointer", display: "flex", flexDirection: "column",
gap: "6px", minHeight: "104px", boxShadow: "0 1px 3px rgba(0,0,0,.08)"
},
children: [
jsxRuntime.jsx("div", { key: "top", style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
jsxRuntime.jsx("span", { key: "i", style: { fontSize: "16px", lineHeight: 1 }, children: it.icon }),
jsxRuntime.jsx("span", { key: "t", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: it.title }),
jsxRuntime.jsx("span", { key: "g", className: "pa-go", style: { marginLeft: "auto", fontSize: "14px", color: T.dim }, children: "›" })
] }),
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.dim, lineHeight: 1.6 }, children: it.desc }),
jsxRuntime.jsx("div", { key: "n", style: { marginTop: "auto", fontSize: "17px", fontWeight: 500, color: T.text, textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: it.badge })
]
});
}
function modalBody() {
if (modal === "users") {
return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
jsxRuntime.jsx("div", { key: "hd", style: { display: "flex", background: T.field, fontSize: "12px", color: T.dim, borderRadius: "8px 8px 0 0" }, children: [
jsxRuntime.jsx("div", { key: "a", style: { flex: "1 1 auto", padding: "6px 10px" }, children: t("pa.colUser") }),
jsxRuntime.jsx("div", { key: "b", style: { flex: "0 0 90px", padding: "6px 10px" }, children: t("pa.colRole") }),
jsxRuntime.jsx("div", { key: "c", style: { flex: "0 0 190px", padding: "6px 10px" }, children: t("pa.colOp") }) ] }),
(userList.length === 0
? jsxRuntime.jsx("div", { key: "e", style: { padding: "12px", fontSize: "13px", color: T.dim }, children: t("pa.noUsers") })
: userList.map(function (u) {
var roleText = u.role === "admin" ? t("pa.roleAdmin") : u.role === "pending" ? t("pa.rolePending") : u.role === "disabled" ? t("pa.roleDisabled") : t("pa.roleActive");
var ops = [];
if (u.role === "pending") ops.push(["a", t("pa.opApprove"), "POST", "/api/admin/users/" + u.id + "/approve"]);
if (u.role === "disabled") ops.push(["e", t("pa.opEnable"), "POST", "/api/admin/users/" + u.id + "/enable"]);
if (u.role === "active") ops.push(["d", t("pa.opDisable"), "POST", "/api/admin/users/" + u.id + "/disable"]);
ops.push(["r", t("pa.opRemove"), "DELETE", "/api/admin/users/" + u.id]);
return jsxRuntime.jsx("div", { key: u.id, style: { display: "flex", alignItems: "center", borderTop: "1px solid " + T.border, fontSize: "13px", color: T.text }, children: [
jsxRuntime.jsx("div", { key: "n", style: { flex: "1 1 auto", padding: "7px 10px", wordBreak: "break-all" }, children: u.username }),
jsxRuntime.jsx("div", { key: "r", style: { flex: "0 0 90px", padding: "7px 10px", color: u.role === "pending" ? T.warn : u.role === "disabled" ? T.dim : T.sub }, children: roleText }),
jsxRuntime.jsx("div", { key: "o", style: { flex: "0 0 190px", padding: "7px 10px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: ops.map(function (o) {
return jsxRuntime.jsx("button", { key: o[0], type: "button", disabled: busy, onClick: function () { doUserAction(u, o[3], o[2], o[1]); },
style: { padding: "3px 10px", borderRadius: "6px", cursor: busy ? "default" : "pointer", fontSize: "12px", background: "transparent", border: "1px solid " + (o[0] === "r" ? T.danger : T.border), color: o[0] === "r" ? T.danger : T.text, opacity: busy ? 0.5 : 1 },
children: o[1] });
}) }),
] });
})),
note ? jsxRuntime.jsx("div", { key: "n", style: { fontSize: "12px", color: noteErr ? T.danger : T.success }, children: note }) : null
] });
}
if (modal === "plugins") {
return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children:
pool.length === 0 ? jsxRuntime.jsx("div", { style: { fontSize: "13px", color: T.dim }, children: t("pa.empty") }) : pool.map(function (p, i) {
return jsxRuntime.jsx("div", { key: (p.id || i), style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (p.name || p.id || "插件") + (p.version ? " v" + p.version : "") });
}) });
}
if (modal === "runtime") {
return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: rt.map(function (x, i) {
return jsxRuntime.jsx("div", { key: i, style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (x.name || x.title || "条目") + (x.version ? " " + x.version : "") });
}) });
}
if (modal === "storage") {
return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: stUsers.length === 0
? jsxRuntime.jsx("div", { style: { fontSize: "13px", color: T.dim }, children: (s.storage && s.storage.note) || "—" })
: stUsers.map(function (u, i) { return jsxRuntime.jsx("div", { key: i, style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (u.username || u.id || "—") + (u.usedMb != null ? " " + u.usedMb + " MiB" : "") }); }) });
}
// instance
var i2 = s.status || {};
return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px", fontSize: "13px", color: T.text }, children: [
jsxRuntime.jsx("div", { key: "a", children: t("pa.inst") + ":" + inst }),
jsxRuntime.jsx("div", { key: "b", children: t("pa.port") + ":" + ((i2.instance && i2.instance.port) || "—") }),
jsxRuntime.jsx("div", { key: "c", children: t("pa.restarts") + ":" + ((i2.instance && i2.instance.restarts) || 0) }),
jsxRuntime.jsx("div", { key: "d", children: t("pa.breaker") + ":" + (brk ? t("pa.breakerOpen") : t("pa.breakerOk")) }) ] });
}
var rows = [
jsxRuntime.jsx("div", { key: "hd", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: t("pa.label") }),
jsxRuntime.jsx("div", { key: "sub", style: { fontSize: "12px", color: T.dim }, children: t("pa.readonlyHint") }),
jsxRuntime.jsx("div", { key: "items", style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(158px, 1fr))", gap: "12px" }, children: ITEMS.map(itemCard) }),
];
if (modal) {
var it = ITEMS.filter(function (x) { return x.id === modal; })[0] || { title: modal };
rows.push(jsxRuntime.jsx("div", {
key: "overlay",
onClick: function () { setModal(null); },
style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,.35)", display: "flex", alignItems: "center", justifyContent: "center", padding: "24px" },
className: "pa-modal",
children: jsxRuntime.jsx("div", {
onClick: function (e) { e.stopPropagation(); },
style: { background: T.text === "" ? "#fff" : ("var(--dsw-alias-bg-layer-1, #ffffff)"), border: "1px solid " + T.border, borderRadius: "14px", width: "520px", maxWidth: "92vw", maxHeight: "80vh", overflow: "auto", padding: "22px 26px" },
children: [
jsxRuntime.jsx("div", { key: "mh", style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }, children: [
jsxRuntime.jsx("div", { key: "t", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: it.title }),
jsxRuntime.jsx("button", { key: "x", type: "button", onClick: function () { setModal(null); }, style: { border: 0, background: "transparent", cursor: "pointer", fontSize: "16px", color: T.dim, padding: "2px 6px" }, children: "✕" }) ] }),
modalBody()
]
})
}));
}
return jsxRuntime.jsx("div", { style: wrap, children: rows });
}
// ── 「系统管理」分区:**仅 admin 注册**(2026-09-13 用户指出:普通用户不该看到这一栏)
// 依据:官方 settings 外壳按 `ctx.slots.getVersion("settings.section")` 失效重算导航
// (`dsh-client-ui-settings/lib/client.js:560`)⇒ **晚注册会被正确反映**。
// 所以先异步判角色:**非 admin 干脆不注册**(而非注册后显示一行「仅管理员」)。
fetch(portalHost() + "/api/auth/me", { credentials: "include" })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var role = d && d.user && d.user.role;
if (role !== "admin") return;
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(
{
name: "settings.section",
id: "platform-admin",
order: 102,
label: function () { return t("pa.label"); }
},
PlatformAdminSection
);
});
})
// 判角色失败 = 按非 admin 处理,不注册(fail-closed:宁可少显示,不越权暴露)
.catch(function () {});
}
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});