// @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)" }; // ── 内存模型(档案 84;口径**与平台编排器同源**)────────────────────────── // // 权威定义在 `src/supervisor/orchestrator.ts`: // 配额 = clamp(BASE_MEM_MB + Σ(PLUGIN_MEM_MB[bundles]), MIN_MEM_MB, MAX_MEM_MB) // 下面的常量**逐键与它对齐**,并由 `scripts/verify-mem-model.mjs` 在 `npm run verify` // 里交叉断言 —— 两处一旦不同步,构建直接失败。 // // ★ 历史教训(2026-09-13 实测):这里曾自建一套**独立**模型(基线 285 / univer 64 / // mcn-suite 39,且把 384 当硬上限)⇒ 界面显示「勾选后 388 MiB」并报「⚠ 将超出上限」, // 而平台真实配额是 672(guest)/ 384(admin),**384 早已只是下限** ⇒ 纯属误报。 // 修法:① 常量与规则对齐平台;② 优先读 `/api/dsh/status` 的 `quota.memMb/heapMb` // (平台**实际使用**的值)直接显示;③「超限」改为判断是否顶到硬顶 MAX。 /** 与编排器 `BASE_MEM_MB` 一致。 */ var MEM_BASE_MIB = 160; /** 与编排器 `MIN_MEM_MB` 一致 —— ⚠️ 这是**下限**,不是上限。 */ var MEM_MIN_MIB = 384; /** 与编排器 `MAX_MEM_MB` 一致 —— 单实例硬顶。 */ var MEM_MAX_MIB = 1024; /** 与编排器 `PLUGIN_MEM_MB` 一致;未识别的插件平台按 0 计。 */ var MEM_TABLE = { "dsh-univer-office": 512, "dsh-plugin-mcn-suite": 128 }; /** 与编排器 `HEAP_HEADROOM_MB` 一致。 */ var MEM_HEAP_HEADROOM_MIB = 96; /** 越过该比例即提前警示(相对**硬顶**),给页缓存与用户自己的任务留余量。 */ var MEM_TIGHT_RATIO = 0.85; /** 单个插件的加载成本(MiB);未识别 = 0(与平台同口径:宁可少给也不虚高)。 */ function memMiB(p) { if (p && p.name && Object.prototype.hasOwnProperty.call(MEM_TABLE, p.name)) { return MEM_TABLE[p.name]; } return 0; } /** 未 clamp 的原始需求(MiB)= 基线 + Σ命中 `key` 为真的插件。 */ function rawMiB(list, key) { var total = MEM_BASE_MIB; for (var i = 0; i < list.length; i++) { if (list[i][key]) total += memMiB(list[i]); } return total; } /** 按平台同款规则把原始需求折算成实际配额(clamp 到 [MIN, MAX])。 */ function quotaOf(raw) { return Math.min(Math.max(raw, MEM_MIN_MIB), MEM_MAX_MIB); } /** V8 老生代上限(与编排器 `heapMbFor` 一致):配额 − 96,夹在 [128, 256]。 */ function heapMiBFor(memMb) { return Math.max(128, Math.min(256, memMb - MEM_HEAP_HEADROOM_MIB)); } /** 本插件的 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.real": "实际配额", "mem.heap": "V8 堆", "mem.now": "当前", "mem.planned": "勾选后", "mem.limit": "硬顶", "mem.left": "剩余", "mem.over": "顶到硬顶", "mem.tight": "接近硬顶", "mem.safe": "余量充足", "mem.overWarn": "勾选后的内存需求已顶到单实例硬顶(1024 MiB),配额不会再增加,实例可能起不来。建议先停用部分插件再应用。", "mem.perCard": "预估内存", // ── 「系统管理」分区(第 3 轮 · 2026-09-13)────────────────────────────── // 用户要求:弹窗里的**功能名称 / 表格 / 功能**必须与门户 `web/portal.html` 一致。 // ⇒ 分区内不再是自造的 5 项摘要,而是门户的 **6 个功能页**,成对命名: // `pa.p.*` = 卡片名 / `pa.d.*` = 卡片说明 / `pa.s.*` = 页头副标题 // (三者的中文逐字取自门户 `renderHome()` 与 `pageHead()`) // 命名规则:`pa.<域>.<项>`;`{x}` 为占位符,由 `pafmt()` 填充。 "pa.label": "系统管理", "pa.homeSub": "与门户同一批功能;点开任一功能,在弹窗内直接管理。", "pa.group.svc": "服务", "pa.group.mgmt": "管理", "pa.back": "← 返回", "pa.close": "关闭", "pa.retry": "重试", "pa.loading": "加载中…", "pa.loadingDesc": "正在读取平台状态…", "pa.loadFailed": "平台状态读取失败", "pa.notAdminTitle": "系统管理仅对管理员显示", "pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。", "pa.empty": "暂无数据", "pa.done": "已更新", "pa.failed": "操作失败", "pa.working": "处理中…", "pa.sk.delFail": "删除失败", "pa.p.files": "服务管理", "pa.d.files": "文件树 + DSH 启动", "pa.p.keys": "密钥管理", "pa.d.keys": "全局 API 密钥", "pa.p.users": "用户管理", "pa.d.users": "审批 / 禁用 / 删除", "pa.p.skills": "技能管理", "pa.d.skills": "共享技能上传", "pa.p.plugins": "插件管理", "pa.d.plugins": "功能插件投放", "pa.p.runtime": "运行环境", "pa.d.runtime": "共享运行时与工具", "pa.s.files": "浏览文件、在此文件夹启动 DSH", "pa.s.keys": "全局 API 密钥(所有用户共用,仅管理员可改)", "pa.s.users": "审批 / 禁用 / 删除用户", "pa.s.skills": "共享技能(所有用户可用 · 只读)", "pa.s.plugins": "功能插件(系统外插件)的投放与管理", "pa.s.runtime": "实例共享的运行时与工具(服务器装一次,全部用户共享)", "pa.k.name": "名称", "pa.k.type": "类型", "pa.k.size": "大小", "pa.k.mtime": "修改时间", "pa.k.op": "操作", "pa.k.user": "用户名", "pa.k.role": "角色", "pa.k.reg": "注册时间", "pa.k.skill": "技能名", "pa.k.desc": "说明", "pa.k.files": "文件", "pa.k.updated": "更新时间", "pa.k.plugin": "插件名", "pa.k.ver": "版本", "pa.k.deployed": "投放时间", "pa.k.src": "来源", "pa.k.script": "安装/升级脚本", "pa.k.removable": "可卸载", "pa.k.item": "项目", "pa.k.value": "值", "pa.k.used": "已用", "pa.k.pluginDesc": "插件说明", "pa.k.cat": "分类", "pa.k.importKind": "导入方式", "pa.k.downloads": "下载量", "pa.k.detail": "详情", "pa.st.running": "运行中", "pa.st.stopped": "未运行", "pa.st.dir": "文件夹", "pa.st.file": "文件", "pa.st.enabled": "启用中", "pa.st.removable": "可卸载", "pa.st.required": "平台必备", "pa.role.admin": "管理员", "pa.role.active": "正常", "pa.role.pending": "待审核", "pa.role.disabled": "已禁用", "pa.op.approve": "通过", "pa.op.disable": "禁用", "pa.op.enable": "恢复", "pa.op.del": "删除", "pa.op.add": "添加", "pa.op.select": "启用", "pa.op.upload": "上传 / 替换", "pa.op.newFolder": "新建文件夹", "pa.op.uploadFile": "上传文件", "pa.op.launch": "启动 DSH", "pa.op.stop": "停止", "pa.op.openDsh": "打开 DSH ↗", "pa.op.search": "搜索", "pa.op.refreshDir": "重新拉取目录", "pa.op.import": "导入到平台", "pa.op.detail": "详情 ↗", "pa.op.repull": "正在重新拉取…", "pa.f.launchDir": "启动目录:", "pa.f.root": "根目录", "pa.f.rootShort": "根", "pa.f.newFolderPrompt": "文件夹名称:", "pa.f.alreadyRunning": "已有运行中的 DSH", "pa.f.launchFail": "启动失败", "pa.f.launched": "DSH 已启动", "pa.f.uploadFail": "上传失败:", "pa.f.createFail": "创建失败:", "pa.key.empty": "暂无全局密钥。添加后所有用户的 DSH 才能调用模型。", "pa.key.namePh": "名称(如 家用 / 服务器)", "pa.key.invalid": "名称或密钥格式无效", "pa.key.needBoth": "请填名称和密钥", "pa.key.confirmDel": "删除这个密钥?", "pa.u.confirmDel": "删除后不可恢复。输入用户名「{name}」以确认:", "pa.u.mismatch": "用户名不匹配,已取消", "pa.u.delFail": "删除失败(admin 不可删除)", "pa.u.noUsers": "暂无用户", "pa.sk.hint": "仅支持 .zip:包内单个顶层目录且含 SKILL.md(name 小写连字符 + description)。上传先做安全检测;同名确认后整体替换。", "pa.sk.empty": "暂无共享技能 —— 上传 .zip 投放第一个", "pa.sk.loadFail": "加载失败", "pa.sk.pickZip": "请先选择 .zip 文件", "pa.sk.zipOnly": "仅支持 .zip 文件", "pa.sk.tooBig": "文件过大(>150MB)", "pa.sk.detecting": "检测中…", "pa.sk.uploadFail": "上传失败:", "pa.sk.installed": "✓ 已安装技能「{name}」", "pa.sk.replaceConfirm": "检测到同名技能「{name}」已存在。确认整体替换?\n\n⚠️ 替换将删除旧技能全部文件,不可恢复。", "pa.sk.canceled": "已取消", "pa.sk.replaceFail": "替换失败:", "pa.sk.replaced": "✓ 已整体替换技能「{name}」", "pa.sk.delConfirm": "删除共享技能「{name}」?所有用户将立即不可用。", "pa.sk.deleted": "✓ 已删除", "pa.pl.tabOfficial": "官方推荐插件", "pa.pl.tabManual": "手动添加 / 管理", "pa.pl.searchPh": "搜索插件名 / 描述 / npm 包名", "pa.pl.allCat": "全部分类", "pa.pl.onlyImportable": "只看可导入", "pa.pl.hintOfficial": "来源:awesome-dsh-plugin 官方插件目录(dsh 官方社区精选,按下载量排序,取前 300 条)。导入只接受预构建包:官方 npm 包(registry tarball)或作者发布的 release 资产 —— 平台不执行任何第三方构建脚本。标记「需源码构建」的插件暂不支持一键导入,可自行构建后到「手动添加 / 管理」上传。", "pa.pl.importNpm": "npm 预构建", "pa.pl.importTarball": "release 资产", "pa.pl.importSource": "需源码构建", "pa.pl.publishing": "投放中…", "pa.pl.published": "✓ 已投放功能插件「{name}」", "pa.pl.replacedOk": "✓ 已替换功能插件「{name}」", "pa.pl.trusted": "(已记录信任声明)", "pa.pl.uploadFail": "上传失败:", "pa.pl.wlLoading": "加载目录中…(首次会从官方站下载一次,之后读本地缓存)", "pa.pl.wlFail": "拉取失败", "pa.pl.wlEmpty": "无匹配", "pa.pl.wlInfo": "清单 {total} 条 | 可导入 {importable} 条 | 匹配 {count} 条 | 显示 {shown} 条 | 目录缓存更新于 {when}(6 小时内直接复用,不联网)", "pa.pl.wlStale": " | ⚠ 官方站暂不可达,正在使用本地缓存", "pa.pl.empty": "暂无已投放插件 —— 从「官方推荐插件」导入,或在上方上传 .tgz", "pa.pl.repullOk": "✓ 目录已重新拉取(缓存已更新)", "pa.pl.repullFail": "重新拉取失败(网络或官方站不可达)", "pa.pl.selInfo": "已选 {n}", "pa.pl.importPick": "请先勾选插件", "pa.pl.importMax": "单次最多导入 20 个", "pa.pl.importConfirm": "将 {n} 个插件投放(收录)到平台?\n\n投放后默认禁用,对用户不生效;用户需在实例「设置 → 功能插件」里自行启用。", "pa.pl.importing": "导入中…", "pa.pl.importResult": "投放完成:成功 {ok} / {total}", "pa.pl.importFailTail": ",失败 {fail}", "pa.pl.importOkBox": "✓ 已投放 {n} 个插件到平台(默认禁用,用户未启用前不生效)。切到「手动添加 / 管理」可查看或删除。", "pa.pl.failTitle": "失败 {n} 个", "pa.pl.importErr": "投放失败:网络错误", "pa.pl.cardPublish": "投放插件", "pa.pl.cardPublishSub": "上传 .tgz 收录到平台", "pa.pl.cardPublished": "已投放插件", "pa.pl.poolHint": "共 {n} 个 · 全部默认禁用", "pa.pl.hintManual": "投放 ≠ 生效。这里只是把插件收录到平台,对任何用户都不生效;用户要真正用上,需在自己实例的「设置 → 功能插件」里勾选启用 —— 启用时平台才会把插件装进他的环境,重启实例后生效。", "pa.pl.hintTgz": "仅支持 .tgz:dsh 插件 bundle(含 package.json,name 为 npm 包名)。上传先做安全检测;同名按替换策略(旧包先删再落新包)。命中 P0 规则时默认拒绝,但会逐条列出命中内容 —— 确认可信可显式声明信任后投放(会记入审计)。", "pa.pl.pickTgz": "请先选择 .tgz 文件", "pa.pl.tgzOnly": "仅支持 .tgz 文件", "pa.pl.delConfirm": "删除已投放插件「{name}」?\n\n将从平台移除;已启用它的用户实例会失去该插件。", "pa.pl.deleted": "✓ 已删除", "pa.pl.delFail": "删除失败", "pa.pl.scanP0": "安全检测命中 P0 规则,已拒绝投放(共 {n} 处)", "pa.pl.scanCompat": "与当前平台 dsh 版本不兼容,已拒绝投放(共 {n} 条依据)", "pa.pl.warnP1": "另有 {n} 条 P1 告警(不阻断)", "pa.pl.trustPh": "信任理由(可选,会记入审计)", "pa.pl.trustBtn": "我已逐条确认,信任并投放", "pa.pl.trustNote": "⚠ 只在确认命中确属误报或风险可控时继续。平台会记录「谁 / 何时 / 命中什么 / 理由」。", "pa.rt.drift": "⚠️ 检测到版本漂移(与基线不一致):{list}", "pa.rt.driftHint": "若是有意升级,请跑 {script} 刷新基线。", "pa.rt.ok": "✅ 版本与基线一致", "pa.rt.okAt": "(基线时间 {t})", "pa.rt.dir": "目录", "pa.rt.size": "体积", "pa.rt.changed": "最近变更", "pa.rt.q1": "升级 / 卸载 / 迁移怎么做?", "pa.rt.a1": "升级:改脚本里的固定版本号 → 重跑对应安装脚本(幂等 + 官方 sha256 校验)→ 再跑 {script} 刷新版本基线,否则页面会一直提示漂移。\n卸载:删 /usr/local/bin/<名字> 软链即可(「平台必备」项请勿删);\n迁移:整个 {dir} 就是一个打包单元 —— tar czf dsh-runtime.tar.gz -C /usr/local dsh-runtime,目标机解压回原位后重跑两个安装脚本(只重建软链)。\n实例内 /usr 只读 → 用户无法自行安装或修改,这里的变更对全部用户立即生效。", "pa.rt.q2": "安装清单原文(SHARED-TOOLS.md)", "pa.rt.loadFail": "加载失败:" }; /** 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 quota", "mem.est": "estimate (platform quota unavailable; same formula as the orchestrator)", "mem.real": "actual quota", "mem.heap": "V8 heap", "mem.now": "now", "mem.planned": "after selection", "mem.limit": "hard cap", "mem.left": "left", "mem.over": "hit the hard cap", "mem.tight": "close to the hard cap", "mem.safe": "headroom is fine", "mem.overWarn": "The selected plugins need more than the per-instance hard cap (1024 MiB), so the quota will not grow further and the instance may fail to start. Disable some plugins first.", "mem.perCard": "load cost", "pa.label": "System management", "pa.homeSub": "The same feature set as the portal — open any item to manage it in a popup.", "pa.group.svc": "Service", "pa.group.mgmt": "Administration", "pa.back": "← Back", "pa.close": "Close", "pa.retry": "Retry", "pa.loading": "Loading…", "pa.loadingDesc": "Reading platform status…", "pa.loadFailed": "Failed to read platform status", "pa.notAdminTitle": "System management is admin-only", "pa.notAdminDesc": "This account is not an admin, so platform info is hidden here.", "pa.empty": "No data", "pa.done": "Updated", "pa.failed": "Action failed", "pa.working": "Working…", "pa.sk.delFail": "Delete failed", "pa.p.files": "Service management", "pa.d.files": "File tree + launch DSH", "pa.p.keys": "API keys", "pa.d.keys": "Global API keys", "pa.p.users": "User management", "pa.d.users": "Approve / disable / delete", "pa.p.skills": "Skill management", "pa.d.skills": "Shared skill upload", "pa.p.plugins": "Plugin management", "pa.d.plugins": "Feature plugin publishing", "pa.p.runtime": "Runtime", "pa.d.runtime": "Shared runtimes and tools", "pa.s.files": "Browse files and launch DSH from this folder", "pa.s.keys": "Global API keys (shared by all users; admin-editable)", "pa.s.users": "Approve / disable / delete users", "pa.s.skills": "Shared skills (available to all users · read-only)", "pa.s.plugins": "Publishing and managing feature (out-of-tree) plugins", "pa.s.runtime": "Runtimes and tools shared by every instance (installed once, shared by all users)", "pa.k.name": "Name", "pa.k.type": "Type", "pa.k.size": "Size", "pa.k.mtime": "Modified", "pa.k.op": "Actions", "pa.k.user": "Username", "pa.k.role": "Role", "pa.k.reg": "Registered", "pa.k.skill": "Skill", "pa.k.desc": "Description", "pa.k.files": "Files", "pa.k.updated": "Updated", "pa.k.plugin": "Plugin", "pa.k.ver": "Version", "pa.k.deployed": "Published", "pa.k.src": "Source", "pa.k.script": "Install/upgrade script", "pa.k.removable": "Removable", "pa.k.item": "Item", "pa.k.value": "Value", "pa.k.used": "Used", "pa.k.pluginDesc": "Plugin", "pa.k.cat": "Category", "pa.k.importKind": "Import", "pa.k.downloads": "Downloads", "pa.k.detail": "Details", "pa.st.running": "Running", "pa.st.stopped": "Stopped", "pa.st.dir": "Folder", "pa.st.file": "File", "pa.st.enabled": "Enabled", "pa.st.removable": "Removable", "pa.st.required": "Platform-required", "pa.role.admin": "Admin", "pa.role.active": "Active", "pa.role.pending": "Pending", "pa.role.disabled": "Disabled", "pa.op.approve": "Approve", "pa.op.disable": "Disable", "pa.op.enable": "Restore", "pa.op.del": "Delete", "pa.op.add": "Add", "pa.op.select": "Enable", "pa.op.upload": "Upload / replace", "pa.op.newFolder": "New folder", "pa.op.uploadFile": "Upload file", "pa.op.launch": "Launch DSH", "pa.op.stop": "Stop", "pa.op.openDsh": "Open DSH ↗", "pa.op.search": "Search", "pa.op.refreshDir": "Re-fetch catalog", "pa.op.import": "Import to platform", "pa.op.detail": "Details ↗", "pa.op.repull": "Re-fetching…", "pa.f.launchDir": "Launch folder:", "pa.f.root": "root", "pa.f.rootShort": "root", "pa.f.newFolderPrompt": "Folder name:", "pa.f.alreadyRunning": "A DSH instance is already running", "pa.f.launchFail": "Launch failed", "pa.f.launched": "DSH launched", "pa.f.uploadFail": "Upload failed: ", "pa.f.createFail": "Create failed: ", "pa.key.empty": "No global key yet. Add one so every user's DSH can call the model.", "pa.key.namePh": "Name (e.g. home / server)", "pa.key.invalid": "Invalid name or key format", "pa.key.needBoth": "Enter both name and key", "pa.key.confirmDel": "Delete this key?", "pa.u.confirmDel": "Deletion cannot be undone. Type the username \"{name}\" to confirm:", "pa.u.mismatch": "Username mismatch — cancelled", "pa.u.delFail": "Delete failed (admin cannot be deleted)", "pa.u.noUsers": "No users", "pa.sk.hint": ".zip only: a single top-level directory containing SKILL.md (lowercase-hyphenated name + description). Uploads are security-scanned first; a same-name skill is replaced as a whole after confirmation.", "pa.sk.empty": "No shared skill yet — upload a .zip to publish the first one", "pa.sk.loadFail": "Failed to load", "pa.sk.pickZip": "Choose a .zip file first", "pa.sk.zipOnly": ".zip files only", "pa.sk.tooBig": "File too large (>150MB)", "pa.sk.detecting": "Scanning…", "pa.sk.uploadFail": "Upload failed: ", "pa.sk.installed": "✓ Installed skill \"{name}\"", "pa.sk.replaceConfirm": "A skill named \"{name}\" already exists. Replace it entirely?\n\n⚠️ Replacement deletes all of the old skill's files. This cannot be undone.", "pa.sk.canceled": "Cancelled", "pa.sk.replaceFail": "Replace failed: ", "pa.sk.replaced": "✓ Replaced skill \"{name}\"", "pa.sk.delConfirm": "Delete shared skill \"{name}\"? It becomes unavailable to all users immediately.", "pa.sk.deleted": "✓ Deleted", "pa.pl.tabOfficial": "Recommended plugins", "pa.pl.tabManual": "Manual upload / manage", "pa.pl.searchPh": "Search name / description / npm package", "pa.pl.allCat": "All categories", "pa.pl.onlyImportable": "Importable only", "pa.pl.hintOfficial": "Source: the awesome-dsh-plugin official catalog (community picks, sorted by downloads, top 300). Import accepts prebuilt packages only — official npm tarballs or author release assets; the platform runs no third-party build scripts. Entries marked \"needs source build\" cannot be imported directly; build them yourself and upload under \"Manual upload / manage\".", "pa.pl.importNpm": "npm prebuilt", "pa.pl.importTarball": "release asset", "pa.pl.importSource": "needs source build", "pa.pl.publishing": "Publishing…", "pa.pl.published": "✓ Published feature plugin \"{name}\"", "pa.pl.replacedOk": "✓ Replaced feature plugin \"{name}\"", "pa.pl.trusted": " (trust declaration recorded)", "pa.pl.uploadFail": "Upload failed: ", "pa.pl.wlLoading": "Loading catalog… (downloaded from the official site on first open, cached afterwards)", "pa.pl.wlFail": "Fetch failed", "pa.pl.wlEmpty": "No match", "pa.pl.wlInfo": "{total} listed | {importable} importable | {count} matched | {shown} shown | catalog cached {when} (reused within 6 hours, no network)", "pa.pl.wlStale": " | ⚠ official site unreachable, using the local cache", "pa.pl.empty": "No published plugin yet — import from \"Recommended plugins\" or upload a .tgz above", "pa.pl.repullOk": "✓ Catalog re-fetched (cache updated)", "pa.pl.repullFail": "Re-fetch failed (network or official site unreachable)", "pa.pl.selInfo": "{n} selected", "pa.pl.importPick": "Select at least one plugin", "pa.pl.importMax": "Up to 20 plugins per import", "pa.pl.importConfirm": "Publish (list) {n} plugins on the platform?\n\nThey stay disabled and have no effect until each user enables them under Settings → Feature plugins.", "pa.pl.importing": "Importing…", "pa.pl.importResult": "Publish finished: {ok} / {total} succeeded", "pa.pl.importFailTail": ", {fail} failed", "pa.pl.importOkBox": "✓ Published {n} plugins to the platform (disabled by default; no effect until a user enables them). Switch to \"Manual upload / manage\" to inspect or delete them.", "pa.pl.failTitle": "{n} failed", "pa.pl.importErr": "Publish failed: network error", "pa.pl.cardPublish": "Publish a plugin", "pa.pl.cardPublishSub": "Upload a .tgz to list it on the platform", "pa.pl.cardPublished": "Published plugins", "pa.pl.poolHint": "{n} total · all disabled by default", "pa.pl.hintManual": "Publishing ≠ enabling. This only lists the plugin on the platform — it stays inert for every user. To actually use it, a user must tick it under Settings → Feature plugins in their own instance; the platform then installs it into their environment and it takes effect after an instance restart.", "pa.pl.hintTgz": ".tgz only: a dsh plugin bundle (with package.json whose name is the npm package name). Uploads are security-scanned first; same-name uploads follow the replace policy (old package removed before the new one lands). A P0 hit is rejected by default, but every hit is listed — if you trust it, declare that explicitly and it will be published (and audited).", "pa.pl.pickTgz": "Choose a .tgz file first", "pa.pl.tgzOnly": ".tgz files only", "pa.pl.delConfirm": "Delete published plugin \"{name}\"?\n\nIt will be removed from the platform; instances that enabled it will lose it.", "pa.pl.deleted": "✓ Deleted", "pa.pl.delFail": "Delete failed", "pa.pl.scanP0": "Security scan hit P0 rules — publishing rejected ({n} findings)", "pa.pl.scanCompat": "Incompatible with the platform's current dsh version — publishing rejected ({n} findings)", "pa.pl.warnP1": "{n} further P1 warnings (non-blocking)", "pa.pl.trustPh": "Reason for trusting (optional, audited)", "pa.pl.trustBtn": "I reviewed every finding — trust and publish", "pa.pl.trustNote": "⚠ Continue only if the hits are genuine false positives or the risk is acceptable. The platform records who / when / what was hit / why.", "pa.rt.drift": "⚠️ Version drift detected (differs from the baseline): {list}", "pa.rt.driftHint": "If this upgrade was intentional, run {script} to refresh the baseline.", "pa.rt.ok": "✅ Versions match the baseline", "pa.rt.okAt": " (baseline {t})", "pa.rt.dir": "Directory", "pa.rt.size": "Size", "pa.rt.changed": "Last change", "pa.rt.q1": "How do I upgrade / uninstall / migrate?", "pa.rt.a1": "Upgrade: bump the pinned version in the script → re-run that install script (idempotent + official sha256 check) → run {script} to refresh the baseline, otherwise this page keeps reporting drift.\nUninstall: just remove the /usr/local/bin/ symlink (do not remove platform-required entries).\nMigrate: {dir} is a single packaging unit — tar czf dsh-runtime.tar.gz -C /usr/local dsh-runtime, unpack it back in place on the target and re-run both install scripts (they only rebuild symlinks).\n/usr is read-only inside instances → users cannot install or modify anything; changes here apply to all users immediately.", "pa.rt.q2": "Manifest source (SHARED-TOOLS.md)", "pa.rt.loadFail": "Failed to load: " }; /** * 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)}", // 官方推荐插件列表:**拉高到「离弹窗底部约 100px」**(2026-09-13 用户要求)。 // 弹窗高 = min(92vh,1040px);表上方固定消耗 ≈470px(弹窗头 + 页签 + 工具条 + 说明 + 信息行), // 表下方还有按钮行 + 结果区 + 内边距 ⇒ 用 `92vh - 470px` 反推,再夹到 [280, 580]: // 下限 280px 防小屏被压成一条线;上限 580px 对应弹窗触顶 1040px 时的可用高度。 ".pa-plist{max-height:max(280px,min(calc(92vh - 470px),580px))}", ".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}}", // ── 第 3 轮(2026-09-13):门户 `web/portal.html` **页面级组件**逐条译为 `.pa-*` ── // 译法与第 2 轮一致:**字号 / 间距 / 圆角 / 动效逐值照抄**,颜色换 dsh 官方 token。 // 对应关系(门户 → 本节): // .card → .pa-box | .btn / .btn-primary / .btn:disabled → .pa-btn* // .page-head → .pa-phead | #view 内边距 → .pa-page // .pg-tabs/.pg-tab/.pg-cnt → .pa-tabs/.pa-tab/.pa-tcnt(下划线式页内 tab) // .dsh-bar/.dot → .pa-dshbar/.pa-dot | .pathbar/.crumb → .pa-pathbar/.pa-crumb // .upload-row/.hint → .pa-row/.pa-hint | .key-row → .pa-keyrow // .wl-cell/.wl-desc/.wl-meta/.wl-cat/.wl-link → .pa-wl* // .empty → .pa-empty | .toast → .pa-toast | td.desc/td.act → td.pa-desc/td.pa-act ".pa-box{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;padding:18px}", ".pa-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;font-size:14px;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-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed)}", ".pa-btn.pa-primary{background:var(--dsw-alias-brand-primary,#2f6fed);border-color:var(--dsw-alias-brand-primary,#2f6fed);color:#fff}", ".pa-btn.pa-primary:hover:not(:disabled){background:#2a5fd0;color:#fff}", ".pa-btn:disabled{opacity:.5;cursor:default}", ".pa-phead{display:flex;align-items:center;gap:14px;flex:1;min-width:0}", ".pa-phead .pa-hd{margin:0}", ".pa-phead .pa-sub{margin:4px 0 0}", ".pa-page{padding:18px 20px 28px}", ".pa-mhead{display:flex;align-items:center;gap:14px;padding:18px 20px 14px;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea);flex:none}", ".pa-mbody{flex:1;min-height:0;overflow:auto}", // 「需要多大就多大」:宽度对齐门户功能页(`.page` max-width 1440)、高度吃满视口。 // 门户首页是 960,但弹窗只在**打开具体功能页**时出现 ⇒ 统一取功能页宽度即可。 ".pa-modal.pa-wide{width:min(1440px,96vw);height:min(92vh,1040px);display:flex;flex-direction:column;overflow:hidden;padding:0}", ".pa-tabs{display:flex;gap:22px;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea);margin:0 0 16px}", ".pa-tab{padding:8px 2px 10px;font-size:15px;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s,border-color .15s;user-select:none;white-space:nowrap}", ".pa-tab:hover{color:var(--dsw-alias-brand-primary,#2f6fed)}", ".pa-tab.pa-on{color:var(--dsw-alias-brand-primary,#2f6fed);font-weight:600;border-bottom-color:var(--dsw-alias-brand-primary,#2f6fed)}", ".pa-tcnt{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-tab:not(.pa-on) .pa-tcnt{background:rgba(107,114,128,.12);color:var(--dsw-alias-label-tertiary,#6b7280)}", ".pa-dshbar{display:flex;align-items:center;gap:12px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;padding:14px 16px;margin-bottom:16px}", ".pa-dshbar .pa-stat{font-size:14px}", ".pa-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}", ".pa-dot.pa-on{background:#1fb56a}", ".pa-dot.pa-off{background:#c4c9d0}", ".pa-pathbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}", ".pa-crumbs{display:flex;align-items:center;gap:4px}", ".pa-crumb{color:var(--dsw-alias-brand-primary,#2f6fed);cursor:pointer;font-size:14px}", ".pa-crumb:hover{text-decoration:underline}", ".pa-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}", ".pa-row input[type=file]{flex:1;min-width:220px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:8px;font-size:14px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#24292f)}", ".pa-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280);margin:0 0 14px;line-height:1.6}", ".pa-keyrow{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea)}", ".pa-keyrow:last-child{border-bottom:none}", ".pa-wlcell{white-space:normal}", ".pa-wldesc{font-size:14px;line-height:1.5;color:var(--dsw-alias-label-primary,#24292f);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-width:620px}", ".pa-wldesc.pa-fb{font-weight:600}", ".pa-wlmeta{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:3px;max-width:520px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", ".pa-wlcat{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280);white-space:nowrap}", ".pa-wllink{text-decoration:none;white-space:nowrap}", ".pa-tbl td.pa-desc{max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary,#6b7280)}", ".pa-tbl td.pa-act,.pa-tbl th.pa-act{text-align:center}", ".pa-tbl td.pa-cell{white-space:normal}", ".pa-csep{color:var(--dsw-alias-label-tertiary,#6b7280)}", // 门户的文件夹行没有任何可点提示(无手型 / 无颜色)⇒ 弹窗内补上,否则文件树没法导航。 ".pa-dir{cursor:pointer;color:var(--dsw-alias-brand-primary,#2f6fed)}", ".pa-dir:hover{text-decoration:underline}", ".pa-empty{padding:48px 20px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:14px}", ".pa-toast{position:fixed;bottom:32px;left:50%;transform:translateX(-50%);background:#24292f;color:#fff;padding:10px 18px;border-radius:8px;font-size:14px;z-index:10001;display:none;white-space:pre-wrap;max-width:90vw}", ".pa-toast.pa-show{display:block}", ".pa-input{flex:1;min-width:120px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:8px;font-size:14px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#24292f)}", "select.pa-input{cursor:pointer}", ".pa-check{display:inline-flex;align-items:center;gap:6px;font-size:14px;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer}", ".pa-note{border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:8px;padding:12px 14px;font-size:13px;line-height:1.6}", "@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]; // 平台**实际下发**的内存配额(档案 84)—— 来自 `/api/dsh/status` 的 `quota` // (`{memMb, heapMb}`,与编排器 spawn 时用的是同一个值)。有它就以它为准显示; // 拿不到(老平台 / k8s 模式)才退回本地算式,并在文案里如实标注为估算。 var quotaState = useState(null); var quotaInfo = quotaState[0]; var setQuotaInfo = quotaState[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")); }); // 并行取「平台实际配了多少内存」(档案 84)。失败静默 —— 只影响内存条, // 不该连带把插件列表也报成加载失败。 fetch(portalHost() + "/api/dsh/status", { credentials: "include" }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { setQuotaInfo(d && d.quota ? d.quota : null); }) .catch(function () { setQuotaInfo(null); }); } 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) { // 进度条的 100% 基准 = **硬顶**(`MEM_MAX_MIB`)。 // ⚠️ 不能拿「配额」当基准:配额本身是 clamp 出来的(≥384),当基准会让所有实例都满格。 var limit = props.limit; var quotaNow = props.quotaNow; // 当前实际配额(优先 status 真值) var quotaPlanned = props.quotaPlanned; // 勾选后的配额 var rawNow = props.rawNow; // 未 clamp 的原始需求 var rawPlanned = props.rawPlanned; var factText = props.factText; // 事实行文案(调用方算好,确认弹窗复用同一条) // 「顶到硬顶」= 原始需求已超过 MAX ⇒ 再加也不会给更多 ⇒ 这才是真会起不来的情形。 // (旧版把 clamp 下限 384 当上限判,导致 388 就报「将超出上限」的**误报**。) var over = rawPlanned > limit; var tight = !over && rawPlanned > limit * MEM_TIGHT_RATIO; var accent = over ? T.danger : (tight ? T.warn : T.success); var nowPct = Math.max(0, Math.min(100, (rawNow / limit) * 100)); var planPct = Math.max(0, Math.min(100, (rawPlanned / limit) * 100)); var delta = quotaPlanned - quotaNow; var stateText = over ? t("mem.over") : (tight ? t("mem.tight") : t("mem.safe")); var line = t("mem.now") + " " + quotaNow + " MiB"; if (delta !== 0) { line += " → " + t("mem.planned") + " " + quotaPlanned + " MiB"; } else { line += " / " + t("mem.limit") + " " + limit + " MiB"; } // 事实行:平台**实际下发**的配额与 V8 堆(来自 /api/dsh/status;文案由调用方传入)。 var fact = factText; 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: fact }) ] }), 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; // 内存配额推演(档案 84,**与平台编排器同一套算式**): // · raw* = 基线 + Σ 成本(未 clamp);quota* = clamp(raw, MIN, MAX) // · Now = 服务端快照里「已启用」的那批(savedIds)→ 当前状态 // · Planned = 当前勾选状态(用户点一下就变)→ 应用后会是多少 // 若 `/api/dsh/status` 给了 `quota.memMb`(平台实际用的值),**以它为准**显示「当前」。 var rawNow = rawMiB(plugins.filter(function (p) { return savedIds.indexOf(p.id) >= 0; }), "enabled"); var rawPlanned = rawMiB(plugins, "enabled"); var quotaNow = (quotaInfo && quotaInfo.memMb != null) ? quotaInfo.memMb : quotaOf(rawNow); var quotaPlanned = quotaOf(rawPlanned); var realHeapMb = (quotaInfo && quotaInfo.heapMb != null) ? quotaInfo.heapMb : heapMiBFor(quotaNow); // 事实行文案:优先展示平台**实际下发**的配额与堆;读不到才如实标注为估算。 var memFactText = (quotaInfo && quotaInfo.memMb != null) ? (t("mem.real") + " " + quotaInfo.memMb + " MiB · " + t("mem.heap") + " " + realHeapMb + " MiB") : t("mem.est"); 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_MAX_MIB, quotaNow: quotaNow, quotaPlanned: quotaPlanned, rawNow: rawNow, rawPlanned: rawPlanned, factText: memFactText })); // 工具行:搜索 + 全选/清空 + 计数(规范 §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 用户要求):数值越大越"贵",按硬顶量级分色 // (≥256 红 / ≥64 黄 / 其余次要色)。⚠️ 未进 `MEM_TABLE` 的插件平台按 0 计 // (「宁可少给也不虚高」)⇒ 此时**不显示徽章**,免得满屏「≈ 0 MiB」噪音。 memMiB(p) > 0 ? 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) >= 256 ? T.danger : (memMiB(p) >= 64 ? T.warn : T.sub) }, children: t("mem.perCard") + " ≈ " + memMiB(p) + " MiB" }) }) : null ] }), 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 = rawPlanned > MEM_MAX_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") + " " + quotaNow + " MiB → " + t("mem.planned") + " " + quotaPlanned + " MiB / " + t("mem.limit") + " " + MEM_MAX_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: memFactText }) ] }), 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 ); }); // ═══════════════════════════════════════════════════════════════════════════ // 「系统管理」页面族 —— **逐函数移植门户 `web/portal.html` 的