Files
dsh_shenxian/scripts/find-ui.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

107 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
/**
* 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/<name>/`,见 `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/<pkg>/`**;不在 ⇒ 先把部署中的版本取出来入仓(约定:产物只从仓内源码构建)。')
console.log(' · 改完跑 `npm run verify`(含 verify-my-skills / verify-portal-entry 等结构断言),再走投放。')
console.log(' · ⛔ 改分区 ⇒ **同一次改动里刷新 `dsh-server-docs/07-实例UI分区登记表.md`**(可用 `--md` 生成表行)。')
}
}