From dc6838a0c1d7a76da52ad20f0b2d41966363b3c1 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Sun, 26 Apr 2026 18:14:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=81=94=E7=BD=91=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20Tavily=20Search=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/app.js | 102 +++++++++++++++++++++++++++++++++++++++++++++---- www/index.html | 6 +-- www/style.css | 82 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 10 deletions(-) diff --git a/www/app.js b/www/app.js index b9d74fd..852921f 100644 --- a/www/app.js +++ b/www/app.js @@ -5,7 +5,10 @@ const CONFIG = { apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', apiKey: '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm', model: 'glm-4.5-air', - maxTokens: 2048 + maxTokens: 2048, + // Tavily Search API + tavilyApiUrl: 'https://api.tavily.com/search', + tavilyApiKey: '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' }; // 数据结构 @@ -760,12 +763,20 @@ async function streamGenerate(userMsgIndex) { sendBtn.disabled = true; const aiMessageIndex = currentConversation.messages.length; + const userMessage = currentConversation.messages[userMsgIndex]; - // 只有开启深度思考时才添加 thinking 字段 + // 如果开启联网搜索,先执行搜索 + let searchResults = null; + if (enableSearch && userMessage.role === 'user') { + searchResults = await performSearch(userMessage.content); + } + + // 只有开启深度思考时才添加 thinking 字段,开启搜索时添加 search_results 字段 currentConversation.messages.push({ role: 'assistant', content: '', - ...(enableThinking ? { thinking: '' } : {}) + ...(enableThinking ? { thinking: '' } : {}), + ...(searchResults ? { search_results: searchResults } : {}) }); renderMessages(); @@ -786,13 +797,25 @@ async function streamGenerate(userMsgIndex) { showStopGenerateBtn(); try { + // 构建消息数组 + let messagesToSend = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ + role: m.role, + content: m.content + })); + + // 如果有搜索结果,将搜索内容添加到消息中 + if (searchResults) { + const searchContext = formatSearchResultsForLLM(searchResults); + messagesToSend.push({ + role: 'system', + content: `以下是搜索结果,请根据这些信息回答用户问题:\n\n${searchContext}` + }); + } + // 构建请求体 - 统一使用 glm-4.5-air,通过 thinking 参数控制 const requestBody = { model: CONFIG.model, - messages: currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ - role: m.role, - content: m.content - })), + messages: messagesToSend, max_tokens: CONFIG.maxTokens, stream: true, thinking: { @@ -915,6 +938,44 @@ async function streamGenerate(userMsgIndex) { } } +// 执行 Tavily 搜索 +async function performSearch(query) { + try { + const response = await fetch(CONFIG.tavilyApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CONFIG.tavilyApiKey}` + }, + body: JSON.stringify({ + query: query, + max_results: 5, + include_raw_content: false + }) + }); + + if (!response.ok) { + console.error('搜索失败:', response.status); + return null; + } + + const data = await response.json(); + return data.results || []; + } catch (error) { + console.error('搜索错误:', error); + return null; + } +} + +// 格式化搜索结果给 LLM +function formatSearchResultsForLLM(results) { + if (!results || results.length === 0) return '无搜索结果'; + + return results.map((r, i) => + `${i + 1}. 【${r.title}】\n来源: ${r.url}\n摘要: ${r.content || '无摘要'}\n` + ).join('\n'); +} + // 显示停止生成按钮 function showStopGenerateBtn() { // 检查是否已存在 @@ -1121,6 +1182,27 @@ function renderMessages() { `; } + // 搜索结果块(仅AI消息,放在思考块前面) + let searchHtml = ''; + if (!isUser && msg.search_results && msg.search_results.length > 0) { + searchHtml = ` +
+
+ + 搜索结果 (${msg.search_results.length}) + +
+
+ ${msg.search_results.map((r, i) => ` + + `).join('')} +
+
`; + } + const copyIcon = ``; const actions = isUser @@ -1144,6 +1226,7 @@ function renderMessages() {
${avatar}
+ ${searchHtml} ${thinkingHtml}
${contentHtml}
${actions} @@ -1171,6 +1254,11 @@ function toggleThinking(block) { block.classList.toggle('expanded'); } +// 折叠/展开搜索结果 +function toggleSearchResults(block) { + block.classList.toggle('expanded'); +} + // ==================== 工具函数 ==================== // 渲染 Markdown diff --git a/www/index.html b/www/index.html index 5a064ae..c8ea2c9 100644 --- a/www/index.html +++ b/www/index.html @@ -8,12 +8,12 @@ AI助手 - +
- - + + \ No newline at end of file diff --git a/www/style.css b/www/style.css index 96a0a67..ac4f80d 100644 --- a/www/style.css +++ b/www/style.css @@ -897,6 +897,88 @@ body { margin: 8px 0; } +/* 搜索结果块 */ +.search-results-block { + margin-bottom: 12px; + background: rgba(102, 126, 234, 0.05); + border: 1px solid rgba(102, 126, 234, 0.15); + border-radius: 10px; + overflow: hidden; + cursor: pointer; + transition: all 0.2s; +} + +.search-results-block:hover { + border-color: rgba(102, 126, 234, 0.3); +} + +.search-results-header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: rgba(102, 126, 234, 0.1); + font-size: 13px; + color: var(--primary); + font-weight: 500; +} + +.search-results-header svg:first-child { + color: var(--primary); +} + +.search-results-arrow { + margin-left: auto; + transition: transform 0.2s; +} + +.search-results-block.expanded .search-results-arrow { + transform: rotate(180deg); +} + +.search-results-content { + padding: 0 12px; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, padding 0.3s ease; +} + +.search-results-block.expanded .search-results-content { + padding: 8px 12px; + max-height: 300px; +} + +.search-result-link { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; +} + +.search-result-num { + display: flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + background: var(--primary); + color: white; + border-radius: 50%; + font-size: 12px; + font-weight: 500; +} + +.search-result-link a { + color: var(--text-color); + font-size: 14px; + text-decoration: none; + transition: color 0.2s; +} + +.search-result-link a:hover { + color: var(--primary); +} + /* 输入区域 */ .input-area { display: flex;