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:
1 parent
eb50ca2d5b
commit
918f1d3a51
20 files changed
+2073
-276
No files matched your search
@@ -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"); },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@dsh-local/business-plugins",
|
||||
"version": "0.3.8",
|
||||
"description": "功能管理(原「功能插件」)section for dsh web profile — v0.3.8(2026-09-13):**撤掉「我的密钥」分区** —— 模型配置统一走**官方「设置 → 模型」页**(用户要求「界面交互和官方一模一样」⇒ 不仿制、直接放开官方页,见档案 86)。平台侧同步两处:① `ensure-role-profile-patch.cjs` 不再禁用 `ui-settings-models`(实测官方页在本环境可用:`/api/session/modelCatalog` 返回 200);② `src/web/server.ts` 的 `resolveApiKey()` 改为「用户已自配 ⇒ **不注入共享 env**」——因为 dsh 凭据解析里 `inherited process environment` **优先级最高**,不这样做用户配的 key 会被静默盖掉。|v0.3.7(2026-09-13 · 原拟 0.3.6,因并行会话已用同号 0.3.6 铺发过「内存条」版本 ⇒ 提升为 0.3.7):① ~~新增「我的密钥」分区~~(**已于 0.3.8 撤除**,原因是两套入口会互相干扰) —— 用户自助配置自己的模型密钥(自配自用),不配置就用「平台共享密钥」(管理员配置);页面分两段:我的密钥(添加 / 启用 / 删除)+ 当前生效(明示在用谁的 key,并提示改 key 只重启自己的实例、不影响他人)。配套后端:`/api/me/keys` 由 requireAdmin 放开为 requireAuth,`resolveApiKey(userId)` 改为「先取自己的 → 取不到回落管理员的共享 key」两级;门户「密钥管理」文案同步改为「平台共享密钥」。② **内存条改读平台真实配额,消灭「388 MiB 误报」**(用户问「实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多」)。根因:本插件自建了**第二套**内存模型(基线 285 / univer 64 / mcn 39,并把 clamp 下限 384 当硬上限判超限),而平台编排器自 R1 起为「按插件集合推导」(base 160 / univer 512 / mcn 128 / clamp 384–1024),两处从未对齐 ⇒ 全勾显示 388 并报「⚠ 将超出上限」,真值却是 672(guest)/ 384(admin)。本轮:① 常量与规则逐项对齐编排器,并新增 `scripts/verify-mem-model.mjs` 在 `npm run verify` 里交叉断言(漂了就构建失败);② 优先读 `/api/dsh/status` 新增的 `quota{memMb,heapMb}`(平台**实际使用**的值,与 spawn 同源)直接显示「实际配额 · V8 堆」;③ 超限判断改为「原始需求 > 硬顶 1024」;④ 未进成本表的插件不再显示 ≈0 MiB 徽章。|v0.3.5(2026-09-13):**插件管理 →「官方推荐插件」列表高度拉高**(用户要求「高度可以增加,距离底部 100px 就行」)—— 该表 max-height 由固定 420px 改为 `max(280px, min(calc(92vh - 470px), 580px))`(类 `.pa-plist`),按弹窗高度反推、使列表底部**距弹窗底部约 100px**;下限 280px 防小屏压扁,上限 580px 对应弹窗触顶 1040px。|v0.3.4(2026-09-13):**「系统管理」全面对齐门户 web/portal.html 的 6 个功能页**(用户要求「点开弹窗里面展示的内容,要和 portal 点击功能后那种详细的表格和功能一致」)—— 分区首页 = 门户 renderHome(「服务」「管理」两组 + .nav-card 三行卡片);点开每一项 = 门户**那一页的完整页面**:服务管理(文件树 + 启动/停止/打开 DSH + 新建文件夹/上传)、密钥管理(全局 API 密钥增删启用)、用户管理(审批/禁用/恢复/删除,需输入用户名二次确认)、技能管理(.zip 上传/替换/删除)、插件管理(官方推荐插件 + 手动添加/管理双页签、搜索/分类/只看可导入/重新拉取/批量导入、.tgz 投放含 P0 扫描与显式信任)、运行环境(版本表 + 漂移提示 + 目录/体积 + 折叠说明);表格列 / 按钮 / 文案逐字一致(`PA_PAGES` 逐函数移植 portal 的 renderXxx/initXxx,只改 3 处:局部 `$` 查询器、跨子域 `paReq`、去掉 hash 路由);弹窗宽度对齐门户功能页 min(1440px,96vw)、高 92vh。写操作就地调平台 admin API(跨子域 + credentials:include,CORS 白名单已含 GET/POST/DELETE)。**已删除**旧版自造的 5 项只读摘要(用户管理 / 候选池 / 运行时 / 存储 / 当前实例)。⚠️ 顺带修掉门户 `readAsBase64()` 缺 await 导致上传恒传空数据的缺陷(弹窗侧已修正,门户侧待同修)。|v0.3.3(2026-09-13):「系统管理」视觉层照抄门户组件(.nav-card / .pg-cnt / .table-wrap + table.tbl / .badge / .btn-sm / .page-title)。|v0.2.8(2026-09-13):所有弹窗改页内弹窗(弃用 window.confirm)。|v0.2.7(档案 75):卡片加内存预估徽章 + 列表上方内存预估状态条。|v0.2.4(档案 67):对齐 06-工作台UI规范(信息层次反转 / 字号阶梯 / 行 hover)。|v0.2.2:分区名由「功能插件」改为「功能管理」。|v0.2.0:配色改用官方 --dsw-* design token(跟Line truncated
|
||||
"version": "0.3.11",
|
||||
"description": "功能管理(原「功能插件」)section for dsh web profile — v0.3.10(2026-09-13):① **「服务管理」改名「实例管理」**、**「密钥管理」改名「模型管理」**(用户要求;门户导航/卡片同步改名);② **「实例管理」页可管任意用户**(用户要求「admin 要能管所有用户的服务」)—— 工具条新增「用户」下拉,切换后文件树/启停/打开全部针对所选用户,走新开的 `/api/admin/users/:id/{fs,dsh}`(requireAdmin)|v0.3.8(2026-09-13):**撤掉「我的密钥」分区** —— 模型配置统一走**官方「设置 → 模型」页**(用户要求「界面交互和官方一模一样」⇒ 不仿制、直接放开官方页,见档案 86)。平台侧同步两处:① `ensure-role-profile-patch.cjs` 不再禁用 `ui-settings-models`(实测官方页在本环境可用:`/api/session/modelCatalog` 返回 200);② `src/web/server.ts` 的 `resolveApiKey()` 改为「用户已自配 ⇒ **不注入共享 env**」——因为 dsh 凭据解析里 `inherited process environment` **优先级最高**,不这样做用户配的 key 会被静默盖掉。|v0.3.7(2026-09-13 · 原拟 0.3.6,因并行会话已用同号 0.3.6 铺发过「内存条」版本 ⇒ 提升为 0.3.7):① ~~新增「我的密钥」分区~~(**已于 0.3.8 撤除**,原因是两套入口会互相干扰) —— 用户自助配置自己的模型密钥(自配自用),不配置就用「平台共享密钥」(管理员配置);页面分两段:我的密钥(添加 / 启用 / 删除)+ 当前生效(明示在用谁的 key,并提示改 key 只重启自己的实例、不影响他人)。配套后端:`/api/me/keys` 由 requireAdmin 放开为 requireAuth,`resolveApiKey(userId)` 改为「先取自己的 → 取不到回落管理员的共享 key」两级;门户「密钥管理」文案同步改为「平台共享密钥」。② **内存条改读平台真实配额,消灭「388 MiB 误报」**(用户问「实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多」)。根因:本插件自建了**第二套**内存模型(基线 285 / univer 64 / mcn 39,并把 clamp 下限 384 当硬上限判超限),而平台编排器自 R1 起为「按插件集合推导」(base 160 / univer 512 / mcn 128 / clamp 384–1024),两处从未对齐 ⇒ 全勾显示 388 并报「⚠ 将超出上限」,真值却是 672(guest)/ 384(admin)。本轮:① 常量与规则逐项对齐编排器,并新增 `scripts/verify-mem-model.mjs` 在 `npm run verify` 里交叉断言(漂了就构建失败);② 优先读 `/api/dsh/status` 新增的 `quota{memMb,heapMb}`(平台**实际使用**的值,与 spawn 同源)直接显示「实际配额 · V8 堆」;③ 超限判断改为「原始需求 > 硬顶 1024」;④ 未进成本表的插件不再显示 ≈0 MiB 徽章。|v0.3.5(2026-09-13):**插件管理 →「官方推荐插件」列表高度拉高**(用户要求「高度可以增加,距离底部 100px 就行」)—— 该表 max-height 由固定 420px 改为 `max(280px, min(calc(92vh - 470px), 580px))`(类 `.pa-plist`),按弹窗高度反推、使列表底部**距弹窗底部约 100px**;下限 280px 防小屏压扁,上限 580px 对应弹窗触顶 1040px。|v0.3.4(2026-09-13):**「系统管理」全面对齐门户 web/portal.html 的 6 个功能页**(用户要求「点开弹窗里面展示的内容,要和 portal 点击功能后那种详细的表格和功能一致」)—— 分区首页 = 门户 renderHome(「服务」「管理」两组 + .nav-card 三行卡片);点开每一项 = 门户**那一页的完整页面**:服务管理(文件树 + 启动/停止/打开 DSH + 新建文件夹/上传)、密钥管理(全局 API 密钥增删启用)、用户管理(审批/禁用/恢复/删除,需输入用户名二次确认)、技能管理(.zip 上传/替换/删除)、插件管理(官方推荐插件 + 手动添加/管理双页签、搜索/分类/只看可导入/重新拉取/批量导入、.tgz 投放含 P0 扫描与显式信任)、运行环境(版本表 + 漂移提示 + 目录/体积 + 折叠说明);表格列 / 按钮 / 文案逐字一致(`PA_PAGES` 逐函数移植 portal 的 renderXxx/initXxx,只改 3 处:局部 `$` 查询器、跨子域 `paReq`、去掉 hash 路由);弹窗宽度对齐门户功能页 min(1440px,96vw)、高 92vh。写操作就地调平台 admin API(跨子域 + credentials:include,CORS 白名单已含 GET/POST/DELETE)。**已删除**旧版自造的 5 项只读摘要(用户管理 / 候选池 / 运行时 / 存储 / 当前实例)。⚠️ 顺带修掉门户 `readAsBase64()` 缺 await 导致上传恒传空数据的缺陷(弹窗侧已修正,门户侧待同修)。|v0.3.3(2026-09-13):「系统管理」视觉层照抄门户组件(.nav-card / .pg-cnt / .table-wrap + table.Line truncated
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
|
||||
Reference in new issue
Block a user