feat(web): add reasoning mode toggle

This commit is contained in:
JOJO 2025-11-18 10:12:16 +08:00
parent e1704f87ea
commit f7ce0559b7
9 changed files with 235 additions and 112 deletions

View File

@ -4,6 +4,11 @@ API_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
API_KEY = "3e96a682-919d-45c1-acb2-53bc4e9660d3" API_KEY = "3e96a682-919d-45c1-acb2-53bc4e9660d3"
MODEL_ID = "kimi-k2-250905" MODEL_ID = "kimi-k2-250905"
# 推理模型配置(智能思考模式使用)
THINKING_API_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
THINKING_API_KEY = "3e96a682-919d-45c1-acb2-53bc4e9660d3"
THINKING_MODEL_ID = "kimi-k2-250905"
# Tavily 搜索 # Tavily 搜索
TAVILY_API_KEY = "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" TAVILY_API_KEY = "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"
@ -16,10 +21,13 @@ __all__ = [
"MODEL_ID", "MODEL_ID",
"TAVILY_API_KEY", "TAVILY_API_KEY",
"DEFAULT_RESPONSE_MAX_TOKENS", "DEFAULT_RESPONSE_MAX_TOKENS",
"THINKING_API_BASE_URL",
"THINKING_API_KEY",
"THINKING_MODEL_ID",
] ]
''' '''
API_BASE_URL = "https://api.moonshot.cn/v1", API_BASE_URL = "https://api.moonshot.cn/v1"
API_KEY = "sk-xW0xjfQM6Mp9ZCWMLlnHiRJcpEOIZPTkXcN0dQ15xpZSuw2y", API_KEY = "sk-xW0xjfQM6Mp9ZCWMLlnHiRJcpEOIZPTkXcN0dQ15xpZSuw2y",
MODEL_ID = "kimi-k2-0905-preview" MODEL_ID = "kimi-k2-0905-preview"
''' '''

View File

@ -2066,6 +2066,11 @@ class MainTerminal:
if sub_agent_prompt: if sub_agent_prompt:
messages.append({"role": "system", "content": sub_agent_prompt}) messages.append({"role": "system", "content": sub_agent_prompt})
if self.thinking_mode:
thinking_prompt = self.load_prompt("thinking_mode_guidelines").strip()
if thinking_prompt:
messages.append({"role": "system", "content": thinking_prompt})
# 添加对话历史保留完整结构包括tool_calls和tool消息 # 添加对话历史保留完整结构包括tool_calls和tool消息
for conv in context["conversation"]: for conv in context["conversation"]:
metadata = conv.get("metadata") or {} metadata = conv.get("metadata") or {}

View File

@ -284,13 +284,7 @@ class WebTerminal(MainTerminal):
def get_thinking_mode_status(self) -> str: def get_thinking_mode_status(self) -> str:
"""获取思考模式状态描述""" """获取思考模式状态描述"""
if not self.thinking_mode: return "思考模式" if self.thinking_mode else "快速模式"
return "快速模式"
else:
if self.api_client.current_task_first_call:
return "思考模式(等待新任务)"
else:
return "思考模式(任务进行中)"
def get_focused_files_info(self) -> Dict: def get_focused_files_info(self) -> Dict:
"""获取聚焦文件信息用于WebSocket更新- 使用与 /api/focused 一致的格式""" """获取聚焦文件信息用于WebSocket更新- 使用与 /api/focused 一致的格式"""

View File

@ -111,6 +111,7 @@ async function bootstrapApp() {
projectPath: '', projectPath: '',
agentVersion: '', agentVersion: '',
thinkingMode: '未知', thinkingMode: '未知',
nextThinkingMode: null,
// 消息相关 // 消息相关
messages: [], messages: [],
@ -161,6 +162,7 @@ async function bootstrapApp() {
// 对话历史侧边栏 // 对话历史侧边栏
sidebarCollapsed: true, // 默认收起对话侧边栏 sidebarCollapsed: true, // 默认收起对话侧边栏
panelMode: 'files', // files | todo | subAgents panelMode: 'files', // files | todo | subAgents
panelMenuOpen: false,
subAgents: [], subAgents: [],
subAgentPollTimer: null, subAgentPollTimer: null,
conversations: [], conversations: [],
@ -253,6 +255,7 @@ async function bootstrapApp() {
document.addEventListener('click', this.handleClickOutsideSettings); document.addEventListener('click', this.handleClickOutsideSettings);
document.addEventListener('click', this.handleClickOutsideToolMenu); document.addEventListener('click', this.handleClickOutsideToolMenu);
document.addEventListener('click', this.handleClickOutsidePanelMenu);
window.addEventListener('popstate', this.handlePopState); window.addEventListener('popstate', this.handlePopState);
this.onDocumentClick = (event) => { this.onDocumentClick = (event) => {
@ -292,6 +295,7 @@ async function bootstrapApp() {
beforeUnmount() { beforeUnmount() {
document.removeEventListener('click', this.handleClickOutsideSettings); document.removeEventListener('click', this.handleClickOutsideSettings);
document.removeEventListener('click', this.handleClickOutsideToolMenu); document.removeEventListener('click', this.handleClickOutsideToolMenu);
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
window.removeEventListener('popstate', this.handlePopState); window.removeEventListener('popstate', this.handlePopState);
if (this.onDocumentClick) { if (this.onDocumentClick) {
document.removeEventListener('click', this.onDocumentClick); document.removeEventListener('click', this.onDocumentClick);
@ -590,6 +594,7 @@ async function bootstrapApp() {
this.projectPath = data.project_path || ''; this.projectPath = data.project_path || '';
this.agentVersion = data.version || this.agentVersion; this.agentVersion = data.version || this.agentVersion;
this.thinkingMode = data.thinking_mode || '未知'; this.thinkingMode = data.thinking_mode || '未知';
this.nextThinkingMode = null;
console.log('系统就绪:', data); console.log('系统就绪:', data);
// 系统就绪后立即加载对话列表 // 系统就绪后立即加载对话列表
@ -1318,7 +1323,13 @@ async function bootstrapApp() {
const statusData = await statusResponse.json(); const statusData = await statusResponse.json();
this.projectPath = statusData.project_path || ''; this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion; this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = statusData.thinking_mode || '未知'; if (statusData.thinking_mode) {
this.thinkingMode = statusData.thinking_mode.label || '未知';
this.nextThinkingMode = statusData.thinking_mode.next ?? null;
} else {
this.thinkingMode = '未知';
this.nextThinkingMode = null;
}
// 获取当前对话信息 // 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id; const statusConversationId = statusData.conversation && statusData.conversation.current_id;
@ -1629,30 +1640,34 @@ async function bootstrapApp() {
}; };
} }
// 处理思考内容 - 支持多种格式
const content = message.content || ''; const content = message.content || '';
const thinkPatterns = [ let reasoningText = (message.reasoning_content || '').trim();
/<think>([\s\S]*?)<\/think>/g,
/<thinking>([\s\S]*?)<\/thinking>/g
];
let allThinkingContent = ''; if (!reasoningText) {
for (const pattern of thinkPatterns) { const thinkPatterns = [
let match; /<think>([\s\S]*?)<\/think>/g,
while ((match = pattern.exec(content)) !== null) { /<thinking>([\s\S]*?)<\/thinking>/g
allThinkingContent += match[1].trim() + '\n'; ];
let extracted = '';
for (const pattern of thinkPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
extracted += (match[1] || '').trim() + '\n';
}
} }
reasoningText = extracted.trim();
} }
if (allThinkingContent) { if (reasoningText) {
currentAssistantMessage.actions.push({ currentAssistantMessage.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`, id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking', type: 'thinking',
content: allThinkingContent.trim(), content: reasoningText,
streaming: false, streaming: false,
timestamp: Date.now() timestamp: Date.now()
}); });
console.log('添加思考内容:', allThinkingContent.substring(0, 50) + '...'); console.log('添加思考内容:', reasoningText.substring(0, 50) + '...');
} }
// 处理普通文本内容(移除思考标签后的内容) // 处理普通文本内容(移除思考标签后的内容)
@ -1660,10 +1675,15 @@ async function bootstrapApp() {
const appendPayloadMeta = metadata.append_payload; const appendPayloadMeta = metadata.append_payload;
const modifyPayloadMeta = metadata.modify_payload; const modifyPayloadMeta = metadata.modify_payload;
let textContent = content let textContent = content;
.replace(/<think>[\s\S]*?<\/think>/g, '') if (!message.reasoning_content) {
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '') textContent = textContent
.trim(); .replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '')
.trim();
} else {
textContent = textContent.trim();
}
if (appendPayloadMeta) { if (appendPayloadMeta) {
currentAssistantMessage.actions.push({ currentAssistantMessage.actions.push({
@ -2081,6 +2101,24 @@ async function bootstrapApp() {
} }
}, },
togglePanelMenu() {
this.panelMenuOpen = !this.panelMenuOpen;
},
selectPanelMode(mode) {
if (this.panelMode === mode) {
this.panelMenuOpen = false;
return;
}
this.panelMode = mode;
this.panelMenuOpen = false;
if (mode === 'todo') {
this.fetchTodoList();
} else if (mode === 'subAgents') {
this.fetchSubAgents();
}
},
formatTaskStatus(task) { formatTaskStatus(task) {
if (!task) { if (!task) {
return ''; return '';
@ -2114,10 +2152,10 @@ async function bootstrapApp() {
return; return;
} }
const { protocol, hostname } = window.location; const { protocol, hostname } = window.location;
const base = `${protocol}//${hostname}:8092`;
const parentConv = agent.conversation_id || this.currentConversationId || ''; const parentConv = agent.conversation_id || this.currentConversationId || '';
const convSegment = this.stripConversationPrefix(parentConv); const convSegment = this.stripConversationPrefix(parentConv);
const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id; const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id;
const base = `${protocol}//${hostname}:8092`;
const pathSuffix = convSegment const pathSuffix = convSegment
? `/${convSegment}+${agentLabel}` ? `/${convSegment}+${agentLabel}`
: `/sub_agent/${agent.task_id}`; : `/sub_agent/${agent.task_id}`;
@ -2332,6 +2370,17 @@ async function bootstrapApp() {
} }
}, },
handleClickOutsidePanelMenu(event) {
if (!this.panelMenuOpen) {
return;
}
const wrapper = this.$refs.panelMenuWrapper;
if (wrapper && wrapper.contains(event.target)) {
return;
}
this.panelMenuOpen = false;
},
applyToolSettingsSnapshot(categories) { applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) { if (!Array.isArray(categories)) {
return; return;
@ -2492,7 +2541,8 @@ async function bootstrapApp() {
'todo_finish': '🏁', 'todo_finish': '🏁',
'todo_finish_confirm': '❗', 'todo_finish_confirm': '❗',
'create_sub_agent': '🤖', 'create_sub_agent': '🤖',
'wait_sub_agent': '⏳' 'wait_sub_agent': '⏳',
'close_sub_agent': '🛑'
}; };
return icons[toolName] || '⚙️'; return icons[toolName] || '⚙️';
}, },

View File

@ -128,13 +128,29 @@
<!-- 左侧文件树 --> <!-- 左侧文件树 -->
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }"> <aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
<div class="sidebar-header"> <div class="sidebar-header">
<button class="sidebar-view-toggle" <div class="panel-menu-wrapper" ref="panelMenuWrapper">
@click="cycleSidebarPanel" <button class="sidebar-view-toggle"
:title="panelMode === 'files' ? '查看待办列表' : (panelMode === 'todo' ? '查看子智能体' : '查看项目文件')"> @click.stop="togglePanelMenu"
<span v-if="panelMode === 'files'">{{ todoEmoji }}</span> title="切换侧边栏">
<span v-else-if="panelMode === 'todo'">🤖</span>
<span v-else>{{ fileEmoji }}</span> </button>
</button> <transition name="fade">
<div class="panel-menu" v-if="panelMenuOpen">
<button type="button"
:class="{ active: panelMode === 'files' }"
@click.stop="selectPanelMode('files')"
title="项目文件">📁</button>
<button type="button"
:class="{ active: panelMode === 'todo' }"
@click.stop="selectPanelMode('todo')"
title="待办列表">{{ todoEmoji }}</button>
<button type="button"
:class="{ active: panelMode === 'subAgents' }"
@click.stop="selectPanelMode('subAgents')"
title="子智能体">🤖</button>
</div>
</transition>
</div>
<button class="sidebar-manage-btn" <button class="sidebar-manage-btn"
@click="openGuiFileManager" @click="openGuiFileManager"
title="打开桌面式文件管理器"> title="打开桌面式文件管理器">
@ -524,12 +540,12 @@
:disabled="compressing || streamingMessage || !isConnected"> :disabled="compressing || streamingMessage || !isConnected">
{{ compressing ? '压缩中...' : '压缩' }} {{ compressing ? '压缩中...' : '压缩' }}
</button> </button>
<button type="button" <button type="button"
class="menu-btn clear-entry" class="menu-btn mode-entry"
@click="clearChat" @click="toggleNextThinkingMode"
:disabled="streamingMessage || !isConnected"> :disabled="streamingMessage || !isConnected">
清除 {{ nextThinkingMode ? '下一次: 思考模式' : '下一次: 快速模式' }}
</button> </button>
</div> </div>
</transition> </transition>
</div> </div>

View File

@ -492,6 +492,49 @@ body {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
} }
.panel-menu-wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
.panel-menu {
position: absolute;
left: calc(100% + 8px);
top: 0;
display: flex;
gap: 6px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(118, 103, 84, 0.2);
border-radius: 8px;
padding: 6px 8px;
box-shadow: 0 6px 18px rgba(61, 57, 41, 0.12);
z-index: 20;
}
.panel-menu button {
border: none;
background: transparent;
font-size: 18px;
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
}
.panel-menu button.active {
background: rgba(108, 92, 231, 0.1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.sidebar.right-sidebar.collapsed { .sidebar.right-sidebar.collapsed {
width: 0 !important; width: 0 !important;
min-width: 0 !important; min-width: 0 !important;

View File

@ -6,26 +6,47 @@ import json
import asyncio import asyncio
from typing import List, Dict, Optional, AsyncGenerator from typing import List, Dict, Optional, AsyncGenerator
try: try:
from config import API_BASE_URL, API_KEY, MODEL_ID, OUTPUT_FORMATS, DEFAULT_RESPONSE_MAX_TOKENS from config import (
API_BASE_URL,
API_KEY,
MODEL_ID,
OUTPUT_FORMATS,
DEFAULT_RESPONSE_MAX_TOKENS,
THINKING_API_BASE_URL,
THINKING_API_KEY,
THINKING_MODEL_ID
)
except ImportError: except ImportError:
import sys import sys
from pathlib import Path from pathlib import Path
project_root = Path(__file__).resolve().parents[1] project_root = Path(__file__).resolve().parents[1]
if str(project_root) not in sys.path: if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from config import API_BASE_URL, API_KEY, MODEL_ID, OUTPUT_FORMATS, DEFAULT_RESPONSE_MAX_TOKENS from config import (
API_BASE_URL,
API_KEY,
MODEL_ID,
OUTPUT_FORMATS,
DEFAULT_RESPONSE_MAX_TOKENS,
THINKING_API_BASE_URL,
THINKING_API_KEY,
THINKING_MODEL_ID
)
class DeepSeekClient: class DeepSeekClient:
def __init__(self, thinking_mode: bool = True, web_mode: bool = False): def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
self.api_base_url = API_BASE_URL self.fast_api_config = {
self.api_key = API_KEY "base_url": API_BASE_URL,
self.model_id = MODEL_ID "api_key": API_KEY,
"model_id": MODEL_ID
}
self.thinking_api_config = {
"base_url": THINKING_API_BASE_URL or API_BASE_URL,
"api_key": THINKING_API_KEY or API_KEY,
"model_id": THINKING_MODEL_ID or MODEL_ID
}
self.thinking_mode = thinking_mode # True=智能思考模式, False=快速模式 self.thinking_mode = thinking_mode # True=智能思考模式, False=快速模式
self.web_mode = web_mode # Web模式标志用于禁用print输出 self.web_mode = web_mode # Web模式标志用于禁用print输出
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 每个任务的独立状态 # 每个任务的独立状态
self.current_task_first_call = True # 当前任务是否是第一次调用 self.current_task_first_call = True # 当前任务是否是第一次调用
self.current_task_thinking = "" # 当前任务的思考内容 self.current_task_thinking = "" # 当前任务的思考内容
@ -103,6 +124,24 @@ class DeepSeekClient:
self.current_task_first_call = True self.current_task_first_call = True
self.current_task_thinking = "" self.current_task_thinking = ""
def _build_headers(self, api_key: str) -> Dict[str, str]:
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def _select_api_config(self, use_thinking: bool) -> Dict[str, str]:
"""
根据当前模式选择API配置确保缺失字段回退到默认模型
"""
config = self.thinking_api_config if use_thinking else self.fast_api_config
fallback = self.fast_api_config
return {
"base_url": config.get("base_url") or fallback["base_url"],
"api_key": config.get("api_key") or fallback["api_key"],
"model_id": config.get("model_id") or fallback["model_id"]
}
def get_current_thinking_mode(self) -> bool: def get_current_thinking_mode(self) -> bool:
"""获取当前应该使用的思考模式""" """获取当前应该使用的思考模式"""
if not self.thinking_mode: if not self.thinking_mode:
@ -203,6 +242,8 @@ class DeepSeekClient:
# 决定是否使用思考模式 # 决定是否使用思考模式
current_thinking_mode = self.get_current_thinking_mode() current_thinking_mode = self.get_current_thinking_mode()
api_config = self._select_api_config(current_thinking_mode)
headers = self._build_headers(api_config["api_key"])
# 如果是思考模式且不是当前任务的第一次,显示提示 # 如果是思考模式且不是当前任务的第一次,显示提示
if self.thinking_mode and not self.current_task_first_call: if self.thinking_mode and not self.current_task_first_call:
@ -216,12 +257,13 @@ class DeepSeekClient:
max_tokens = 4096 max_tokens = 4096
payload = { payload = {
"model": self.model_id, "model": api_config["model_id"],
"messages": messages, "messages": messages,
"stream": stream, "stream": stream,
"thinking": {"type": "enabled" if current_thinking_mode else "disabled"},
"max_tokens": max_tokens "max_tokens": max_tokens
} }
if current_thinking_mode:
payload["thinking"] = {"type": "enabled"}
if tools: if tools:
payload["tools"] = tools payload["tools"] = tools
@ -232,9 +274,9 @@ class DeepSeekClient:
if stream: if stream:
async with client.stream( async with client.stream(
"POST", "POST",
f"{self.api_base_url}/chat/completions", f"{api_config['base_url']}/chat/completions",
json=payload, json=payload,
headers=self.headers headers=headers
) as response: ) as response:
# 检查响应状态 # 检查响应状态
if response.status_code != 200: if response.status_code != 200:
@ -255,9 +297,9 @@ class DeepSeekClient:
continue continue
else: else:
response = await client.post( response = await client.post(
f"{self.api_base_url}/chat/completions", f"{api_config['base_url']}/chat/completions",
json=payload, json=payload,
headers=self.headers headers=headers
) )
if response.status_code != 200: if response.status_code != 200:
error_text = response.text error_text = response.text
@ -294,22 +336,6 @@ class DeepSeekClient:
iteration = 0 iteration = 0
all_tool_results = [] # 记录所有工具调用结果 all_tool_results = [] # 记录所有工具调用结果
# 如果是思考模式且不是当前任务的第一次调用,注入本次任务的思考
# 注意:这里重置的是当前任务的第一次调用标志,确保新用户请求重新思考
# 只有在同一个任务的多轮迭代中才应该注入
# 对于新的用户请求,应该重新开始思考,而不是使用之前的思考内容
# 只有在当前任务有思考内容且不是第一次调用时才注入
if (self.thinking_mode and
not self.current_task_first_call and
self.current_task_thinking and
iteration == 0): # 只在第一次迭代时注入,避免多次注入
# 在messages末尾添加一个系统消息包含本次任务的思考
thinking_context = f"\n=== 📋 本次任务的思考 ===\n{self.current_task_thinking}\n=== 思考结束 ===\n提示:这是本次任务的初始思考,你可以基于此继续处理。"
messages.append({
"role": "system",
"content": thinking_context
})
while iteration < max_iterations: while iteration < max_iterations:
iteration += 1 iteration += 1
@ -409,13 +435,13 @@ class DeepSeekClient:
# 构建助手消息 - 始终包含所有收集到的内容 # 构建助手消息 - 始终包含所有收集到的内容
assistant_content_parts = [] assistant_content_parts = []
# 添加思考内容(如果有)
if current_thinking:
assistant_content_parts.append(f"<think>\n{current_thinking}\n</think>")
# 添加正式回复内容(如果有) # 添加正式回复内容(如果有)
if full_response: if full_response:
assistant_content_parts.append(full_response) assistant_content_parts.append(full_response)
elif append_result["handled"] and append_result["assistant_content"]:
assistant_content_parts.append(append_result["assistant_content"])
elif modify_result["handled"] and modify_result.get("assistant_content"):
assistant_content_parts.append(modify_result["assistant_content"])
# 添加工具调用说明 # 添加工具调用说明
if tool_calls: if tool_calls:
@ -556,14 +582,6 @@ class DeepSeekClient:
# 获取当前是否应该显示思考 # 获取当前是否应该显示思考
should_show_thinking = self.get_current_thinking_mode() should_show_thinking = self.get_current_thinking_mode()
# 如果是思考模式且不是当前任务的第一次调用,注入本次任务的思考
if self.thinking_mode and not self.current_task_first_call and self.current_task_thinking:
thinking_context = f"\n=== 📋 本次任务的思考 ===\n{self.current_task_thinking}\n=== 思考结束 ===\n"
messages.append({
"role": "system",
"content": thinking_context
})
try: try:
async for chunk in self.chat(messages, tools=None, stream=True): async for chunk in self.chat(messages, tools=None, stream=True):
if "choices" not in chunk: if "choices" not in chunk:

View File

@ -648,7 +648,8 @@ class ContextManager:
tool_calls: Optional[List[Dict]] = None, tool_calls: Optional[List[Dict]] = None,
tool_call_id: Optional[str] = None, tool_call_id: Optional[str] = None,
name: Optional[str] = None, name: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None metadata: Optional[Dict[str, Any]] = None,
reasoning_content: Optional[str] = None
): ):
"""添加对话记录(改进版:集成自动保存 + 智能token统计""" """添加对话记录(改进版:集成自动保存 + 智能token统计"""
message = { message = {
@ -660,6 +661,9 @@ class ContextManager:
if metadata: if metadata:
message["metadata"] = metadata message["metadata"] = metadata
if reasoning_content:
message["reasoning_content"] = reasoning_content
# 如果是assistant消息且有工具调用保存完整格式 # 如果是assistant消息且有工具调用保存完整格式
if role == "assistant" and tool_calls: if role == "assistant" and tool_calls:
# 确保工具调用格式完整 # 确保工具调用格式完整

View File

@ -1807,7 +1807,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
sender('ai_message_start', {}) sender('ai_message_start', {})
# 增量保存相关变量 # 增量保存相关变量
has_saved_thinking = False # 是否已保存思考内容
accumulated_response = "" # 累积的响应内容 accumulated_response = "" # 累积的响应内容
is_first_iteration = True # 是否是第一次迭代 is_first_iteration = True # 是否是第一次迭代
@ -2618,12 +2617,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
sender('thinking_end', {'full_content': current_thinking}) sender('thinking_end', {'full_content': current_thinking})
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# ===== 增量保存:保存思考内容 =====
if current_thinking and not has_saved_thinking and is_first_iteration:
thinking_content = f"<think>\n{current_thinking}\n</think>"
web_terminal.context_manager.add_conversation("assistant", thinking_content)
has_saved_thinking = True
debug_log(f"💾 增量保存:思考内容 ({len(current_thinking)} 字符)")
expecting_modify = bool(pending_modify) or bool(getattr(web_terminal, "pending_modify_request", None)) expecting_modify = bool(pending_modify) or bool(getattr(web_terminal, "pending_modify_request", None))
expecting_append = bool(pending_append) or bool(getattr(web_terminal, "pending_append_request", None)) expecting_append = bool(pending_append) or bool(getattr(web_terminal, "pending_append_request", None))
@ -2851,12 +2844,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
# === API响应完成后只计算输出token === # === API响应完成后只计算输出token ===
try: try:
# 计算AI输出的token包括thinking、文本内容、工具调用 ai_output_content = full_response or append_result.get("assistant_content") or modify_result.get("assistant_content") or ""
ai_output_content = ""
if current_thinking:
ai_output_content += f"<think>\n{current_thinking}\n</think>\n"
if full_response:
ai_output_content += full_response
if tool_calls: if tool_calls:
ai_output_content += json.dumps(tool_calls, ensure_ascii=False) ai_output_content += json.dumps(tool_calls, ensure_ascii=False)
@ -2892,12 +2880,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
sender('thinking_end', {'full_content': current_thinking}) sender('thinking_end', {'full_content': current_thinking})
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# 保存思考内容
if current_thinking and not has_saved_thinking and is_first_iteration:
thinking_content = f"<think>\n{current_thinking}\n</think>"
web_terminal.context_manager.add_conversation("assistant", thinking_content)
has_saved_thinking = True
debug_log(f"💾 增量保存:延迟思考内容 ({len(current_thinking)} 字符)")
# 确保text_end事件被发送 # 确保text_end事件被发送
if text_started and text_has_content and not append_result["handled"] and not modify_result["handled"]: if text_started and text_has_content and not append_result["handled"] and not modify_result["handled"]:
@ -2907,10 +2889,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
text_streaming = False text_streaming = False
# ===== 增量保存:保存当前轮次的文本内容 =====
if full_response.strip(): if full_response.strip():
web_terminal.context_manager.add_conversation("assistant", full_response) debug_log(f"流式文本内容长度: {len(full_response)} 字符")
debug_log(f"💾 增量保存:文本内容 ({len(full_response)} 字符)")
if append_result["handled"]: if append_result["handled"]:
append_metadata = append_result.get("assistant_metadata") append_metadata = append_result.get("assistant_metadata")
@ -3066,8 +3046,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
# 保存思考内容(如果这是第一次迭代且有思考) # 保存思考内容(如果这是第一次迭代且有思考)
if web_terminal.thinking_mode and web_terminal.api_client.current_task_first_call and current_thinking: if web_terminal.thinking_mode and web_terminal.api_client.current_task_first_call:
web_terminal.api_client.current_task_thinking = current_thinking web_terminal.api_client.current_task_thinking = current_thinking or ""
web_terminal.api_client.current_task_first_call = False web_terminal.api_client.current_task_first_call = False
# 检测是否有格式错误的工具调用 # 检测是否有格式错误的工具调用
@ -3101,9 +3081,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
# 构建助手消息用于API继续对话 # 构建助手消息用于API继续对话
assistant_content_parts = [] assistant_content_parts = []
if current_thinking:
assistant_content_parts.append(f"<think>\n{current_thinking}\n</think>")
if full_response: if full_response:
assistant_content_parts.append(full_response) assistant_content_parts.append(full_response)
elif append_result["handled"] and append_result["assistant_content"]: elif append_result["handled"] and append_result["assistant_content"]:
@ -3122,6 +3099,14 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
messages.append(assistant_message) messages.append(assistant_message)
if assistant_content or current_thinking:
web_terminal.context_manager.add_conversation(
"assistant",
assistant_content,
tool_calls=tool_calls if tool_calls else None,
reasoning_content=current_thinking or None
)
if append_result["handled"] and append_result.get("tool_content"): if append_result["handled"] and append_result.get("tool_content"):
tool_call_id = append_result.get("tool_call_id") or f"append_{int(time.time() * 1000)}" tool_call_id = append_result.get("tool_call_id") or f"append_{int(time.time() * 1000)}"
system_notice = format_tool_result_notice("append_to_file", tool_call_id, append_result["tool_content"]) system_notice = format_tool_result_notice("append_to_file", tool_call_id, append_result["tool_content"])