@@ -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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'
} ;
// 数据结构
@@ -584,11 +587,6 @@ function openConversation(id) {
thinkingBtn . addEventListener ( 'click' , ( ) => {
enableThinking = ! enableThinking ;
thinkingBtn . classList . toggle ( 'active' , enableThinking ) ;
// 如果开启深度思考,关闭联网搜索(智谱思考模型不支持联网)
if ( enableThinking && enableSearch ) {
enableSearch = false ;
searchBtn . classList . remove ( 'active' ) ;
}
} ) ;
}
@@ -596,11 +594,6 @@ function openConversation(id) {
searchBtn . addEventListener ( 'click' , ( ) => {
enableSearch = ! enableSearch ;
searchBtn . classList . toggle ( 'active' , enableSearch ) ;
// 如果开启联网搜索,关闭深度思考
if ( enableSearch && enableThinking ) {
enableThinking = false ;
thinkingBtn . classList . remove ( 'active' ) ;
}
} ) ;
}
@@ -760,12 +753,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 +787,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 : {
@@ -865,12 +878,17 @@ async function streamGenerate(userMsgIndex) {
if ( thinkingEl ) {
thinkingEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . thinking ) + '<span class="streaming-cursor">▌</span>' ;
}
// 确保思考块展开
const thinkingBlock = lastMessageEl . querySelector ( '.thinking-block' ) ;
if ( thinkingBlock && ! thinkingBlock . classList . contains ( 'expanded' ) ) {
thinkingBlock . classList . add ( 'expanded' ) ;
}
scrollToBottom ( ) ;
}
// 处理正式回复内容
if ( delta . content ) {
// 如果开启深度思考且开始输出正式内容,折叠思考块
// 如果开启深度思考且开始输出正式内容,说明思考完成,立即 折叠思考块
if ( enableThinking && ! thinkingOutputStarted && currentConversation . messages [ aiMessageIndex ] . thinking ) {
thinkingOutputStarted = true ;
// 折叠思考内容
@@ -909,12 +927,51 @@ async function streamGenerate(userMsgIndex) {
// 自动总结标题: 第一次对话和每隔5次对话
const totalMessages = currentConversation . messages . length ;
if ( totalMessages === 1 || totalMessages % 5 === 0 ) {
// 第一次对话(用户+AI=2条) 或每5次对话( 10条)
if ( totalMessages === 2 || totalMessages % 10 === 0 ) {
await generateConversationTitle ( ) ;
}
}
}
// 执行 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 : 10 ,
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 ( ) {
// 检查是否已存在
@@ -946,6 +1003,8 @@ function hideStopGenerateBtn() {
async function generateConversationTitle ( ) {
if ( ! currentConversation ) return ;
console . log ( '开始生成标题,当前消息数:' , currentConversation . messages . length ) ;
// 构建对话摘要
const conversationText = currentConversation . messages . map ( m =>
` ${ m . role === 'user' ? '用户' : 'AI' } : ${ m . content . slice ( 0 , 200 ) } `
@@ -964,24 +1023,46 @@ ${conversationText}`;
body : JSON . stringify ( {
model : CONFIG . model ,
messages : [ { role : 'user' , content : titlePrompt } ] ,
max _tokens : 50
max _tokens : 100 ,
thinking : { type : 'disabled' } // 禁用思考模式,直接输出标题
} )
} ) ;
console . log ( '标题API响应状态:' , response . status ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
console . log ( '标题API响应数据:' , data ) ;
const newTitle = data . choices ? . [ 0 ] ? . message ? . content ? . trim ( ) ;
if ( newTitle && newTitle . length > 0 && newTitle . length <= 15 ) {
currentConversation . title = newTitle ;
currentConversation . updatedAt = Date . now ( ) ;
saveConversations ( ) ;
// 更新页面标题显示
const titleEl = document . querySelector ( '.header h1' ) ;
if ( titleEl ) {
titleEl . textContent = newTitle ;
console . log ( '生成的标题:' , newTitle ) ;
if ( newTitle && newTitle . length > 0 ) {
// 去掉可能的引号和多余符号
const cleanTitle = newTitle . replace ( /^["'"']+|["'"']+$/g , '' ) . trim ( ) ;
if ( cleanTitle . length > 0 && cleanTitle . length <= 20 ) {
currentConversation . title = cleanTitle ;
currentConversation . updatedAt = Date . now ( ) ;
saveConversations ( ) ;
console . log ( '标题已保存:' , cleanTitle ) ;
// 更新页面标题显示
const titleEl = document . querySelector ( '.header h1' ) ;
if ( titleEl ) {
titleEl . textContent = cleanTitle ;
}
// 更新侧边栏标题(如果在对话列表页面)
const convItem = document . querySelector ( ` .conversation-item[data-id=" ${ currentConversation . id } "] .conv-title ` ) ;
if ( convItem ) {
convItem . textContent = cleanTitle ;
}
showToast ( '标题已更新' ) ;
}
}
} else {
const errorText = await response . text ( ) ;
console . error ( '标题API错误:' , errorText ) ;
}
} catch ( error ) {
console . error ( '生成标题失败:' , error ) ;
@@ -1109,15 +1190,41 @@ function renderMessages() {
// 思考内容块( 仅AI消息)
let thinkingHtml = '' ;
if ( ! isUser && msg . thinkin g) {
if ( ! isUser && 'thinking' in ms g) {
// 判断是否是当前正在生成的消息( 有thinking字段且正在加载)
const isGenerating = index === currentConversation . messages . length - 1 && isLoading && enableThinking ;
// 思考进行中且没有正式内容时展开
const expandedClass = isGenerating && ! msg . content ? 'expanded' : '' ;
thinkingHtml = `
<div class="thinking-block" onclick="toggleThinking(this)">
<div class="thinking-block ${ expandedClass } " onclick="toggleThinking(this)">
<div class="thinking-header">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>思考过程</span>
<svg class="thinking-arrow" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</div>
<div class="thinking-content"> ${ renderMarkdown ( msg . thinking ) } </div>
<div class="thinking-content"> ${ renderMarkdown ( msg . thinking || '思考中...' ) } </div>
</div> ` ;
}
// 搜索结果块( 仅AI消息, 放在思考块前面)
let searchHtml = '' ;
if ( ! isUser && msg . search _results && msg . search _results . length > 0 ) {
searchHtml = `
<div class="search-results-block" onclick="toggleSearchResults(this)">
<div class="search-results-header">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<span>搜索结果 ( ${ msg . search _results . length } )</span>
<svg class="search-results-arrow" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</div>
<div class="search-results-content">
${ msg . search _results . map ( ( r , i ) => `
<div class="search-result-link">
<span class="search-result-num"> ${ i + 1 } </span>
<a href=" ${ r . url } " target="_blank" rel="noopener"> ${ escapeHtml ( r . title ) } </a>
</div>
` ) . join ( '' ) }
</div>
</div> ` ;
}
@@ -1144,6 +1251,7 @@ function renderMessages() {
<div class="message ${ msg . role } " data-index=" ${ index } ">
<div class="message-avatar"> ${ avatar } </div>
<div class="message-body">
${ searchHtml }
${ thinkingHtml }
<div class="message-content"> ${ contentHtml } </div>
${ actions }
@@ -1171,6 +1279,11 @@ function toggleThinking(block) {
block . classList . toggle ( 'expanded' ) ;
}
// 折叠/展开搜索结果
function toggleSearchResults ( block ) {
block . classList . toggle ( 'expanded' ) ;
}
// ==================== 工具函数 ====================
// 渲染 Markdown