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, }); }