Files
memind/mindsearch-prefetch.mjs
T
John 58909a21a7 fix(search): 实时问答先预取 SearXNG,避免热榜整页截断崩溃
Portal 在 web 技能提交 Goose 前注入专用搜索结果,并要求先 tkmind_search、禁止先 fetch_url 抓热榜整页。
2026-09-12 11:04:23 +08:00

142 lines
4.2 KiB
JavaScript

import { searchSearxng } from './mindsearch-providers.mjs';
const LIVE_SEARCH_SKILLS = new Set(['web', 'search-enhanced', 'web-search']);
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
export function resolvePortalSearxngEndpoint(env = process.env, config = null) {
const raw = String(
env.TKMIND_SEARCH_SEARXNG_URL
?? config?.settings?.searxngEndpoint
?? '',
).trim();
if (!raw) return '';
try {
const url = new URL(raw);
if (url.hostname === 'host.docker.internal') url.hostname = '127.0.0.1';
return url.toString();
} catch {
return '';
}
}
export function shouldPrefetchLiveSearch({
selectedSkill = '',
suggestedSkill = '',
env = process.env,
} = {}) {
if (!envFlag(env.MEMIND_LIVE_SEARCH_PREFETCH, true)) return false;
const skill = String(selectedSkill || suggestedSkill || '').trim();
return LIVE_SEARCH_SKILLS.has(skill);
}
export function formatPrefetchedSearchContext(results = [], { query = '' } = {}) {
const rows = (Array.isArray(results) ? results : [])
.filter((item) => item?.title || item?.url)
.slice(0, 10);
if (!rows.length) return '';
const lines = [
'【联网搜索预取】系统已先调用专用联网搜索。请优先使用下列结果回答;不要向用户提及 SearXNG/DuckDuckGo 等中间件名称。',
`查询:${String(query ?? '').trim()}`,
'禁止先 fetch_url 抓取热榜/热搜整页。若需补充,先调用 tkmind_search,再按需读取个别可靠来源。',
'',
];
for (const item of rows) {
lines.push(`${item.rank ?? ''}. ${item.title || item.url}`);
if (item.url) lines.push(` URL: ${item.url}`);
if (item.snippet) lines.push(` ${item.snippet}`);
}
return lines.join('\n').trim();
}
export function appendAgentVisibleText(userMessage, extraText) {
const block = String(extraText ?? '').trim();
if (!block) return userMessage;
const content = Array.isArray(userMessage?.content) ? [...userMessage.content] : [];
const extra = `\n\n${block}`;
if (!content.length) {
return {
...userMessage,
content: [{ type: 'text', text: block }],
metadata: {
...(userMessage?.metadata ?? {}),
agentVisible: true,
},
};
}
const first = content[0];
if (typeof first === 'string') {
content[0] = `${first}${extra}`;
} else if (first?.type === 'text') {
content[0] = { ...first, text: `${String(first.text ?? '')}${extra}` };
} else {
content.push({ type: 'text', text: block });
}
return {
...userMessage,
content,
metadata: {
...(userMessage?.metadata ?? {}),
agentVisible: userMessage?.metadata?.agentVisible ?? true,
},
};
}
export async function prefetchLiveSearchResults({
query,
selectedSkill,
suggestedSkill,
env = process.env,
config = null,
searchImpl = searchSearxng,
} = {}) {
if (!shouldPrefetchLiveSearch({ selectedSkill, suggestedSkill, env })) {
return null;
}
const q = String(query ?? '').trim();
if (!q) return null;
const endpoint = resolvePortalSearxngEndpoint(env, config);
if (!endpoint) return null;
try {
const results = await searchImpl(q, {
endpoint,
limit: Number(env.TKMIND_SEARCH_MAX_RESULTS ?? 10) || 10,
timeoutMs: Number(env.TKMIND_SEARCH_TIMEOUT_MS ?? 8000) || 8000,
});
const injectionText = formatPrefetchedSearchContext(results, { query: q });
if (!injectionText) return null;
return {
provider: 'searxng',
query: q,
resultCount: Array.isArray(results) ? results.length : 0,
injectionText,
};
} catch {
return null;
}
}
export async function defaultPrefetchLiveSearch({
userMessage,
routing = null,
env = process.env,
} = {}) {
const selectedSkill = String(
userMessage?.metadata?.memindRun?.selectedChatSkill
?? userMessage?.metadata?.selectedChatSkill
?? '',
).trim();
const suggestedSkill = String(routing?.suggestedSkill ?? routing?.suggested_skill ?? '').trim();
const query = String(userMessage?.metadata?.displayText ?? '').trim();
return prefetchLiveSearchResults({
query,
selectedSkill,
suggestedSkill,
env,
});
}