2026-09-15 21:17:12 +08:00
|
|
|
|
#!/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'
|
2026-09-19 15:42:56 +08:00
|
|
|
|
import cfg from '../config/index.cjs'
|
2026-09-15 21:17:12 +08:00
|
|
|
|
|
|
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
|
|
const SSH = process.env.DSHS_SSH ?? 'bt-server'
|
|
|
|
|
|
|
2026-09-19 15:42:56 +08:00
|
|
|
|
// 用户数据根:从 `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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-15 21:17:12 +08:00
|
|
|
|
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',
|
2026-09-19 15:42:56 +08:00
|
|
|
|
['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '-o', 'StrictHostKeyChecking=accept-new', SSH, `DSHS_USERS_DIR='${USERS_DIR}' python3 -`],
|
2026-09-15 21:17:12 +08:00
|
|
|
|
{ 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` 生成表行)。')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|