update at 2025-10-15 15:07:45
This commit is contained in:
@@ -58,6 +58,9 @@
|
||||
<n-tag v-if="msg.status === 'sending'" type="info" size="small">
|
||||
发送中...
|
||||
</n-tag>
|
||||
<n-tag v-else-if="msg.status === 'paused'" type="warning" size="small">
|
||||
已停止
|
||||
</n-tag>
|
||||
<n-tag v-else-if="msg.status === 'error'" type="error" size="small">
|
||||
发送失败
|
||||
</n-tag>
|
||||
@@ -75,7 +78,7 @@
|
||||
{{ msg.error }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.role === 'assistant' && msg.status === 'success'" class="message-actions">
|
||||
<div v-if="msg.role === 'assistant' && (msg.status === 'success' || msg.status === 'paused')" class="message-actions">
|
||||
<n-button text size="tiny" @click="handleCopyMessage(msg.content)">
|
||||
<n-icon :component="CopyIcon" size="14" />
|
||||
复制
|
||||
@@ -147,15 +150,14 @@
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<!-- 发送/停止按钮 -->
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="!inputText.trim() || store.state.isSending"
|
||||
:loading="store.state.isSending"
|
||||
@click="handleSendMessage"
|
||||
:type="store.state.isSending ? 'error' : 'primary'"
|
||||
:disabled="!store.state.isSending && !inputText.trim()"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
确认
|
||||
{{ store.state.isSending ? '停止' : '发送' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,6 +587,22 @@ const handleSelectMCP = (key: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的按钮点击处理(参考 cherry-studio 的 PAUSED 状态逻辑)
|
||||
const handleButtonClick = () => {
|
||||
if (store.state.isSending) {
|
||||
handleStopGeneration()
|
||||
} else {
|
||||
handleSendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// 停止生成
|
||||
const handleStopGeneration = () => {
|
||||
console.log('🛑 [handleStopGeneration] 用户请求停止生成')
|
||||
store.stopGeneration()
|
||||
message.info('已停止生成')
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputText.value.trim() || store.state.isSending) return
|
||||
|
||||
@@ -264,17 +264,31 @@ export class MCPClientService {
|
||||
const { client } = serverInfo;
|
||||
|
||||
try {
|
||||
console.log(`🔧 调用工具: ${toolName}`, parameters);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log(`🔧 [MCPClientService.callTool] 准备调用工具`)
|
||||
console.log(` - 服务器ID: ${serverId}`)
|
||||
console.log(` - 工具名称: ${toolName}`)
|
||||
console.log(` - 参数:`, JSON.stringify(parameters, null, 2))
|
||||
console.log(` - MCP协议调用: tools/call`)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
|
||||
const result = await client.call('tools/call', {
|
||||
name: toolName,
|
||||
arguments: parameters
|
||||
});
|
||||
|
||||
console.log(`✅ 工具调用成功: ${toolName}`, result);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log(`✅ [MCPClientService.callTool] 工具调用成功`)
|
||||
console.log(` - 工具名称: ${toolName}`)
|
||||
console.log(` - 返回结果:`, JSON.stringify(result, null, 2))
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`❌ 工具调用失败: ${toolName}`, error);
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.error(`❌ [MCPClientService.callTool] 工具调用失败`)
|
||||
console.error(` - 工具名称: ${toolName}`)
|
||||
console.error(` - 错误信息:`, error)
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,8 @@ class ChatService {
|
||||
async sendMessageStream(
|
||||
options: SendMessageOptions,
|
||||
onChunk: (event: StreamEvent) => void,
|
||||
mcpServerId?: string // 新增:可选的 MCP 服务器 ID
|
||||
mcpServerId?: string, // 新增:可选的 MCP 服务器 ID
|
||||
signal?: AbortSignal // 新增:取消信号
|
||||
): Promise<void> {
|
||||
const { topicId, content, role = 'user', model } = options
|
||||
|
||||
@@ -378,7 +379,8 @@ class ChatService {
|
||||
this.saveConversations()
|
||||
onChunk({ type: 'delta', content: chunk, messageId: assistantMessage.id })
|
||||
},
|
||||
mcpServerId // 传递 MCP 服务器 ID
|
||||
mcpServerId, // 传递 MCP 服务器 ID
|
||||
signal // 传递取消信号
|
||||
)
|
||||
|
||||
assistantMessage.status = 'success'
|
||||
@@ -396,14 +398,41 @@ class ChatService {
|
||||
this.saveTopics()
|
||||
}
|
||||
} catch (error) {
|
||||
assistantMessage.status = 'error'
|
||||
assistantMessage.error = error instanceof Error ? error.message : '发送失败'
|
||||
// 检查是否是用户主动取消(参考 cherry-studio 的 PAUSED 状态)
|
||||
const isAborted = error instanceof Error && error.name === 'AbortError'
|
||||
|
||||
if (isAborted) {
|
||||
// 用户主动停止,保留已生成的内容,状态标记为 paused
|
||||
console.log('⏸️ [sendMessageStream] 用户主动停止生成,保留已生成内容')
|
||||
assistantMessage.status = 'paused'
|
||||
assistantMessage.error = undefined // 清除错误信息
|
||||
} else {
|
||||
// 其他错误
|
||||
assistantMessage.status = 'error'
|
||||
assistantMessage.error = error instanceof Error ? error.message : '发送失败'
|
||||
}
|
||||
|
||||
conversation.updatedAt = new Date()
|
||||
this.conversations.set(conversation.id, conversation)
|
||||
this.saveConversations()
|
||||
onChunk({
|
||||
type: 'error',
|
||||
error: assistantMessage.error,
|
||||
messageId: assistantMessage.id
|
||||
})
|
||||
|
||||
if (isAborted) {
|
||||
onChunk({ type: 'paused', messageId: assistantMessage.id })
|
||||
// 更新话题(暂停)
|
||||
if (topic) {
|
||||
topic.messageCount = conversation.messages.length
|
||||
topic.lastMessage = this.getMessagePreview(assistantMessage.content)
|
||||
topic.updatedAt = new Date()
|
||||
this.topics.set(topicId, topic)
|
||||
this.saveTopics()
|
||||
}
|
||||
} else {
|
||||
onChunk({
|
||||
type: 'error',
|
||||
error: assistantMessage.error,
|
||||
messageId: assistantMessage.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,31 +612,57 @@ class ChatService {
|
||||
conversation: Conversation,
|
||||
model: string | undefined,
|
||||
onChunk: (chunk: string) => void,
|
||||
mcpServerId?: string // 可选的 MCP 服务器 ID
|
||||
mcpServerId?: string, // 可选的 MCP 服务器 ID
|
||||
signal?: AbortSignal // 取消信号
|
||||
): Promise<void> {
|
||||
const streamStartTime = performance.now()
|
||||
console.log('⏱️ [callModelStream] 开始真流式处理')
|
||||
|
||||
// 获取 MCP 工具列表(如果选择了 MCP 服务器)
|
||||
let tools: any[] = []
|
||||
let mcpServerName = ''
|
||||
if (mcpServerId) {
|
||||
console.log('🔧 [callModelStream] 获取 MCP 服务器工具:', mcpServerId)
|
||||
const mcpTools = this.mcpClient.getTools(mcpServerId)
|
||||
const serverInfo = this.mcpClient.getServerInfo(mcpServerId)
|
||||
mcpServerName = serverInfo?.name || 'mcp'
|
||||
console.log('🔧 [callModelStream] MCP 服务器名称:', mcpServerName)
|
||||
console.log('🔧 [callModelStream] MCP 原始工具列表:', mcpTools)
|
||||
tools = this.convertToolsToOpenAIFormat(mcpTools)
|
||||
tools = this.convertToolsToOpenAIFormat(mcpTools, mcpServerName)
|
||||
console.log('🔧 [callModelStream] 转换后的工具:', tools.length, '个', tools)
|
||||
} else {
|
||||
console.log('⚠️ [callModelStream] 未选择 MCP 服务器,不注入工具')
|
||||
}
|
||||
|
||||
// 准备消息历史
|
||||
const messages = conversation.messages
|
||||
let messages = conversation.messages
|
||||
.filter(m => m.status === 'success')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
|
||||
// 如果有工具,添加系统提示词指导 AI 使用工具
|
||||
if (tools.length > 0 && messages.length > 0 && messages[0].role !== 'system') {
|
||||
const systemPrompt = this.createSystemPromptWithTools(tools, mcpServerName)
|
||||
messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...messages
|
||||
]
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log('🎯 [callModelStream] === 完整的消息列表 ===')
|
||||
console.log(' 消息总数:', messages.length)
|
||||
messages.forEach((msg, idx) => {
|
||||
console.log(` 消息 [${idx}]:`, {
|
||||
role: msg.role,
|
||||
content: msg.content?.substring(0, 100) + (msg.content?.length > 100 ? '...' : ''),
|
||||
contentLength: msg.content?.length || 0
|
||||
})
|
||||
})
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
|
||||
// 获取已连接的服务
|
||||
const allServices = modelServiceManager.getAllServices()
|
||||
const services = allServices.filter(s => s.status === 'connected')
|
||||
@@ -673,7 +728,8 @@ class ChatService {
|
||||
onChunk(output)
|
||||
}
|
||||
},
|
||||
tools.length > 0 ? tools : undefined
|
||||
tools.length > 0 ? tools : undefined,
|
||||
signal // 传递取消信号
|
||||
)
|
||||
|
||||
// 输出剩余的缓冲区内容
|
||||
@@ -690,9 +746,21 @@ class ChatService {
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
console.log('🔍 [callModelStream] 检查工具调用:', {
|
||||
hasData: !!result.data,
|
||||
hasToolCalls: !!result.data?.toolCalls,
|
||||
toolCallsCount: result.data?.toolCalls?.length || 0,
|
||||
hasMcpServerId: !!mcpServerId,
|
||||
mcpServerId,
|
||||
toolCalls: result.data?.toolCalls
|
||||
})
|
||||
|
||||
if (result.data?.toolCalls && result.data.toolCalls.length > 0 && mcpServerId) {
|
||||
console.log('🔧 [callModelStream] 开始执行工具调用')
|
||||
await this.executeToolCalls(conversation, result.data.toolCalls, mcpServerId, model, onChunk)
|
||||
console.log('🔧 [callModelStream] 开始执行工具调用,共', result.data.toolCalls.length, '个')
|
||||
// 传递 tools 参数,让 AI 可以继续调用其他工具
|
||||
await this.executeToolCalls(conversation, result.data.toolCalls, mcpServerId, model, onChunk, tools)
|
||||
} else {
|
||||
console.log('⚠️ [callModelStream] 没有工具调用需要执行')
|
||||
}
|
||||
|
||||
const endTime = performance.now()
|
||||
@@ -821,13 +889,73 @@ class ChatService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具转换为 OpenAI 函数调用格式
|
||||
* 创建包含工具信息的系统提示词
|
||||
* @param tools OpenAI 格式的工具列表
|
||||
* @param serverName MCP 服务器名称
|
||||
*/
|
||||
private convertToolsToOpenAIFormat(mcpTools: any[]): any[] {
|
||||
private createSystemPromptWithTools(tools: any[], serverName: string): string {
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log('📝 [createSystemPromptWithTools] 开始生成 System Prompt')
|
||||
console.log(' - 服务器名称:', serverName)
|
||||
console.log(' - 工具数量:', tools.length)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
|
||||
const toolDescriptions = tools.map(tool => {
|
||||
const func = tool.function
|
||||
const params = func.parameters?.properties || {}
|
||||
const required = func.parameters?.required || []
|
||||
|
||||
console.log(` 工具: ${func.name}`)
|
||||
console.log(` 描述: ${func.description}`)
|
||||
|
||||
// 生成参数描述
|
||||
const paramDesc = Object.entries(params).map(([name, schema]: [string, any]) => {
|
||||
const isRequired = required.includes(name)
|
||||
const requiredMark = isRequired ? '[必填]' : '[可选]'
|
||||
return ` - ${name} ${requiredMark}: ${schema.description || schema.type}`
|
||||
}).join('\n')
|
||||
|
||||
return `• ${func.name}\n 描述: ${func.description}\n 参数:\n${paramDesc || ' 无参数'}`
|
||||
}).join('\n\n')
|
||||
|
||||
const systemPrompt = `你是一个智能助手,可以使用以下工具完成任务:
|
||||
|
||||
${toolDescriptions}
|
||||
|
||||
使用指南:
|
||||
1. 当用户需要完成某个任务时,请分析哪个工具最合适
|
||||
2. 如果需要发布内容(如文章、笔记等),请根据用户意图创作完整的内容
|
||||
3. 为内容生成合适的标题、正文、标签等所有必需参数
|
||||
4. 自动调用相应工具,将生成的内容作为参数传递
|
||||
5. 根据工具执行结果,给用户友好的反馈
|
||||
|
||||
注意事项:
|
||||
- **标题必须控制在20字以内**(重要!超过会导致发布失败)
|
||||
- 保持内容质量和平台特色
|
||||
- 标签要相关且有吸引力
|
||||
- 分类要准确
|
||||
- 如果工具执行失败,给出明确的错误说明和建议
|
||||
|
||||
当前连接的 MCP 服务器: ${serverName}`
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log('📝 [createSystemPromptWithTools] === System Prompt 内容 ===')
|
||||
console.log(systemPrompt)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
|
||||
return systemPrompt
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具转换为 OpenAI 函数调用格式
|
||||
* @param mcpTools MCP 工具列表
|
||||
* @param serverName 服务器名称,用于工具名称前缀
|
||||
*/
|
||||
private convertToolsToOpenAIFormat(mcpTools: any[], serverName: string): any[] {
|
||||
return mcpTools.map(tool => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
name: `${serverName}__${tool.name}`, // 添加服务器前缀避免冲突
|
||||
description: tool.description || '',
|
||||
parameters: tool.inputSchema || {
|
||||
type: 'object',
|
||||
@@ -846,7 +974,8 @@ class ChatService {
|
||||
toolCalls: any[],
|
||||
mcpServerId: string,
|
||||
model: string | undefined,
|
||||
onChunk: (chunk: string) => void
|
||||
onChunk: (chunk: string) => void,
|
||||
tools?: any[] // 添加 tools 参数
|
||||
): Promise<void> {
|
||||
console.log('🔧 [executeToolCalls] 执行', toolCalls.length, '个工具调用')
|
||||
|
||||
@@ -861,21 +990,32 @@ class ChatService {
|
||||
const toolResults = []
|
||||
for (const toolCall of toolCalls) {
|
||||
try {
|
||||
const functionName = toolCall.function.name
|
||||
const fullFunctionName = toolCall.function.name
|
||||
// 解析工具名称:serverName__toolName
|
||||
const toolName = fullFunctionName.includes('__')
|
||||
? fullFunctionName.split('__')[1]
|
||||
: fullFunctionName
|
||||
|
||||
const functionArgs = JSON.parse(toolCall.function.arguments)
|
||||
|
||||
console.log(`🔧 [executeToolCalls] 调用工具: ${functionName}`, functionArgs)
|
||||
onChunk(`\n\n🔧 正在调用工具: ${functionName}...\n`)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log(`🔧 [executeToolCalls] 工具调用详情:`)
|
||||
console.log(` - 完整工具名: ${fullFunctionName}`)
|
||||
console.log(` - 提取工具名: ${toolName}`)
|
||||
console.log(` - MCP服务器ID: ${mcpServerId}`)
|
||||
console.log(` - 参数:`, JSON.stringify(functionArgs, null, 2))
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
onChunk(`\n\n🔧 正在调用工具: ${toolName}...\n`)
|
||||
|
||||
const result = await this.mcpClient.callTool(mcpServerId, functionName, functionArgs)
|
||||
const result = await this.mcpClient.callTool(mcpServerId, toolName, functionArgs)
|
||||
|
||||
console.log(`✅ [executeToolCalls] 工具调用成功: ${functionName}`, result)
|
||||
console.log(`✅ [executeToolCalls] 工具调用成功: ${toolName}`, result)
|
||||
onChunk(`✅ 工具执行完成\n`)
|
||||
|
||||
toolResults.push({
|
||||
tool_call_id: toolCall.id,
|
||||
role: 'tool',
|
||||
name: functionName,
|
||||
name: fullFunctionName, // 保持与 AI 调用时的名称一致
|
||||
content: JSON.stringify(result)
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -925,14 +1065,24 @@ class ChatService {
|
||||
|
||||
// 向 AI 发送工具结果,获取最终回复
|
||||
console.log('🤖 [executeToolCalls] 将工具结果发送给 AI')
|
||||
console.log('🔧 [executeToolCalls] 继续传递工具列表:', tools?.length || 0, '个')
|
||||
onChunk('\n\n🤖 正在生成回复...\n')
|
||||
|
||||
await modelServiceManager.sendChatRequestStream(
|
||||
const result = await modelServiceManager.sendChatRequestStream(
|
||||
service.id,
|
||||
messages,
|
||||
selectedModel,
|
||||
onChunk
|
||||
onChunk,
|
||||
tools // ← 传递工具列表,让 AI 可以继续调用工具
|
||||
)
|
||||
|
||||
// 递归处理:如果 AI 再次调用工具,继续执行
|
||||
if (result.data?.toolCalls && result.data.toolCalls.length > 0) {
|
||||
console.log('🔁 [executeToolCalls] AI 再次调用工具,递归执行:', result.data.toolCalls.length, '个')
|
||||
await this.executeToolCalls(conversation, result.data.toolCalls, mcpServerId, model, onChunk, tools)
|
||||
} else {
|
||||
console.log('✅ [executeToolCalls] 工具调用链完成')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -293,9 +293,12 @@ export class ModelServiceManager {
|
||||
|
||||
case 'dashscope':
|
||||
return [
|
||||
'qwen3-max',
|
||||
'qwen3-vl-30b-a3b-thinking',
|
||||
'qwen3-vl-8b-thinking',
|
||||
'qwen-flash',
|
||||
'qwen-turbo-latest', // 通义千问 Turbo 最新版 - 高性价比,响应快
|
||||
'qwen-plus', // 通义千问增强版 - 推理能力强
|
||||
'qwen3-max',
|
||||
'qwen-long', // 通义千问长文本版 - 支持超长上下文(1M tokens)
|
||||
'qwen3-omni-flash' // 通义千问全能闪电版 - 多模态,极速响应
|
||||
]
|
||||
@@ -307,11 +310,9 @@ export class ModelServiceManager {
|
||||
// DeepSeek-V3 系列 - 深度思考模型
|
||||
'deepseek-v3-1-terminus', // DeepSeek V3.1 terminus版本
|
||||
'deepseek-v3-1-250821', // DeepSeek V3.1 250821版本
|
||||
|
||||
// Doubao Seed 1.6 系列 - 深度思考模型(推荐)
|
||||
'doubao-seed-1-6-flash', // 快速多模态深度思考
|
||||
'doubao-seed-1-6-vision-250815', // 多模态深度思考(图片+视频+GUI)
|
||||
'doubao-seed-1-6-250615', // 纯文本深度思考
|
||||
'doubao-seed-1-6-flash-250828', // 快速多模态深度思考
|
||||
'doubao-seed-1-6-thinking-250715', // 纯思考模型
|
||||
]
|
||||
|
||||
@@ -408,7 +409,8 @@ export class ModelServiceManager {
|
||||
messages: any[],
|
||||
model: string,
|
||||
onChunk: (chunk: string) => void,
|
||||
tools?: any[]
|
||||
tools?: any[],
|
||||
signal?: AbortSignal
|
||||
): Promise<ApiResponse<{ toolCalls?: any[] }>> {
|
||||
const startTime = performance.now()
|
||||
console.log('🚀🚀🚀 [sendChatRequestStream] === 进入流式请求方法 ===')
|
||||
@@ -431,7 +433,7 @@ export class ModelServiceManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const toolCalls = await this.makeChatRequestStream(service, messages, model, onChunk, tools)
|
||||
const toolCalls = await this.makeChatRequestStream(service, messages, model, onChunk, tools, signal)
|
||||
|
||||
const endTime = performance.now()
|
||||
console.log('⏱️ [sendChatRequestStream] 流式请求完成,总耗时:', (endTime - startTime).toFixed(2), 'ms')
|
||||
@@ -600,7 +602,8 @@ export class ModelServiceManager {
|
||||
messages: any[],
|
||||
model: string,
|
||||
onChunk: (text: string) => void,
|
||||
tools?: any[]
|
||||
tools?: any[],
|
||||
signal?: AbortSignal
|
||||
): Promise<any[] | undefined> {
|
||||
const requestStartTime = performance.now()
|
||||
|
||||
@@ -675,8 +678,9 @@ export class ModelServiceManager {
|
||||
console.log(' URL:', url)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 60000) // 流式请求60秒超时
|
||||
// 使用外部传入的 signal,或创建一个带超时的内部 controller
|
||||
const controller = signal ? null : new AbortController()
|
||||
const timeoutId = controller ? setTimeout(() => controller.abort(), 60000) : undefined // 流式请求60秒超时
|
||||
|
||||
try {
|
||||
const beforeFetch = performance.now()
|
||||
@@ -686,10 +690,10 @@ export class ModelServiceManager {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
signal: signal || controller?.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
@@ -715,6 +719,13 @@ export class ModelServiceManager {
|
||||
const toolCallsMap = new Map<number, any>()
|
||||
|
||||
while (true) {
|
||||
// 检查是否被中止(参考 cherry-studio 的实现)
|
||||
if (signal?.aborted) {
|
||||
console.log('🛑 [makeChatRequestStream] 检测到中止信号,停止读取流')
|
||||
reader.cancel()
|
||||
throw new DOMException('用户中止操作', 'AbortError')
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
@@ -753,6 +764,11 @@ export class ModelServiceManager {
|
||||
|
||||
// 处理工具调用
|
||||
if (delta?.tool_calls) {
|
||||
// 只在第一次检测到时输出日志
|
||||
if (toolCallsMap.size === 0) {
|
||||
console.log('🔧 [makeChatRequestStream] 检测到工具调用,开始收集...')
|
||||
}
|
||||
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
const index = toolCall.index
|
||||
if (!toolCallsMap.has(index)) {
|
||||
@@ -785,7 +801,18 @@ export class ModelServiceManager {
|
||||
// 收集所有工具调用
|
||||
if (toolCallsMap.size > 0) {
|
||||
collectedToolCalls = Array.from(toolCallsMap.values())
|
||||
console.log('🔧 [makeChatRequestStream] 检测到工具调用:', collectedToolCalls.length, '个')
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log('🔧 [makeChatRequestStream] 最终收集到工具调用:', collectedToolCalls.length, '个')
|
||||
collectedToolCalls.forEach((tc, idx) => {
|
||||
console.log(` 工具 [${idx}]:`, {
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments
|
||||
})
|
||||
})
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
} else {
|
||||
console.log('⚠️ [makeChatRequestStream] 没有检测到工具调用')
|
||||
}
|
||||
|
||||
const endTime = performance.now()
|
||||
@@ -795,10 +822,18 @@ export class ModelServiceManager {
|
||||
|
||||
return collectedToolCalls.length > 0 ? collectedToolCalls : undefined
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
|
||||
// 如果是 AbortError,直接抛出原始错误(可能是用户中止或超时)
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('流式请求超时(60秒)')
|
||||
console.log('⚠️ [makeChatRequestStream] 请求被中止:', error.message)
|
||||
throw error
|
||||
}
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
console.log('⚠️ [makeChatRequestStream] 请求被中止:', error.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ChatState {
|
||||
filter: TopicFilter
|
||||
isLoading: boolean
|
||||
isSending: boolean
|
||||
abortController: AbortController | null
|
||||
}
|
||||
|
||||
const state = reactive<ChatState>({
|
||||
@@ -17,7 +18,8 @@ const state = reactive<ChatState>({
|
||||
messages: [],
|
||||
filter: {},
|
||||
isLoading: false,
|
||||
isSending: false
|
||||
isSending: false,
|
||||
abortController: null
|
||||
})
|
||||
|
||||
// Getters
|
||||
@@ -94,6 +96,8 @@ export const useChatStore = () => {
|
||||
) => {
|
||||
if (!state.currentTopicId || !content.trim()) return
|
||||
|
||||
// 创建新的 AbortController
|
||||
state.abortController = new AbortController()
|
||||
state.isSending = true
|
||||
const currentTopicId = state.currentTopicId // 保存当前 ID
|
||||
|
||||
@@ -119,7 +123,8 @@ export const useChatStore = () => {
|
||||
onChunk(event.content)
|
||||
}
|
||||
},
|
||||
mcpServerId // 传递 MCP 服务器 ID
|
||||
mcpServerId, // 传递 MCP 服务器 ID
|
||||
state.abortController.signal // 传递 abort signal
|
||||
)
|
||||
|
||||
// 最终更新
|
||||
@@ -127,11 +132,31 @@ export const useChatStore = () => {
|
||||
state.messages = [...chatService.getMessages(currentTopicId)]
|
||||
}
|
||||
loadTopics()
|
||||
} catch (error: any) {
|
||||
// 如果是用户主动取消,也要更新消息列表(显示 paused 状态)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('⏸️ [sendMessageStream] 用户中止,更新消息状态')
|
||||
if (state.currentTopicId === currentTopicId) {
|
||||
state.messages = [...chatService.getMessages(currentTopicId)]
|
||||
}
|
||||
loadTopics()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
state.isSending = false
|
||||
state.abortController = null
|
||||
}
|
||||
}
|
||||
|
||||
const stopGeneration = () => {
|
||||
if (state.abortController) {
|
||||
state.abortController.abort()
|
||||
state.abortController = null
|
||||
}
|
||||
state.isSending = false
|
||||
}
|
||||
|
||||
const deleteMessage = (messageId: string) => {
|
||||
if (!state.currentTopicId) return
|
||||
chatService.deleteMessage(state.currentTopicId, messageId)
|
||||
@@ -213,6 +238,7 @@ export const useChatStore = () => {
|
||||
loadMessages,
|
||||
sendMessage,
|
||||
sendMessageStream,
|
||||
stopGeneration,
|
||||
deleteMessage,
|
||||
regenerateMessage,
|
||||
updateTopic,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// 消息角色
|
||||
export type MessageRole = 'user' | 'assistant' | 'system'
|
||||
|
||||
// 消息状态
|
||||
export type MessageStatus = 'pending' | 'sending' | 'success' | 'error'
|
||||
// 消息状态(参考 cherry-studio 的 PAUSED 状态)
|
||||
export type MessageStatus = 'pending' | 'sending' | 'success' | 'error' | 'paused'
|
||||
|
||||
// 消息
|
||||
export interface Message {
|
||||
@@ -75,9 +75,9 @@ export interface SendMessageOptions {
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
// 流式响应事件
|
||||
// 流式响应事件(添加 paused 事件类型)
|
||||
export interface StreamEvent {
|
||||
type: 'start' | 'delta' | 'end' | 'error'
|
||||
type: 'start' | 'delta' | 'end' | 'error' | 'paused'
|
||||
content?: string
|
||||
error?: string
|
||||
messageId?: string
|
||||
|
||||
Reference in New Issue
Block a user