From f25723b74390c31f6dd2d347242b6004f291024d Mon Sep 17 00:00:00 2001
From: maogeigei
Date: Sun, 13 Sep 2026 19:27:30 +0800
Subject: [PATCH] =?UTF-8?q?feat(R2)+refactor:=20=E5=B9=B3=E5=8F=B0?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E5=88=86=E5=8C=BA=20+=20=E5=88=A0=203=20?=
=?UTF-8?q?=E6=A1=A9=E9=A1=B5=EF=BC=88=E6=A1=A3=E6=A1=8880=20#7=EF=BC=89+?=
=?UTF-8?q?=20=E6=97=A0=E6=B5=8F=E8=A7=88=E5=99=A8=E9=AA=8C=E6=94=B6?=
=?UTF-8?q?=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- R2:business-plugins 0.2.9 新增原生「平台管理」只读分区(已铺发到 admin/guest profile 并生效)
- 档案80 #7:删 web/{desktop,plugins,skills}.html(9→6 页)+ 5 处引用改直达 portal(nginx 已加 301)
- 新增 scripts/verify-platform-admin-section.mjs(打桩 react/jsx-runtime 真执行 client.js 做渲染断言)并接入 npm run verify
---
package.json | 2 +-
scripts/verify-platform-admin-section.mjs | 115 ++++++++++++++++++++++
src/web/routes/dsh.ts | 2 +-
web/admin.html | 2 +-
web/desktop.html | 11 ---
web/index.html | 2 +-
web/login.html | 4 +-
web/plugins.html | 11 ---
web/skills.html | 11 ---
9 files changed, 121 insertions(+), 39 deletions(-)
create mode 100644 scripts/verify-platform-admin-section.mjs
delete mode 100644 web/desktop.html
delete mode 100644 web/plugins.html
delete mode 100644 web/skills.html
diff --git a/package.json b/package.json
index 9996a45..8f7225a 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"prepare": "npm run build",
"dev": "node lib/cli.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
- "verify": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js && node scripts/verify-static.mjs",
+ "verify": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js && node scripts/verify-static.mjs && node scripts/verify-platform-admin-section.mjs",
"test": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js",
"smoke": "node scripts/smoke.mjs",
"smoke:admin": "node scripts/smoke-admin.mjs",
diff --git a/scripts/verify-platform-admin-section.mjs b/scripts/verify-platform-admin-section.mjs
new file mode 100644
index 0000000..9a97dce
--- /dev/null
+++ b/scripts/verify-platform-admin-section.mjs
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+/**
+ * verify-platform-admin-section.mjs —— R2「平台管理」分区的**无浏览器**渲染验收(档案 82)
+ *
+ * 为什么需要:本机 `agent-browser` 的 daemon 起不来(2026-09-13),浏览器截图验收受阻;
+ * 而 `06-工作台UI规范 §7.3` 三段式在 bundle **按需动态加载** 时也拿不到带 rev 的完整 URL。
+ * 做法:打桩 `react` / `react/jsx-runtime` + 最小 hooks 循环 + **真执行** client.js,
+ * 断言分区**被注册**、**admin 渲染出 6 张卡**、**非 admin 被门禁挡住**。
+ * 用法:node scripts/verify-platform-admin-section.mjs 退出码 0=全绿 / 1=有失败
+ */
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import vm from "node:vm";
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
+const src = readFileSync(join(ROOT, "poc/business-plugins/lib/client.js"), "utf8");
+let failed = 0;
+const ok = (name, cond, extra = "") => { console.log((cond ? " ✓ " : " ✗ ") + name + (extra ? " " + extra : "")); if (!cond) failed++; };
+
+let slots = [], cursor = 0, dirty = false;
+const React = {
+ useState(init) { const i = cursor++; if (!(i in slots)) slots[i] = init; return [slots[i], (v) => { slots[i] = typeof v === "function" ? v(slots[i]) : v; dirty = true; }]; },
+ useEffect(fn) { const i = cursor++; if (!slots[i]) { slots[i] = 1; fn(); } },
+};
+const jsx = (type, props) => ({ type, props: props || {} });
+const jsxRuntime = { jsx, jsxs: jsx, Fragment: "Fragment" };
+
+let def = null;
+const win = {
+ __ModuleLoader__: { load: (d) => { def = d; } },
+ location: { hostname: "admin.alotbuy.com", protocol: "https:", origin: "https://admin.alotbuy.com" },
+ open: (u) => { globalThis.__OPENED = u; },
+ document: { createElement: () => ({ setAttribute() {}, remove() {}, textContent: "" }), head: { appendChild() {} } },
+};
+let ROLE = "admin";
+const DATA = {
+ "/api/dsh/status": { running: true, instance: { port: 43095, restarts: 2 }, breaker: null },
+ "/api/admin/users": { users: [{ role: "admin" }, { role: "active" }, { role: "pending" }, { role: "disabled" }] },
+ "/api/admin/storage": { generatedAt: 1, users: [{}, {}] },
+ "/api/plugins/business": { plugins: [{}, {}, {}] },
+ "/api/admin/runtime": { items: [{}, {}, {}, {}, {}] },
+};
+const sandbox = {
+ console, setTimeout, clearTimeout,
+ JSON, Object, Promise, Buffer, URL, globalThis: undefined,
+ window: win, document: win.document,
+ fetch: async (url) => {
+ const p = String(url).replace("https://alotbuy.com", "");
+ const body = p === "/api/auth/me" ? { user: { id: "u1", username: ROLE, role: ROLE } } : DATA[p];
+ return { ok: body !== undefined, status: body === undefined ? 404 : 200, json: async () => body };
+ },
+ require: (m) => (m === "react" ? React : m === "react/jsx-runtime" ? jsxRuntime : (() => { throw new Error("require " + m); })()),
+};
+sandbox.globalThis = sandbox;
+vm.createContext(sandbox);
+vm.runInContext(src, sandbox);
+const mod = def.factory(sandbox.require);
+
+let DICT = {}, regs = [];
+const ctx = {
+ effect: (fn, name) => { try { fn(); } catch (e) { console.log(" effect 抛错:", name, e.message); } },
+ locale: { register: (ns, d) => { DICT = Object.assign({}, dICT_zh(d)); }, bind: () => (k) => DICT[k] ?? k },
+ slots: { inject: (n, fn) => fn(), register: (meta, Comp) => { regs.push({ meta, Comp }); return () => {}; } },
+};
+function dICT_zh(d) { return d.zh || {}; }
+const inj = Array.isArray(mod.inject) ? mod.inject : [];
+console.log(" inject 声明:", JSON.stringify(inj));
+mod.apply(ctx);
+console.log(" 已注册 section:", regs.map(r => r.meta.id + " / order=" + r.meta.order + " / label=" + r.meta.label()).join(" | "));
+
+function text(n, out = [], depth = 0) {
+ if (n == null || depth > 12) return out;
+ if (typeof n === "string" || typeof n === "number") { out.push(String(n)); return out; }
+ if (Array.isArray(n)) { n.forEach(x => text(x, out, depth + 1)); return out; }
+ if (typeof n === "object") {
+ if (typeof n.type === "function") { text(n.type(n.props || {}), out, depth + 1); return out; }
+ if (n.props) text(n.props.children, out, depth + 1);
+ }
+ return out;
+}
+async function render(Comp) {
+ cursor = 0; slots = [];
+ let tree = Comp();
+ for (let i = 0; i < 6; i++) { await new Promise(r => setTimeout(r, 30)); if (!dirty) break; dirty = false; cursor = 0; tree = Comp(); }
+ return tree;
+}
+for (const { meta, Comp } of regs.filter(r => r.meta.id === "platform-admin")) {
+ for (const role of ["admin", "active"]) {
+ ROLE = role;
+ delete globalThis.__OPENED;
+ const tree = await render(Comp);
+ const t = text(tree).join(" | ");
+ console.log("\n ── role=" + role + " ──");
+ console.log(" 渲染文本:", t.slice(0, 300));
+ console.log(" 含「平台管理」:", t.includes("平台管理"), "| 含 6 张卡:", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => t.includes(k)));
+ if (role === "active") console.log(" 含仅管理员说明:", t.includes("仅对管理员显示"));
+ }
+}
+
+ok("注册了两个 settings.section", regs.length === 2, "-> " + regs.map(r => r.meta.id).join(", "));
+const pa = regs.find(r => r.meta.id === "platform-admin");
+ok("存在 platform-admin 分区", !!pa);
+if (pa) ok("其 order = 102", pa.meta.order === 102);
+ROLE = "admin"; delete globalThis.__OPENED;
+const tAdmin = text(await render(pa.Comp)).join(" | ");
+ok("admin:含分区标题「平台管理」", tAdmin.includes("平台管理"));
+ok("admin:6 张卡齐全", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => tAdmin.includes(k)));
+ok("admin:含只读声明", tAdmin.includes("只读"));
+ROLE = "active";
+const tUser = text(await render(pa.Comp)).join(" | ");
+ok("非 admin:被门禁挡住", tUser.includes("仅对管理员显示"));
+ok("非 admin:不出现任何指标卡", !["候选池","运行时"].some(k => tUser.includes(k)));
+console.log(failed === 0 ? "\n结论:全绿 ✅" : "\n结论:有 " + failed + " 项失败 ❌");
+process.exit(failed === 0 ? 0 : 1);
diff --git a/src/web/routes/dsh.ts b/src/web/routes/dsh.ts
index c003903..665aea7 100644
--- a/src/web/routes/dsh.ts
+++ b/src/web/routes/dsh.ts
@@ -184,7 +184,7 @@ export const dshRoutes: FastifyPluginAsync = async (app) => {
// 所有已放行角色(admin / active)统一:已运行实例复用其 launchToken URL,
// 未运行则在 ws 根目录 launch 后返回带 token 的会话 URL。
// desktop 管理台不再作为登录落点,admin 经会话侧栏"门户/管理台"入口或直
- // 接访问 /desktop.html 进入。
+ // 接访问 /portal.html 进入。
app.post('/api/dsh/enter', { preHandler: requireAuth }, async (request, reply) => {
const user = request.user!
if (user.role !== 'active' && user.role !== 'admin') return reply.code(403).send({ error: 'not_allowed' })
diff --git a/web/admin.html b/web/admin.html
index 35c3507..3c358ba 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -40,7 +40,7 @@
diff --git a/web/desktop.html b/web/desktop.html
deleted file mode 100644
index d509014..0000000
--- a/web/desktop.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-跳转中…
-
-
-
-
-
diff --git a/web/index.html b/web/index.html
index 8354100..f24777e 100644
--- a/web/index.html
+++ b/web/index.html
@@ -24,7 +24,7 @@
} catch {
// fall through to desktop
}
- location.href = '/desktop.html'
+ location.href = '/portal.html'
})()
-
-
diff --git a/web/login.html b/web/login.html
index 6ebd83b..8c2a1dd 100644
--- a/web/login.html
+++ b/web/login.html
@@ -91,13 +91,13 @@
location.href = b.url
return
}
- // 失败兜底:admin 可手动进管理台 /desktop.html
+ // 失败兜底:admin 可手动进管理台 /portal.html
const errText =
b.error === 'instance_starting' ? '实例正在启动,请稍后重试'
: (b.error ?? 'HTTP ' + r.status)
msg.innerHTML =
'进入工作台失败:' + errText +
- (role === 'admin' ? '(前往管理台)' : '')
+ (role === 'admin' ? '(前往管理台)' : '')
setBusy(false)
} catch {
msg.textContent = '进入工作台失败:网络错误'
diff --git a/web/plugins.html b/web/plugins.html
deleted file mode 100644
index a3a7c78..0000000
--- a/web/plugins.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-