#!/usr/bin/env node /** * find-ui.mjs —— 「**这个 UI 分区是谁提供的、源码在哪、我能不能改**」一条命令问清楚。 * * ## 为什么有它(2026-09-15 实录) * 为搞清设置面板里的「用户设置」是谁渲染的,我在 6 个官方包 + 3 个自研包之间逐个 grep, * 绕了约 30 次工具调用。根因**不是**代码烂,而是三件事叠加: * ① 那个 label 用**转义 unicode** 存(`"\u7528\u6237\u8bbe\u7f6e"`)⇒ 按 UTF-8 grep 永远 0 命中; * ② `portal-entry` 的**源码不在仓里**(只有服务器上的 tgz)⇒ 只能从产物反推; * ③ **没有"哪个包提供哪个 UI"的索引**,id/order/label 散在各包里。 * 本脚本一次性解决 ①③(② 的处置:把源码补进 `poc//`,见 `07-实例UI分区登记表.md`)。 * * ## 用法 * node scripts/find-ui.mjs # 列出全部实例 UI 分区(自研在前) * node scripts/find-ui.mjs 用户设置 # 按关键词定位(**UTF-8 与转义形态都搜**) * node scripts/find-ui.mjs locale --grep # 顺带打印命中行上下文 * node scripts/find-ui.mjs --md # 输出 Markdown 表格行(用于刷新 `07-实例UI分区登记表.md`) * DSHS_SSH=other-host node scripts/find-ui.mjs # 指定 ssh 目标(默认 bt-server) * * ## 判据(为什么这么做才对) * · **权威事实 = 活着的 profile 里装了什么**,不是仓里快照、不是文档库历史副本、不是备份目录; * · 标签必须**两种形态都搜**(字面量 + `\uXXXX`),否则会像 2026-09-15 那样全域 0 命中; * · 「能不能改」= 包在 `@dsh-local/`(我们自己的,可改)还是在 `@deepseek-ai/`(官方,R2 不可改)。 */ import { readFileSync } from 'node:fs' import { spawnSync } from 'node:child_process' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import cfg from '../config/index.cjs' const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') const SSH = process.env.DSHS_SSH ?? 'bt-server' // 用户数据根:从 `config/` 读(本机权威单一来源),随 ssh 命令注入远端扫描器 —— // ⛔ 扫描器内不写死服务器布局。解析不出来就**显式失败**,不猜、不静默(静默 0 命中 = 假阴性)。 const USERS_DIR = cfg.usersDir().replace(/\\/g, '/').replace(/\/+$/, '') if (!USERS_DIR.startsWith('/')) { console.error( '!! 解析不出远端用户数据根:' + USERS_DIR + '\n' + ' 本机 config/platform.env 需含 DSHS_DATA_ROOT(或用 DSHS_USERS_DIR 覆盖),见 config/README.md。', ) process.exit(2) } const argv = process.argv.slice(2) const asMd = argv.includes('--md') const withGrep = argv.includes('--grep') const keyword = argv.find((a) => !a.startsWith('--')) ?? '' // ── 1. 把扫描器喂给服务器执行(脚本不落地在服务器上)── const scanner = readFileSync(join(ROOT, 'scripts', 'find-ui-scan.py'), 'utf8') const r = spawnSync( 'ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '-o', 'StrictHostKeyChecking=accept-new', SSH, `DSHS_USERS_DIR='${USERS_DIR}' python3 -`], { input: scanner, encoding: 'utf8', maxBuffer: 1 << 24 }, ) if (r.status !== 0 && (r.stdout ?? '') === '') { console.error('!! 远程扫描失败:' + (r.stderr || '').split('\n').filter((l) => !/post-quantum|store now|openssh/.test(l)).join('\n').slice(0, 600)) process.exit(1) } const rows = (r.stdout ?? '') .split('\n') .filter((l) => l.includes('\t')) .map((l) => { const [pkg, origin, file, id, order, label] = l.split('\t') return { pkg, origin, file, id, order, label } }) // ── 2. 关键词过滤:**字面量 + 转义形态都搜**(本脚本存在的理由之一)── const escapeOf = (s) => Array.from(s).map((c) => (c.codePointAt(0) > 127 ? '\\u' + c.codePointAt(0).toString(16).padStart(4, '0') : c)).join('') const kwEsc = escapeOf(keyword).toLowerCase() const hit = (v) => { const s = String(v ?? '').toLowerCase() return s.includes(keyword.toLowerCase()) || (kwEsc !== keyword.toLowerCase() && s.includes(kwEsc)) } const shown = keyword === '' ? rows : rows.filter((x) => hit(x.id) || hit(x.label) || hit(x.pkg)) // ── 3. 输出 ── if (asMd) { console.log('| order | id | label | 提供者 | 来源 |') console.log('|---|---|---|---|---|') for (const x of shown) console.log(`| ${x.order} | \`${x.id}\` | ${x.label} | \`${x.pkg}\` | ${x.origin} |`) } else if (shown.length === 0) { console.log(`没有匹配「${keyword}」的分区。共扫到 ${rows.length} 个分区:`) for (const x of rows) console.log(` · ${x.order.toString().padStart(4)} ${x.id} [${x.label}] ${x.pkg} (${x.origin})`) console.log('\n提示:关键词会同时按 UTF-8 与 \\uXXXX 转义搜;仍找不到 ⇒ 该文案可能不在「分区」层,去 06/07 看。') } else { console.log(`命中 ${shown.length} / 共 ${rows.length} 个实例 UI 分区${keyword ? `(关键词「${keyword}」)` : ''}:\n`) for (const x of shown) { console.log(`● ${x.label} [id=${x.id} order=${x.order}]`) console.log(` 提供者:${x.pkg} (${x.origin} ⇒ ${x.origin === '自研' ? '✅ 可改' : '⛔ 官方,R2 不可改'})`) console.log(` 源码 :${x.file}`) if (withGrep) { const g = spawnSync('ssh', ['-o', 'BatchMode=yes', SSH, `grep -n "settings.section" -A 6 "${x.file}" | head -12`], { encoding: 'utf8' }) console.log((g.stdout ?? '').split('\n').map((l) => ' | ' + l).join('\n')) } } const mine = shown.filter((x) => x.origin === '自研') if (mine.length > 0) { console.log('\n下一步:') console.log(' · 自研包 ⇒ **源码应在仓内 `poc//`**;不在 ⇒ 先把部署中的版本取出来入仓(约定:产物只从仓内源码构建)。') console.log(' · 改完跑 `npm run verify`(含 verify-my-skills / verify-portal-entry 等结构断言),再走投放。') console.log(' · ⛔ 改分区 ⇒ **同一次改动里刷新 `dsh-server-docs/07-实例UI分区登记表.md`**(可用 `--md` 生成表行)。') } }