// @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, )` 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 { var loc = hostCtx && hostCtx.locale; if (loc) { var v = null; if (typeof loc.getLocale === "function") v = loc.getLocale(); // 2026-09-20 修复:dsh 0.1.5-rc.1 的 getLocale() 返回**快照对象** // { active, locales, revision }(0.1.2-rc.1 时代返回 id 字符串)⇒ 直接 String() // 得到 "[object Object]",下拉框永远匹配不上任何 option、浏览器只显示第一项 // (English),用户选「中文」看着"没反应"。两种形态都认。 if (v && typeof v === "object") v = v.active; if (!v && typeof loc.getSnapshot === "function") { var snap = loc.getSnapshot(); if (snap && typeof snap === "object") v = snap.active; } if (v) return String(v).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; } }); ;