feat(models): 平台自建「模型设置」—— 用户自配厂家 / 条目各自开关 / 共享模型开关(档案 87)

- 官方「设置 → 模型」页在平台环境**必然报错**(判据在**浏览器页面**的 loopback 判定;官方 README 原文 Non-loopback pages get no durable settings)⇒ 该分区对全角色(含 admin)隐藏,用户自配改走平台自建页(插件 0.3.11)
- DB 迁移 V6:credential_vault.route/base_url/api/models + users.shared_model_enabled;并**重定义 getEnabledCredentialKeyRef**(互斥删除后原实现无 ORDER BY ⇒ 「任取一条」)
- 新落地层 src/web/model-landing.ts:spawn 时把「已启用条目」写进实例 .credentials.yaml 与 settings.yaml 的 llm-pi-ai.providers.<route>;字段名与官方包实测对齐(apiKeyEnv / baseURL / api,**不是** protocol);只碰自己写过的 + 一次性交接
- 接口 /api/me/keys、/api/me/keys/:id/toggle、/api/me/models/shared;前端新增「设置 → 模型设置」分区(settings.section id=model-settings / order 100 / 全角色)
- 顺手修两处:ensure-role-profile-patch.cjs 的 --force 整文件覆盖会抹掉 admin 的 disable-hmr 与 workspace-scoped-picker 两个平台块(改为 stripManagedBlock 只替换自己那段);verify-mem-model.mjs 因档案 86 重构而长期失败的陈旧断言

⚠️ 本提交同时包含**档案 86(admin 跨用户实例管理 + 两处改名)**的代码改动 —— 该部分已上线并端到端验证;其 import/register 与本次改动同处 src/web/server.ts、poc/business-plugins/lib/client.js 等文件,按**文件粒度无法拆分**,且不带它会让仓库 tsc 直接失败(缺 src/web/routes/admin-user-ops.ts)。
This commit is contained in:
admin committed 2026-09-14 00:00:15 +08:00
1 parent eb50ca2d5b
commit 918f1d3a51
20 files changed
+2073 -276

No files matched your search

+353 -21
View File
@@ -209,9 +209,9 @@ window.__ModuleLoader__.load({
"pa.failed": "操作失败",
"pa.working": "处理中…",
"pa.sk.delFail": "删除失败",
"pa.p.files": "服务管理",
"pa.d.files": "文件树 + DSH 启动",
"pa.p.keys": "密钥管理",
"pa.p.files": "实例管理",
"pa.d.files": "用户实例与工作区",
"pa.p.keys": "模型管理",
"pa.d.keys": "全局 API 密钥",
"pa.p.users": "用户管理",
"pa.d.users": "审批 / 禁用 / 删除",
@@ -221,8 +221,8 @@ window.__ModuleLoader__.load({
"pa.d.plugins": "功能插件投放",
"pa.p.runtime": "运行环境",
"pa.d.runtime": "共享运行时与工具",
"pa.s.files": "浏览文件、在此文件夹启动 DSH",
"pa.s.keys": "全局 API 密钥(所有用户共用,仅管理员可改)",
"pa.s.files": "按用户查看工作区、启动 / 停止其 DSH 实例",
"pa.s.keys": "平台共享密钥(未自配密钥的用户默认使用;仅管理员可改)",
"pa.s.users": "审批 / 禁用 / 删除用户",
"pa.s.skills": "共享技能(所有用户可用 · 只读)",
"pa.s.plugins": "功能插件(系统外插件)的投放与管理",
@@ -282,6 +282,8 @@ window.__ModuleLoader__.load({
"pa.op.detail": "详情 ↗",
"pa.op.repull": "正在重新拉取…",
"pa.f.launchDir": "启动目录:",
"pa.f.targetUser": "用户:",
"pa.f.selfOnly": "仅管理员可切换用户",
"pa.f.root": "根目录",
"pa.f.rootShort": "根",
"pa.f.newFolderPrompt": "文件夹名称:",
@@ -373,7 +375,60 @@ window.__ModuleLoader__.load({
"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": "加载失败:"
"pa.rt.loadFail": "加载失败:",
// ── 档案 87「模型设置」分区 ────────────────────────────────────────────
// 为什么平台自己做这一页:官方「设置 → 模型」页在平台环境**必然报错**(它要
// Host settings 镜像,而平台是浏览器经域名访问远程服务器 ⇒ `isLoopback=false`
// ⇒ persistence 降级 memory ⇒ 页面报「加载提供方目录失败」)。所以官方那个分区
// 被 `ensure-role-profile-patch.cjs` 对**所有角色(含 admin)**隐藏。
// 三条口径(用户 2026-09-13 定,勿再当选择题):① 条目各自开关、可同时启用
// ② admin 配的共享模型**也列在这里**、用户可开关 ③ 用哪个模型在 dsh 对话框选。
"ms.title": "模型设置",
"ms.sub": "管理你自用的模型厂家:每条可单独开关、可同时启用;具体用哪个模型,在对话界面的模型选择器里选。保存后需重启实例生效。",
"ms.loadFailed": "加载失败,请刷新重试",
"ms.saveFailed": "保存失败,请重试",
"ms.saved": "已保存,重启实例后生效",
"ms.needNameKey": "请填写名称与 API Key",
"ms.needModels": "自定义厂家至少要填一个模型",
"ms.shared": "平台共享模型",
"ms.sharedOwner": "由 {who} 配置",
"ms.sharedHint": "关掉后,你的实例不再接收平台共享的模型条目(你自己配的仍照常生效)。",
"ms.available": "可用",
"ms.unavailable": "平台未配置",
"ms.sharedOn": "停用共享模型",
"ms.sharedOff": "启用共享模型",
"ms.add": "新增模型条目",
"ms.ph.name": "名称(如 我的中转网关)",
"ms.ph.key": "API Key",
"ms.ph.url": "Endpoint(留空 = 内置 DeepSeek)",
"ms.ph.route": "厂家标识(可选,英文小写)",
"ms.ph.models": "模型 id,一行一个",
"ms.formHint": "Endpoint 留空即使用平台内置 DeepSeek;一旦填写,就必须给至少一个模型 id。厂家标识会写进实例配置,同一个标识不能重复。",
"ms.save": "保存",
"ms.list": "我的模型条目",
"ms.col.name": "名称",
"ms.col.route": "厂家标识",
"ms.col.url": "Endpoint",
"ms.col.api": "协议",
"ms.col.models": "模型数",
"ms.col.state": "状态",
"ms.col.act": "操作",
"ms.empty": "还没有条目 —— 在上面添加一个即可",
"ms.builtin": "内置 DeepSeek",
"ms.builtinHint": "官方内置,无需 endpoint",
"ms.on": "已启用",
"ms.off": "已停用",
"ms.enable": "启用",
"ms.disable": "停用",
"ms.del": "删除",
"ms.e.name": "名称不合法",
"ms.e.key": "API Key 不合法(只允许字母数字与 - _ .)",
"ms.e.url": "Endpoint 必须以 http(s):// 开头",
"ms.e.route": "厂家标识不合法(小写字母/数字/短横线,字母开头)",
"ms.e.api": "不支持该协议",
"ms.e.models": "请至少填一个模型 id",
"ms.e.taken": "该厂家标识已被占用,换一个"
};
/** English dictionary, key-set complete against zh. */
var en = {
@@ -439,9 +494,9 @@ window.__ModuleLoader__.load({
"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.p.files": "Instance management",
"pa.d.files": "User instances and workspaces",
"pa.p.keys": "Model management",
"pa.d.keys": "Global API keys",
"pa.p.users": "User management",
"pa.d.users": "Approve / disable / delete",
@@ -451,8 +506,8 @@ window.__ModuleLoader__.load({
"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.files": "Browse a user's workspace and start/stop their DSH instance",
"pa.s.keys": "Platform-shared key (used by users who have not set their own; admin-only)",
"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",
@@ -512,6 +567,8 @@ window.__ModuleLoader__.load({
"pa.op.detail": "Details ↗",
"pa.op.repull": "Re-fetching…",
"pa.f.launchDir": "Launch folder:",
"pa.f.targetUser": "User:",
"pa.f.selfOnly": "Only an admin can switch users",
"pa.f.root": "root",
"pa.f.rootShort": "root",
"pa.f.newFolderPrompt": "Folder name:",
@@ -603,7 +660,53 @@ window.__ModuleLoader__.load({
"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/<name> 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: "
"pa.rt.loadFail": "Failed to load: ",
"ms.title": "Model settings",
"ms.sub": "Manage your own model providers: each entry has its own switch and several can be on at once. Pick which model to use in the chat model selector. Restart the instance for changes to take effect.",
"ms.loadFailed": "Failed to load — refresh and try again",
"ms.saveFailed": "Save failed — try again",
"ms.saved": "Saved — restart the instance to apply",
"ms.needNameKey": "Please fill in a name and an API key",
"ms.needModels": "A custom provider needs at least one model",
"ms.shared": "Platform-shared model",
"ms.sharedOwner": "Configured by {who}",
"ms.sharedHint": "When off, your instance stops receiving the platform-shared model entries (your own entries still apply).",
"ms.available": "Available",
"ms.unavailable": "Not configured",
"ms.sharedOn": "Disable shared model",
"ms.sharedOff": "Enable shared model",
"ms.add": "Add a model entry",
"ms.ph.name": "Name (e.g. My gateway)",
"ms.ph.key": "API key",
"ms.ph.url": "Endpoint (empty = built-in DeepSeek)",
"ms.ph.route": "Provider id (optional, lowercase)",
"ms.ph.models": "One model id per line",
"ms.formHint": "Leave Endpoint empty to use the platform's built-in DeepSeek; if you fill it in you must give at least one model id. The provider id is written into the instance config and must be unique.",
"ms.save": "Save",
"ms.list": "My model entries",
"ms.col.name": "Name",
"ms.col.route": "Provider id",
"ms.col.url": "Endpoint",
"ms.col.api": "Protocol",
"ms.col.models": "Models",
"ms.col.state": "State",
"ms.col.act": "Actions",
"ms.empty": "No entries yet — add one above",
"ms.builtin": "Built-in DeepSeek",
"ms.builtinHint": "Built in; no endpoint needed",
"ms.on": "Enabled",
"ms.off": "Disabled",
"ms.enable": "Enable",
"ms.disable": "Disable",
"ms.del": "Delete",
"ms.e.name": "Invalid name",
"ms.e.key": "Invalid API key (letters, digits, - _ . only)",
"ms.e.url": "Endpoint must start with http(s)://",
"ms.e.route": "Invalid provider id (lowercase letters, digits, dashes; must start with a letter)",
"ms.e.api": "Unsupported protocol",
"ms.e.models": "Add at least one model id",
"ms.e.taken": "That provider id is taken — pick another"
};
/**
@@ -1428,6 +1531,211 @@ window.__ModuleLoader__.load({
return jsxRuntime.jsx("div", { style: wrap, children: rows });
}
// ═══════════════════════════════════════════════════════════════════════════
// 「模型设置」分区(档案 87 · 2026-09-13 第三轮口径)
//
// 为什么平台自己做这一页:官方「设置 → 模型」页在平台环境**必然报错** ——
// 它要求 Host settings 镜像,而平台是"浏览器经域名访问远程服务器"⇒
// `isLoopback = transport.ownsHost || pageLocation === undefined ||
// isLoopbackHostname(page)` 三条皆不成立 ⇒ persistence 降级为 `memory`
// ⇒ 页面必报「加载提供方目录失败: settings are unavailable in this browser」。
// 所以官方那个分区被 `ensure-role-profile-patch.cjs` 对**所有角色(含 admin)**
// 隐藏(见其 `DISABLE_MODELS_BLOCK` / `ADMIN_MODELS_BLOCK`),用户自配改走本页。
//
// 用户定下的三条口径(**已定,不要再拿去当选择题**):
// ① 条目**各自开关、可同时启用**(互斥已删);
// ② admin 配的**平台共享模型也列在这里**,用户可开关;
// ③ 具体用哪个模型**在 dsh 对话框的模型选择器里选** —— 本页只负责把
// 「已启用」的都配好(平台在 spawn 时写 `$DSH_HOME/.credentials.yaml`
// 与 `settings.yaml` 的 `llm-pi-ai.providers.<route>`)。
// ⚠️ 改动**重启实例后生效**(落地发生在 spawn 时),页面文案已如实说明。
//
// 接口:平台 `src/web/routes/auth.ts` 的 `/api/me/keys`(GET/POST)、
// `/api/me/keys/:id/toggle`、`/api/me/models/shared`、`DELETE /api/me/keys/:id`
// —— 与门户既有 API 一样走 `paReq`(跨子域 + credentials)。
// ═══════════════════════════════════════════════════════════════════════════
/** 后端错误码 → 词典键(认不出的码退回通用失败文案,不把码裸露给用户)。 */
var MS_ERR = {
invalid_name: "ms.e.name", invalid_api_key: "ms.e.key", invalid_base_url: "ms.e.url",
invalid_route: "ms.e.route", invalid_api: "ms.e.api", models_required: "ms.e.models",
route_taken: "ms.e.taken"
};
function ModelSettingsSection() {
var useState = React.useState;
var useEffect = React.useEffect;
var dataState = useState(null);
var data = dataState[0];
var setData = dataState[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 fName = useState("");
var fKey = useState("");
var fUrl = useState("");
var fRoute = useState("");
var fApi = useState("");
var fModels = useState("");
function load() {
setLoading(true);
paReq("/api/me/keys")
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (d) {
setData(d);
// 协议默认取官方表的第一项(`dsh-llm-pi-ai` 的 PROTOCOLS 顺序即默认优先级)。
if (!fApi[0]) fApi[1]((d.protocols || [])[0] || "");
}
setLoading(false);
})
.catch(function () { setLoading(false); setMsg(t("ms.loadFailed")); });
}
useEffect(function () { load(); }, []);
/** 统一收尾:跑一个写操作 → 刷新列表 / 或把后端错误码翻成文案。 */
function act(promise) {
if (busy) return;
setBusy(true);
setMsg("");
promise
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
setBusy(false);
if (!res.ok) {
var code = res.d && res.d.error;
setMsg(t(MS_ERR[code] || "ms.saveFailed"));
return;
}
if (res.d && res.d.saved) setMsg(t("ms.saved"));
load();
})
.catch(function () { setBusy(false); setMsg(t("ms.saveFailed")); });
}
function submit() {
var name = fName[0].trim();
var key = fKey[0].trim();
if (name === "" || key === "") { setMsg(t("ms.needNameKey")); return; }
var url = fUrl[0].trim();
var body = { name: name, apiKey: key };
if (url !== "") {
var ids = fModels[0].split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s !== ""; });
if (ids.length === 0) { setMsg(t("ms.needModels")); return; }
body.baseUrl = url;
body.models = ids;
if (fRoute[0].trim() !== "") body.route = fRoute[0].trim();
if (fApi[0]) body.api = fApi[0];
}
act(papostJson("/api/me/keys", body));
fName[1](""); fKey[1](""); fUrl[1](""); fRoute[1](""); fModels[1]("");
}
if (loading) {
return jsxRuntime.jsx("div", { className: "pa-page", children: jsxRuntime.jsx("div", { className: "pa-empty", children: t("loading") }) });
}
var keys = (data && data.keys) || [];
var shared = (data && data.shared) || {};
var sharedOn = !!(data && data.sharedModelEnabled);
var protocols = (data && data.protocols) || (fApi[0] ? [fApi[0]] : []);
var rows = keys.map(function (k) {
var custom = !!(k.baseUrl && k.baseUrl !== "");
var n = 0;
try { var arr = JSON.parse(k.models || "[]"); n = Array.isArray(arr) ? arr.length : 0; } catch (e) { n = 0; }
return jsxRuntime.jsxs("tr", { children: [
jsxRuntime.jsx("td", { children: k.name }),
jsxRuntime.jsx("td", { className: "pa-dim", children: custom ? (k.route || "—") : t("ms.builtin") }),
jsxRuntime.jsx("td", { className: "pa-dim", children: custom ? k.baseUrl : t("ms.builtinHint") }),
jsxRuntime.jsx("td", { className: "pa-dim", children: custom ? (k.api || "openai-completions") : "—" }),
jsxRuntime.jsx("td", { className: "pa-num", children: custom ? String(n) : "—" }),
jsxRuntime.jsx("td", { children: jsxRuntime.jsx("span", { className: "pa-badge " + (k.enabled ? "active" : "disabled"), children: k.enabled ? t("ms.on") : t("ms.off") }) }),
jsxRuntime.jsxs("td", { children: [
jsxRuntime.jsx("button", { key: "t", className: "pa-sm", disabled: busy, onClick: function () { act(papostJson("/api/me/keys/" + k.id + "/toggle", { enabled: !k.enabled })); }, children: k.enabled ? t("ms.disable") : t("ms.enable") }),
jsxRuntime.jsx("button", { key: "d", className: "pa-sm danger", style: { marginLeft: 6 }, disabled: busy, onClick: function () { act(paReq("/api/me/keys/" + k.id, { method: "DELETE" })); }, children: t("ms.del") })
] })
] }, k.id);
});
return jsxRuntime.jsxs("div", { className: "pa-page", children: [
jsxRuntime.jsx("h1", { className: "pa-hd", children: t("ms.title") }),
jsxRuntime.jsx("p", { className: "pa-sub", children: t("ms.sub") }),
// ── 平台共享模型(口径②:列入 + 可开关;开关只影响自己,不动 admin 的配置)──
jsxRuntime.jsxs("div", { className: "pa-box", style: { marginBottom: 16 }, children: [
jsxRuntime.jsxs("div", { className: "pa-card-h", children: [
jsxRuntime.jsx("span", { children: t("ms.shared") }),
shared.owner ? jsxRuntime.jsx("span", { className: "sub", children: t("ms.sharedOwner").replace("{who}", shared.owner) }) : null
] }),
jsxRuntime.jsx("p", { className: "pa-hint", children: t("ms.sharedHint") }),
jsxRuntime.jsxs("div", { className: "pa-row", style: { margin: 0 }, children: [
jsxRuntime.jsx("span", { className: "pa-badge " + (shared.available ? "active" : "disabled"), children: shared.available ? (shared.name || t("ms.available")) : t("ms.unavailable") }),
jsxRuntime.jsx("button", { className: "pa-btn" + (sharedOn ? "" : " pa-primary"), disabled: busy || !shared.available, onClick: function () { act(papostJson("/api/me/models/shared", { enabled: !sharedOn })); }, children: sharedOn ? t("ms.sharedOn") : t("ms.sharedOff") })
] })
] }),
// ── 新增条目(Endpoint 留空 = 内置 DeepSeek;填了就必须给模型清单)──
jsxRuntime.jsxs("div", { className: "pa-box", style: { marginBottom: 16 }, children: [
jsxRuntime.jsx("div", { className: "pa-card-h", children: jsxRuntime.jsx("span", { children: t("ms.add") }) }),
jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("input", { className: "pa-input", placeholder: t("ms.ph.name"), value: fName[0], onChange: function (e) { fName[1](e.target.value); } }),
jsxRuntime.jsx("input", { className: "pa-input", type: "password", autoComplete: "off", placeholder: t("ms.ph.key"), value: fKey[0], onChange: function (e) { fKey[1](e.target.value); } })
] }),
jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("input", { className: "pa-input", placeholder: t("ms.ph.url"), value: fUrl[0], onChange: function (e) { fUrl[1](e.target.value); } }),
jsxRuntime.jsx("input", { className: "pa-input", placeholder: t("ms.ph.route"), value: fRoute[0], onChange: function (e) { fRoute[1](e.target.value); } })
] }),
jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("select", { className: "pa-input", value: fApi[0], onChange: function (e) { fApi[1](e.target.value); }, children: protocols.map(function (p) { return jsxRuntime.jsx("option", { value: p, children: p }, p); }) })
] }),
jsxRuntime.jsx("textarea", { className: "pa-input", rows: 3, style: { width: "100%", boxSizing: "border-box" }, placeholder: t("ms.ph.models"), value: fModels[0], onChange: function (e) { fModels[1](e.target.value); } }),
jsxRuntime.jsx("p", { className: "pa-hint", children: t("ms.formHint") }),
jsxRuntime.jsxs("div", { className: "pa-row", style: { marginBottom: 0 }, children: [
jsxRuntime.jsx("button", { className: "pa-btn pa-primary", disabled: busy, onClick: submit, children: t("ms.save") }),
msg ? jsxRuntime.jsx("span", { className: "pa-hint", style: { margin: 0 }, children: msg }) : null
] })
] }),
// ── 我的条目(口径①:每条独立开关,可同时启用)──
jsxRuntime.jsx("div", { className: "pa-sec", children: t("ms.list") }),
jsxRuntime.jsx("div", { className: "pa-wrap", children: jsxRuntime.jsxs("table", { className: "pa-tbl", children: [
jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [
jsxRuntime.jsx("th", { children: t("ms.col.name") }),
jsxRuntime.jsx("th", { children: t("ms.col.route") }),
jsxRuntime.jsx("th", { children: t("ms.col.url") }),
jsxRuntime.jsx("th", { children: t("ms.col.api") }),
jsxRuntime.jsx("th", { children: t("ms.col.models") }),
jsxRuntime.jsx("th", { children: t("ms.col.state") }),
jsxRuntime.jsx("th", { children: t("ms.col.act") })
] }) }),
jsxRuntime.jsx("tbody", {
children: rows.length > 0
? rows
: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { className: "pa-empty-cell", colSpan: 7, children: t("ms.empty") }) })
})
] }) })
] });
}
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(
{
name: "settings.section",
id: "model-settings",
order: 100,
label: function () { return t("ms.title"); }
},
ModelSettingsSection
);
});
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(
{
@@ -1526,6 +1834,9 @@ window.__ModuleLoader__.load({
sub: function () { return t("pa.s.files"); },
html: function () {
return '<div class="pa-dshbar">' +
// 档案 86:admin 可切换目标用户 —— 下方文件树 / 启停 / 打开 全部针对所选用户
'<span style="font-size:13px;' + PA_DIM + '">' + paesc(t("pa.f.targetUser")) + "</span>" +
'<select class="pa-input" id="svcUser" style="flex:none;min-width:130px"></select>' +
'<span class="pa-stat" id="dshState"><span class="pa-dot pa-off"></span>' + paesc(t("pa.st.stopped")) + "</span>" +
'<button class="pa-btn pa-primary" id="launchBtn">' + paesc(t("pa.op.launch")) + "</button>" +
'<button class="pa-btn" id="stopBtn" style="display:none">' + paesc(t("pa.op.stop")) + "</button>" +
@@ -1548,11 +1859,14 @@ window.__ModuleLoader__.load({
'<input id="fileInput" type="file" style="display:none" />';
},
init: function ($, root) {
var cur = [];
var cur = []; // 目录栈
var uid = ""; // 目标用户(档案 86:admin 可切到任意用户;不再是"自己")
/** admin 视角的基路径 —— 文件与实例操作全部打到这里(requireAdmin)。 */
function base() { return "/api/admin/users/" + encodeURIComponent(uid); }
function pathString() { return cur.join("/"); }
function load() {
var p = pathString();
return paReq("/api/desktop/tree" + (p ? "?path=" + encodeURIComponent(p) : ""))
return paReq(base() + "/fs/tree" + (p ? "?path=" + encodeURIComponent(p) : ""))
.then(function (r) { return r.json(); })
.then(function (body) {
var rows = $("rows");
@@ -1595,7 +1909,7 @@ window.__ModuleLoader__.load({
});
}
function refreshDsh() {
return paReq("/api/dsh/status").then(function (r) { return r.ok ? r.json() : null; }).then(function (body) {
return paReq(base() + "/dsh/status").then(function (r) { return r.ok ? r.json() : null; }).then(function (body) {
if (!body) return;
var running = body.running;
$("dshState").innerHTML = '<span class="pa-dot ' + (running ? "pa-on" : "pa-off") + '"></span>' +
@@ -1606,7 +1920,7 @@ window.__ModuleLoader__.load({
});
}
function doCreate(name, type) {
return papostJson("/api/fs/create", { path: pathString(), name: name, type: type }).then(function (res) {
return papostJson(base() + "/fs/create", { path: pathString(), name: name, type: type }).then(function (res) {
if (!res.ok) {
return res.json().catch(function () { return {}; }).then(function (e) {
root.toast(t("pa.f.createFail") + (e.error || res.status));
@@ -1616,7 +1930,7 @@ window.__ModuleLoader__.load({
});
}
$("launchBtn").onclick = function () {
return papostJson("/api/dsh/launch", { folder: pathString() }).then(function (res) {
return papostJson(base() + "/dsh/launch", { folder: pathString() }).then(function (res) {
if (!res.ok) {
return res.json().catch(function () { return {}; }).then(function (b) {
root.toast(b.error === "already_running" ? t("pa.f.alreadyRunning") : t("pa.f.launchFail"));
@@ -1627,7 +1941,7 @@ window.__ModuleLoader__.load({
});
};
$("stopBtn").onclick = function () {
return paReq("/api/dsh/stop", { method: "POST" }).then(function () { return refreshDsh(); });
return paReq(base() + "/dsh/stop", { method: "POST" }).then(function () { return refreshDsh(); });
};
$("newFolderBtn").onclick = function () {
var name = window.prompt(t("pa.f.newFolderPrompt"));
@@ -1639,7 +1953,7 @@ window.__ModuleLoader__.load({
if (!file) return;
var name = file.name;
return pareadAsBase64(file).then(function (data) {
return papostJson("/api/fs/upload", { path: pathString(), name: name, data: data }).then(function (res) {
return papostJson(base() + "/fs/upload", { path: pathString(), name: name, data: data }).then(function (res) {
if (!res.ok) {
return res.json().catch(function () { return {}; }).then(function (e) {
root.toast(t("pa.f.uploadFail") + (e.error || res.status));
@@ -1650,11 +1964,29 @@ window.__ModuleLoader__.load({
});
});
};
return Promise.all([load(), refreshDsh()]);
/** 填「用户」下拉(`/api/admin/users`,requireAdmin);默认选自己。 */
function loadUsers() {
return paReq("/api/admin/users")
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var list = (d && d.users) || [];
var sel = $("svcUser");
if (!sel) return;
sel.innerHTML = list.map(function (u) {
return '<option value="' + paesc(u.id) + '">' + paesc(u.username) +
(u.role === "admin" ? "(" + paesc(t("pa.role.admin")) + ")" : "") + "</option>";
}).join("");
var mine = list.filter(function (u) { return u.role === "admin"; })[0] || list[0];
uid = mine ? mine.id : "";
sel.value = uid;
sel.onchange = function () { uid = sel.value; cur = []; load(); refreshDsh(); };
});
}
return loadUsers().then(function () { return Promise.all([load(), refreshDsh()]); });
}
};
/** 密钥管理 ← 门户 `#/keys`(`renderKeys` + `initKeys`)。 */
/** 模型管理 ← 门户 `#/keys`(`renderKeys` + `initKeys`)。 */
PA_PAGES.keys = {
icon: "🔑",
title: function () { return t("pa.p.keys"); },