Files
dsh_shenxian/scripts/verify-portal-entry.mjs
T
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

92 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* verify-portal-entry.mjs —— `@dsh-local/portal-entry` 客户端面的**结构防线**(档案 102)
*
* 为什么需要(三条都真踩过):
* ① **v0.5.1 事故**:打出来的 tgz **漏了 `lib/index.js`(host 半边)** ⇒ 每个实例
* `ERR_MODULE_NOT_FOUND` 起不来。⇒ 本脚本把"两个半边文件都在"钉成断言。
* ② **v0.4.1 事故 / R3**:`exports.default = apply` 会让 loader 选中裸函数当插件体 ⇒
* `ctx.slots` 抛「cannot get property slots without inject」⇒ 分区**永不渲染**(静默)。
* ③ **2026-09-15 变更**:「界面语言」切换搬进本区(原在 business-plugins 的「偏好设置」分区,
* 该分区已撤)⇒ 需要钉住"语言行确实在、且真的调官方 locale API"。
*
* ⚠️ **label 存的是转义 unicode**(`"\u7528\u6237\u8bbe\u7f6e"` = 用户设置)—— 这是本 bundle 的
* 既有写法,本脚本按**转义形态**断言(按 UTF-8 字面量 grep 会永远搜不到,2026-09-15 我为此
* 绕了很久)。
*
* 用法:node scripts/verify-portal-entry.mjs 退出码 0=全绿 / 1=有失败
*/
import { readFileSync, existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const P = 'poc/portal-entry'
const read = (p) => readFileSync(join(ROOT, p), 'utf8')
let failed = 0
const ok = (name, cond, extra = '') => {
console.log((cond ? ' ✓ ' : ' ✗ ') + name + (extra ? ' ' + extra : ''))
if (!cond) failed++
}
// ── ① 源码必须在仓里(2026-09-15 之前不在 ⇒ 只能从 tgz 反推源码,排查成本极高)──
ok('源码在仓内(poc/portal-entry/lib/client.js)', existsSync(join(ROOT, P, 'lib', 'client.js')))
ok('host 半边也在(lib/index.js —— v0.5.1 事故的防线)', existsSync(join(ROOT, P, 'lib', 'index.js')))
ok('package.json 在', existsSync(join(ROOT, P, 'package.json')))
ok('cordis.patch.yml 在(bundle patch 必需)', existsSync(join(ROOT, P, 'cordis.patch.yml')))
const src = read(P + '/lib/client.js')
const pkg = JSON.parse(read(P + '/package.json'))
// ── ② 版本与元数据 ──
// ⚠️ 0.5.4 事故:目录里上一版的 `.tgz` 被一并打进产物(package/<name>-0.5.3.tgz)—— 与
// business-plugins 当年 0.2.5 完全同一个坑。防线:`.npmignore` 必须在且排除 `*.tgz`。
ok('.npmignore 存在', existsSync(join(ROOT, P, '.npmignore')))
ok('.npmignore 排除 *.tgz(防"旧产物打进新包"复发)',
existsSync(join(ROOT, P, '.npmignore')) && read(P + '/.npmignore').includes('*.tgz'))
ok('version ≥ 0.5.3(语言行引入版)', /^0\.5\.[3-9]$/.test(pkg.version) || /^0\.[6-9]/.test(pkg.version), '-> ' + pkg.version)
ok('client bundle 元数据在(dsh.client.platform = web)', pkg.dsh && pkg.dsh.client && pkg.dsh.client.platform === 'web')
// ── ③ 分区:id user-settings / order 103 / label = 用户设置(转义形态)──
ok('注册 settings.section', src.includes('settings.section'))
ok('section id = user-settings', src.includes('id: "user-settings"'))
ok('order = 103', /order:\s*103/.test(src))
ok('label = 用户设置(转义 unicode \\u7528\\u6237\\u8bbe\\u7f6e)', src.includes('\\u7528\\u6237\\u8bbe\\u7f6e'))
// ── ④ 语言切换(2026-09-15 搬入)──
ok('inject 里要了 locale 服务', /const inject = \["slots", "locale"\]/.test(src))
ok('读当前语言走官方 ctx.locale.getLocale()', /\.getLocale\(\)/.test(src))
// 2026-09-20:dsh 0.1.5-rc.1 的 `getLocale()` 返回**快照对象** {active,locales,revision}
// (0.1.2-rc.1 时代返回 id 字符串)⇒ 必须取 `.active`,否则下拉永远显示第一项、切换看着"没反应"。
ok('getLocale() 返回快照对象时取 .active(0.1.5-rc.1)', /typeof v === "object"\) v = v\.active/.test(src))
ok('切语言走官方 ctx.locale.setLocale()', /hostCtx\.locale\.setLocale\(id\)/.test(src))
ok('注册了自己的双语词典(ctx.locale.register)', /locale\.register\(PE_NS, PE_DICT\)/.test(src))
ok('语言项 en / zh 都在', src.includes('{ id: "en"') && src.includes('{ id: "zh"'))
ok('渲染了 select(界面语言)', src.includes('"aria-label": tPe("lang")'))
ok('切换后强制重渲染(setTick)', /setTick\(function \(n\)/.test(src))
{
// 语言行必须真在 rows 里(否则写了不渲染 = 用户看不到)。
// ⚠️ 断言方向:代码是 `rows.push(jsxRuntime.jsxs("div", { key: "lang", …` ⇒
// `rows.push` 在 `key: "lang"` **之前**(首跑按"之后"判,误红一次)。
const at = src.indexOf('key: "lang"')
ok('语言行确实 push 进 rows', at > 0 && src.slice(Math.max(0, at - 400), at).includes('rows.push'), '-> offset ' + at)
}
// ── ④b 语言偏好持久化(v0.5.4):调平台把选择写进 settings.yaml ──
// 官方只对 loopback 持久化 ⇒ 平台侧补一刀;断言"确实调了",防止将来被误删成"切了记不住"。
ok('切换时调平台持久化(POST /api/me/locale)', /\/api\/me\/locale/.test(src))
ok('持久化请求带 locale 字段', /JSON\.stringify\(\{ locale: id \}\)/.test(src))
ok('持久化用 credentials: "include"(跨子域带会话)', /credentials: "include"/.test(src))
ok('持久化失败不影响本次切换(catch 兜底)', /\.catch\(function \(e\) \{ console\.warn\("\[portal-entry\] 语言偏好持久化失败/.test(src))
ok('颜色硬编码(本区在 DARK 主题,v0.4.3 结论:不可用 var(--dsw-*))',
src.includes('#f9fafb') && src.includes('#43464d') && !/var\(--dsw-alias-label-primary/.test(src))
// ── ⑤ R3:不得出现 exports.default 赋值(v0.4.1 事故的根因)──
ok('未出现 exports.default 赋值(R3)', /(?:^|\n)\s*(?:module\.)?exports\.default\s*=/.test(src) === false)
ok('导出 apply + inject', /exports\.apply = apply/.test(src) && /exports\.inject = inject/.test(src))
console.log(failed === 0 ? '\n全部通过(portal-entry:结构 + 语言行 + R3 防线)' : `\n${failed} 项失败`)
process.exit(failed === 0 ? 0 : 1)