**背景**:这些改动原属本工作区另外几个会话(T01/T02 等),**那些会话已被用户删除** ⇒
工作树里的成果处于"无主"状态,一次错误 checkout / 覆盖即**永久丢失** ⇒ 代入库保全。
口径遵循本项目**先例**(`39a1f2e` / `b617cdb`:**别人的活,代入库并在提交信息里注明**)。
**内容**:档案 101「能力管理」改名 + 页内 tab 分页|档案 102 语言切换搬入「用户设置」|
`07-实例UI分区登记表.md`|`scripts/find-ui*.mjs`(UI 元素定位工具)|`poc/portal-entry/`(0.5.3)|
`src/web/locale-pref.ts` + `home-files.ts`(语言偏好持久化)|`test/locale-pref.test.mjs`|
`package.json`|`BRIEF.md` / `INDEX.md` / `docs-manifest.json` / `03-路线图与待办.md` / 档案 100 增量。
**已做最小健全性检查**(⚠️ **未跑完整构建 / 单测** —— 那是原会话的验收职责,本次只求"不丢"):
- JSON 合法:`package.json` / `poc/business-plugins/package.json` / `docs-manifest.json` ✓
- 4 个 TS 文件 `{}`/`()` 配平 ✓;新增文件均非空 ✓
- 规模:13 文件改动 +340/−210,新增 12 条
**未 push**(按 §4 提交边界:用户说"提交",未说"推送")。
95 lines
5.4 KiB
JavaScript
95 lines
5.4 KiB
JavaScript
#!/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'
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||
const SSH = process.env.DSHS_SSH ?? 'bt-server'
|
||
|
||
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, '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` 生成表行)。')
|
||
}
|
||
}
|