iter(g176): 支持按参数说明检索组件
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
"intro": "工程层工具包(private,不发布)。所有 Agent 与人都通过它读取组件库:清单驱动索引、文档、MCP 三件套,外加 style / verify 两道门禁。",
|
||||
"steps": [
|
||||
"装好依赖后先跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify)",
|
||||
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/props/步骤模糊找(拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
||||
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤模糊找(「处理中」这类说明里的词也能搜到,命中非 id 标题时会标出命中字段;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
||||
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
|
||||
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
|
||||
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
|
||||
],
|
||||
"components": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user