build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
92 lines
6.1 KiB
JavaScript
92 lines
6.1 KiB
JavaScript
#!/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)
|