feat(web): 模型设置页复刻官方交互 + 官方推荐插件按 dsh 版本过滤(档案 91/92/93)

档案 91 ——「模型设置」页折行 + 复刻官方交互(插件 0.3.15)
· 折行根因不是"窄":原「我已添加的厂家」是 4 列表格(列宽 auto)⇒ 名称/状态/两按钮三处折行同源;
  改官方 dsh-client-ui-settings-models 的**卡片行**(rowHead + rowActions margin-left:auto,两侧 nowrap)
· 「新增」改官方**两步式**:默认两个虚线按钮 → 点开才出卡片,主字段只剩「API 密钥」
  (官方原文:页面从不询问环境变量名);次要字段收进 <details>「自定义设置」
· CSS 数值逐值照抄官方 CSS 模块;顺修「由 admin 配置」「密钥可留空」两处错文案,补删除二次确认

档案 92 —— 官方推荐插件列表按 dsh 版本过滤(插件 0.3.16)
· 官方目录不带 dsh 版本字段 ⇒ 逐包查 npm 声明与平台真实版本比,分类 match/none/newer/older/unknown
· 默认隐藏「仅兼容更旧版本」,「需更高版本」橙色标注;勾选框为逃生口;说明行给隐藏计数(不静默消失)
· ⚠️ 判 satisfies 必须带 includePrerelease(平台是 prerelease,默认语义会把在跑的 dshmarket 之类判成更旧)

档案 93 —— 兼容判定收口(服务器 CI 由红转绿)
· 「单测长期红」真相 = 服务器源码落后于 git(本机 48/0 全绿)⇒ 同步即修,非代码问题
· 伞包 @deepseek-ai/dsh 收口到 dsh-install.platformPackageVersion()(它不在自己的 node_modules 里,
  原先恒判 null ⇒ 声明在伞包上的"要求更高版本"全被放行);闸门与列表共用同一份版本表
· 闸门判据 A 加 prerelease 兜底:实测抽样 300 里 prerelease-artifact 117 条(≈39% 假阳性,
  含在跑的 dshmarket)⇒ 容忍下能满足则只计数(prereleaseOnly)不阻断

校验
· 新增 scripts/verify-dsh-compat.mjs(21 条回归锚点)与 verify-models-dict.mjs / verify-models-render.cjs
· verify-platform-admin-section.mjs:断言从旧 UI(表格/下拉「自定义厂家…」)改成断言新 UI
  (含模拟点击两个新增入口 + 选中目录厂家)
· 两个仓库外的动作:服务器 npm run build + ci.sh(CI OK)+ systemctl restart dshs;插件 0.3.16 已铺发

⚠️ 本提交未包含 src/supervisor/orchestrator.ts —— 那是别人在途的内存配额改动
(BASE 160→448 / MIN 384→512 / MAX 1024→1536 / mcn 128→256),其对应的客户端常量尚未同步,
本机 verify-mem-model 因此有 4 项红;提交后仓库树两边均为旧值,该断言在 HEAD 上一致。
This commit is contained in:
admin committed 2026-09-14 21:13:32 +08:00
1 parent 1d72e8f9c8
commit 29c57a8207
11 files changed
+1474 -209

No files matched your search

+605 -171
View File
@@ -321,7 +321,7 @@ window.__ModuleLoader__.load({
"pa.pl.searchPh": "搜索插件名 / 描述 / npm 包名",
"pa.pl.allCat": "全部分类",
"pa.pl.onlyImportable": "只看可导入",
"pa.pl.hintOfficial": "来源:awesome-dsh-plugin 官方插件目录(dsh 官方社区精选,按下载量排序,取前 300 条)。导入只接受预构建包:官方 npm 包(registry tarball)或作者发布的 release 资产 —— 平台不执行任何第三方构建脚本。标记「需源码构建」的插件暂不支持一键导入,可自行构建后到「手动添加 / 管理」上传。",
"pa.pl.hintOfficial": "来源:awesome-dsh-plugin 官方插件目录(dsh 官方社区精选,按下载量排序,取前 300 条)。导入只接受预构建包:官方 npm 包(registry tarball)或作者发布的 release 资产 —— 平台不执行任何第三方构建脚本。标记「需源码构建」的插件暂不支持一键导入,可自行构建后到「手动添加 / 管理」上传。列表**已按当前 dsh 版本过滤**:只显示「匹配」与「需更高版本」(后者橙色标注,升级 dsh 前用不了);仅声明了更旧版本的会被隐藏(勾「含仅兼容更旧版本的」可查)。",
"pa.pl.importNpm": "npm 预构建",
"pa.pl.importTarball": "release 资产",
"pa.pl.importSource": "需源码构建",
@@ -335,6 +335,25 @@ window.__ModuleLoader__.load({
"pa.pl.wlEmpty": "无匹配",
"pa.pl.wlInfo": "清单 {total} 条 | 可导入 {importable} 条 | 匹配 {count} 条 | 显示 {shown} 条 | 目录缓存更新于 {when}(6 小时内直接复用,不联网)",
"pa.pl.wlStale": " | ⚠ 官方站暂不可达,正在使用本地缓存",
// ── 档案 92:按 dsh 版本过滤官方推荐列表(用户 2026-09-14 要求「只能显示匹配当前
// dsh 版本 和 超过当前版本 的插件,超过的要明确标注」)──
// 判定在平台侧做(官方目录**不带** dsh 版本字段 ⇒ 逐包查 npm 声明),
// 口径与实测依据见 `src/web/plugin-dsh-compat.ts` 头部注释。
"pa.pl.compatCol": "dsh 版本",
"pa.pl.showAllCompat": "含仅兼容更旧版本的",
"pa.pl.cmpMatch": "匹配",
"pa.pl.cmpNewer": "需更高版本",
"pa.pl.cmpNone": "未声明",
"pa.pl.cmpOlder": "仅兼容更旧",
"pa.pl.cmpUnknown": "未验证",
"pa.pl.cmpMatchTip": "声明范围满足当前 dsh {cur}。",
"pa.pl.cmpNewerTip": "该插件声明需要 dsh ≥ {v}(当前 {cur})—— 升级 dsh 之前装不上。",
"pa.pl.cmpNoneTip": "该插件没有声明任何 @deepseek-ai/* 依赖,据此判不了(≠ 不兼容)。",
"pa.pl.cmpOlderTip": "该插件只声明了更旧的 dsh 版本;实测未必不能跑,但不属于「兼容当前版本」。",
"pa.pl.cmpUnknownTip": "没取到该包的元数据(网络或 registry 问题),未做判断 —— 不是「不兼容」。",
"pa.pl.cmpHidden": " | 已按 dsh {cur} 过滤:**本次候选中**隐藏 {n} 个仅兼容更旧版本的插件",
"pa.pl.cmpAll": " | 未按 dsh 版本过滤",
"pa.pl.cmpWarming": " | 部分条目「未验证」:兼容判定仍在后台预热,稍后重新搜索即可",
"pa.pl.empty": "暂无已投放插件 —— 从「官方推荐插件」导入,或在上方上传 .tgz",
"pa.pl.repullOk": "✓ 目录已重新拉取(缓存已更新)",
"pa.pl.repullFail": "重新拉取失败(网络或官方站不可达)",
@@ -385,62 +404,84 @@ window.__ModuleLoader__.load({
// 三条口径(用户 2026-09-13 定,勿再当选择题):① 条目各自开关、可同时启用
// ② admin 配的共享模型**也列在这里**、用户可开关 ③ 用哪个模型在 dsh 对话框选。
"ms.title": "模型设置",
"ms.sub": "管理你自用的模型厂家:每条可单独开关、可同时启用;具体用哪个模型,在对话界面的模型选择器里选。保存后需重启实例生效。",
"ms.sub": "填入各提供方的 API 密钥即可使用其模型:每条可单独开关、可同时启用;具体用哪个模型,在对话界面的模型选择器里选。保存后需重启实例生效。",
"ms.loadFailed": "加载失败,请刷新重试",
"ms.saveFailed": "保存失败,请重试",
"ms.saved": "已保存,重启实例后生效",
"ms.needNameKey": "请填写名称与 API Key",
"ms.needModels": "自定义厂家至少要填一个模型",
"ms.saved": "已保存 —— 重启实例后生效",
"ms.needName": "请填写显示名称",
"ms.needKey": "请填写 API 密钥",
"ms.needUrl": "自定义提供方需要填写 API 地址。",
"ms.needModels": "自定义提供方至少需要一个模型。",
"ms.shared": "平台共享模型",
"ms.sharedOwner": "由 {who} 配置",
"ms.sharedOwnerMe": "由你配置",
"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.pickProvider": "厂家:",
"ms.optBuiltin": "内置 DeepSeek(平台共享)",
"ms.optCustom": "自定义厂家(OpenAI 兼容网关)…",
"ms.grpCn": "中国大陆",
"ms.grpIntl": "国际",
"ms.catalogHint": "端点与 {n} 个模型由官方目录提供,你只需填 API 密钥。",
"ms.showModels": "看看自带哪些模型",
"ms.hideModels": "收起模型清单",
"ms.builtinPickHint": "内置 DeepSeek:平台内置通道;密钥留空则回落到平台共享密钥。",
"ms.needUrl": "请填 Endpoint",
"ms.save": "保存",
"ms.list": "我已添加的厂家",
"ms.col.provider": "厂家",
"ms.col.desc": "说明",
"ms.col.state": "状态",
"ms.col.act": "操作",
"ms.empty": "还没有厂家 —— 在下面选一个添加即可",
"ms.list": "我已添加的提供方",
"ms.empty": "还没有添加任何提供方 —— 用下面的按钮添加。",
"ms.tagBuiltin": "内置",
"ms.tagCatalog": "目录",
"ms.tagCustom": "自定义",
"ms.kindBuiltin": "内置 DeepSeek",
"ms.builtinHint": "平台内置通道,无需 endpoint",
"ms.catalogRow": "官方目录厂家(端点 / 协议 / 模型由目录提供)",
"ms.on": "已启用",
"ms.off": "已停用",
"ms.builtinHint": "平台内置通道,无需端点",
"ms.catalogRow": "官方目录提供方(端点 / 协议 / 模型由目录提供)",
"ms.stateOn": "已启用",
"ms.stateOff": "已停用",
"ms.enable": "启用",
"ms.disable": "停用",
"ms.del": "删除",
"ms.delTitle": "删除 {who}?",
"ms.delDesc": "删除 {who} 会移除该条目的配置与已存储的 API 密钥,且无法撤销。",
"ms.delConfirm": "删除 {who}",
"ms.addProvider": "添加提供方",
"ms.addCustom": "添加自定义提供方",
"ms.provider": "提供方",
"ms.keyInput": "API 密钥",
"ms.keyPlaceholder": "输入 API 密钥",
"ms.keyPlaceholderBuiltin": "输入你的 DeepSeek API 密钥",
"ms.keyHintCatalog": "端点与 {n} 个模型由官方目录提供,你只需填 API 密钥。",
"ms.customized": "自定义设置",
"ms.displayName": "显示名称",
"ms.displayNamePlaceholder": "留空时使用提供方名称",
"ms.baseUrl": "API 地址",
"ms.baseUrlPlaceholder": "https://gateway.example/v1",
"ms.customApi": "API 协议",
"ms.customApiUnset": "未选择",
"ms.customTitle": "自定义提供方",
"ms.customRoute": "Provider ID",
"ms.customRouteHint": "以小写字母开头的标识,在请求中唯一标识该提供方,并用于派生凭据名。",
"ms.customRouteInvalid": "需以小写字母开头,之后可用小写字母、数字和短横线。",
"ms.catalogModels": "模型目录",
"ms.catalogModelsMeta": "由官方目录提供,共 {n} 个模型:",
"ms.addModel": "添加模型",
"ms.removeModel": "删除模型",
"ms.modelIdPlaceholder": "模型 ID",
"ms.modelEmpty": "尚未添加模型 —— 至少需要一个,否则模型选择器里不会出现该提供方。",
"ms.advancedHint": "其余字段在 settings.yaml 中,请直接编辑对应段。",
"ms.cancel": "取消",
"ms.create": "创建提供方",
"ms.creating": "创建中…",
"ms.save": "保存",
"ms.saving": "保存中…",
"ms.showModels": "查看模型清单",
"ms.hideModels": "收起模型清单",
"ms.optBuiltin": "内置 DeepSeek(平台共享)",
"ms.builtinPickHint": "内置 DeepSeek:平台内置通道,固定官方端点,无需填 API 地址。",
"ms.grpCn": "中国大陆",
"ms.grpIntl": "国际",
"ms.e.name": "名称不合法",
"ms.e.key": "API Key 不合法(只允许字母数字与 - _ .)",
"ms.e.url": "Endpoint 必须以 http(s):// 开头",
"ms.e.route": "厂家标识不合法(小写字母/数字/短横线,字母开头)",
"ms.e.key": "API 密钥不合法(只允许字母数字与 - _ .)",
"ms.e.url": "API 地址必须以 http(s):// 开头",
"ms.e.route": "Provider ID 不合法(小写字母/数字/短横线,字母开头)",
"ms.e.api": "不支持该协议",
"ms.e.models": "请至少填一个模型 id",
"ms.e.taken": "该厂家标识已被占用,换一个",
"ms.e.provider": "官方目录里没有这个厂家",
// 目录可读性(2026-09-14):读不到时必须**如实说明**,别让用户以为是"功能没做"
"ms.catUnreadable": "⚠️ 平台未能读取官方厂家目录({dir})—— 当前只能使用内置 DeepSeek 或自定义厂家。请联系管理员检查安装路径配置。",
"ms.catEmpty": "官方厂家目录为空(该部署可能未随附 pi-ai 厂家数据)—— 当前只能使用内置 DeepSeek 或自定义厂家。"
"ms.e.taken": "该提供方标识已被占用,换一个",
"ms.e.provider": "官方目录里没有这个提供方",
"ms.catUnreadable": "⚠️ 平台未能读取官方提供方目录({dir})—— 当前只能使用内置 DeepSeek 或自定义提供方。请联系管理员检查安装路径配置。",
"ms.catEmpty": "官方提供方目录为空(该部署可能未随附 pi-ai 提供方数据)—— 当前只能使用内置 DeepSeek 或自定义提供方。"
};
/** English dictionary, key-set complete against zh. */
var en = {
@@ -618,7 +659,7 @@ window.__ModuleLoader__.load({
"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.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\". This list is **filtered by the current dsh version**: only \"Match\" and \"Needs newer\" entries are shown (the latter in orange — unusable until dsh is upgraded); entries that only declare older versions are hidden (tick \"Include older-only\" to see them).",
"pa.pl.importNpm": "npm prebuilt",
"pa.pl.importTarball": "release asset",
"pa.pl.importSource": "needs source build",
@@ -632,6 +673,21 @@ window.__ModuleLoader__.load({
"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.compatCol": "dsh version",
"pa.pl.showAllCompat": "Include older-only",
"pa.pl.cmpMatch": "Match",
"pa.pl.cmpNewer": "Needs newer",
"pa.pl.cmpNone": "Undeclared",
"pa.pl.cmpOlder": "Older only",
"pa.pl.cmpUnknown": "Unverified",
"pa.pl.cmpMatchTip": "The declared range covers the current dsh {cur}.",
"pa.pl.cmpNewerTip": "This plugin requires dsh ≥ {v} (current {cur}) — it cannot be installed before upgrading.",
"pa.pl.cmpNoneTip": "This plugin declares no @deepseek-ai/* dependency, so nothing can be judged from it (≠ incompatible).",
"pa.pl.cmpOlderTip": "This plugin only declares older dsh versions; it may still run, but it is not declared-compatible with the current one.",
"pa.pl.cmpUnknownTip": "Package metadata could not be fetched (network or registry issue) — no judgement was made, which is not the same as incompatible.",
"pa.pl.cmpHidden": " | filtered by dsh {cur}: {n} older-only plugin(s) hidden **in this candidate set**",
"pa.pl.cmpAll": " | not filtered by dsh version",
"pa.pl.cmpWarming": " | some entries are “Unverified”: the compatibility check is still warming up in the background — search again shortly",
"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)",
@@ -675,57 +731,81 @@ window.__ModuleLoader__.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.sub": "Enter each provider's API key to use its models: every 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.needName": "Please fill in a display name",
"ms.needKey": "Please fill in an API key",
"ms.needUrl": "A custom provider needs a base URL.",
"ms.needModels": "A custom provider needs at least one model.",
"ms.shared": "Platform-shared model",
"ms.sharedOwner": "Configured by {who}",
"ms.sharedOwnerMe": "Configured by you",
"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.pickProvider": "Provider:",
"ms.optBuiltin": "Built-in DeepSeek (platform-shared)",
"ms.optCustom": "Custom provider (OpenAI-compatible gateway)…",
"ms.grpCn": "Mainland China",
"ms.grpIntl": "International",
"ms.catalogHint": "Endpoint and {n} models come from the official catalog — you only need the API key.",
"ms.showModels": "Show bundled models",
"ms.hideModels": "Hide models",
"ms.builtinPickHint": "Built-in DeepSeek: the platform channel; leave the key empty to fall back to the platform-shared one.",
"ms.needUrl": "Endpoint is required",
"ms.save": "Save",
"ms.list": "My providers",
"ms.col.provider": "Provider",
"ms.col.desc": "Details",
"ms.col.state": "State",
"ms.col.act": "Actions",
"ms.empty": "No providers yet — pick one below to add",
"ms.empty": "No providers yet — use the buttons below to add one.",
"ms.tagBuiltin": "Built-in",
"ms.tagCatalog": "Catalog",
"ms.tagCustom": "Custom",
"ms.kindBuiltin": "Built-in DeepSeek",
"ms.builtinHint": "Platform channel; no endpoint needed",
"ms.catalogRow": "Catalog provider (endpoint/protocol/models from the catalog)",
"ms.on": "Enabled",
"ms.off": "Disabled",
"ms.stateOn": "Enabled",
"ms.stateOff": "Disabled",
"ms.enable": "Enable",
"ms.disable": "Disable",
"ms.del": "Delete",
"ms.delTitle": "Delete {who}?",
"ms.delDesc": "Deleting {who} removes its configuration and stored API key. This cannot be undone.",
"ms.delConfirm": "Delete {who}",
"ms.addProvider": "Add provider",
"ms.addCustom": "Add a custom provider",
"ms.provider": "Provider",
"ms.keyInput": "API key",
"ms.keyPlaceholder": "Enter your API key",
"ms.keyPlaceholderBuiltin": "Enter your DeepSeek API key",
"ms.keyHintCatalog": "Endpoint and {n} models come from the official catalog — you only need the API key.",
"ms.customized": "Customized settings",
"ms.displayName": "Display name",
"ms.displayNamePlaceholder": "Uses the provider name when empty",
"ms.baseUrl": "Base URL",
"ms.baseUrlPlaceholder": "https://gateway.example/v1",
"ms.customApi": "API protocol",
"ms.customApiUnset": "Not selected",
"ms.customTitle": "Custom provider",
"ms.customRoute": "Provider ID",
"ms.customRouteHint": "Lowercase identifier, starting with a letter, that uniquely names this provider in requests and as its credential name.",
"ms.customRouteInvalid": "Start with a lowercase letter; then lowercase letters, digits, and dashes.",
"ms.catalogModels": "Model catalog",
"ms.catalogModelsMeta": "Supplied by the official catalog — {n} models in total:",
"ms.addModel": "Add model",
"ms.removeModel": "Delete model",
"ms.modelIdPlaceholder": "Model ID",
"ms.modelEmpty": "No models yet — at least one is required, otherwise this provider will not show up in the model selector.",
"ms.advancedHint": "Other fields live in settings.yaml; edit that section directly.",
"ms.cancel": "Cancel",
"ms.create": "Create provider",
"ms.creating": "Creating…",
"ms.save": "Save",
"ms.saving": "Saving…",
"ms.showModels": "Show models",
"ms.hideModels": "Hide models",
"ms.optBuiltin": "Built-in DeepSeek (platform-shared)",
"ms.builtinPickHint": "Built-in DeepSeek: the platform channel with a fixed official endpoint — no base URL needed.",
"ms.grpCn": "Mainland China",
"ms.grpIntl": "International",
"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.url": "The base URL 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",
"ms.e.taken": "That provider ID is taken — pick another",
"ms.e.provider": "That provider is not in the official catalog",
"ms.catUnreadable": "⚠️ The platform could not read the official provider catalog ({dir}) — only the built-in DeepSeek and custom providers are available. Ask your administrator to check the install-path configuration.",
"ms.catEmpty": "The official provider catalog is empty (this deployment may not ship the pi-ai provider data) — only the built-in DeepSeek and custom providers are available."
@@ -891,7 +971,66 @@ window.__ModuleLoader__.load({
"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}}"
// ── 档案 91(2026-09-14):「模型设置」页面视觉层 —— **照抄官方 `dsh-client-ui-settings-models`
// 的 CSS 模块**(官方类名带 CSS Module 哈希前缀 `zGbnIq_`,此处去哈希、前缀改 `ms-`)。
// 圆角 / 字号 / 行高 / 内边距 / gap **逐值照抄**;颜色走同一批 dsh token `--dsw-*`
// (官方原文用的就是这批 token,故连 token 名都一致),只补一个硬编码兜底色。
//
// 为什么把表格换成卡片行:表格 4 列是**自动列宽**,「内置 DeepSeek」和「停用/删除」
// 两个按钮会被挤到换行(2026-09-14 用户截图);官方这套 `rowHead + margin-left:auto`
// +`white-space:nowrap` 的卡片行**结构上不可能换行**,顺带把整页拉齐到官方观感。
".ms-rows{display:flex;flex-direction:column;gap:8px;margin:12px 0 16px;padding:0;list-style:none}",
".ms-rowCard{border:.5px solid var(--dsw-alias-border-l4,#e3e6ea);border-radius:16px;display:flex;flex-direction:column;gap:12px;padding:12px 14px}",
".ms-rowHead{display:flex;align-items:center;gap:10px}",
".ms-rowIdentity{display:inline-flex;align-items:center;gap:6px;min-width:0}",
".ms-rowName{color:var(--dsw-alias-label-primary,#24292f);font-size:14px;font-weight:500;line-height:22px;white-space:nowrap}",
".ms-rowTag{border:.5px solid var(--dsw-alias-border-l3,#d0d5dd);color:var(--dsw-alias-label-secondary,#6b7280);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px;white-space:nowrap}",
".ms-dot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}",
".ms-dot.on{background:var(--dsw-alias-state-success-primary,#1a7f37)}",
".ms-dot.idle{background:var(--dsw-alias-label-dimmed,#c4c9d0)}",
".ms-rowActions{display:inline-flex;align-items:center;gap:4px;margin-left:auto;white-space:nowrap}",
".ms-rowMeta{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#6b7280);word-break:break-all}",
".ms-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:36px;padding:0 14px;border:none;border-radius:18px;background:transparent;color:var(--dsw-alias-label-primary,#24292f);font-family:inherit;font-size:14px;line-height:22px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}",
".ms-btn.outline{border:.5px solid var(--dsw-alias-border-l3,#d0d5dd)}",
".ms-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(9,24,58,.05))}",
".ms-btn.primary{background:var(--dsw-alias-button-primary-fill,#2f6fed);color:#fff}",
".ms-btn.danger{color:var(--dsw-alias-state-error-primary,#d93026)}",
".ms-btn:disabled{opacity:.4;cursor:default}",
".ms-rowActions .ms-btn{height:28px;border-radius:14px;padding:0 10px;font-size:12px;line-height:18px}",
".ms-addBlock{display:flex;flex-direction:column;gap:12px}",
".ms-addActions{display:flex;flex-wrap:wrap;gap:10px}",
".ms-addBtn{box-sizing:border-box;flex:1 1 0;min-width:180px;height:44px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px dashed var(--dsw-alias-border-l3,#d0d5dd);border-radius:16px;background:transparent;color:var(--dsw-alias-label-primary,#24292f);font-family:inherit;font-size:14px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}",
".ms-addBtn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed);background:var(--dsw-alias-interactive-bg-hover,rgba(47,111,237,.05))}",
".ms-addBtn:disabled{opacity:.4;cursor:default}",
".ms-card{background:var(--dsw-alias-bg-layer-2,#f6f8fa);border-radius:12px;display:flex;flex-direction:column;gap:14px;padding:14px 16px}",
".ms-field{display:flex;flex-direction:column;gap:6px}",
".ms-fieldLabel{display:inline-flex;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;font-weight:500;line-height:18px}",
".ms-input{box-sizing:border-box;width:100%;height:32px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l4,#e3e6ea);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#24292f);font-family:inherit;font-size:14px;line-height:22px}",
".ms-input:focus{border-color:var(--dsw-alias-brand-primary,#2f6fed);outline:none}",
".ms-input::placeholder{color:var(--dsw-alias-label-dimmed,#9aa1ab)}",
"select.ms-input{cursor:pointer;max-width:320px}",
".ms-editorActions{display:flex;justify-content:flex-end;gap:8px}",
".ms-hint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".ms-err{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary,#d93026)}",
".ms-ok{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary,#1a7f37)}",
".ms-details{border-top:.5px solid var(--dsw-alias-border-l2,#e3e6ea);padding-top:10px}",
".ms-summary{display:flex;align-items:center;gap:6px;width:fit-content;margin-left:-4px;padding:2px 4px;border-radius:6px;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;font-weight:500;line-height:18px;cursor:pointer;list-style:none}",
".ms-summary::-webkit-details-marker{display:none}",
".ms-summary::before{content:'';width:5px;height:5px;border-bottom:1.5px solid;border-right:1.5px solid;transform:rotate(-45deg) translate(-1px,-1px);transition:transform .12s}",
".ms-details[open]>.ms-summary::before{transform:rotate(45deg) translate(-1px,-1px)}",
".ms-summary:hover{color:var(--dsw-alias-label-primary,#24292f)}",
".ms-detailsBody{display:flex;flex-direction:column;gap:12px;padding-top:12px}",
".ms-modelList{display:flex;flex-direction:column;gap:8px}",
".ms-modelEntry{border:.5px solid var(--dsw-alias-border-l4,#e3e6ea);border-radius:10px;padding:6px}",
".ms-modelRow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px}",
".ms-iconBtn{box-sizing:border-box;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);font-family:inherit;font-size:14px;line-height:1;cursor:pointer}",
".ms-iconBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(9,24,58,.05));color:var(--dsw-alias-label-primary,#24292f)}",
".ms-iconBtn.danger:hover:not(:disabled){background:rgba(217,48,38,.08);color:var(--dsw-alias-state-error-primary,#d93026)}",
".ms-chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}",
".ms-chip{border:.5px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:6px;padding:2px 7px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280)}",
"@media (prefers-reduced-motion: reduce){.pa-card,.pa-modal{animation:none;transition:none}.pa-card:hover{transform:none}.ms-summary::before{transition:none}}"
].join("");
document.head.appendChild(el);
return function () { el.remove(); };
@@ -1614,15 +1753,31 @@ window.__ModuleLoader__.load({
var msgState = useState("");
var msg = msgState[0];
var setMsg = msgState[1];
// 「新增」流程的状态 —— **官方同构:虚线按钮 → 卡片**,同一时刻只开一张。
// `mode`:"" = 未开始(露两个虚线按钮)/"add" = 添加提供方/"custom" = 自定义提供方。
// 官方原文:「每一类卡片各自持有自己的展开状态,因此关掉其中一张绝不会丢弃另一张里的草稿」
// —— 我们只有一张卡,关掉即整份草稿作废(`closeAdd()`)。
var modeState = useState("");
var mode = modeState[0];
var setMode = modeState[1];
var fPick = useState("");
var fName = useState("");
var fKey = useState("");
var fName = useState("");
var fRoute = useState("");
var fUrl = useState("");
var fApi = useState("");
var fModels = useState("");
// 模型目录 = **行数组**(官方 `ModelListEditor` 的形态)。平台后端 `models` 就是
// `string[]`(`keyAddSchema`),所以只收模型 ID —— 不假装支持官方的
// 「显示名称 / 上下文窗口 / 最大输出」三列(那些字段无处落库)。
var fModels = useState([""]);
var fShow = useState(false);
// 后端给的**厂家目录可读性**诊断:读不到时要说清"为什么只剩两项",
// 删除确认(官方 `deleteDialog`:**点名该提供方**再删)。
// 为什么必须有:删条目 = 连已存的 API 密钥一起删;而「删除」就挨着「停用」
// (2026-09-14 用户截图里这两个按钮还被挤到换行)⇒ 误点的代价不可逆。
var delState = useState(null);
var delTarget = delState[0];
var setDelTarget = delState[1];
// 后端给的**提供方目录可读性**诊断:读不到时要说清"为什么只剩两项",
// 而不是静默给个空列表(2026-09-14:这个静默曾让一个 P1 布局缺陷长期不可见)。
var catState = useState(null);
var catalog = catState[0];
@@ -1666,14 +1821,20 @@ window.__ModuleLoader__.load({
.catch(function () { setBusy(false); setMsg(t("ms.saveFailed")); });
}
/** 收起新增卡片并清空草稿(官方:取消 = 丢弃这张卡的草稿)。 */
function closeAdd() {
setMode("");
fPick[1](""); fKey[1](""); fName[1](""); fRoute[1](""); fUrl[1](""); fApi[1](""); fModels[1]([""]);
setShow(false);
}
var pick = fPick[0];
var isCatalog = pick !== "" && pick !== "custom";
var picked = null;
for (var pi = 0; pi < providers.length; pi++) {
if (providers[pi].id === pick) picked = providers[pi];
}
/** 行标题:内置 → 固定文案;目录厂家 → 官方目录里的中文名;自定义 → route。 */
/** 行标题:内置 → 固定文案;目录提供方 → 官方目录里的中文名;自定义 → route。 */
function labelOf(k) {
if (!k.route) return t("ms.kindBuiltin");
for (var i = 0; i < providers.length; i++) {
@@ -1681,36 +1842,81 @@ window.__ModuleLoader__.load({
}
return k.route;
}
/** 行副标题:内置 → 说明;自定义 → endpoint · 协议;目录厂家 → 「由目录提供」。 */
/** 行标签(官方 `rowTag` 的位置):内置 / 目录 / 自定义。 */
function tagOf(k) {
if (!k.route) return t("ms.tagBuiltin");
for (var i = 0; i < providers.length; i++) {
if (providers[i].id === k.route) return t("ms.tagCatalog");
}
return t("ms.tagCustom");
}
/** 行副标题:内置 → 说明;自定义 → 端点 · 协议;目录提供方 → 「由目录提供」。 */
function descOf(k) {
if (!k.route) return t("ms.builtinHint");
if (k.baseUrl) return k.baseUrl + " · " + (k.api || "openai-completions");
return t("ms.catalogRow");
}
function submit() {
var name = fName[0].trim();
var key = fKey[0].trim();
if (isCatalog && name === "" && picked !== null) name = picked.label;
if (name === "" || key === "") { setMsg(t("ms.needNameKey")); return; }
var body = { name: name, apiKey: key };
if (isCatalog) {
// 目录厂家:只给 apiKey —— 端点 / 协议 / 模型清单全部由官方目录兜底。
body.provider = pick;
} else if (pick === "custom") {
if (fUrl[0].trim() === "") { setMsg(t("ms.needUrl")); return; }
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 = fUrl[0].trim();
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), function () {
fName[1](""); fKey[1](""); fUrl[1](""); fRoute[1](""); fModels[1](""); setShow(false);
/** 模型目录的**行式编辑器**(官方 `ModelListEditor` 的形态:一行一个 + 行尾删除)。 */
function modelRows() {
return fModels[0].map(function (m, i) {
return jsxRuntime.jsx("div", { className: "ms-modelEntry", children: jsxRuntime.jsxs("div", { className: "ms-modelRow", children: [
jsxRuntime.jsx("input", {
className: "ms-input",
placeholder: t("ms.modelIdPlaceholder"),
value: m,
onChange: function (e) {
var next = fModels[0].slice();
next[i] = e.target.value;
fModels[1](next);
}
}),
jsxRuntime.jsx("button", {
type: "button",
className: "ms-iconBtn danger",
title: t("ms.removeModel"),
"aria-label": t("ms.removeModel"),
onClick: function () {
var next = fModels[0].slice();
next.splice(i, 1);
fModels[1](next.length > 0 ? next : [""]);
},
children: "✕"
})
] }) }, "m" + i);
});
}
/**
* 提交「添加提供方」(目录提供方 / 内置 DeepSeek)。
* ⚠️ 密钥**不可留空**:`keyAddSchema` 里 `apiKey` 是 `required + minLength:1`,
* 留空必得 400 `invalid_api_key`(旧文案「留空则回落平台共享密钥」是错的,已改)。
* 显示名留空 ⇒ 取提供方名(官方同做法:页面从不先问名字)。
*/
function submitAdd() {
var key = fKey[0].trim();
if (key === "") { setMsg(t("ms.needKey")); return; }
var name = fName[0].trim() || (picked !== null ? picked.label : "");
if (name === "") { setMsg(t("ms.needName")); return; }
var body = { name: name, apiKey: key };
if (pick !== "") body.provider = pick; // 目录提供方:端点 / 协议 / 模型全由官方目录兜底
act(papostJson("/api/me/keys", body), closeAdd);
}
/** 提交「自定义提供方」:端点 / 协议 / ≥1 模型 / 可选 Provider ID —— 官方同字段集。 */
function submitCustom() {
var key = fKey[0].trim();
if (fUrl[0].trim() === "") { setMsg(t("ms.needUrl")); return; }
var ids = fModels[0].map(function (s) { return s.trim(); }).filter(function (s) { return s !== ""; });
if (ids.length === 0) { setMsg(t("ms.needModels")); return; }
var name = fName[0].trim() || fRoute[0].trim();
if (name === "") { setMsg(t("ms.needName")); return; }
var body = { name: name, apiKey: key, baseUrl: fUrl[0].trim(), models: ids };
if (fRoute[0].trim() !== "") body.route = fRoute[0].trim();
if (fApi[0]) body.api = fApi[0];
act(papostJson("/api/me/keys", body), closeAdd);
}
if (loading) {
return jsxRuntime.jsx("div", { className: "pa-page", children: jsxRuntime.jsx("div", { className: "pa-empty", children: t("loading") }) });
}
@@ -1727,18 +1933,241 @@ window.__ModuleLoader__.load({
});
}
// ── 已添加的提供方:**卡片行**(官方 `rowCard` 形态;原表格已删)──
// 官方 `rowHead` = 「身份(名称+标签+状态点)」左对齐 + 「操作」`margin-left:auto`
// 右对齐,两侧都 `white-space:nowrap` ⇒ **结构上不会换行**(这是换掉表格的全部理由)。
var rows = keys.map(function (k) {
return jsxRuntime.jsxs("tr", { children: [
jsxRuntime.jsx("td", { children: labelOf(k) }),
jsxRuntime.jsx("td", { className: "pa-dim", children: descOf(k) }),
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") })
] })
return jsxRuntime.jsxs("li", { className: "ms-rowCard", children: [
jsxRuntime.jsxs("div", { className: "ms-rowHead", children: [
jsxRuntime.jsxs("span", { className: "ms-rowIdentity", children: [
jsxRuntime.jsx("span", { className: "ms-rowName", children: labelOf(k) }),
jsxRuntime.jsx("span", { className: "ms-rowTag", children: tagOf(k) }),
// 状态点:官方 `credentialDot` 的位置与尺寸(8px 圆点),语义换成「启用 / 停用」。
jsxRuntime.jsx("span", {
className: "ms-dot " + (k.enabled ? "on" : "idle"),
role: "img",
title: k.enabled ? t("ms.stateOn") : t("ms.stateOff"),
"aria-label": k.enabled ? t("ms.stateOn") : t("ms.stateOff")
})
] }),
jsxRuntime.jsxs("span", { className: "ms-rowActions", children: [
jsxRuntime.jsx("button", {
type: "button",
className: "ms-btn outline",
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", {
type: "button",
className: "ms-btn danger",
disabled: busy,
onClick: function () { setDelTarget(k); },
children: t("ms.del")
})
] })
] }),
// 状态文字放**独占一行的副标题**(整行可用宽度 ⇒ 不可能换行),信息量也不丢。
jsxRuntime.jsx("p", { className: "ms-rowMeta", children: descOf(k) + " · " + (k.enabled ? t("ms.stateOn") : t("ms.stateOff")) })
] }, k.id);
});
/** 编辑卡片右下角的动作行(官方的「取消 / 保存」两键,右对齐)。 */
function editorActions(submit, label, busyLabel, disabled) {
return jsxRuntime.jsxs("div", { className: "ms-editorActions", children: [
jsxRuntime.jsx("button", { type: "button", className: "ms-btn outline", disabled: busy, onClick: closeAdd, children: t("ms.cancel") }),
jsxRuntime.jsx("button", { type: "button", className: "ms-btn primary", disabled: disabled, onClick: submit, children: busy ? t(busyLabel) : t(label) })
] });
}
var addBlock = jsxRuntime.jsxs("div", { className: "ms-addBlock", children: [
// ── 卡片 A:添加提供方(官方 `addCard` + `ProviderEditor` 同构)──
// 官方原话:「编辑卡片上的主字段是单独一个 **API 密钥**输入框 —— 页面从不问
// 环境变量名」「收起的『自定义设置』才承载 baseURL / 协议 / 模型目录」。
mode === "add" ? jsxRuntime.jsxs("div", { className: "ms-card", children: [
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.provider") }),
jsxRuntime.jsx("select", {
className: "ms-input",
value: pick,
"aria-label": t("ms.provider"),
onChange: function (e) { fPick[1](e.target.value); },
children: [
jsxRuntime.jsx("option", { value: "", children: t("ms.optBuiltin") }, "_b"),
cnList.length > 0 ? jsxRuntime.jsx("optgroup", { label: t("ms.grpCn"), children: provOpts(cnList) }, "_cn") : null,
intlList.length > 0 ? jsxRuntime.jsx("optgroup", { label: t("ms.grpIntl"), children: provOpts(intlList) }, "_intl") : null
]
})
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsxs("span", { className: "ms-fieldLabel", children: [
t("ms.keyInput"),
picked !== null ? jsxRuntime.jsx("span", { className: "ms-hint", children: t("ms.keyHintCatalog").replace("{n}", String(picked.modelCount)) }) : null
] }),
jsxRuntime.jsx("input", {
className: "ms-input",
type: "password",
autoComplete: "off",
placeholder: pick === "" ? t("ms.keyPlaceholderBuiltin") : t("ms.keyPlaceholder"),
value: fKey[0],
onChange: function (e) { fKey[1](e.target.value); }
})
] }),
jsxRuntime.jsxs("details", { className: "ms-details", children: [
jsxRuntime.jsx("summary", { className: "ms-summary", children: t("ms.customized") }),
jsxRuntime.jsxs("div", { className: "ms-detailsBody", children: [
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.displayName") }),
jsxRuntime.jsx("input", {
className: "ms-input",
placeholder: picked !== null ? picked.label : t("ms.displayNamePlaceholder"),
value: fName[0],
onChange: function (e) { fName[1](e.target.value); }
})
] }),
pick === "" ? jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.builtinPickHint") }) : null,
picked !== null ? jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsxs("span", { className: "ms-fieldLabel", children: [
t("ms.catalogModels"),
jsxRuntime.jsx("button", { type: "button", className: "ms-btn outline", style: { height: 28, borderRadius: 14, padding: "0 10px", fontSize: 12 }, onClick: function () { setShow(!fShow[0]); }, children: fShow[0] ? t("ms.hideModels") : t("ms.showModels") })
] }),
fShow[0]
? jsxRuntime.jsx("ul", { className: "ms-chips", children: (picked.models || []).map(function (m) { return jsxRuntime.jsx("li", { className: "ms-chip", children: m }, m); }) })
: jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.catalogModelsMeta").replace("{n}", String(picked.modelCount)) })
] }) : null,
jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.advancedHint") })
] })
] }),
msg ? jsxRuntime.jsx("p", { className: "ms-err", children: msg }) : null,
editorActions(submitAdd, "ms.save", "ms.saving", busy || fKey[0].trim() === "")
] }) : mode === "custom" ? (
// ── 卡片 B:自定义提供方(官方 `CustomProviderCard` 同字段集)──
// 官方:「创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别
// 的模型,因为没有东西能为它们兜底」;Provider ID 必填 ⇒ 我们把它排在首位,
// 留空时后端会用显示名 slug 化兜底(`slugify`),不额外造字段。
jsxRuntime.jsxs("div", { className: "ms-card", children: [
jsxRuntime.jsx("div", { className: "ms-field", children: jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.customTitle") }) }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.customRoute") }),
jsxRuntime.jsx("input", {
className: "ms-input",
placeholder: "acme-gateway",
value: fRoute[0],
onChange: function (e) { fRoute[1](e.target.value); }
}),
// 与小写字母/数字/短横线的约定不符时**就地报错**(官方同:语法错误在字段处阻止创建)。
fRoute[0] !== "" && !/^[a-z][a-z0-9-]*$/.test(fRoute[0])
? jsxRuntime.jsx("p", { className: "ms-err", children: t("ms.customRouteInvalid") })
: jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.customRouteHint") })
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.displayName") }),
jsxRuntime.jsx("input", {
className: "ms-input",
placeholder: fRoute[0] === "" ? t("ms.displayNamePlaceholder") : fRoute[0],
value: fName[0],
onChange: function (e) { fName[1](e.target.value); }
})
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.baseUrl") }),
jsxRuntime.jsx("input", {
className: "ms-input",
placeholder: t("ms.baseUrlPlaceholder"),
value: fUrl[0],
onChange: function (e) { fUrl[1](e.target.value); }
})
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.customApi") }),
jsxRuntime.jsx("select", {
className: "ms-input",
value: fApi[0],
onChange: function (e) { fApi[1](e.target.value); },
children: [
jsxRuntime.jsx("option", { value: "", children: t("ms.customApiUnset") }, "_u"),
protocols.map(function (p) { return jsxRuntime.jsx("option", { value: p, children: p }, p); })
]
})
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.keyInput") }),
jsxRuntime.jsx("input", {
className: "ms-input",
type: "password",
autoComplete: "off",
placeholder: t("ms.keyPlaceholder"),
value: fKey[0],
onChange: function (e) { fKey[1](e.target.value); }
})
] }),
jsxRuntime.jsxs("div", { className: "ms-field", children: [
jsxRuntime.jsx("span", { className: "ms-fieldLabel", children: t("ms.catalogModels") }),
fModels[0].some(function (s) { return s.trim() !== ""; }) ? null : jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.modelEmpty") }),
jsxRuntime.jsx("div", { className: "ms-modelList", children: modelRows() }),
jsxRuntime.jsx("button", {
type: "button",
className: "ms-btn outline",
style: { alignSelf: "flex-start", height: 28, borderRadius: 14, padding: "0 10px", fontSize: 12 },
onClick: function () { fModels[1](fModels[0].concat([""])); },
children: "+ " + t("ms.addModel")
})
] }),
msg ? jsxRuntime.jsx("p", { className: "ms-err", children: msg }) : null,
jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.advancedHint") }),
editorActions(submitCustom, "ms.create", "ms.creating", busy || fUrl[0].trim() === "" || fModels[0].every(function (s) { return s.trim() === ""; }))
] }) ) : (
// ── 默认态:**两个虚线按钮**(官方 `addActions`;按钮本身不展开任何字段)──
jsxRuntime.jsxs("div", { className: "ms-addActions", children: [
jsxRuntime.jsxs("button", {
type: "button",
className: "ms-addBtn",
onClick: function () { setMsg(""); setMode("add"); },
children: ["+ " + t("ms.addProvider")]
}),
jsxRuntime.jsxs("button", {
type: "button",
className: "ms-addBtn",
onClick: function () { setMsg(""); setMode("custom"); },
children: ["+ " + t("ms.addCustom")]
})
] })
),
// 目录读不到时如实说明(不得静默只剩两个按钮)
providers.length === 0
? jsxRuntime.jsx("p", { className: "ms-hint", children: catalog && catalog.readable === false ? t("ms.catUnreadable").replace("{dir}", catalog.dir || "?") : t("ms.catEmpty") })
: null
] });
/** 删除确认弹窗(官方 `deleteDialog` 的等价物):遮罩点击 / 取消 = 关闭。 */
var delModal = delTarget === null ? null : jsxRuntime.jsxs("div", {
className: "pa-overlay",
onClick: function () { setDelTarget(null); },
children: jsxRuntime.jsxs("div", {
className: "pa-modal",
style: { width: "min(480px,100%)", padding: "18px 22px", display: "flex", flexDirection: "column", gap: 12 },
onClick: function (e) { e.stopPropagation(); },
children: [
jsxRuntime.jsx("div", { className: "pa-card-h", style: { margin: 0 }, children: t("ms.delTitle").replace("{who}", labelOf(delTarget)) }),
jsxRuntime.jsx("p", { className: "ms-hint", children: t("ms.delDesc").replace("{who}", labelOf(delTarget)) }),
msg ? jsxRuntime.jsx("p", { className: "ms-err", children: msg }) : null,
jsxRuntime.jsxs("div", { className: "ms-editorActions", children: [
jsxRuntime.jsx("button", { type: "button", className: "ms-btn outline", disabled: busy, onClick: function () { setDelTarget(null); }, children: t("ms.cancel") }),
jsxRuntime.jsx("button", {
type: "button",
className: "ms-btn danger",
disabled: busy,
onClick: function () { act(paReq("/api/me/keys/" + delTarget.id, { method: "DELETE" }), function () { setDelTarget(null); }); },
children: busy ? t("ms.saving") : t("ms.delConfirm").replace("{who}", labelOf(delTarget))
})
] })
]
})
});
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") }),
@@ -1747,74 +2176,33 @@ window.__ModuleLoader__.load({
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
// ⚠️ 看的人是 admin 本人时不能说「由 admin 配置」(2026-09-14 截图里就是这么写的)
// —— 后端给了 `ownerIsMe`,据此换成「由你配置」。
shared.owner
? jsxRuntime.jsx("span", { className: "sub", children: shared.ownerIsMe ? t("ms.sharedOwnerMe") : 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")) + (shared.count > 1 ? " ×" + shared.count : "") : 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") })
jsxRuntime.jsx("button", { className: "ms-btn " + (sharedOn ? "outline" : "primary"), disabled: busy || !shared.available, onClick: function () { act(papostJson("/api/me/models/shared", { enabled: !sharedOn })); }, children: sharedOn ? t("ms.sharedOn") : t("ms.sharedOff") })
] })
] }),
// ── 我已添加的厂家(官方页的「提供方行」)──
// ── 我已添加的提供方(官方页的「提供方行」)──
jsxRuntime.jsx("div", { className: "pa-sec", children: t("ms.list") }),
jsxRuntime.jsx("div", { className: "pa-wrap", style: { marginBottom: 16 }, children: jsxRuntime.jsxs("table", { className: "pa-tbl", children: [
jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [
jsxRuntime.jsx("th", { children: t("ms.col.provider") }),
jsxRuntime.jsx("th", { children: t("ms.col.desc") }),
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: 4, children: t("ms.empty") }) })
})
] }) }),
rows.length > 0
? jsxRuntime.jsx("ul", { className: "ms-rows", children: rows })
: jsxRuntime.jsx("p", { className: "ms-hint", style: { marginBottom: 16 }, children: t("ms.empty") }),
// ── 新增厂家:**先选厂家**,目录厂家只需填 API 密钥(官方「新增」卡片同构)──
jsxRuntime.jsxs("div", { className: "pa-box", children: [
jsxRuntime.jsx("div", { className: "pa-card-h", children: jsxRuntime.jsx("span", { children: t("ms.add") }) }),
providers.length === 0
? jsxRuntime.jsx("p", {
className: "pa-hint",
children: catalog && catalog.readable === false
? t("ms.catUnreadable").replace("{dir}", catalog.dir || "?")
: t("ms.catEmpty")
})
: null,
jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("span", { className: "pa-hint", style: { margin: 0 }, children: t("ms.pickProvider") }),
jsxRuntime.jsx("select", { className: "pa-input", value: pick, onChange: function (e) { fPick[1](e.target.value); setShow(false); }, children: [
jsxRuntime.jsx("option", { value: "", children: t("ms.optBuiltin") }, "_b"),
cnList.length > 0 ? jsxRuntime.jsx("optgroup", { label: t("ms.grpCn"), children: provOpts(cnList) }, "_cn") : null,
intlList.length > 0 ? jsxRuntime.jsx("optgroup", { label: t("ms.grpIntl"), children: provOpts(intlList) }, "_intl") : null,
jsxRuntime.jsx("option", { value: "custom", children: t("ms.optCustom") }, "_c")
] })
] }),
jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("input", { className: "pa-input", placeholder: isCatalog && picked !== null ? picked.label : 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); } })
] }),
isCatalog && picked !== null ? jsxRuntime.jsxs("div", { className: "pa-row", children: [
jsxRuntime.jsx("span", { className: "pa-hint", style: { margin: 0 }, children: t("ms.catalogHint").replace("{n}", String(picked.modelCount)) }),
jsxRuntime.jsx("button", { className: "pa-sm", onClick: function () { setShow(!fShow[0]); }, children: fShow[0] ? t("ms.hideModels") : t("ms.showModels") })
] }) : null,
isCatalog && picked !== null && fShow[0] ? jsxRuntime.jsx("p", { className: "pa-hint", children: (picked.models || []).join("、") }) : null,
pick === "custom" ? jsxRuntime.jsxs("div", { children: [
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.jsx("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); } })
] }) : null,
pick === "" ? jsxRuntime.jsx("p", { className: "pa-hint", children: t("ms.builtinPickHint") }) : null,
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
] })
] })
// ── 新增区(官方 `addBlock`)──
addBlock,
// 写操作的结果提示(保存成功 / 后端错误码译文)
msg ? jsxRuntime.jsx("p", { className: mode === "" ? "ms-ok" : "ms-err", style: { marginTop: 12 }, role: "status", "aria-live": "polite", children: msg }) : null,
// 删除确认弹窗(点名该提供方)
delModal
] });
}
@@ -2303,6 +2691,8 @@ window.__ModuleLoader__.load({
'<input id="wlQ" class="pa-input" placeholder="' + paesc(t("pa.pl.searchPh")) + '" />' +
'<select id="wlCat" class="pa-input" style="flex:none;min-width:130px"><option value="">' + paesc(t("pa.pl.allCat")) + "</option></select>" +
'<label class="pa-check"><input type="checkbox" id="wlOnlyImportable" checked />' + paesc(t("pa.pl.onlyImportable")) + "</label>" +
// 档案 92:默认按 dsh 版本过滤(隐藏「仅兼容更旧版本」),此勾为显式逃生口。
'<label class="pa-check"><input type="checkbox" id="wlAllCompat" />' + paesc(t("pa.pl.showAllCompat")) + "</label>" +
'<button class="pa-btn" id="wlSearchBtn">' + paesc(t("pa.op.search")) + "</button>" +
'<button class="pa-btn" id="wlRefreshBtn">' + paesc(t("pa.op.refreshDir")) + "</button>" +
"</div>" +
@@ -2316,6 +2706,8 @@ window.__ModuleLoader__.load({
"<th>" + paesc(t("pa.k.pluginDesc")) + "</th>" +
"<th>" + paesc(t("pa.k.cat")) + "</th>" +
"<th>" + paesc(t("pa.k.importKind")) + "</th>" +
// 档案 92:dsh 版本兼容列(「需更高版本」是用户要的显式标注)
"<th>" + paesc(t("pa.pl.compatCol")) + "</th>" +
'<th class="pa-num" style="text-align:right">' + paesc(t("pa.k.downloads")) + "</th>" +
'<th class="pa-act">' + paesc(t("pa.k.detail")) + "</th>" +
'</tr></thead><tbody id="wlTbody"></tbody></table></div>' +
@@ -2356,6 +2748,37 @@ window.__ModuleLoader__.load({
if (kind === "tarball") return { label: t("pa.pl.importTarball"), cls: "admin" };
return { label: t("pa.pl.importSource"), cls: "disabled" };
};
/**
* 档案 92:dsh 版本兼容徽章。判定在平台侧做(官方目录不带 dsh 版本字段 ⇒
* 逐包查 npm 声明),口径与实测依据见 `src/web/plugin-dsh-compat.ts` 头部注释。
* `cur` = 当前平台 dsh 版本,只用于 tooltip 给对比。
*/
function wlCompat(p, cur) {
var v = p.dshCompat;
var label, tip, cls;
if (v === "newer") {
label = t("pa.pl.cmpNewer") + (p.dshRequires ? " ≥ " + p.dshRequires : "");
tip = pafmt(t("pa.pl.cmpNewerTip"), { v: p.dshRequires || "?", cur: cur || "?" });
cls = "pending"; // 橙色:用户点名要「明确标注」的就是这一类
} else if (v === "match") {
label = t("pa.pl.cmpMatch");
tip = pafmt(t("pa.pl.cmpMatchTip"), { cur: cur || "?" });
cls = "active";
} else if (v === "none") {
label = t("pa.pl.cmpNone");
tip = t("pa.pl.cmpNoneTip");
cls = "disabled";
} else if (v === "older") {
label = t("pa.pl.cmpOlder");
tip = t("pa.pl.cmpOlderTip");
cls = "disabled";
} else {
label = t("pa.pl.cmpUnknown");
tip = t("pa.pl.cmpUnknownTip");
cls = "disabled";
}
return '<span class="pa-badge ' + cls + '" title="' + paesc(tip) + '">' + paesc(label) + "</span>";
}
/** 页签切换(门户另写 hash 深链,弹窗内不需要)。 */
function showTab(key) {
$.__querySelectorAll("#pluginTabs .pa-tab").forEach(function (x) {
@@ -2382,13 +2805,15 @@ window.__ModuleLoader__.load({
}
function loadWhitelist() {
var tbody = $("wlTbody");
tbody.innerHTML = paemptyRow(6, t("pa.pl.wlLoading"));
tbody.innerHTML = paemptyRow(7, t("pa.pl.wlLoading"));
var q = encodeURIComponent($("wlQ").value.trim());
var cat = encodeURIComponent($("wlCat").value);
var only = $("wlOnlyImportable").checked ? "1" : "";
return paReq("/api/plugins/whitelist?q=" + q + "&category=" + cat + "&onlyImportable=" + only)
// 档案 92:默认按 dsh 版本过滤;勾了「含仅兼容更旧版本的」才不过滤(排障用)。
var cmp = $("wlAllCompat").checked ? "&dshCompat=all" : "";
return paReq("/api/plugins/whitelist?q=" + q + "&category=" + cat + "&onlyImportable=" + only + cmp)
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (d) {
if (!r.ok) { tbody.innerHTML = paemptyRow(6, t("pa.pl.wlFail") + ":" + (d.error == null ? r.status : d.error)); return; }
if (!r.ok) { tbody.innerHTML = paemptyRow(7, t("pa.pl.wlFail") + ":" + (d.error == null ? r.status : d.error)); return; }
var sel = $("wlCat");
if (sel.dataset.filled !== "1") {
(d.categories || []).forEach(function (c) {
@@ -2397,10 +2822,17 @@ window.__ModuleLoader__.load({
sel.dataset.filled = "1";
}
var shown = (d.plugins || []).length;
// 档案 92:把「按 dsh 版本过滤了什么」说清楚,绝不让条目静默消失。
var unknownN = (d.plugins || []).filter(function (x) { return x.dshCompat === "unknown"; }).length;
var cmpNote = d.dshVersion
? (d.compatFiltered === false
? t("pa.pl.cmpAll")
: pafmt(t("pa.pl.cmpHidden"), { cur: d.dshVersion, n: d.hiddenByDsh || 0 }))
: "";
$("wlInfo").textContent = pafmt(t("pa.pl.wlInfo"), {
total: d.total, importable: d.importable, count: d.count, shown: shown, when: parelTime(d.fetchedAt)
}) + (d.stale ? t("pa.pl.wlStale") : "");
if (shown === 0) { tbody.innerHTML = paemptyRow(6, t("pa.pl.wlEmpty")); return; }
}) + cmpNote + (unknownN > 0 ? t("pa.pl.cmpWarming") : "") + (d.stale ? t("pa.pl.wlStale") : "");
if (shown === 0) { tbody.innerHTML = paemptyRow(7, t("pa.pl.wlEmpty")); return; }
tbody.innerHTML = d.plugins.map(function (p) {
var k = WL_KIND(p.importKind);
var dis = p.importKind === "source";
@@ -2419,6 +2851,7 @@ window.__ModuleLoader__.load({
paesc(primary) + '</div><div class="pa-wlmeta" title="' + paesc(meta) + '">' + paesc(meta) + "</div></td>" +
'<td><span class="pa-wlcat">' + (paesc(p.categoryLabel || p.category) || "—") + "</span></td>" +
'<td><span class="pa-badge ' + k.cls + '">' + paesc(k.label) + "</span></td>" +
'<td style="white-space:nowrap">' + wlCompat(p, d.dshVersion) + "</td>" +
'<td class="pa-num">' + (p.downloads || 0) + "</td>" +
'<td class="pa-act">' + (p.page
? '<a class="pa-sm pa-wllink" href="' + paesc(p.page) + '" target="_blank" rel="noreferrer noopener">' + paesc(t("pa.op.detail")) + "</a>"
@@ -2438,6 +2871,7 @@ window.__ModuleLoader__.load({
$("wlQ").onkeydown = function (e) { if (e.key === "Enter") loadWhitelist(); };
$("wlCat").onchange = function () { return loadWhitelist(); };
$("wlOnlyImportable").onchange = function () { return loadWhitelist(); };
$("wlAllCompat").onchange = function () { return loadWhitelist(); };
$("wlRefreshBtn").onclick = function () {
var b = $("wlRefreshBtn");
b.disabled = true;
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@dsh-local/business-plugins",
"version": "0.3.13",
"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
"version": "0.3.16",
"description": "功能管理(原「功能插件」)section for dsh web profile — v0.3.16(2026-09-14):**官方推荐插件列表按 dsh 版本过滤**(用户:「插件管理中的官方推荐插件列表 只能显示匹配当前dsh版本 和 超过当前版本的插件(超过的要明确标注)」)。① 官方目录(`awesome-dsh-plugin.com/plugins.json`)**不带 dsh 版本字段** ⇒ 平台侧逐包查 npm 精简 packument 的 `peerDependencies / engines` 与**平台真实版本**比对,分类 `match / none / newer / older / unknown`(判定在 `src/web/plugin-dsh-compat.ts`);② **默认隐藏 `older`**(只声明了更旧版本),`newer`(要求更高的 dsh)**橙色显著标注**并在 tooltip 给出「需 ≥ X,当前 Y」;③ 说明行给出「已按 dsh X 过滤,隐藏 N 个…」且**勾选框「含仅兼容更旧版本的」是显式逃生口** —— 绝不让条目静默消失;④ 判定结果按 `<npm>@<版本>` 落盘缓存 7 天,首次进页后台预热全量,请求内只给 6s 预算、超预算判 `unknown` 并**照常显示**(网络抖动不该等同「不兼容」);⑤ ⚠️ 语义:判 `satisfies` **必须带 `includePrerelease: true`** —— 平台版本是 prerelease,默认语义下 `^0.1.2` 甚至 `*` 都不满足,实测 TOP300 会把 `dsh-univer-office`/`dshmarket` 这类**在跑的**插件误判为 older(`match 97/older 139` → 容忍后 `match 214/older 22`);且**伞包 `@deepseek-ai/dsh` 必须单独解析**(它不在自己的 node_modules 里,而那 3 条 `newer` 全部只写在伞包上)。|v0.3.15(2026-09-14):**删除条目补二次确认**(官方 `deleteDialog` 的等价物)。原「删除」紧挨着「停用」且**无任何确认** ⇒ 一次误点就连已存储的 API 密钥一起删掉(`DELETE /api/me/keys/:id` 不可撤销,用户只能重新粘贴密钥);现改为弹窗**点名该提供方**(`ms.delTitle` / `ms.delDesc` / `ms.delConfirm`)后才执行,遮罩点击或「取消」可关。|v0.3.14(2026-09-14):**「模型设置」页按官方 `dsh-client-ui-settings-models` 复刻交互**(用户:「admin模型设置和状态 一个名称换行了,另一个两个按钮换行了,交互体验需要优化…新增模型的操作交互和官方原始新增模型的交互差距很大,建议仔细参考 复刻官方的交互」)。① **换行根因**:原「我已添加的厂家」是 4 列表格(`pa-tbl` 自动列宽)⇒「内置 DeepSeek」与「停用/删除」被挤到换行;改为**官方卡片行**(`ms-rowCard`/`ms-rowHead`/`ms-rowIdentity`/`ms-rowActions`,名称与按钮两侧均 `nowrap`)⇒ **结构上不可能换行**,状态文字下沉到独占一行的副标题。② **新增流程改为官方的两步式**:默认只露**两个虚线按钮**(「+ 添加提供方」/「+ 添加自定义提供方」,`flex:1 1 0;min-width:180px;height:44px`),点击才展开卡片 —— 旧版是「常开表单 + 厂家下拉里混一项『自定义厂家…』」。③ 新增卡片**主字段是单独一个「API 密钥」输入框**(官方原文:页面从不问环境变量名),显示名挪进收起的「自定义设置」`<details>`;自定义卡片按官方字段序(Provider ID → 显示名称 → API 地址 → API 协议 → API 密钥 → 模型目录),模型目录由**一行一个的文本框**改成官方的**行式编辑器**(`+ 添加模型` / 行尾 ✕ 删除)。④ CSS 数值**逐值照抄**官方 CSS 模块(圆角 16/18/14/12/10/8/6/4、字号 14/12/11、行高 22/18/16、按钮高 36/28/44px),颜色走同一批 `--dsw-*` token。⑤ 顺带修两处文案缺陷:admin 自己看共享模型卡片时原写「由 admin 配置」(后端早已给 `ownerIsMe`)⇒ 改「由你配置」;「内置 DeepSeek 密钥留空则回落平台共享密钥」是**错的**(`keyAddSchema` 里 `apiKey` 是 `required + minLength:1`,留空必得 400)⇒ 改为「输入你的 DeepSeek API 密钥」。⑥ 新增 `scripts/verify-models-dict.mjs` 断言 zh/en 键集一致 + 代码引用的键都已声明(本轮就是靠它抓到 `ms.needUrl`/`ms.needModels` 被引用未声明)。|v0.3.10(2026-09-13):① **「服务管理」改名「实例管理」**、**「密钥管理」改名「模型管理」**(用户要求;门户导航/卡片同步改名);② **「实例管理」页可管任意用户**(用户要求「admin 要能管所有用户的服务」)—— 工具条新增「用户」下拉,切换后文件树/启停/打开全部针对所选用户,走新开的 `/api/admin/users/:id/{fs,dsh}`(requireAdmin)|v0.3.8(2026-09-13):**撤掉「我的密钥」分区** —— 模型配置统一走**官方「设置 → 模型」页**(用户要求「界面交互和官方一模一样」⇒ 不仿制、直接放开官方页,见档案 86)。平台侧同步两处:① `ensure-role-profile-patchLine truncated
"type": "module",
"main": "lib/index.js",
"exports": {