iter(g176): 支持按参数说明检索组件

This commit is contained in:
HIsheR
2026-10-26 16:26:53 +08:00
parent 31650b1275
commit cc9ec02c94
5 changed files with 44 additions and 27 deletions
+37 -20
View File
@@ -203,25 +203,39 @@ function nearIds(name) {
.map(([id]) => id);
}
/*** 按关键词搜组件:多词取最弱命中(AND 语义),id 命中优先于 title,其次 props 名与步骤文案;未命中退出码 1 ***/
/*** 组件的可检索字段:权重从高到低(id > 标题 > 参数名 > 参数说明 > 步骤),search 与 MCP 共用一份 ***/
function searchFields(comp) {
const props = comp.props ?? [];
return [
{ label: 'id', weight: 3, text: String(comp.id ?? '') },
{ label: '标题', weight: 2, text: String(comp.title ?? '') },
{ label: '参数', weight: 1, text: props.map((p) => String(p.n ?? '')).join(' ') },
{ label: '参数说明', weight: 0.8, text: props.map((p) => String(p.d ?? '')).join(' ') },
{ label: '步骤', weight: 0.5, text: (comp.steps ?? []).join(' ') },
].map((f) => ({ label: f.label, weight: f.weight, text: f.text.toLowerCase() }));
}
/*** 单个关键词在组件上的最佳命中:返回最高档字段(id 全等额外抬到最前),未命中 score 为 0 ***/
function matchOf(kw, comp) {
let best = { score: 0, label: '' };
for (const f of searchFields(comp)) {
if (!f.text.includes(kw)) continue;
const score = f.label === 'id' && f.text === kw ? 4 : f.weight; // id 全等单独一档,避免被 id 含稀释
if (score > best.score) best = { score, label: f.label };
}
return best;
}
/*** 按关键词搜组件:多词取最弱命中(AND 语义),并标出该词命中的字段,便于判断为何被召回 ***/
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 scores = kws.map((kw) => scoreOf(kw, comp));
return scores.every((s) => s > 0) ? { pkg, comp, score: Math.min(...scores) } : null; // 任一词不中即淘汰
const ms = kws.map((kw) => ({ kw, ...matchOf(kw, comp) }));
if (ms.some((m) => !m.score)) return null; // 任一词不中即淘汰
const weakest = ms.reduce((a, b) => (b.score < a.score ? b : a)); // 排序与提示都看最弱那一词
return { pkg, comp, score: weakest.score, field: weakest.label, keyword: weakest.kw };
})
.filter(Boolean)
.sort((a, b) => b.score - a.score || String(a.comp.id).localeCompare(String(b.comp.id)));
@@ -231,9 +245,14 @@ function cmdSearch(words = [], args = []) {
return process.exit(1);
}
if (args.includes('--json')) {
return console.log(JSON.stringify(hits.map((h) => ({ package: h.pkg.name, ...h.comp })), null, 2));
return console.log(
JSON.stringify(hits.map((h) => ({ package: h.pkg.name, ...h.comp, matched: { field: h.field, keyword: h.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}`);
}
for (const h of hits) console.log(`${String(h.comp.id).padEnd(16)} ${h.comp.title ?? ''} (${h.pkg.name})`);
console.log(`\n共 ${hits.length} 个组件命中;查参数:ua info <组件id>`);
}
@@ -583,7 +602,7 @@ function cmdMcp() {
},
{
name: 'search_components',
description: 'Search by keyword: returns matched packages plus matched components (id/title/props)',
description: 'Search by keyword: returns matched packages plus matched components (id/title/prop names/prop descriptions/steps)',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
@@ -647,9 +666,7 @@ function cmdMcp() {
)
.map((p) => ({ name: p.name, version: p.version, type: p.type, description: p.description }));
const comps = collectComponents(packages) // 组件级命中:包级 exports 只给出包名,Agent 还得再查一次
.filter(({ comp }) =>
[comp.id, comp.title, ...(comp.props ?? []).map((p) => p.n)].join(' ').toLowerCase().includes(q),
)
.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 };