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:
1 parent
03c8363960
commit
3efd68517f
27 files changed
+1712
-206
No files matched your search
+107
-130
@@ -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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@dsh-local/business-plugins",
|
||||
"version": "0.3.21",
|
||||
"description": "功能管理(原「功能插件」)section for dsh web profile — v0.3.21(2026-09-15):**实例内「我的技能」分组**(档案 100 / 交接单 T01)—— 用户在同一页管理「功能插件 + 我的技能」两类功能。① **形态**:并入既有「功能管理」section 内**分组**,**不新开 section**(依据用户口径「不要分开管理」+ 档案 60 的分区命名);**仅入口层合并、机制层独立** —— 技能是 watch 驱动(启停**立即生效、无需重启**)而插件要重启实例,两条 API(`/api/skills/mine*` vs `/api/plugins/mine*`)与两套落盘位置一律不动(档案 16 §2.1 决策 2)。② **技能行**:技能名(截断三件套)+ 来源徽章(共享/我的)+ 状态(● 已启用 / ○ 已停用)+ 事实(文件数 · 体积,`tabular-nums`)+ 动作(启用/停用、删除);沿用官方卡片行语汇(动作区 `margin-left:auto` + `nowrap`)⇒ **结构上不可能换行**。③ **平台共享技能**(`source:'shared'`、`locked:true`)显示 🔒只读并**不渲染任何动作按钮** —— 与后端 `assertNotShared()` 的 409 双向一致(前端不给注定失败的按钮,后端仍兜底)。④ **上传**:虚线拖拽区 + 点击选文件(`label` 包隐藏 `input`,无需 ref),客户端先校 `.zip` / ≤150MB;**同名走两阶段**(先暂存 → 页内弹窗确认,写明「替换将删除旧技能全部文件、不可恢复」+ 旧/新文件数对照 → `apply`),与门户同语义。⑤ **删除**同样必须过页内确认(不可逆);启用/停用**可逆故不弹窗**。⑥ 失败一律**行内展示后端中文原文**(如「「x」是平台共享技能…请改用其他技能名」),不自己造词、不裸露状态码。⑦ zh/en 全量词条(`msk.*`,共 46 条/语言);新增 `scripts/verify-my-skills.mjs` 把「不换行 / 危险操作必须二次确认 / 锁定行无按钮」钉成机械断言并入 `npm run verify`。|v0.3.19(2026-09-14):**内存口径最终版 —— 基础 MIN、最多浮动到 MAX**(用户:「改成基础 min 最大可以浮动到 max」)。平台侧 = cgroup **两个参数**:`MemoryHigh=MIN`(448,软限/基础,超过即回收·限速)+ `MemoryMax=MAX`(1024,硬限/上界,越界 OOM);**与插件开关无关**(不读 profile 的 bundles)。客户端:`quotaOf()` 兜底值改为**硬顶上界**(进度条 100% 基准与「超限」判据同它),事实行改为显示「**基础 448 MiB → 最多 1024 MiB** · V8 堆 256」;`/api/dsh/status` 的 `quota` 增加 `baseMb`。|v0.3.18(2026-09-14):**内存口径修正 —— 实例配额与「插件开关」解耦**(用户:「实例内存不要受插件开关影响,只受 min 和 max 值影响」)。① 编排器 `instanceMemMb()` 改为只受 MIN/MAX 决定:`min(MIN, MAX)` = **MIN 448 MiB**(删掉 `PLUGIN_MEM_MB` 与 `BASE_MEM_MB`,不再读 profile 的 bundles);② 客户端保留 `MEM_TABLE`,但**只用于给用户看的预估**(`quotaOf()` 不再 clamp 预估值),显示「实际配额」仍优先取 `/api/dsh/status` 的真值;③ 进度条 100% 基准与确认弹窗的「超限」判据都改为**实际配额**(原为硬顶 MAX),文案改为「预估已超过实例配额(配额固定,不会因为多装插件而增加)」;④ 理由(实测):用「估计表」定「硬上限」时估偏低就真 OOM —— admin 带 mcn 后**峰值 409 MiB** 而当时上限 384 ⇒ 一分钟内被 cgroup OOM 杀 4 次、打出 crash 熔断;且配额随开关跳动会让界面「预估」与真实上限绑死。`verify-mem-model.mjs` 已换向断言(反回退:编排器不得再用插件集合算配额)。|v0.3.17(2026-09-14):**内存口径定案**(BASE 448 / MIN 448 / MAX 1024,插件成本表 univer 512、mcn 128)—— 与编排器 `instanceMemMb()` 逐值对齐(`verify-mem-model.mjs` 会红着提醒)。① BASE 160→448:0.1.5 基座**实测 312 MiB**,旧值按 0.1.2 的 106~145 估的,严重低估 ⇒ 384 的上限里只剩 72 MiB 给插件,这正是宿主 swap 被吃掉的原因;② **MIN 必须 ≥ BASE**(否则 `clamp(raw, MIN, MAX)` 的下限失去意义)⇒ MIN 384→448;③ **MAX 保持 1024**(不是偏好:编排器头部已写「上限 1024M 是单实例硬顶(宿主 1870M)」这一不变量,在途草稿的 1536 与宿主容量前提自相矛盾,已驳回);④ mcn 套件**保持 128**:无可支撑抬高的实测,反向有证据(装了它的用户在旧 384 上限下一直跑得住 ⇒ 需求 ≤384),而抬高会直接吃宿主余量(1870M / 可用 ≈761M / swap 已用 452M)。|v0.3.16(2026-09-14):**官方推荐插件列表按 dsh 版本过滤**(用户:「插件管理中的官方推荐插件列表 只能显示匹配当前dsh版本 和 超过当前版本的插件(超过的要Line truncated
|
||||
"version": "0.3.23",
|
||||
"description": "能力管理(原「功能管理」/「功能插件」)section for dsh web profile — v0.3.23(2026-09-15):**撤除「偏好设置」分区**(用户:「把偏好设置中的 语言切换功能放到用户设置中,去掉偏好设置这个栏目」)。语言切换搬到 `@dsh-local/portal-entry` 的「用户设置」分区(id `user-settings`,order 103)—— 语言是**用户级偏好**,与「账号 / 退出登录」同区即可,再单开一个分区就是**重复入口**。撤掉:`PreferencesSection` 整段、`ctx.slots.inject` 里 id `preferences` order 99 的注册、zh/en 各 3 条 `pref.*` 词条 ⇒ 本 section 现只剩 **2 个分区**(能力管理 101 + 模型设置 100)。配套:`verify-platform-admin-section.mjs` 分区数断言 3 → 2 并新增「preferences 已撤除」断言;语言切换的功能断言移到新脚本 `scripts/verify-portal-entry.mjs`。|v0.3.22(2026-09-15):**分区改名「能力管理」+ 页内 tab 分页(插件 / 技能)+ 插件卡片中文用途描述三行**。① **改名**(用户:「设置中 功能管理改为能力管理」)—— **只改 label 级**(zh `能力管理` / en `Capability management`),代码标识符 / section id / 包名 / API 路径一律不动(素材库 U10「术语统一只改用户可见那层」);② **页内 tab**(用户:「能力管理页面改造为 tab可切换 插件和技能分别进行操作」)—— 复用既有 `.pa-tabs/.pa-tab/.pa-tcnt`(下划线式;规范 §4.4「数据页用下划线式」),两个页签各带**计数**;**插件页内容原样**(内存条 / 工具行 / 卡片网格 / 应用更改),**技能页**去掉重复的分组标题(标题已由 tab 承担)只留「生效方式说明 + 上传入口」工具行;两页**各自独立操作**,默认落插件页(与改造前一致);③ **插件卡片用途描述 = 3 行**(用户:「插件卡片中增加中文用途描述 显示三行」)—— 新增 `.bp-plugDesc`(`-webkit-line-clamp:3` + 显式 `white-space:normal`,否则继承单行截断的 nowrap),单行 ellipsis 会把「这个插件干什么」截得看不全;`title` 仍挂全文。④ **「内置 DeepSeek」→「DeepSeek」**(用户:「内置 DeepSeek,去掉内置就叫 DeepSeek 就行」)—— 仍只动**用户可见文案**:行标题 `ms.kindBuiltin`、新增下拉选项 `ms.optBuiltin`(原「内置 DeepSeek(平台共享)」,那个「(平台共享)」是**误导**:该选项走内置通道但**要填用户自己的 key**)、选择提示 `ms.builtinPickHint`、目录不可用时的兜底文案(`ms.catUnreadable`/`ms.catEmpty`)、以及种类小标 `ms.tagBuiltin`「内置」→「官方」+ `ms.builtinHint`「平台内置通道」→「官方通道」;⛔ **词典键名与代码标识符一律不动**(U10)。|v0.3.21(2026-09-15):**实例内「我的技能」分组**(档案 100 / 交接单 T01)—— 用户在同一页管理「功能插件 + 我的技能」两类功能。① **形态**:并入既有「功能管理」section 内**分组**,**不新开 section**(依据用户口径「不要分开管理」+ 档案 60 的分区命名);**仅入口层合并、机制层独立** —— 技能是 watch 驱动(启停**立即生效、无需重启**)而插件要重启实例,两条 API(`/api/skills/mine*` vs `/api/plugins/mine*`)与两套落盘位置一律不动(档案 16 §2.1 决策 2)。② **技能行**:技能名(截断三件套)+ 来源徽章(共享/我的)+ 状态(● 已启用 / ○ 已停用)+ 事实(文件数 · 体积,`tabular-nums`)+ 动作(启用/停用、删除);沿用官方卡片行语汇(动作区 `margin-left:auto` + `nowrap`)⇒ **结构上不可能换行**。③ **平台共享技能**(`source:'shared'`、`locked:true`)显示 🔒只读并**不渲染任何动作按钮** —— 与后端 `assertNotShared()` 的 409 双向一致(前端不给注定失败的按钮,后端仍兜底)。④ **上传**:虚线拖拽区 + 点击选文件(`label` 包隐藏 `input`,无需 ref),客户端先校 `.zip` / ≤150MB;**同名走两阶段**(先暂存 → 页内弹窗确认,写明「替换将删除旧技能全部文件、不可恢复」+ 旧/新文件数对照 → `apply`),与门户同语义。⑤ **删除**同样必须过页内确认(不可逆);启用/停用**可逆故不弹窗**。⑥ 失败一律**行内展示后端中文原文**(如「「x」是平台共享技能…请改用其他技能名」),不自己造词、不裸露状态码。⑦ zh/en 全量词条(`msk.*`);`scripts/verify-my-skills.mjs` 把「不换行 / 危险操作必须二次确认 / 锁定行无按钮 / tab 分页」钉成机械断言并入 `npm run verify`。|v0.3.19(2026-09-14):**内存口径最终版 —— 基础 MIN、最多浮动到 MAX**(用户:「改成基础 min 最大可以浮动Line truncated
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 打包排除项(2026-09-15 加)
|
||||
#
|
||||
# 背景:0.5.4 打包时,目录里上一版的 dsh-local-portal-entry-0.5.3.tgz 被一并打进了产物
|
||||
# (package/dsh-local-portal-entry-0.5.3.tgz)—— 与 business-plugins 当年 0.2.5 的坑**完全同一个**
|
||||
# (见该包 .npmignore 的注释)。用忽略规则根治,而不是每次记得先手工删。
|
||||
*.tgz
|
||||
*.log
|
||||
*.tmp
|
||||
.DS_Store
|
||||
@@ -0,0 +1,14 @@
|
||||
# @dsh-local/portal-entry — PoC bundle patch (host row only, v1)
|
||||
#
|
||||
# A bundle patch is a YAML list of patch operations. This layer inserts a
|
||||
# single host row into the profile cordis tree. Row shape mirrors official
|
||||
# rows (see dsh-web-app/cordis.patch.yml): id + name (package module) + config.
|
||||
#
|
||||
# v1: no inject (defensive). v0.1.1: row declares `inject: [tools]` — cordis
|
||||
# then starts this row only after the `tools` service exists, so apply() may
|
||||
# use ctx.tools directly (mirrors official rows like webserver -> webStartup).
|
||||
|
||||
- insert:
|
||||
- id: portal-entry
|
||||
name: '@dsh-local/portal-entry'
|
||||
inject: [tools]
|
||||
@@ -0,0 +1,329 @@
|
||||
// @dsh-local/portal-entry — client half (dsh 0.1.2-rc.1, v0.5.0)
|
||||
//
|
||||
// dsh client bundles are executed as plain scripts that register a factory
|
||||
// with window.__ModuleLoader__; materialization (factory(require) -> exports)
|
||||
// happens lazily on first import by the browser module system. The require
|
||||
// handed to the factory resolves platform seed words (react/jsx-runtime) and
|
||||
// graph rows (other dsh.client packages). Mirror the shipped shape of official
|
||||
// client bundles exactly (see @deepseek-ai/dsh-client-ui-brand-official
|
||||
// lib/client.js): CJS-style factory, named `apply` export.
|
||||
//
|
||||
// v0.3.x saga: footer.action (kind:list) registrations were served with the
|
||||
// correct bundle yet never rendered by the sidebar shell (footerActions stayed
|
||||
// empty even for the official cordis-panel entry) — the slot appears unused by
|
||||
// the 0.1.2-rc.1 sidebar consumer. Abandoned.
|
||||
//
|
||||
// v0.4.0: the SETTINGS panel, in contrast, is proven: official sections
|
||||
// (general / models / plugins) register into `settings.section` (kind:list,
|
||||
// scope root, declared by dsh-client-ui-settings-general's SettingsRoot) and
|
||||
// DO render in the left nav.
|
||||
//
|
||||
// v0.4.1: BROWSER-VERIFIED root cause for the section never rendering despite
|
||||
// `[portal-entry] apply RUN`: the extra `exports.default = apply` made the
|
||||
// loader's ESM/CJS interop pick the bare `apply` FUNCTION as the plugin body
|
||||
// (function form has no inject declaration site), so ctx.slots threw
|
||||
// `cannot get property "slots" without inject`. Official bundles export ONLY
|
||||
// `apply` + `inject` — no `default`. R3 red line: never reintroduce it.
|
||||
//
|
||||
// v0.4.2 / v0.4.3: the settings panel runs on the DARK theme, where
|
||||
// `--dsw-alias-label-primary` AND `--dsw-alias-button-elevated-fill` both
|
||||
// resolve to #f9fafb, so `var(--name, <fallback>)` always took the variable
|
||||
// value (light text on light background -> invisible). v0.4.3 HARDCODES the
|
||||
// colors instead of relying on theme variables. Keep that: do not reintroduce
|
||||
// var() for contrast-critical props.
|
||||
//
|
||||
// v0.5.0 (2026-09-11):
|
||||
// · section renamed 「平台管理」→「用户管理」(id `platform` →
|
||||
// `user-management`) and moved BELOW 「功能插件」: order 100 → 102
|
||||
// (business-plugins registers the feature-plugins section at order 101;
|
||||
// official sections use models=10 / plugins=15, so 102 is last).
|
||||
// · the section is now shown to EVERY user, not only admins — therefore the
|
||||
// platform must install this bundle into every profile (see
|
||||
// scripts/ensure-portal-entry.cjs + /usr/local/bin/provision-new-users.sh).
|
||||
// An admin-only install would leave ordinary users with no logout entry.
|
||||
// · content is role-aware: admins see the portal origin + 「打开管理台」;
|
||||
// non-admins see 「退出登录」 only. Role comes from the portal's
|
||||
// `GET /api/auth/me` (requireAuth) over the shared `.alotbuy.com` session
|
||||
// cookie with credentials:"include" — the same cross-subdomain mechanism
|
||||
// business-plugins already uses for /api/plugins/mine (portal CORS allows
|
||||
// baseDomain + subdomains). On ANY failure we fall back to the non-admin
|
||||
// view: fail-closed, never advertise a management page the viewer cannot
|
||||
// open.
|
||||
|
||||
// v0.5.3 (2026-09-15 · 用户要求): 「界面语言」切换**搬进本区**(用户设置),同时撤掉
|
||||
// business-plugins 的「偏好设置」分区 —— 语言是"用户级偏好",与账号 / 退出登录同属本区,
|
||||
// 单独再开一个分区就是重复入口。实现走官方扩展点(零官方改动):
|
||||
// `ctx.locale.getLocale()` 读、`ctx.locale.setLocale(id)` 切、`ctx.locale.register(NS,{zh,en})`
|
||||
// 提供本行自己的双语文案 ⇒ `inject` 相应加上 `"locale"`。
|
||||
// ⚠️ 颜色沿用本文件硬编码:本区渲染在 DARK 主题(见 v0.4.2/v0.4.3 的结论)。
|
||||
|
||||
window.__ModuleLoader__.load({
|
||||
id: "@dsh-local/portal-entry",
|
||||
factory: (require) => {
|
||||
var module = { exports: {} };
|
||||
var exports = module.exports;
|
||||
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||
var jsxRuntime = require("react/jsx-runtime");
|
||||
var React = require("react");
|
||||
|
||||
/**
|
||||
* 宿主 ctx(在 `apply(ctx)` 里赋值)—— 组件内要读官方 locale 服务,而组件是
|
||||
* 顶层函数、拿不到 apply 的入参,所以存在这个闭包变量里。
|
||||
*/
|
||||
var hostCtx = null;
|
||||
/** 官方 `dsh-client-locale` 的 LOCALE_IDS(`en` / `zh`);`en` = 官方 FALLBACK。 */
|
||||
var LOCALES = [{ id: "en", name: "English" }, { id: "zh", name: "中文" }];
|
||||
/** 本 bundle 自己的词典(走官方 locale 扩展点,零官方改动)。 */
|
||||
var PE_NS = "@dsh-local/portal-entry";
|
||||
var PE_DICT = {
|
||||
zh: { lang: "界面语言", langHint: "切换后立即生效" },
|
||||
en: { lang: "Interface language", langHint: "Applies immediately" }
|
||||
};
|
||||
|
||||
/** 当前界面语言:优先问官方 locale;拿不到就退回官方 FALLBACK `en`。 */
|
||||
function currentLocale() {
|
||||
try {
|
||||
if (hostCtx && hostCtx.locale && typeof hostCtx.locale.getLocale === "function") {
|
||||
var id = hostCtx.locale.getLocale();
|
||||
if (id) return String(id).split("-")[0];
|
||||
}
|
||||
} catch (e) { /* 官方 API 变动 ⇒ 只影响本行展示,不让整个分区崩掉 */ }
|
||||
return "en";
|
||||
}
|
||||
|
||||
/** 切语言:官方 API 立即生效 **+** 让平台把选择写进 `settings.yaml`(跨页面 / 跨重启保留)。 */
|
||||
function pickLocale(id) {
|
||||
if (!id || id === currentLocale()) return;
|
||||
try {
|
||||
if (hostCtx && hostCtx.locale && typeof hostCtx.locale.setLocale === "function") {
|
||||
hostCtx.locale.setLocale(id);
|
||||
} else {
|
||||
console.warn("[portal-entry] ctx.locale.setLocale 不可用,语言未切换");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[portal-entry] setLocale 失败:", e);
|
||||
}
|
||||
// ⚠️ 官方 `dsh-client-locale` **只对 loopback 页面**持久化;平台是"浏览器经域名访问远程
|
||||
// 实例" ⇒ 非 loopback ⇒ 官方只为当前进程保留选择,刷新即回默认英语。
|
||||
// 这里让平台**替官方把它自己的设置写进它自己的文件**(`locale.preference`)——
|
||||
// 官方语义不破(启动时读自己的设置即生效),用户的选择也不会丢(档案 102 §六)。
|
||||
// 持久化失败**不影响本次切换**(就地已生效)。
|
||||
try {
|
||||
fetch(portalHost() + "/api/me/locale", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ locale: id })
|
||||
}).catch(function (e) { console.warn("[portal-entry] 语言偏好持久化失败:", e); });
|
||||
} catch (e) {
|
||||
console.warn("[portal-entry] 语言偏好持久化调用失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 词典取值:官方 locale 优先(会随语言切换),否则退回自带字典。 */
|
||||
function tPe(key) {
|
||||
try {
|
||||
if (hostCtx && hostCtx.locale && typeof hostCtx.locale.bind === "function") {
|
||||
var v = hostCtx.locale.bind(PE_NS)(key);
|
||||
if (v && v !== key) return v;
|
||||
}
|
||||
} catch (e) {}
|
||||
var d = PE_DICT[currentLocale()] || PE_DICT.en;
|
||||
return d[key] || key;
|
||||
}
|
||||
|
||||
// Portal origin + management page on the registered site host minus the
|
||||
// per-user subdomain label (admin.alotbuy.com -> alotbuy.com).
|
||||
function portalHost() {
|
||||
try {
|
||||
var labels = window.location.hostname.split(".");
|
||||
if (labels.length > 2) {
|
||||
return window.location.protocol + "//" + labels.slice(1).join(".");
|
||||
}
|
||||
} catch (e) {}
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
function openManagement(event) {
|
||||
if (event) event.preventDefault();
|
||||
window.open(portalHost() + "/desktop.html", "_blank", "noopener");
|
||||
}
|
||||
|
||||
// 退出登录:整页跳转到门户同源 /logout(清 sid cookie + 删 session → 302 登录页)。
|
||||
// 必须整页跳转而非 fetch:浏览器侧跨子域调用门户 API 会被 CORS 拦截。
|
||||
function doLogout(event) {
|
||||
if (event) event.preventDefault();
|
||||
try {
|
||||
window.location.href = portalHost() + "/logout";
|
||||
} catch (e) {
|
||||
window.location.href = "/logout";
|
||||
}
|
||||
}
|
||||
|
||||
function rowButton(key, style, children, onClick) {
|
||||
var base = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "6px 14px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #43464d",
|
||||
background: "transparent",
|
||||
color: "#f9fafb",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px"
|
||||
};
|
||||
var merged = Object.assign(base, style || {});
|
||||
// Always force contrast-critical props after caller override so
|
||||
// any inherited theme var(--...) that the caller passed cannot
|
||||
// collapse text/background to the same value again (v0.4.3).
|
||||
if (merged.color == null || /var\(/i.test(String(merged.color))) merged.color = "#f9fafb";
|
||||
if (merged.background == null || /var\(/i.test(String(merged.background))) merged.background = "transparent";
|
||||
return jsxRuntime.jsx(
|
||||
"button",
|
||||
{
|
||||
key: key,
|
||||
type: "button",
|
||||
onClick: onClick,
|
||||
style: merged,
|
||||
children: [children]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Settings section content: user-management card.
|
||||
// role: null = still probing, "admin" = platform admin, "user" = anyone else.
|
||||
function UserManagementSection() {
|
||||
var useState = React.useState;
|
||||
var useEffect = React.useEffect;
|
||||
var state = useState(null);
|
||||
var role = state[0];
|
||||
var setRole = state[1];
|
||||
var meState = useState(null);
|
||||
var me = meState[0];
|
||||
var setMe = meState[1];
|
||||
/** 只用来在「切语言」后强制重渲染本行 —— 官方 locale 的通知不会打到这里。 */
|
||||
var tickState = useState(0);
|
||||
var setTick = tickState[1];
|
||||
|
||||
useEffect(function () {
|
||||
var alive = true;
|
||||
// 跨子域读门户会话(共享 .alotbuy.com cookie)。失败一律按非管理员渲染。
|
||||
fetch(portalHost() + "/api/auth/me", { credentials: "include" })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!alive) return;
|
||||
var r = d && d.user && d.user.role;
|
||||
setMe((d && d.user) || null);
|
||||
setRole(r === "admin" ? "admin" : "user");
|
||||
})
|
||||
.catch(function () { if (alive) setRole("user"); });
|
||||
return function () { alive = false; };
|
||||
}, []);
|
||||
|
||||
var isAdmin = role === "admin";
|
||||
var rows = [];
|
||||
if (isAdmin) {
|
||||
rows.push(jsxRuntime.jsx("div", {
|
||||
key: "origin",
|
||||
style: { fontSize: "13px", lineHeight: "20px", color: "#c9cdd4" },
|
||||
children: "账号:" + (me && me.username ? me.username : "—") + " 角色:" + (isAdmin ? "管理员" : "普通用户")
|
||||
}));
|
||||
}
|
||||
// ── 界面语言(v0.5.3 · 2026-09-15 用户要求:语言切换从 business-plugins 的
|
||||
// 「偏好设置」分区**搬到本区**,并撤掉那个分区)────────────────────────────
|
||||
// 走官方扩展点(**零官方改动**):`ctx.locale.getLocale()` 读、`setLocale(id)` 切。
|
||||
// ⚠️ 本分区渲染在 **DARK 主题** 下 ⇒ 颜色沿用本文件既有硬编码(v0.4.3 结论:
|
||||
// 这里不能依赖 `--dsw-*`,其 fallback 会让文字与底色同色而看不见)。
|
||||
rows.push(jsxRuntime.jsxs("div", {
|
||||
key: "lang",
|
||||
style: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" },
|
||||
children: [
|
||||
jsxRuntime.jsx("span", {
|
||||
key: "lb",
|
||||
style: { fontSize: "13px", lineHeight: "20px", color: "#c9cdd4" },
|
||||
children: tPe("lang")
|
||||
}),
|
||||
jsxRuntime.jsx("select", {
|
||||
key: "sel",
|
||||
"aria-label": tPe("lang"),
|
||||
value: currentLocale(),
|
||||
onChange: function (e) {
|
||||
pickLocale(e.target.value);
|
||||
// 官方 locale 的通知不会重渲染本组件 ⇒ 自己 bump 一次,让 select 立刻显示新值。
|
||||
if (typeof setTick === "function") setTick(function (n) { return n + 1; });
|
||||
},
|
||||
style: {
|
||||
minWidth: "140px", padding: "6px 8px", borderRadius: "6px", cursor: "pointer",
|
||||
border: "1px solid #43464d", background: "#1f2127", color: "#f9fafb", fontSize: "13px"
|
||||
},
|
||||
children: LOCALES.map(function (l) {
|
||||
return jsxRuntime.jsx("option", { value: l.id, children: l.name }, l.id);
|
||||
})
|
||||
}),
|
||||
jsxRuntime.jsx("span", {
|
||||
key: "hint",
|
||||
style: { fontSize: "12px", lineHeight: "18px", color: "#8b8f99" },
|
||||
children: tPe("langHint")
|
||||
})
|
||||
]
|
||||
}));
|
||||
|
||||
// 2026-09-13(用户要求):**去掉「打开管理台」入口** —— 平台管理已就地化到
|
||||
// 「平台管理」分区(见 @dsh-local/business-plugins),本区不再跳转门户。
|
||||
var actions = [];
|
||||
actions.push(rowButton("logout", { color: "#ff4d4f", borderColor: "#ff4d4f" }, "退出登录", doLogout));
|
||||
rows.push(jsxRuntime.jsx("div", {
|
||||
key: "actions",
|
||||
style: { display: "flex", gap: "8px", flexWrap: "wrap" },
|
||||
children: actions
|
||||
}));
|
||||
|
||||
return jsxRuntime.jsx("div", {
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
padding: "4px 2px",
|
||||
color: "#f9fafb"
|
||||
},
|
||||
children: rows
|
||||
});
|
||||
}
|
||||
|
||||
/** Services required by this client surface: the UI slot registry. */
|
||||
const inject = ["slots", "locale"];
|
||||
|
||||
/** Client-side apply: add the 用户管理 section to the settings panel. */
|
||||
function apply(ctx) {
|
||||
try {
|
||||
console.log("[portal-entry] apply RUN");
|
||||
hostCtx = ctx;
|
||||
// 词典注册(官方扩展点)。失败不影响分区本身。
|
||||
try {
|
||||
ctx.locale.register(PE_NS, PE_DICT);
|
||||
} catch (e) {
|
||||
console.warn("[portal-entry] locale.register 失败(本行文案退回自带字典):", e);
|
||||
}
|
||||
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
||||
{
|
||||
name: "settings.section",
|
||||
id: "user-settings",
|
||||
order: 103,
|
||||
label: () => "\u7528\u6237\u8bbe\u7f6e"
|
||||
},
|
||||
UserManagementSection
|
||||
));
|
||||
} catch (err) {
|
||||
try {
|
||||
console.warn("portal-entry: settings.section registration failed", err);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
exports.apply = apply;
|
||||
exports.inject = inject;
|
||||
return module.exports;
|
||||
}
|
||||
});
|
||||
;
|
||||
@@ -0,0 +1,101 @@
|
||||
// @dsh-local/portal-entry — PoC host plugin for the dsh web profile.
|
||||
//
|
||||
// Goal: prove the official profile-plugin mechanism on dsh 0.1.2-rc.1
|
||||
// (install -> load -> persist across restart -> remove), with zero risk to
|
||||
// the running profile: every side effect is guarded, nothing can throw out
|
||||
// of apply().
|
||||
//
|
||||
// v1 evidence channel: an append-only marker file
|
||||
// $DSH_HOME/.dsh-poc-portal-entry.log
|
||||
// written when the row is instantiated. Tool registration (defineTool from
|
||||
// @deepseek-ai/dsh-tools) is attempted defensively; failure is logged, never
|
||||
// fatal.
|
||||
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const MARKER = ".dsh-poc-portal-entry.log";
|
||||
const PORTAL_ROOT = process.env.DSH_SERVER_LOGIN_URL ?? "http://127.0.0.1:3080";
|
||||
|
||||
/** Append one line to the marker file. Never throws. */
|
||||
function log(line) {
|
||||
try {
|
||||
const home = process.env.DSH_HOME || process.env.HOME || ".";
|
||||
appendFileSync(join(home, MARKER), `${new Date().toISOString()} ${line}\n`);
|
||||
} catch {
|
||||
// marker writes must never take the profile down
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the portal_ping tool definition (lazy import of defineTool). */
|
||||
async function portalPingDefinition() {
|
||||
const { defineTool } = await import("@deepseek-ai/dsh-tools");
|
||||
return defineTool({
|
||||
name: "portal_ping",
|
||||
description:
|
||||
"PoC tool: probe the dsh-server-login portal reachable from this instance. Returns HTTP status and body size of the portal root.",
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
ok: { type: "boolean", required: true },
|
||||
status: { type: "number" },
|
||||
bytes: { type: "number" },
|
||||
note: { type: "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute() {
|
||||
try {
|
||||
const res = await fetch(PORTAL_ROOT, { redirect: "manual" });
|
||||
const body = await res.arrayBuffer();
|
||||
return {
|
||||
ok: true,
|
||||
status: res.status,
|
||||
bytes: body.byteLength,
|
||||
note: `portal root reachable from instance (${PORTAL_ROOT})`
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, note: `portal unreachable: ${String(err?.message ?? err)}` };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Cordis-style plugin apply. Row declares inject:[tools], so ctx.tools is
|
||||
* available; every failure is still contained and logged. */
|
||||
export function apply(ctx) {
|
||||
log(`apply pid=${process.pid} dshHome=${process.env.DSH_HOME ?? ""} home=${process.env.HOME ?? ""}`);
|
||||
|
||||
const register = () => {
|
||||
try {
|
||||
if (!ctx.tools || typeof ctx.tools.register !== "function") {
|
||||
log("tools service unavailable at apply time");
|
||||
return;
|
||||
}
|
||||
portalPingDefinition()
|
||||
.then((def) => {
|
||||
try {
|
||||
ctx.tools.register(def);
|
||||
log("tool portal_ping registered");
|
||||
} catch (err) {
|
||||
log(`tool register threw: ${String(err?.message ?? err)}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => log(`tool build failed: ${String(err?.message ?? err)}`));
|
||||
} catch (err) {
|
||||
log(`tools access failed: ${String(err?.message ?? err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
register();
|
||||
} catch (err) {
|
||||
log(`register wrapper failed: ${String(err?.message ?? err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default apply;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@dsh-local/portal-entry",
|
||||
"version": "0.5.5",
|
||||
"description": "portal-entry plugin for dsh web profile — v0.5.5(2026-09-15):**修打包**(0.5.4 把上一版 tgz 打进了产物 ⇒ 补 .npmignore 并升号重发;pnpm 会按版本复用同名 tgz,所以必须升号)。|v0.5.4(2026-09-15):**语言偏好持久化**。官方 `dsh-client-locale` 只对 **loopback** 页面持久化到 `$DSH_HOME/settings.yaml`;平台是「浏览器经域名访问远程实例」⇒ 非 loopback ⇒ 官方只为当前进程保留选择、刷新即回默认英语。v0.5.4 在切换时**额外调平台** `POST /api/me/locale` ⇒ 平台把 `locale.preference` 写进用户 `settings.yaml`(官方读自己的设置文件即生效),既守住官方语义、又记住用户选择;持久化失败不影响本次切换(就地已生效)。|host boot marker + portal_ping tool + settings 「用户设置」section (id user-settings, order 103; role-aware: admins see account/role, everyone sees 界面语言 + 退出登录). v0.5.3 (2026-09-15 用户要求): 「界面语言」切换**搬进本区**(原在 business-plugins 的「偏好设置」分区,该分区已撤 —— 语言是用户级偏好,与账号/退出登录同区即可,另开分区属重复入口)。实现走官方扩展点(零官方改动):ctx.locale.getLocale()/setLocale(id) + ctx.locale.register(NS,{zh,en}),`inject` 加 \"locale\";颜色沿用本文件硬编码(本区在 DARK 主题下渲染,见 v0.4.3)。v0.5.0: section renamed from 平台管理, id platform→user-management, order 100→102 (below 功能插件=101), installed for ALL users (scripts/ensure-portal-entry.cjs), role probed via portal GET /api/auth/me with fail-closed fallback to the non-admin view. v0.5.1: repack ONLY — v0.5.0's tarball was built without lib/index.js (host half), which made every instance fail with ERR_MODULE_NOT_FOUND on @dsh-local/portal-entry/lib/index.js; version bumped because pnpm reuses a same-name/same-version tarball from its cache. v0.4.3 HARDCODES colors (text #f9fafb, btn border #43464d) because both --dsw-alias-label-primary and --dsw-alias-button-elevated-fill are defined as #f9fafb in the dark theme, so var() fallbacks never engaged.",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./client": "./lib/client.js"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"platform": "web",
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-ui-settings-general"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-tools": "0.1.2-rc.1"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
Reference in new issue
Block a user