iter(g153): ua search 支持多关键词

多词按「最弱命中」做 AND 过滤,排序仍按 id > 标题 > props > 步骤。
未命中输出相近建议并以 1 退出,脚本能直接判断成败。
This commit is contained in:
HIsheR
2026-10-26 15:53:21 +08:00
parent 313954f464
commit 06091757c7
2 changed files with 29 additions and 18 deletions
+8
View File
@@ -1580,6 +1580,14 @@
"acceptance": "ua list 每行带上组件数;ua list --components 按包分组列出 id / 标题 / 演示锚点并给出总数与后续命令提示,原有无参输出保持可用。",
"notes": "",
"status": "done"
},
{
"id": "g153",
"title": "ua search 支持多关键词与脚本退出码",
"motivation": "单一关键词只能粗筛:想找「日期区间」时搜日期会连 calendar/timeline 一起返回;且未命中仍退 0,脚本里判断不了。",
"acceptance": "ua search a b 取各词命中的最弱档做 AND 过滤仍保持 id>标题>props>步骤 的排序;未命中输出相近建议并 exit 1;无参数打印用法。",
"notes": "",
"status": "open"
}
]
}
+19 -16
View File
@@ -192,29 +192,32 @@ function nearIds(name) {
.map(([id]) => id);
}
/*** 按关键词搜组件:id 命中优先于 title,其次 props 名与步骤文案;--json 输出结构化结果 ***/
function cmdSearch(keyword = '', args = []) {
const kw = String(keyword).toLowerCase().trim();
if (!kw) return console.error('usage: ua search <关键词> [--json]');
const hits = collectComponents()
.map(({ pkg, comp }) => {
/*** 按关键词搜组件:多词取最弱命中(AND 语义),id 命中优先于 title,其次 props 名与步骤文案;未命中退出码 1 ***/
function cmdSearch(words = [], args = []) {
const kws = [...new Set(words.map((w) => String(w).toLowerCase().trim()).filter(Boolean))];
if (!kws.length) return console.error('usage: ua search <关键词...> [--json]');
const scoreOf = (kw, comp) => {
const id = String(comp.id ?? '').toLowerCase();
const title = String(comp.title ?? '').toLowerCase();
const props = (comp.props ?? []).map((p) => String(p.n ?? '').toLowerCase()).join(' ');
const steps = (comp.steps ?? []).join(' ').toLowerCase();
let score = 0; // 分数只用于排序:id 命中 > 标题 > props 名 > 步骤
if (id === kw) score = 4;
else if (id.includes(kw)) score = 3;
else if (title.includes(kw)) score = 2;
else if (props.includes(kw)) score = 1;
else if (steps.includes(kw)) score = 0.5;
return score ? { pkg, comp, score } : null;
if (id === kw) return 4; // 分档只用于排序:id 全等 > id 含 > 标题 > props 名 > 步骤
if (id.includes(kw)) return 3;
if (title.includes(kw)) return 2;
if (props.includes(kw)) return 1;
return steps.includes(kw) ? 0.5 : 0;
};
const hits = collectComponents()
.map(({ pkg, comp }) => {
const scores = kws.map((kw) => scoreOf(kw, comp));
return scores.every((s) => s > 0) ? { pkg, comp, score: Math.min(...scores) } : null; // 任一词不中即淘汰
})
.filter(Boolean)
.sort((a, b) => b.score - a.score || String(a.comp.id).localeCompare(String(b.comp.id)));
if (!hits.length) {
const near = nearIds(kw);
return console.error(`no match: ${keyword}${near.length ? `(相近:${near.join(' / ')})` : ''}`);
const near = nearIds(kws.join(''));
console.error(`no match: ${words.join(' ')}${near.length ? `(相近:${near.join(' / ')})` : ''}`);
return process.exit(1);
}
if (args.includes('--json')) {
return console.log(JSON.stringify(hits.map((h) => ({ package: h.pkg.name, ...h.comp })), null, 2));
@@ -1288,7 +1291,7 @@ switch (cmd) {
cmdList(args);
break;
case 'search':
cmdSearch(args[0], args.slice(1));
cmdSearch(args.filter((a) => !a.startsWith('--')), args);
break;
case 'info':
if (!args[0]) {