Files
dsh_ai1net_server/poc/business-plugins/lib/client.js
T

1318 lines
60 KiB
JavaScript
Raw Normal View History

// @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": "处理中…",
"pa.colName": "插件",
"pa.colVer": "版本",
"pa.colDesc": "说明",
"pa.colItem": "项目",
"pa.colValue": "值",
"pa.colUsed": "已用",
"pa.detail": "详情",
"pa.close": "关闭"
};
/** 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…",
"pa.colName": "Plugin",
"pa.colVer": "Version",
"pa.colDesc": "Description",
"pa.colItem": "Item",
"pa.colValue": "Value",
"pa.colUsed": "Used",
"pa.detail": "Details",
"pa.close": "Close"
};
/**
* 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 第 2 轮 ·「系统管理」视觉层 —— **照抄门户 `web/portal.html` 的对应组件**
// (用户要求:「说了要用之前门户的里面的对应样式」)。逐条对应关系,便于核对:
// .pa-hd / .pa-sub ← .page-title(20-600) / .page-sub(13, dim)
// .pa-sec ← .home-section-title(13, dim, 600)
// .pa-grid ← .nav-grid(repeat(auto-fill,minmax(200px,1fr)) / gap 14px)
// .pa-card ← .nav-card(r10 / 18px 16px / hover: 蓝边 + 0 4px 12px rgba(47,111,237,.12) + 上浮 -2px)
// .pa-card .pa-ic/.pa-t/.pa-d ← .nav-card .ic(主色) / .t(15-600) / .d(12, dim)
// .pa-cnt ← .pg-tab .pg-cnt(12-600 胶囊 / 主色底)
// .pa-wrap ← .table-wrap(r8 / 白底 / 1px 边框)
// .pa-tbl th/td ← table.tbl th(灰底 8px 10px 600) / td(7px 10px) / 行 hover 浅蓝
// .pa-badge ← .badge(2px 8px / r10 / 13px / 四色)
// .pa-sm ← .btn-sm(3px 10px / r6 / 14px;.danger 红字红边)
// .pa-empty-cell ← 门户 `emptyRow()`(表格内空态:居中 + 40px 上下留白)
// .pa-card-h ← .card-h(15-600 + .sub 13 dim)
// .pa-modal ← 门户无弹窗 ⇒ 面板取 .card(r10 / 18px) + 门户家族阴影(design.css `--shadow`)
// ⚠️ 唯一差异:颜色走 dsh 官方 token `--dsw-*`(本 section 嵌在 dsh 面板内、须随 dsh 主题;
// 门户那套 `--bp-*` 是给亮色静态页的硬编码)。字号/间距/圆角/动效与门户**逐值一致**。
".pa-hd{font-size:20px;font-weight:600;margin:0 0 4px}",
".pa-sub{font-size:13px;color:var(--dsw-alias-label-tertiary,#6b7280);margin:0 0 20px}",
".pa-sec{font-size:13px;font-weight:600;color:var(--dsw-alias-label-tertiary,#6b7280);margin:0 0 10px}",
".pa-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px}",
".pa-card{display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;padding:18px 16px;cursor:pointer;transition:all .15s}",
".pa-card:hover{border-color:var(--dsw-alias-brand-primary,#2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.12);transform:translateY(-2px)}",
".pa-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}",
".pa-card .pa-ic{color:var(--dsw-alias-brand-primary,#2f6fed);font-size:18px;line-height:1}",
".pa-card .pa-t{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,#24292f)}",
".pa-card .pa-d{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-cnt{display:inline-block;min-width:18px;padding:1px 6px;margin-left:6px;border-radius:9px;background:rgba(47,111,237,.10);color:var(--dsw-alias-brand-primary,#2f6fed);font-size:12px;font-weight:600;line-height:16px;text-align:center}",
".pa-wrap{overflow:auto;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff)}",
".pa-tbl{width:100%;border-collapse:collapse;font-size:15px}",
".pa-tbl th{background:var(--dsw-alias-bg-layer-2,#f6f8fa);padding:8px 10px;text-align:left;font-weight:600;white-space:nowrap;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea)}",
".pa-tbl td{padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea);color:var(--dsw-alias-label-primary,#24292f)}",
".pa-tbl tbody tr:last-child td{border-bottom:none}",
".pa-tbl tbody tr:hover{background:rgba(47,111,237,.06)}",
".pa-tbl td.pa-num{text-align:right;font-size:13px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-tbl td.pa-dim{color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:13px}",
".pa-badge.admin{background:rgba(47,111,237,.10);color:var(--dsw-alias-brand-primary,#2f6fed)}",
".pa-badge.active{background:rgba(26,127,55,.10);color:#1a7f37}",
".pa-badge.pending{background:rgba(232,89,12,.10);color:#e8590c}",
".pa-badge.disabled{background:rgba(107,114,128,.10);color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-sm{display:inline-flex;align-items:center;padding:3px 10px;font-size:14px;border-radius:6px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#24292f);transition:all .15s}",
".pa-sm:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed)}",
".pa-sm.danger{color:#d93026}",
".pa-sm.danger:hover:not(:disabled){border-color:#d93026;color:#d93026}",
".pa-sm:disabled{opacity:.5;cursor:default}",
".pa-tbl td.pa-empty-cell{text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);padding:40px 10px}",
".pa-card-h{display:flex;align-items:baseline;gap:10px;font-size:15px;font-weight:600;margin:0 0 12px}",
".pa-card-h .sub{font-size:13px;font-weight:400;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-overlay{position:fixed;inset:0;z-index:9999;background:rgba(15,28,51,.35);display:flex;align-items:center;justify-content:center;padding:24px}",
".pa-modal{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;box-shadow:0 18px 50px -18px rgba(9,24,58,.35);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", display: "flex", flexDirection: "column", gap: "12px" };
// 加载 / 失败 / 非管理员三种态也走门户排版(.page-title → .pa-hd、.page-sub → .pa-sub、.btn-sm → .pa-sm)
if (s.loading) return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", children: t("pa.loading") }),
jsxRuntime.jsx("p", { key: "d", className: "pa-sub", children: t("pa.loadingDesc") }) ] });
if (s.err) return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", style: { color: T.danger }, children: s.err }),
jsxRuntime.jsx("div", { key: "b", children: jsxRuntime.jsx("button", { type: "button", onClick: load, className: "pa-sm", children: t("pa.retry") }) }) ] });
if (!s.me || !s.me.user || s.me.user.role !== "admin") return jsxRuntime.jsx("div", { style: wrap, children: [
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", children: t("pa.notAdminTitle") }),
jsxRuntime.jsx("p", { key: "d", className: "pa-sub", 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 要求)
// 卡片取值口径与门户 `renderHome()` 的 sec2 一致:图标 + 标题 + 一句说明(+ 计数胶囊)
var ITEMS = [
{ id: "users", icon: "👥", title: t("pa.usersTitle"), count: userList.length, desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled },
{ id: "plugins", icon: "🧩", title: t("pa.pool"), count: pool.length, desc: t("pa.poolCount") },
{ id: "runtime", icon: "📦", title: t("pa.runtime"), count: rt.length, desc: t("pa.items") },
{ id: "storage", icon: "💾", title: t("pa.storage"), count: stUsers.length, desc: t("pa.storageUsers") },
{ id: "instance", icon: "🖥️", title: t("pa.inst"), count: null, desc: inst + (brk ? " · " + t("pa.breakerOpen") : "") + " · " + t("pa.port") + " " + ((s.status && s.status.instance && s.status.instance.port) || "—") },
];
// 卡片 = 门户 `.nav-card`(ic 一行 / t 一行 / d 一行;计数借门户 `.pg-tab .pg-cnt` 的胶囊)
function itemCard(it) {
return jsxRuntime.jsx("div", {
key: it.id, role: "button", tabIndex: 0, className: "pa-card",
onClick: function () { setNote(""); setNoteErr(false); setModal(it.id); },
onKeyDown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setModal(it.id); } },
children: [
jsxRuntime.jsx("div", { key: "i", className: "pa-ic", children: it.icon }),
jsxRuntime.jsx("div", { key: "t", className: "pa-t", children: [
jsxRuntime.jsx("span", { key: "l", children: it.title }),
it.count == null ? null : jsxRuntime.jsx("span", { key: "c", className: "pa-cnt", children: it.count })
] }),
jsxRuntime.jsx("div", { key: "d", className: "pa-d", children: it.desc })
]
});
}
// 弹窗正文统一 = 门户 `.table-wrap` + `table.tbl`
// (表头灰底 8px 10px/600、单元格 7px 10px、行 hover 浅蓝、末行去下边框;
// 空态对齐门户 `emptyRow()`:居中 + 40px 上下留白)
function tbl(head, bodyRows, emptyText) {
return jsxRuntime.jsx("div", { className: "pa-wrap", children:
jsxRuntime.jsx("table", { className: "pa-tbl", children: [
jsxRuntime.jsx("thead", { key: "h", children: jsxRuntime.jsx("tr", { children: head.map(function (h, i) {
return jsxRuntime.jsx("th", { key: i, children: h });
}) }) }),
jsxRuntime.jsx("tbody", { key: "b", children: bodyRows.length === 0
? jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { colSpan: head.length, className: "pa-empty-cell", children: emptyText || t("pa.empty") }) })
: bodyRows })
] }) });
}
// 两列「项目 / 值」表:当前实例这类键值信息用(仍走门户表格样式)
function kvTbl(pairs) {
return tbl([t("pa.colItem"), t("pa.colValue")], pairs.map(function (r, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "k", className: "pa-dim", children: r[0] }),
jsxRuntime.jsx("td", { key: "v", children: r[1] })
] });
}));
}
function modalBody() {
if (modal === "users") {
return tbl([t("pa.colUser"), t("pa.colRole"), t("pa.colOp")], userList.map(function (u) {
var roleCls = u.role === "admin" ? "admin" : u.role === "pending" ? "pending" : u.role === "disabled" ? "disabled" : "active";
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, "danger"]);
return jsxRuntime.jsx("tr", { key: u.id, children: [
jsxRuntime.jsx("td", { key: "n", children: u.username }),
jsxRuntime.jsx("td", { key: "r", children: jsxRuntime.jsx("span", { className: "pa-badge " + roleCls, children: roleText }) }),
jsxRuntime.jsx("td", { key: "o", children: ops.map(function (o) {
return jsxRuntime.jsx("button", { key: o[0], type: "button", disabled: busy, className: "pa-sm" + (o[4] ? " " + o[4] : ""),
style: { marginRight: "6px" },
onClick: function () { doUserAction(u, o[3], o[2], o[1]); },
children: o[1] });
}) })
] });
}), t("pa.noUsers"));
}
if (modal === "plugins") {
return tbl([t("pa.colName"), t("pa.colVer")], pool.map(function (p, i) {
return jsxRuntime.jsx("tr", { key: (p.id || i), children: [
jsxRuntime.jsx("td", { key: "n", children: p.name || p.id || "—" }),
jsxRuntime.jsx("td", { key: "v", className: "pa-dim", children: p.version ? "v" + p.version : "—" })
] });
}));
}
if (modal === "runtime") {
return tbl([t("pa.colItem"), t("pa.colVer")], rt.map(function (x, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "n", children: x.name || x.title || "—" }),
jsxRuntime.jsx("td", { key: "v", className: "pa-dim", children: x.version || "—" })
] });
}));
}
if (modal === "storage") {
return tbl([t("pa.colUser"), t("pa.colUsed")], stUsers.map(function (u, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "n", children: u.username || u.id || "—" }),
jsxRuntime.jsx("td", { key: "u", className: "pa-num", children: u.usedMb != null ? u.usedMb + " MiB" : "—" })
] });
}), (s.storage && s.storage.note) || t("pa.empty"));
}
// instance
var i2 = s.status || {};
return kvTbl([
[t("pa.inst"), inst],
[t("pa.port"), (i2.instance && i2.instance.port) || "—"],
[t("pa.restarts"), (i2.instance && i2.instance.restarts) || 0],
[t("pa.breaker"), brk ? t("pa.breakerOpen") : t("pa.breakerOk")]
]);
}
var rows = [
jsxRuntime.jsx("h1", { key: "hd", className: "pa-hd", children: t("pa.label") }),
jsxRuntime.jsx("p", { key: "sub", className: "pa-sub", children: t("pa.readonlyHint") }),
jsxRuntime.jsx("div", { key: "sec", className: "pa-sec", children: t("pa.detail") }),
jsxRuntime.jsx("div", { key: "items", className: "pa-grid", 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", className: "pa-overlay",
onClick: function () { setModal(null); },
children: jsxRuntime.jsx("div", {
className: "pa-modal",
onClick: function (e) { e.stopPropagation(); },
style: { width: "560px", maxWidth: "92vw", maxHeight: "80vh", overflow: "auto", padding: "18px" },
children: [
// 弹窗头 = 门户 `.card-h`(标题 15-600;右上角关闭按钮用门户 `.btn-sm`)
jsxRuntime.jsx("div", { key: "mh", className: "pa-card-h", children: [
jsxRuntime.jsx("span", { key: "t", children: it.title }),
jsxRuntime.jsx("span", { key: "g", style: { marginLeft: "auto" }, children:
jsxRuntime.jsx("button", { type: "button", className: "pa-sm", onClick: function () { setModal(null); }, children: t("pa.close") }) })
] }),
modalBody(),
note ? jsxRuntime.jsx("p", { key: "n", className: "pa-sub", style: { margin: "12px 0 0", color: noteErr ? T.danger : T.success }, children: note }) : null
]
})
}));
}
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;
}
});