diff --git a/.ua/backlog.json b/.ua/backlog.json index 94f0c23..a76cc6c 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -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" } ] } diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index b48d47d..24aeccf 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -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]'); +/*** 按关键词搜组件:多词取最弱命中(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(); + 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 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; + 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]) {