iter(g231): search 覆盖配方域三层

This commit is contained in:
HIsheR
2026-10-26 20:46:01 +08:00
parent 051baf0e01
commit cd15c7079c
5 changed files with 51 additions and 12 deletions
+45 -6
View File
@@ -228,6 +228,23 @@ function matchOf(kw, comp) {
}
/*** 按关键词搜组件:多词取最弱命中(AND 语义),并标出该词命中的字段,便于判断为何被召回 ***/
/*** 单个配方对多关键词的命中:标题 3 / 组件 2 / 要点 1,返回最弱档(与组件搜索同一排序口径),未命中返回 null ***/
function recipeMatch(kws, r) {
const fields = [
{ label: '配方标题', weight: 3, text: String(r.title ?? '').toLowerCase() },
{ label: '配方组件', weight: 2, text: recipeComps(r).join(' ').toLowerCase() },
{ label: '配方要点', weight: 1, text: (r.notes ?? []).join(' ').toLowerCase() },
];
let weakest = null;
for (const kw of kws) {
let best = null;
for (const f of fields) if (f.text.includes(kw) && (!best || f.weight > best.weight)) best = { ...f, keyword: kw };
if (!best) return null; // 任一词不中即淘汰
if (!weakest || best.weight < weakest.weight) weakest = best;
}
return weakest;
}
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]');
@@ -240,21 +257,39 @@ function cmdSearch(words = [], args = []) {
})
.filter(Boolean)
.sort((a, b) => b.score - a.score || String(a.comp.id).localeCompare(String(b.comp.id)));
if (!hits.length) {
const recipeHits = parseRecipes() // 配方域一并搜:跨组件诉求(怎么拼)的直达载体
.map((r) => ({ r, m: recipeMatch(kws, r) }))
.filter((x) => x.m)
.sort((a, b) => b.m.weight - a.m.weight || Number(a.r.id) - Number(b.r.id));
if (!hits.length && !recipeHits.length) {
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, matched: { field: h.field, keyword: h.keyword } })), null, 2),
JSON.stringify(
{
components: hits.map((h) => ({ package: h.pkg.name, ...h.comp, matched: { field: h.field, keyword: h.keyword } })),
recipes: recipeHits.map(({ r, m }) => ({ id: r.id, title: r.title, components: recipeComps(r), matched: { field: m.label, keyword: m.keyword } })),
},
null,
2,
),
);
}
for (const h of hits) {
const why = h.field === 'id' || h.field === '标题' ? '' : ` ← ${h.field}含「${h.keyword}」`;
console.log(`${String(h.comp.id).padEnd(16)} ${h.comp.title ?? ''} (${h.pkg.name})${why}`);
}
console.log(`\n共 ${hits.length} 个组件命中;查参数:ua info <组件id>`);
for (const { r, m } of recipeHits) {
const why = m.label === '配方标题' ? '' : ` ← ${m.label}含「${m.keyword}」`;
console.log(`配方 ${r.id.padEnd(10)} ${r.title}${why} → 看骨架:ua recipes ${r.id}`);
}
const parts = [];
if (hits.length) parts.push(`${hits.length} 个组件`);
if (recipeHits.length) parts.push(`${recipeHits.length} 条配方`);
console.log(`\n共 ${parts.join('、')}命中;查参数:ua info <组件id>,看骨架:ua recipes <编号>`);
}
/*** 输出单包或单组件信息:默认人读摘要,--json 输出原始 JSON ***/
@@ -656,7 +691,7 @@ function cmdMcp() {
},
{
name: 'search_components',
description: 'Search by keyword: returns matched packages plus matched components (id/title/prop names/prop descriptions/steps)',
description: 'Search by keyword: returns matched packages, matched components (id/title/prop names/prop descriptions/steps) and matched recipes (title/components/notes)',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
@@ -720,8 +755,12 @@ function cmdMcp() {
const comps = collectComponents(packages) // 组件级命中:包级 exports 只给出包名,Agent 还得再查一次
.filter(({ comp }) => searchFields(comp).some((f) => f.text.includes(q))) // 与 CLI search 同一份字段表
.map(({ pkg, comp }) => ({ package: pkg.name, id: comp.id, title: comp.title }));
if (!pkgs.length && !comps.length) throw new Error(`no match: ${args.query}`);
return { packages: pkgs, components: comps };
const recs = parseRecipes() // 配方域:跨组件诉求(怎么拼)的直达载体,与 CLI search 同一套三层字段
.map((r) => ({ r, m: recipeMatch([q], r) }))
.filter((x) => x.m)
.map(({ r, m }) => ({ id: r.id, title: r.title, components: recipeComps(r), matched: { field: m.label, keyword: m.keyword } }));
if (!pkgs.length && !comps.length && !recs.length) throw new Error(`no match: ${args.query}`);
return { packages: pkgs, components: comps, recipes: recs };
}
if (name === 'get_tokens') {
const list = loadTokenList();