chore(web): 降级留痕 + 收尾 —— 静默失效改为可观测;picker 升 0.1.5 消漂移(档案 88 / T07)
- 【留痕】model-catalog 读不到目录:console.warn(每进程一次)+ 新增 catalogDiagnostics(),并把结果透出到 GET /api/me/model-providers 的 catalog 字段;plugin-compat 的 platformPkgCount()=0 同样告警一次 ⇒「功能没做」与「做了但读不到目录」从此**可区分**(这正是那个 P1 长期潜伏的原因) - 【前端】插件 0.3.13:厂家目录不可读 / 为空时**如实提示**(含目录路径),不再静默给空列表 - 【消漂移】poc/workspace-scoped-picker 升 0.1.5 并重打包铺发 —— 两实例已装 0.1.5 且含按序探测的新代码(此前只改了源码、线上还是旧候选) - 【文档】README 补 DSHS_PACKAGE_DIR / DSHS_COMPAT_ROOT / DSHS_PI_AI_DATA_DIR(逃生口此前**未在任何地方暴露**) - 【回归】verify-dsh-install 扩到 6 组(含「读不到 ≠ 目录为空」的区分);verify-platform-admin-section 加 2 条断言(词典 284/284) - 真环境回归:picker 在 /usr/local 仍 import OK(没把本来能用的改坏)|providers 39(端点回 38)|platformPkgCount 223
This commit is contained in:
1 parent
cbaf3ad653
commit
1d72e8f9c8
9 files changed
+127
-6
No files matched your search
@@ -96,6 +96,9 @@ node lib/cli.js --port 3080 --db ./dev.local.db
|
|||||||
| `DSHS_COOKIE_DOMAIN` | 空 | 会话 cookie 的 `Domain`(如 `.dsh.example.com`,注意前导点);空 = host-only |
|
| `DSHS_COOKIE_DOMAIN` | 空 | 会话 cookie 的 `Domain`(如 `.dsh.example.com`,注意前导点);空 = host-only |
|
||||||
| `DSHS_SESSION_TTL` | `604800` | 会话有效期(秒,默认 7 天) |
|
| `DSHS_SESSION_TTL` | `604800` | 会话有效期(秒,默认 7 天) |
|
||||||
| `DSHS_MAX_UPLOAD` | `25MB` | 上传请求体上限(base64 JSON) |
|
| `DSHS_MAX_UPLOAD` | `25MB` | 上传请求体上限(base64 JSON) |
|
||||||
|
| `DSHS_PACKAGE_DIR` | (自动探测) | **平台内置 dsh 的包根**。默认**按序探测**:`DSHS_DSH_BIN` 解软链反推 → `/usr/local/lib/node_modules`、`/usr/lib/node_modules` → `npm root -g`。⚠️ `npm i -g` 的落点随发行版变(发行版包管理器装的 Node 常落 `/usr/lib/node_modules`),**写死会让「模型设置」的厂家目录与「插件兼容性预检」静默失效**(无报错、只剩降级行为)⇒ 探测失败时才需要显式设它。 |
|
||||||
|
| `DSHS_COMPAT_ROOT` | (= `DSHS_PACKAGE_DIR`) | 同上的旧名(兼容);同时接受 `DSH_PACKAGE_DIR` / `DSH_COMPAT_ROOT`(导出侧命名) |
|
||||||
|
| `DSHS_PI_AI_DATA_DIR` | (自动探测) | `@earendil-works/pi-ai` 的**厂家目录**数据位置(`<包根>/node_modules/@earendil-works/pi-ai/dist/providers/data`);一般无需设置 |
|
||||||
| `DSHS_RESTART_BACKOFF` | `1000` | 子 DSH 崩溃后的自动重启延迟(毫秒) |
|
| `DSHS_RESTART_BACKOFF` | `1000` | 子 DSH 崩溃后的自动重启延迟(毫秒) |
|
||||||
| `DSHS_ENABLE_PATCH` | `false` | 是否向子 DSH 注入 `--patch`(运行时插件 + 每文件夹插件;旧版 dsh 不支持时可关) |
|
| `DSHS_ENABLE_PATCH` | `false` | 是否向子 DSH 注入 `--patch`(运行时插件 + 每文件夹插件;旧版 dsh 不支持时可关) |
|
||||||
| `DSHS_SECRET` | 自动生成 | 每用户密钥库的加密主密钥;未设置时生成并持久化到 `<dataRoot>/secret.key`(0600) |
|
| `DSHS_SECRET` | 自动生成 | 每用户密钥库的加密主密钥;未设置时生成并持久化到 `<dataRoot>/secret.key`(0600) |
|
||||||
|
|||||||
@@ -436,7 +436,11 @@ window.__ModuleLoader__.load({
|
|||||||
"ms.e.api": "不支持该协议",
|
"ms.e.api": "不支持该协议",
|
||||||
"ms.e.models": "请至少填一个模型 id",
|
"ms.e.models": "请至少填一个模型 id",
|
||||||
"ms.e.taken": "该厂家标识已被占用,换一个",
|
"ms.e.taken": "该厂家标识已被占用,换一个",
|
||||||
"ms.e.provider": "官方目录里没有这个厂家"
|
"ms.e.provider": "官方目录里没有这个厂家",
|
||||||
|
|
||||||
|
// 目录可读性(2026-09-14):读不到时必须**如实说明**,别让用户以为是"功能没做"
|
||||||
|
"ms.catUnreadable": "⚠️ 平台未能读取官方厂家目录({dir})—— 当前只能使用内置 DeepSeek 或自定义厂家。请联系管理员检查安装路径配置。",
|
||||||
|
"ms.catEmpty": "官方厂家目录为空(该部署可能未随附 pi-ai 厂家数据)—— 当前只能使用内置 DeepSeek 或自定义厂家。"
|
||||||
};
|
};
|
||||||
/** English dictionary, key-set complete against zh. */
|
/** English dictionary, key-set complete against zh. */
|
||||||
var en = {
|
var en = {
|
||||||
@@ -722,7 +726,9 @@ window.__ModuleLoader__.load({
|
|||||||
"ms.e.api": "Unsupported protocol",
|
"ms.e.api": "Unsupported protocol",
|
||||||
"ms.e.models": "Add at least one model id",
|
"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.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."
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1616,6 +1622,11 @@ window.__ModuleLoader__.load({
|
|||||||
var fApi = useState("");
|
var fApi = useState("");
|
||||||
var fModels = useState("");
|
var fModels = useState("");
|
||||||
var fShow = useState(false);
|
var fShow = useState(false);
|
||||||
|
// 后端给的**厂家目录可读性**诊断:读不到时要说清"为什么只剩两项",
|
||||||
|
// 而不是静默给个空列表(2026-09-14:这个静默曾让一个 P1 布局缺陷长期不可见)。
|
||||||
|
var catState = useState(null);
|
||||||
|
var catalog = catState[0];
|
||||||
|
var setCatalog = catState[1];
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -1625,7 +1636,10 @@ window.__ModuleLoader__.load({
|
|||||||
])
|
])
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res[0]) setData(res[0]);
|
if (res[0]) setData(res[0]);
|
||||||
if (res[1] && res[1].providers) setProviders(res[1].providers);
|
if (res[1]) {
|
||||||
|
if (res[1].providers) setProviders(res[1].providers);
|
||||||
|
setCatalog(res[1].catalog || null);
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(function () { setLoading(false); setMsg(t("ms.loadFailed")); });
|
.catch(function () { setLoading(false); setMsg(t("ms.loadFailed")); });
|
||||||
@@ -1761,6 +1775,14 @@ window.__ModuleLoader__.load({
|
|||||||
// ── 新增厂家:**先选厂家**,目录厂家只需填 API 密钥(官方「新增」卡片同构)──
|
// ── 新增厂家:**先选厂家**,目录厂家只需填 API 密钥(官方「新增」卡片同构)──
|
||||||
jsxRuntime.jsxs("div", { className: "pa-box", children: [
|
jsxRuntime.jsxs("div", { className: "pa-box", children: [
|
||||||
jsxRuntime.jsx("div", { className: "pa-card-h", children: jsxRuntime.jsx("span", { children: t("ms.add") }) }),
|
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.jsxs("div", { className: "pa-row", children: [
|
||||||
jsxRuntime.jsx("span", { className: "pa-hint", style: { margin: 0 }, children: t("ms.pickProvider") }),
|
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("select", { className: "pa-input", value: pick, onChange: function (e) { fPick[1](e.target.value); setShow(false); }, children: [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dsh-local/business-plugins",
|
"name": "@dsh-local/business-plugins",
|
||||||
"version": "0.3.12",
|
"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
|
"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",
|
"type": "module",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dsh-local/workspace-scoped-picker",
|
"name": "@dsh-local/workspace-scoped-picker",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"description": "会话内工作区目录选择器:列举/建目录的根固定在当前用户自有目录(档案 18;所有用户含 admin)",
|
"description": "会话内工作区目录选择器:列举/建目录的根固定在当前用户自有目录(档案 18;所有用户含 admin)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
|||||||
@@ -113,6 +113,32 @@ const real = dshPackageRoot()
|
|||||||
ok('返回非空绝对路径', typeof real === 'string' && real.length > 0 && real.startsWith('/'), real)
|
ok('返回非空绝对路径', typeof real === 'string' && real.length > 0 && real.startsWith('/'), real)
|
||||||
ok('dshScopeDir 形状正确', dshScopeDir().endsWith(join('node_modules', '@deepseek-ai')), dshScopeDir())
|
ok('dshScopeDir 形状正确', dshScopeDir().endsWith(join('node_modules', '@deepseek-ai')), dshScopeDir())
|
||||||
|
|
||||||
|
// ── 6. 目录可读性诊断:**「读不到」必须与「目录为空」可区分**(2026-09-14 留痕)─────
|
||||||
|
console.log('\n[6] 目录可读性诊断(静默降级 → 可观测)')
|
||||||
|
const cat = await import(pathToFileURL(join(ROOT, 'lib/web/model-catalog.js')).href)
|
||||||
|
const warnings = []
|
||||||
|
const realWarn = console.warn
|
||||||
|
console.warn = (...a) => warnings.push(a.join(' '))
|
||||||
|
|
||||||
|
clearEnv()
|
||||||
|
process.env.PI_AI_DATA_DIR = join(T, 'no-such-data-dir') // 不存在 ⇒ 读不到
|
||||||
|
process.env.DSH_PACKAGE_DIR = '/nonexistent'
|
||||||
|
await cat.listCatalogProviders(true)
|
||||||
|
const d1 = cat.catalogDiagnostics()
|
||||||
|
ok('读不到时 readable=false', d1 !== null && d1.readable === false, JSON.stringify(d1))
|
||||||
|
ok('读不到时 count=0', d1 !== null && d1.count === 0)
|
||||||
|
ok('读不到时**告警过一次**(不再静默)', warnings.some((w) => w.includes('厂家目录不可读')), warnings[0]?.slice(0, 60) ?? '(无)')
|
||||||
|
|
||||||
|
clearEnv()
|
||||||
|
const emptyDir = join(T, 'empty-data')
|
||||||
|
mkdirSync(emptyDir, { recursive: true })
|
||||||
|
process.env.PI_AI_DATA_DIR = emptyDir // 存在但没有任何 <id>.json ⇒ "目录为空"
|
||||||
|
await cat.listCatalogProviders(true)
|
||||||
|
const d2 = cat.catalogDiagnostics()
|
||||||
|
ok('目录可读但为空时 readable=true(与"读不到"可区分)', d2 !== null && d2.readable === true && d2.count === 0, JSON.stringify(d2))
|
||||||
|
ok('已告警过就不再刷日志', warnings.filter((w) => w.includes('厂家目录不可读')).length === 1, String(warnings.length))
|
||||||
|
console.warn = realWarn
|
||||||
|
|
||||||
rmSync(T, { recursive: true, force: true })
|
rmSync(T, { recursive: true, force: true })
|
||||||
console.log('')
|
console.log('')
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const DATA = {
|
|||||||
{ id: "moonshotai-cn", label: "月之暗面 Moonshot(中国)", api: "openai-completions", baseURL: "https://api.moonshot.cn/v1", cn: true, modelCount: 12, models: ["kimi-k2", "moonshot-v1-8k"] },
|
{ id: "moonshotai-cn", label: "月之暗面 Moonshot(中国)", api: "openai-completions", baseURL: "https://api.moonshot.cn/v1", cn: true, modelCount: 12, models: ["kimi-k2", "moonshot-v1-8k"] },
|
||||||
{ id: "openai", label: "OpenAI", api: "openai-responses", baseURL: null, cn: false, modelCount: 20, models: ["gpt-4o"] },
|
{ id: "openai", label: "OpenAI", api: "openai-responses", baseURL: null, cn: false, modelCount: 20, models: ["gpt-4o"] },
|
||||||
],
|
],
|
||||||
|
catalog: { dir: "/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@earendil-works/pi-ai/dist/providers/data", readable: true, count: 2 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const sandbox = {
|
const sandbox = {
|
||||||
@@ -242,6 +243,22 @@ ok("非 admin:不出现任何功能项", !PORTAL_ITEMS.some(k => tUser.include
|
|||||||
tMs.includes("中国大陆") && tMs.includes("国际") && tMs.includes("月之暗面 Moonshot(中国)") && tMs.includes("OpenAI"));
|
tMs.includes("中国大陆") && tMs.includes("国际") && tMs.includes("月之暗面 Moonshot(中国)") && tMs.includes("OpenAI"));
|
||||||
ok("模型设置:默认选中「内置 DeepSeek」时给出回落说明",
|
ok("模型设置:默认选中「内置 DeepSeek」时给出回落说明",
|
||||||
tMs.includes("密钥留空则回落到平台共享密钥"));
|
tMs.includes("密钥留空则回落到平台共享密钥"));
|
||||||
|
// 2026-09-14 留痕:目录读不到时要**如实说明**,不能静默给个空列表
|
||||||
|
{
|
||||||
|
const saved = DATA["/api/me/model-providers"];
|
||||||
|
DATA["/api/me/model-providers"] = {
|
||||||
|
providers: [],
|
||||||
|
catalog: { dir: "/opt/nope/pi-ai/dist/providers/data", readable: false, count: 0 },
|
||||||
|
};
|
||||||
|
const tBad = text(await render(ms.Comp)).join(" | ");
|
||||||
|
ok("模型设置:目录不可读时如实提示(含目录路径,不静默空列表)",
|
||||||
|
tBad.includes("未能读取官方厂家目录") && tBad.includes("/opt/nope/pi-ai"), tBad.slice(0, 0) + "命中");
|
||||||
|
DATA["/api/me/model-providers"] = { providers: [], catalog: { dir: "/x", readable: true, count: 0 } };
|
||||||
|
const tEmpty = text(await render(ms.Comp)).join(" | ");
|
||||||
|
ok("模型设置:目录为空的文案与「读不到」不同",
|
||||||
|
tEmpty.includes("官方厂家目录为空") && !tEmpty.includes("未能读取官方厂家目录"));
|
||||||
|
DATA["/api/me/model-providers"] = saved;
|
||||||
|
}
|
||||||
ok("模型设置:词典备齐「只填密钥」相关文案(目录提示 / 展开模型)",
|
ok("模型设置:词典备齐「只填密钥」相关文案(目录提示 / 展开模型)",
|
||||||
!!ZH["ms.catalogHint"] && !!ZH["ms.showModels"] && !!ZH["ms.hideModels"] && !!ZH["ms.pickProvider"]);
|
!!ZH["ms.catalogHint"] && !!ZH["ms.showModels"] && !!ZH["ms.hideModels"] && !!ZH["ms.pickProvider"]);
|
||||||
// 落地在 spawn 时 ⇒ 文案必须说清"重启才生效",否则用户会以为保存即生效
|
// 落地在 spawn 时 ⇒ 文案必须说清"重启才生效",否则用户会以为保存即生效
|
||||||
|
|||||||
@@ -175,9 +175,37 @@ function parseProvider(id: string, raw: string): CatalogProvider | null {
|
|||||||
let cache: { at: number; list: CatalogProvider[] } | null = null
|
let cache: { at: number; list: CatalogProvider[] } | null = null
|
||||||
const TTL_MS = 10 * 60 * 1000
|
const TTL_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
|
/** 读取结果的可读性诊断。 */
|
||||||
|
export interface CatalogDiagnostics {
|
||||||
|
/** 实际使用的目录(`dsh-install.ts` 探测出来的)。 */
|
||||||
|
dir: string
|
||||||
|
/** 能否读到(`false` = 探测/读取失败 ⇒ 界面只剩内置 DeepSeek + 自定义网关)。 */
|
||||||
|
readable: boolean
|
||||||
|
/** 读到了几个厂家。 */
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目录可读性诊断(2026-09-14 加,见 `04-调整方案/88`)。
|
||||||
|
*
|
||||||
|
* 为什么必须加:定位/读取失败原本**完全静默**(catch 后返回 `[]`),于是
|
||||||
|
* 「功能没做」与「功能做了但读不到目录」在界面上**一模一样** —— 一个 P1 缺陷就这样在
|
||||||
|
* 另一种安装布局(`npm root -g` = `/usr/lib/node_modules`)上潜伏了很久,最后靠人工
|
||||||
|
* 比对才看出来。⇒ ① 读不到时 `console.warn`(每进程一次,不刷日志);
|
||||||
|
* ② 把结果透出到 `/api/me/model-providers`,让「到底读没读到」可观测。
|
||||||
|
*/
|
||||||
|
let diag: CatalogDiagnostics | null = null
|
||||||
|
let warnedUnreadable = false
|
||||||
|
|
||||||
|
/** 最近一次读取的可读性诊断(尚未读过时返回 `null`)。 */
|
||||||
|
export function catalogDiagnostics(): CatalogDiagnostics | null {
|
||||||
|
return diag
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读出目录里的全部厂家(按中文名/官方名排序)。
|
* 读出目录里的全部厂家(按中文名/官方名排序)。
|
||||||
* 目录不存在(例如本地开发机)⇒ 返回空数组,**不抛错**:调用方据此降级为"只有内置 + 自定义"。
|
* 目录不存在(例如本地开发机)⇒ 返回空数组,**不抛错**:调用方据此降级为"只有内置 + 自定义"。
|
||||||
|
* ⚠️ 降级**不是无声的** —— 见 `catalogDiagnostics()` 与上方的 `console.warn`。
|
||||||
*/
|
*/
|
||||||
export async function listCatalogProviders(force = false): Promise<CatalogProvider[]> {
|
export async function listCatalogProviders(force = false): Promise<CatalogProvider[]> {
|
||||||
if (!force && cache !== null && Date.now() - cache.at < TTL_MS) return cache.list
|
if (!force && cache !== null && Date.now() - cache.at < TTL_MS) return cache.list
|
||||||
@@ -186,6 +214,14 @@ export async function listCatalogProviders(force = false): Promise<CatalogProvid
|
|||||||
try {
|
try {
|
||||||
files = await readdir(dir)
|
files = await readdir(dir)
|
||||||
} catch {
|
} catch {
|
||||||
|
diag = { dir, readable: false, count: 0 }
|
||||||
|
if (!warnedUnreadable) {
|
||||||
|
warnedUnreadable = true
|
||||||
|
console.warn(
|
||||||
|
`[model-catalog] 厂家目录不可读:${dir} —— 「设置 → 模型设置 → 新增厂家」只会显示内置 DeepSeek 与自定义网关。` +
|
||||||
|
' 检查 DSHS_DSH_BIN / DSH_PACKAGE_DIR 是否正确(见 src/web/dsh-install.ts)。',
|
||||||
|
)
|
||||||
|
}
|
||||||
cache = { at: Date.now(), list: [] }
|
cache = { at: Date.now(), list: [] }
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -200,6 +236,7 @@ export async function listCatalogProviders(force = false): Promise<CatalogProvid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.sort((a, b) => a.label.localeCompare(b.label, 'zh-Hans-CN'))
|
out.sort((a, b) => a.label.localeCompare(b.label, 'zh-Hans-CN'))
|
||||||
|
diag = { dir, readable: true, count: out.length }
|
||||||
cache = { at: Date.now(), list: out }
|
cache = { at: Date.now(), list: out }
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,11 +72,24 @@ function platformPkgDir(pkg: string): string {
|
|||||||
return join(dshScopeDir(), shortName(pkg))
|
return join(dshScopeDir(), shortName(pkg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否已经就「平台包目录不可读」告过一次警(避免刷日志)。 */
|
||||||
|
let warnedScopeUnreadable = false
|
||||||
|
|
||||||
/** 平台内置 @deepseek-ai 包的数量(目录级,低成本)。 */
|
/** 平台内置 @deepseek-ai 包的数量(目录级,低成本)。 */
|
||||||
export function platformPkgCount(): number {
|
export function platformPkgCount(): number {
|
||||||
try {
|
try {
|
||||||
return readdirSync(dshScopeDir()).filter((n) => !n.startsWith('.')).length
|
return readdirSync(dshScopeDir()).filter((n) => !n.startsWith('.')).length
|
||||||
} catch {
|
} catch {
|
||||||
|
// ⚠️ 这里**不能只返回 0**:调用方会把它当成"没有平台包"⇒ 预检直接退化成
|
||||||
|
// 「平台包目录不可读」——**安全网静默失效**(上传不兼容插件不再被拦)。
|
||||||
|
// 2026-09-14:加一次告警,让"读不到"这件事可观测(见 `04-调整方案/88`)。
|
||||||
|
if (!warnedScopeUnreadable) {
|
||||||
|
warnedScopeUnreadable = true
|
||||||
|
console.warn(
|
||||||
|
`[plugin-compat] 平台包目录不可读:${dshScopeDir()} —— 插件兼容性预检将退化为「不检查」。` +
|
||||||
|
' 检查 DSHS_DSH_BIN / DSH_COMPAT_ROOT 是否正确(见 src/web/dsh-install.ts)。',
|
||||||
|
)
|
||||||
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { requireAuth } from '../middleware/authn.js'
|
|||||||
import { homeRoot, userRoot } from '../../fs/workspace.js'
|
import { homeRoot, userRoot } from '../../fs/workspace.js'
|
||||||
import { deriveKey, encrypt } from '../../crypto.js'
|
import { deriveKey, encrypt } from '../../crypto.js'
|
||||||
import { toPublicUser } from '../../db/types.js'
|
import { toPublicUser } from '../../db/types.js'
|
||||||
import { isCatalogProvider, isCnProvider, listCatalogProviders } from '../model-catalog.js'
|
import { catalogDiagnostics, isCatalogProvider, isCnProvider, listCatalogProviders } from '../model-catalog.js'
|
||||||
import { PROTOCOLS } from '../model-landing.js'
|
import { PROTOCOLS } from '../model-landing.js'
|
||||||
import {
|
import {
|
||||||
clearSessionCookie,
|
clearSessionCookie,
|
||||||
@@ -243,6 +243,9 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
// 只回前 60 个模型名给界面展示(足量的"看到它自带什么"),不整份下发。
|
// 只回前 60 个模型名给界面展示(足量的"看到它自带什么"),不整份下发。
|
||||||
models: p.models.slice(0, 60).map((m) => m.name ?? m.id),
|
models: p.models.slice(0, 60).map((m) => m.name ?? m.id),
|
||||||
})),
|
})),
|
||||||
|
// 目录**可读性**:读不到时前端要如实说明"为什么只剩两项",而不是静默给个空列表
|
||||||
|
// (2026-09-14:这个静默曾让一个 P1 布局缺陷长期不可见,见 `04-调整方案/88`)。
|
||||||
|
catalog: catalogDiagnostics(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user