feat(tool-search): expand dynamic group activation

This commit is contained in:
云服务部-叶林立
2026-08-28 12:05:11 +08:00
parent bf874455db
commit 2dcec0207c
15 changed files with 897 additions and 119 deletions
+94 -15
View File
@@ -334,6 +334,17 @@ export function writeCachedCatalog(cachePath: string, catalog: ToolCatalog): voi
renameSync(temporaryPath, cachePath);
}
const GROUP_QUERY_ALIASES: Record<string, string[]> = {
"context-execution": ["context mode", "run command", "test build logs", "执行命令", "运行测试", "分析日志"],
"ssh-connection": ["ssh connect", "remote server connection", "连接服务器", "远程连接"],
"ssh-remote-shell": ["ssh shell", "remote command", "remote terminal", "远程命令", "远程终端"],
"ssh-remote-files": ["ssh file", "remote read write edit", "远程文件", "远程读写"],
"ssh-remote-search": ["ssh search", "remote find grep", "远程搜索", "远程查找"],
"chrome-navigation": ["chrome browser navigation", "browser tabs pages", "浏览器导航", "网页导航"],
"chrome-interaction": ["chrome browser interaction", "click type form", "浏览器交互", "点击输入"],
"chrome-debugging": ["chrome browser debugging", "console network screenshot", "浏览器调试", "控制台网络"],
};
function queryTerms(value: string): Set<string> {
const normalized = value.toLowerCase();
const terms = new Set(normalized.match(/[\p{L}\p{N}]+/gu) ?? []);
@@ -343,24 +354,92 @@ function queryTerms(value: string): Set<string> {
return terms;
}
export function rankGroups(catalog: ToolCatalog, query: string): Array<{ group: GroupCard; score: number }> {
const querySet = queryTerms(query);
const GENERIC_QUERY_TERMS = new Set([
"edit",
"execute",
"fetch",
"file",
"files",
"find",
"get",
"list",
"read",
"run",
"search",
"show",
"tool",
"tools",
"web",
"write",
"列出",
"写入",
"工具",
"执行",
"搜索",
"文件",
"显示",
"查找",
"获取",
"编辑",
"网页",
"读取",
"运行",
]);
function addTermWeights(target: Map<string, number>, querySet: Set<string>, value: string, weight: number): void {
const terms = queryTerms(value);
for (const term of querySet) {
if (terms.has(term)) target.set(term, Math.max(target.get(term) ?? 0, weight));
}
}
export interface RankedGroup {
group: GroupCard;
score: number;
matchedTerms: string[];
}
export function rankGroups(catalog: ToolCatalog, query: string): RankedGroup[] {
const normalizedQuery = query.trim().toLowerCase();
const querySet = queryTerms(normalizedQuery);
return catalog.groups
.map((group) => {
const cards = catalog.tools.filter((tool) => tool.primaryGroup === group.id);
const searchable = [
group.id,
group.title,
group.summary,
...group.useWhen,
...group.avoidWhen,
...cards.flatMap((tool) => [tool.name, tool.summary, ...tool.useWhen, ...tool.keywords]),
].join(" ");
const terms = queryTerms(searchable);
let score = 0;
for (const term of querySet) if (terms.has(term)) score += term.length > 1 ? 3 : 1;
if (group.id === query.trim().toLowerCase()) score += 100;
return { group, score };
const aliases = GROUP_QUERY_ALIASES[group.id] ?? [];
const termWeights = new Map<string, number>();
addTermWeights(termWeights, querySet, group.id, 8);
addTermWeights(termWeights, querySet, group.title, 6);
addTermWeights(termWeights, querySet, group.summary, 2);
addTermWeights(termWeights, querySet, group.useWhen.join(" "), 3);
addTermWeights(termWeights, querySet, group.avoidWhen.join(" "), 1);
addTermWeights(termWeights, querySet, aliases.join(" "), 5);
for (const card of cards) {
addTermWeights(termWeights, querySet, card.name, 10);
addTermWeights(termWeights, querySet, card.summary, 2);
addTermWeights(termWeights, querySet, card.useWhen.join(" "), 3);
addTermWeights(termWeights, querySet, card.keywords.join(" "), 5);
}
let score = [...termWeights].reduce(
(total, [term, weight]) => total + (term.length > 1 ? 3 : 1) * weight,
0,
);
if (cards.some((card) => normalizedQuery === card.name.toLowerCase())) score += 800;
if (group.id === normalizedQuery) score += 1_000;
if (aliases.some((alias) => alias.toLowerCase() === normalizedQuery)) score += 500;
return { group, score, matchedTerms: [...termWeights.keys()] };
})
.sort((left, right) => right.score - left.score || left.group.id.localeCompare(right.group.id));
}
/** Avoid auto-activating a generic, weak, or ambiguous natural-language match. */
export function selectConfidentGroup(ranked: RankedGroup[], _query: string): GroupCard | undefined {
const [first, second] = ranked;
if (!first || first.score < 24) return undefined;
if (first.score >= 500) return first.group;
const informativeTerms = first.matchedTerms.filter(
(term) => term.length > 1 && !GENERIC_QUERY_TERMS.has(term),
);
if (informativeTerms.length === 0) return undefined;
if (second && first.score - second.score < 12) return undefined;
return first.group;
}