chore: 并入已删除会话的在途成果(防丢失;原会话已删,未做功能验收)

**背景**:这些改动原属本工作区另外几个会话(T01/T02 等),**那些会话已被用户删除** ⇒
工作树里的成果处于"无主"状态,一次错误 checkout / 覆盖即**永久丢失** ⇒ 代入库保全。
口径遵循本项目**先例**(`39a1f2e` / `b617cdb`:**别人的活,代入库并在提交信息里注明**)。

**内容**:档案 101「能力管理」改名 + 页内 tab 分页|档案 102 语言切换搬入「用户设置」|
`07-实例UI分区登记表.md`|`scripts/find-ui*.mjs`(UI 元素定位工具)|`poc/portal-entry/`(0.5.3)|
`src/web/locale-pref.ts` + `home-files.ts`(语言偏好持久化)|`test/locale-pref.test.mjs`|
`package.json`|`BRIEF.md` / `INDEX.md` / `docs-manifest.json` / `03-路线图与待办.md` / 档案 100 增量。

**已做最小健全性检查**(⚠️ **未跑完整构建 / 单测** —— 那是原会话的验收职责,本次只求"不丢"):
- JSON 合法:`package.json` / `poc/business-plugins/package.json` / `docs-manifest.json` ✓
- 4 个 TS 文件 `{}`/`()` 配平 ✓;新增文件均非空 ✓
- 规模:13 文件改动 +340/−210,新增 12 条

**未 push**(按 §4 提交边界:用户说"提交",未说"推送")。
This commit is contained in:
admin committed 2026-09-15 21:17:12 +08:00
1 parent 03c8363960
commit 3efd68517f
27 files changed
+1712 -206

No files matched your search

+107 -130
View File
@@ -147,14 +147,12 @@ window.__ModuleLoader__.load({
var NS = "business-plugins";
/** 简体中文字典(key 集的事实来源)。 */
var zh = {
"section.label": "功能管理",
"section.label": "能力管理",
// 顶部页内分页(2026-09-15 用户要求:「能力管理」按插件 / 技能分页各自操作)。
// 技能页的标签直接复用 `msk.group`,不另造同义词。
"cap.tabPlugins": "功能插件",
// ── 「偏好设置」section(档案 81 · R5:语言切换)────────────────
// ⚠️ 语言 id 必须与官方 `dsh-client-locale` 的 `LOCALE_IDS` 一致:`en` / `zh`。
// 官方 `FALLBACK_LOCALE = "en"` ⇒ **默认英语**(全球化口径,2026-09-15 用户定)。
"pref.label": "偏好设置",
"pref.lang": "界面语言",
"pref.hint": "设置本实例界面的显示语言。默认英语;切换后立即生效。",
// ── 「我的技能」分组(档案 100 · 2026-09-15)─────────────────────
// 键前缀 `msk.` = my skills。措辞三原则(档案 100 §2.6):
@@ -480,11 +478,11 @@ window.__ModuleLoader__.load({
"ms.sharedOff": "启用共享模型",
"ms.list": "我已添加的提供方",
"ms.empty": "还没有添加任何提供方 —— 用下面的按钮添加。",
"ms.tagBuiltin": "内置",
"ms.tagBuiltin": "官方",
"ms.tagCatalog": "目录",
"ms.tagCustom": "自定义",
"ms.kindBuiltin": "内置 DeepSeek",
"ms.builtinHint": "平台内置通道,无需端点",
"ms.kindBuiltin": "DeepSeek",
"ms.builtinHint": "官方通道,无需端点",
"ms.catalogRow": "官方目录提供方(端点 / 协议 / 模型由目录提供)",
"ms.stateOn": "已启用",
"ms.stateOff": "已停用",
@@ -526,8 +524,8 @@ window.__ModuleLoader__.load({
"ms.saving": "保存中…",
"ms.showModels": "查看模型清单",
"ms.hideModels": "收起模型清单",
"ms.optBuiltin": "内置 DeepSeek(平台共享)",
"ms.builtinPickHint": "内置 DeepSeek:平台内置通道,固定官方端点,无需填 API 地址。",
"ms.optBuiltin": "DeepSeek",
"ms.builtinPickHint": "DeepSeek:官方通道,固定官方端点,无需填 API 地址。",
"ms.grpCn": "中国大陆",
"ms.grpIntl": "国际",
"ms.e.name": "名称不合法",
@@ -538,19 +536,16 @@ window.__ModuleLoader__.load({
"ms.e.models": "请至少填一个模型 id",
"ms.e.taken": "该提供方标识已被占用,换一个",
"ms.e.provider": "官方目录里没有这个提供方",
"ms.catUnreadable": "⚠️ 平台未能读取官方提供方目录({dir})—— 当前只能使用内置 DeepSeek 或自定义提供方。请联系管理员检查安装路径配置。",
"ms.catEmpty": "官方提供方目录为空(该部署可能未随附 pi-ai 提供方数据)—— 当前只能使用内置 DeepSeek 或自定义提供方。"
"ms.catUnreadable": "⚠️ 平台未能读取官方提供方目录({dir})—— 当前只能使用 DeepSeek 或自定义提供方。请联系管理员检查安装路径配置。",
"ms.catEmpty": "官方提供方目录为空(该部署可能未随附 pi-ai 提供方数据)—— 当前只能使用 DeepSeek 或自定义提供方。"
};
/** English dictionary, key-set complete against zh. */
var en = {
"section.label": "Feature management",
"section.label": "Capability management",
// ── 「Preferences」section (spec 81 · R5: language switch) ──────
// ⚠️ Language ids must match the official `dsh-client-locale` `LOCALE_IDS`: `en` / `zh`.
// The official `FALLBACK_LOCALE = "en"` ⇒ **English by default** (global-product rule, 2026-09-15).
"pref.label": "Preferences",
"pref.lang": "Interface language",
"pref.hint": "Choose the display language for this instance. English is the default; the change applies immediately.",
// In-page tabs (2026-09-15): "Capability management" splits into plugins / skills,
// each operating independently. The skills tab reuses `msk.group` as its label.
"cap.tabPlugins": "Feature plugins",
// ── "My skills" group (spec 100 · 2026-09-15) ──────────────────
// Key prefix `msk.` = my skills. Same three wording rules as the zh side:
@@ -859,11 +854,11 @@ window.__ModuleLoader__.load({
"ms.sharedOff": "Enable shared model",
"ms.list": "My providers",
"ms.empty": "No providers yet — use the buttons below to add one.",
"ms.tagBuiltin": "Built-in",
"ms.tagBuiltin": "Official",
"ms.tagCatalog": "Catalog",
"ms.tagCustom": "Custom",
"ms.kindBuiltin": "Built-in DeepSeek",
"ms.builtinHint": "Platform channel; no endpoint needed",
"ms.kindBuiltin": "DeepSeek",
"ms.builtinHint": "Official channel; no endpoint needed",
"ms.catalogRow": "Catalog provider (endpoint/protocol/models from the catalog)",
"ms.stateOn": "Enabled",
"ms.stateOff": "Disabled",
@@ -905,8 +900,8 @@ window.__ModuleLoader__.load({
"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.optBuiltin": "DeepSeek",
"ms.builtinPickHint": "DeepSeek: the official channel with a fixed endpoint — no base URL needed.",
"ms.grpCn": "Mainland China",
"ms.grpIntl": "International",
"ms.e.name": "Invalid name",
@@ -917,8 +912,8 @@ window.__ModuleLoader__.load({
"ms.e.models": "Add at least one model id",
"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."
"ms.catUnreadable": "⚠️ The platform could not read the official provider catalog ({dir}) — only 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 DeepSeek and custom providers are available."
};
/**
@@ -1087,7 +1082,7 @@ window.__ModuleLoader__.load({
// 圆角 / 字号 / 行高 / 内边距 / gap **逐值照抄**;颜色走同一批 dsh token `--dsw-*`
// (官方原文用的就是这批 token,故连 token 名都一致),只补一个硬编码兜底色。
//
// 为什么把表格换成卡片行:表格 4 列是**自动列宽**,「内置 DeepSeek」和「停用/删除」
// 为什么把表格换成卡片行:表格 4 列是**自动列宽**,「DeepSeek」(当时叫「内置 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}",
@@ -1170,6 +1165,13 @@ window.__ModuleLoader__.load({
".bp-err{font-size:13px;line-height:1.6;color:var(--dsw-alias-state-error-primary,#d93026)}",
".bp-ok{font-size:13px;line-height:1.6;color:var(--dsw-alias-state-success-primary,#1a7f37)}",
// 分页工具行(技能页:左说明 / 右上传入口 —— 左对齐、右侧靠 `.bp-groupAct` 的 margin-left:auto)
".bp-tabHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin:0 0 12px}",
// 插件卡片的**中文用途描述 = 最多 3 行**(2026-09-15 用户要求:「插件卡片中增加中文用途
// 描述,显示三行」)。用 `-webkit-line-clamp` 做多行截断:单行 ellipsis 会把"这个插件
// 到底干什么"截得看不全;`white-space:normal` 必须显式写(否则继承单行截断的 nowrap)。
".bp-plugDesc{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-word;line-height:1.45}",
"@media (prefers-reduced-motion: reduce){.pa-card,.pa-modal,.bp-skillRow{animation:none;transition:none}.pa-card:hover{transform:none}.ms-summary::before{transition:none}}"
].join("");
document.head.appendChild(el);
@@ -1278,6 +1280,13 @@ window.__ModuleLoader__.load({
var delSkState = useState(null);
var delSkill = delSkState[0];
var setDelSkill = delSkState[1];
/**
* 顶部分页(`"plugins"` | `"skills"`)—— 2026-09-15 用户要求:
* 「能力管理」按**插件 / 技能分页**,各自独立操作(默认落在插件页,与改造前一致)。
*/
var tabState = useState("plugins");
var tab = tabState[0];
var setTab = tabState[1];
function loadSkills() {
setSkLoading(true);
@@ -1747,6 +1756,42 @@ window.__ModuleLoader__.load({
};
var rows = [];
var cards = [];
/** 当前分页 —— 插件页与技能页**各自独立操作**(2026-09-15 用户要求)。 */
var isPlugins = tab === "plugins";
// 顶部页内分页:**复用既有 `.pa-tabs/.pa-tab/.pa-tcnt`**(下划线式;规范 §4.4
// 「数据页用下划线式」)。计数 = 该类当前有多少项,用户不必点进去才知道。
rows.push(jsxRuntime.jsxs("div", {
key: "__tabs",
className: "pa-tabs",
role: "tablist",
children: [
jsxRuntime.jsxs("div", {
key: "p",
className: "pa-tab" + (isPlugins ? " pa-on" : ""),
role: "tab",
"aria-selected": isPlugins ? "true" : "false",
onClick: function () { setTab("plugins"); },
children: [
t("cap.tabPlugins"),
jsxRuntime.jsx("span", { key: "c", className: "pa-tcnt", children: String(plugins.length) })
]
}),
jsxRuntime.jsxs("div", {
key: "s",
className: "pa-tab" + (isPlugins ? "" : " pa-on"),
role: "tab",
"aria-selected": isPlugins ? "false" : "true",
onClick: function () { setTab("skills"); },
children: [
t("msk.group"),
jsxRuntime.jsx("span", { key: "c", className: "pa-tcnt", children: String(skills.length) })
]
})
]
}));
if (isPlugins) {
// 内存配额状态条:**卡片列表上方**(2026-09-13 用户要求)——
// 当前配额 / 勾选后配额 / 是否顶到硬顶,让用户在点「应用」之前就知道后果。
@@ -1837,12 +1882,13 @@ window.__ModuleLoader__.load({
style: { flex: "1", minWidth: "0", display: "flex", flexDirection: "column", gap: "2px" },
children: [
// 主视觉 = 用途说明(规范 §5「信息层次」;与门户 plugins 页同一决策)
// 主视觉 = **中文用途描述,最多 3 行**(2026-09-15 用户要求:
// 「插件卡片中增加中文用途描述,显示三行」)。截断交给 `.bp-plugDesc`
// 的 `-webkit-line-clamp:3`;`title` 仍挂全文,hover 可看完整。
jsxRuntime.jsx("span", {
key: "p1",
style: {
fontSize: "14px", fontWeight: 500, color: T.text, lineHeight: 1.45,
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"
},
className: "bp-plugDesc",
style: { fontSize: "14px", fontWeight: 500, color: T.text },
title: primary,
children: primary
}),
@@ -1939,11 +1985,16 @@ window.__ModuleLoader__.load({
]
}));
// ══ 「我的技能」分组(档案 100)═════════════════════════════════════════
// 与「功能插件」**同页**呈现(用户口径「不要分开管理」=入口层合并),但机制各自
// 独立:技能是 watch 驱动 ⇒ 启用/停用**立即生效、无需重启**,行内文案如实说明;
}
// ══ 「我的技能」分组(档案 100;2026-09-15 起改为**独立分页**)══════════════
// 与「功能插件」**同一个 section 内**(用户口径「不要分开管理」=入口层合并),
// 但两者**各自一页、各自操作**(用户 2026-09-15:「改造为 tab 可切换 插件和技能
// 分别进行操作」);机制上始终独立:技能是 watch 驱动 ⇒ 启用/停用**立即生效、
// 无需重启**,行内文案如实说明。
// 平台共享技能(`locked:true`)**不渲染任何动作按钮** —— 与后端 `assertNotShared()`
// 的 409 双向一致:前端不给出注定失败的按钮,后端仍然兜底。
if (!isPlugins) {
var skillRows = [];
if (skLoading) {
skillRows.push(jsxRuntime.jsx("div", {
@@ -2049,16 +2100,16 @@ window.__ModuleLoader__.load({
rows.push(jsxRuntime.jsxs("div", {
key: "__skills",
children: [
// 分组头:竖条标题 + 计数 + 右侧主按钮(形态与「功能插件」分组头同源)
// 工具行:左 = 生效方式说明(与「功能插件」需重启形成对照)/ 右 = 上传入口。
// ⚠️ **不再自带分组标题** —— 标题由顶部 tab 承担,避免同页出现两个同名标签。
jsxRuntime.jsxs("div", {
key: "head",
className: "bp-groupHead",
className: "bp-tabHead",
children: [
jsxRuntime.jsx("h4", { key: "t", className: "bp-groupTitle", children: t("msk.group") }),
jsxRuntime.jsx("span", {
key: "c",
className: "bp-groupCnt",
children: t("msk.count").replace("{n}", String(skills.length))
jsxRuntime.jsx("p", {
key: "hint",
style: { margin: 0, fontSize: "12px", lineHeight: "18px", color: T.dim },
children: t("msk.hint")
}),
jsxRuntime.jsx("span", {
key: "act",
@@ -2078,12 +2129,6 @@ window.__ModuleLoader__.load({
})
]
}),
// 说明行(如实说生效方式 —— 与「功能插件」需重启形成对照)
jsxRuntime.jsx("p", {
key: "hint",
style: { margin: "0 0 4px", fontSize: "12px", lineHeight: "18px", color: T.dim },
children: t("msk.hint")
}),
// 上传面板(默认收起;支持点击选文件与拖入)
upOpen
? jsxRuntime.jsxs("div", {
@@ -2181,6 +2226,9 @@ window.__ModuleLoader__.load({
]
}));
}
if (isPlugins) {
if (rejected.length > 0) {
// impeccable craft-floor 明令禁止「卡片/列表项/提示上 >1px 的彩色 border-left/right」
// —— 改为整块浅底 + 圆角,危险语义由标题文字色承担(不靠侧边色条)。
@@ -2325,6 +2373,9 @@ window.__ModuleLoader__.load({
// ── 「我的技能」的两个页内确认弹窗(档案 100 §2.5)──────────────────────
// 共同点:**只点名 + 讲清后果**。替换额外给出「旧 vs 新」的事实行(tabular-nums),
// 因为"删掉几个文件"是用户真正关心的量;删除与替换同样不可逆,故同样必须过弹窗。
}
if (!isPlugins) {
if (replaceTarget) {
rows.push(skillDialog({
dkey: "__repl",
@@ -2383,6 +2434,8 @@ window.__ModuleLoader__.load({
}));
}
}
return jsxRuntime.jsx("div", { style: wrap, children: rows });
}
@@ -2582,7 +2635,7 @@ window.__ModuleLoader__.load({
}
/**
* 提交「添加提供方」(目录提供方 / 内置 DeepSeek)。
* 提交「添加提供方」(目录提供方 / DeepSeek —— 即内置通道那条)。
* ⚠️ 密钥**不可留空**:`keyAddSchema` 里 `apiKey` 是 `required + minLength:1`,
* 留空必得 400 `invalid_api_key`(旧文案「留空则回落平台共享密钥」是错的,已改)。
* 显示名留空 ⇒ 取提供方名(官方同做法:页面从不先问名字)。
@@ -2900,86 +2953,10 @@ window.__ModuleLoader__.load({
] });
}
/**
* 「偏好设置」section(**用户可见**)—— 档案 81 · R5:**语言切换**。
*
* 走官方扩展点(**零官方改动**):`ctx.locale.getLocale()` 读当前语言、`ctx.locale.setLocale(id)` 切换;
* 语言目录 = 官方随包发布的 `LOCALE_IDS`(`en` / `zh`)⇒ **不另造词条运行时**。
*
* ⚠️ **默认英语**:官方 `FALLBACK_LOCALE = "en"`(全球化项目口径,用户 2026-09-15 定)。
* 切换后的持久化由**官方 locale** 写入宿主 `settings.yaml` 的 `locale.preference` —— 本插件不碰文件。
* ⚠️ 切换后只 bump 本 section 的本地 state(本栏立即用新语言重渲染);官方 UI 与其它 section
* 由各自的订阅重渲染,无需我们干预。
*/
function PreferencesSection() {
var useState = React.useState;
var tickState = useState(0);
var setTick = tickState[1];
var LANGS = [{ id: "en", name: "English" }, { id: "zh", name: "中文" }];
function currentLocale() {
try {
if (ctx.locale && typeof ctx.locale.getLocale === "function") {
var id = ctx.locale.getLocale();
if (id) return String(id).split("-")[0];
}
} catch (err) { /* 官方 API 若变动,本栏降级为只读默认值,不至于整栏崩掉 */ }
return "en";
}
var cur = currentLocale();
function pick(id) {
if (id === cur) return;
try {
if (ctx.locale && typeof ctx.locale.setLocale === "function") ctx.locale.setLocale(id);
else console.warn("[business-plugins] ctx.locale.setLocale 不可用,语言未切换");
} catch (err) {
console.warn("[business-plugins] setLocale 失败:", err);
}
setTick(function (n) { return n + 1; });
}
return jsxRuntime.jsx("div", {
className: "bp-pref",
children: [
jsxRuntime.jsx("p", {
className: "ms-hint",
style: { marginBottom: 12 },
children: t("pref.hint")
}, "hint"),
jsxRuntime.jsx("div", {
style: { display: "flex", alignItems: "center", gap: 10 },
children: [
jsxRuntime.jsx("span", { children: t("pref.lang") }, "label"),
jsxRuntime.jsx("select", {
"aria-label": t("pref.lang"),
value: cur,
onChange: function (e) { pick(e.target.value); },
style: {
minWidth: 140, padding: "6px 8px", borderRadius: 6, cursor: "pointer",
border: "1px solid var(--dsw-alias-border-secondary, #d9d9d9)",
background: "var(--dsw-alias-bg-base, transparent)",
color: "var(--dsw-alias-label-primary, inherit)"
},
children: LANGS.map(function (l) {
return jsxRuntime.jsx("option", { value: l.id, children: l.name }, l.id);
})
}, "select")
]
}, "row")
]
});
}
// 「偏好设置」= 语言切换(**用户可见**;order 99 ⇒ 排在「模型设置 / 功能管理」之前)
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(
{
name: "settings.section",
id: "preferences",
order: 99,
label: function () { return t("pref.label"); }
},
PreferencesSection
);
});
// 2026-09-15(用户要求):**「偏好设置」分区已撤除** —— 语言切换搬到
// `@dsh-local/portal-entry` 的「用户设置」分区(用户级偏好与「账号 / 退出登录」同区即可,
// 再单开一个分区就是重复入口)。撤掉的实现(PreferencesSection + id `preferences`
// order 99 + `pref.*` 词条)见 `04-调整方案/102`。
ctx.slots.inject("settings.section", function () {
return ctx.slots.register(