From 89a07e8e743495a0b6fbdd3df898426ee8366b29 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 13 Apr 2026 16:06:28 +0800 Subject: [PATCH] feat: add enable_thinking config to control deep reasoning on web console --- README.md | 3 +- agent/protocol/agent_stream.py | 13 +++++++-- bridge/agent_bridge.py | 20 ++++++++++++-- channel/web/chat.html | 12 ++++++++ channel/web/static/css/console.css | 10 +++++-- channel/web/static/js/console.js | 44 ++++++++++++++---------------- channel/web/web_channel.py | 7 +++-- config.py | 1 + docs/channels/web.mdx | 30 ++++++-------------- docs/intro/architecture.mdx | 4 ++- models/dashscope/dashscope_bot.py | 13 ++++----- models/doubao/doubao_bot.py | 15 +++++++--- models/linkai/link_ai_bot.py | 4 +++ models/moonshot/moonshot_bot.py | 16 +++++++---- run.sh | 16 +++++++++++ 15 files changed, 135 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 67f9b26e..e83857f0 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,8 @@ cow install-browser "agent_workspace": "~/cow", # Agent 的工作空间路径,用于存储 memory、skills、系统设定等 "agent_max_context_tokens": 50000, # Agent 模式下最大上下文 tokens,超出将自动智能压缩处理 "agent_max_context_turns": 20, # Agent 模式下最大上下文记忆轮次,一问一答为一轮,超出后智能压缩处理 - "agent_max_steps": 20 # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具 + "agent_max_steps": 20, # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具 + "enable_thinking": true # 是否启用深度思考,开启后 Web 端展示模型推理过程,关闭后可加速响应 } ``` diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index 45f7d8a5..1b37fea9 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -78,6 +78,11 @@ class AgentStreamExecutor: except Exception as e: logger.error(f"Event callback error: {e}") + def _is_thinking_enabled(self) -> bool: + from config import conf + channel_type = getattr(self.model, 'channel_type', '') or '' + return conf().get("enable_thinking", True) and channel_type == 'web' + def _filter_think_tags(self, text: str) -> str: """ Remove and tags but keep the content inside. @@ -178,7 +183,10 @@ class AgentStreamExecutor: Final response text """ # Log user message with model info - logger.info(f"🤖 {self.model.model} | 👤 {user_message}") + + thinking_enabled = self._is_thinking_enabled() + thinking_label = "💭 thinking" if thinking_enabled else "⚡ fast" + logger.info(f"🤖 {self.model.model} | {thinking_label} | 👤 {user_message}") # Add user message (Claude format - use content blocks for consistency) self.messages.append({ @@ -588,7 +596,8 @@ class AgentStreamExecutor: reasoning_delta = delta.get("reasoning_content") or "" if reasoning_delta: full_reasoning += reasoning_delta - self._emit_event("reasoning_update", {"delta": reasoning_delta}) + if self._is_thinking_enabled(): + self._emit_event("reasoning_update", {"delta": reasoning_delta}) # Handle text content content_delta = delta.get("content") or "" diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 073cfa83..4b178bee 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -160,13 +160,21 @@ class AgentLLMModel(LLMModel): kwargs['system'] = system_prompt # Pass context metadata to bot - channel_type = getattr(self, 'channel_type', None) + channel_type = getattr(self, 'channel_type', None) or '' if channel_type: kwargs['channel_type'] = channel_type session_id = getattr(self, 'session_id', None) if session_id: kwargs['session_id'] = session_id + # Determine thinking: respect global config, then channel_type + from config import conf + global_thinking = conf().get("enable_thinking", True) + if not global_thinking: + kwargs['thinking'] = {"type": "disabled"} + else: + kwargs['thinking'] = {"type": "enabled"} if channel_type == "web" else {"type": "disabled"} + response = self.bot.call_with_tools(**kwargs) return self._format_response(response) else: @@ -205,13 +213,21 @@ class AgentLLMModel(LLMModel): kwargs['system'] = system_prompt # Pass context metadata to bot - channel_type = getattr(self, 'channel_type', None) + channel_type = getattr(self, 'channel_type', None) or '' if channel_type: kwargs['channel_type'] = channel_type session_id = getattr(self, 'session_id', None) if session_id: kwargs['session_id'] = session_id + # Determine thinking: respect global config, then channel_type + from config import conf + global_thinking = conf().get("enable_thinking", True) + if not global_thinking: + kwargs['thinking'] = {"type": "disabled"} + else: + kwargs['thinking'] = {"type": "enabled"} if channel_type == "web" else {"type": "disabled"} + stream = self.bot.call_with_tools(**kwargs) # Convert stream format to our expected format diff --git a/channel/web/chat.html b/channel/web/chat.html index 0083f2a9..48f41798 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -509,6 +509,18 @@ bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100 focus:outline-none focus:border-primary-500 font-mono transition-colors"> +
+ + +
`; stepsEl.appendChild(currentReasoningEl); } - const oneLine = reasoningText.trim().replace(/\n+/g, ' '); - currentReasoningEl.querySelector('.thinking-summary').textContent = - oneLine.length > 80 ? oneLine.substring(0, 80) + '…' : oneLine; currentReasoningEl.querySelector('.thinking-full').innerHTML = renderMarkdown(reasoningText); scrollChatToBottom(); } else if (item.type === 'delta') { ensureBotEl(); if (currentReasoningEl) { - if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80) - currentReasoningEl.classList.add('no-expand'); + finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText); currentReasoningEl = null; reasoningText = ''; } @@ -951,8 +953,7 @@ function startSSE(requestId, loadingEl, timestamp) { } else if (item.type === 'tool_start') { ensureBotEl(); if (currentReasoningEl) { - if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80) - currentReasoningEl.classList.add('no-expand'); + finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText); currentReasoningEl = null; reasoningText = ''; } @@ -1089,8 +1090,7 @@ function startSSE(requestId, loadingEl, timestamp) { if (done) return; if (currentReasoningEl) { - if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80) - currentReasoningEl.classList.add('no-expand'); + finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText); currentReasoningEl = null; reasoningText = ''; } @@ -1214,28 +1214,24 @@ function renderToolCallsHtml(toolCalls) { }).join(''); } +function finalizeThinking(el, startTime, text) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + el.querySelector('.thinking-summary').textContent = t('thinking_done'); + const fullDiv = el.querySelector('.thinking-full'); + fullDiv.innerHTML = `
${t('thinking_duration')} ${elapsed}s
` + renderMarkdown(text); +} + function renderThinkingHtml(text) { if (!text || !text.trim()) return ''; const full = text.trim(); - const oneLine = full.replace(/\n+/g, ' '); - if (oneLine.length > 80) { - const truncated = oneLine.substring(0, 80) + '…'; - return ` + return `
- ${escapeHtml(truncated)} + ${t('thinking_done')}
${renderMarkdown(full)}
-
`; - } - return ` -
-
- - ${escapeHtml(oneLine)} -
`; } @@ -1649,6 +1645,7 @@ function initConfigView(data) { document.getElementById('cfg-max-tokens').value = data.agent_max_context_tokens || 50000; document.getElementById('cfg-max-turns').value = data.agent_max_context_turns || 20; document.getElementById('cfg-max-steps').value = data.agent_max_steps || 20; + document.getElementById('cfg-enable-thinking').checked = data.enable_thinking !== false; const pwdInput = document.getElementById('cfg-password'); const maskedPwd = data.web_password_masked || ''; @@ -1883,6 +1880,7 @@ function saveAgentConfig() { agent_max_context_tokens: parseInt(document.getElementById('cfg-max-tokens').value) || 50000, agent_max_context_turns: parseInt(document.getElementById('cfg-max-turns').value) || 20, agent_max_steps: parseInt(document.getElementById('cfg-max-steps').value) || 20, + enable_thinking: document.getElementById('cfg-enable-thinking').checked, }; const btn = document.getElementById('cfg-agent-save'); diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 57b95217..0a0487a8 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -714,7 +714,7 @@ class ConfigHandler: "api_key_field": "minimax_api_key", "api_base_key": None, "api_base_default": None, - "models": [const.MINIMAX_M2_7, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING], + "models": [const.MINIMAX_M2_7, const.MINIMAX_M2_7_HIGHSPEED, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING], }), ("zhipu", { "label": "智谱AI", @@ -796,7 +796,7 @@ class ConfigHandler: "zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key", "ark_api_key", "minimax_api_key", "linkai_api_key", "agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps", - "web_password", + "enable_thinking", "web_password", } @staticmethod @@ -849,6 +849,7 @@ class ConfigHandler: "agent_max_context_tokens": local_config.get("agent_max_context_tokens", 50000), "agent_max_context_turns": local_config.get("agent_max_context_turns", 20), "agent_max_steps": local_config.get("agent_max_steps", 20), + "enable_thinking": bool(local_config.get("enable_thinking", True)), "api_bases": api_bases, "api_keys": api_keys_masked, "providers": providers, @@ -874,7 +875,7 @@ class ConfigHandler: continue if key in ("agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps"): value = int(value) - if key == "use_linkai": + if key in ("use_linkai", "enable_thinking"): value = bool(value) local_config[key] = value applied[key] = value diff --git a/config.py b/config.py index e5dcd5d8..5e0ce8f2 100644 --- a/config.py +++ b/config.py @@ -202,6 +202,7 @@ available_setting = { "agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens "agent_max_context_turns": 20, # Agent模式下最大上下文记忆轮次 "agent_max_steps": 20, # Agent模式下单次运行最大决策步数 + "enable_thinking": True, # Whether to enable deep thinking for web channel "knowledge": True, # 是否开启知识库功能 } diff --git a/docs/channels/web.mdx b/docs/channels/web.mdx index d8329553..a5f9ac60 100644 --- a/docs/channels/web.mdx +++ b/docs/channels/web.mdx @@ -10,7 +10,9 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏 ```json { "channel_type": "web", - "web_port": 9899 + "web_port": 9899, + "web_password": "", + "enable_thinking": true } ``` @@ -18,6 +20,11 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏 | --- | --- | --- | | `channel_type` | 设为 `web` | `web` | | `web_port` | Web 服务监听端口 | `9899` | +| `web_password` | 访问密码,留空表示不启用密码保护 | `""` | +| `web_session_expire_days` | 登录会话有效天数 | `30` | +| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `true` | + +配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。密码也支持在控制台的「配置」页面中在线修改。 ## 访问地址 @@ -30,30 +37,11 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏 请确保服务器防火墙和安全组已放行对应端口。 -## 密码保护 - -Web 控制台默认无需密码即可访问。如果部署在公网环境,建议配置访问密码: - -```json -{ - "web_password": "your_password" -} -``` - -| 参数 | 说明 | 默认值 | -| --- | --- | --- | -| `web_password` | 访问密码,留空表示不启用密码保护 | `""` | -| `web_session_expire_days` | 登录会话有效天数 | `30` | - -配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。修改密码后,所有已登录的会话将自动失效。 - -密码也支持在控制台的「配置」页面中在线修改。 - ## 功能介绍 ### 对话界面 -支持流式输出,可实时展示 Agent 的思考过程(Reasoning)和工具调用过程(Tool Calls),更直观地观察 Agent 的决策过程: +支持流式输出,可实时展示 Agent 的思考过程(Reasoning)和工具调用过程(Tool Calls),更直观地观察 Agent 的决策过程。深度思考功能可通过配置或控制台的「Agent 配置」开关控制。 diff --git a/docs/intro/architecture.mdx b/docs/intro/architecture.mdx index b83dd65a..2cb5a8f0 100644 --- a/docs/intro/architecture.mdx +++ b/docs/intro/architecture.mdx @@ -69,7 +69,8 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词 "agent_workspace": "~/cow", "agent_max_context_tokens": 40000, "agent_max_context_turns": 30, - "agent_max_steps": 15 + "agent_max_steps": 15, + "enable_thinking": true } ``` @@ -80,4 +81,5 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词 | `agent_max_context_tokens` | 最大上下文 token 数 | `50000` | | `agent_max_context_turns` | 最大上下文记忆轮次 | `20` | | `agent_max_steps` | 单次任务最大决策步数 | `20` | +| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `true` | | `knowledge` | 是否启用个人知识库 | `true` | diff --git a/models/dashscope/dashscope_bot.py b/models/dashscope/dashscope_bot.py index 4d4d628f..651ec80b 100644 --- a/models/dashscope/dashscope_bot.py +++ b/models/dashscope/dashscope_bot.py @@ -262,20 +262,17 @@ class DashscopeBot(Bot): if kwargs.get("tool_choice"): parameters["tool_choice"] = kwargs["tool_choice"] - # Add thinking parameters for Qwen3 models (disabled by default for stability) + # Add thinking parameters for Qwen3/QwQ models if "qwen3" in model_name.lower() or "qwq" in model_name.lower(): - # Only enable thinking mode if explicitly requested - enable_thinking = kwargs.get("enable_thinking", False) - if enable_thinking: + thinking = kwargs.get("thinking", {"type": "enabled"}) + if thinking.get("type") == "enabled": parameters["enable_thinking"] = True - - # Set thinking budget if specified if kwargs.get("thinking_budget"): parameters["thinking_budget"] = kwargs["thinking_budget"] - - # Qwen3 requires incremental_output=true in thinking mode if stream: parameters["incremental_output"] = True + else: + parameters["enable_thinking"] = False # Always use incremental_output for streaming (for better token-by-token streaming) # This is especially important for tool calling to avoid incomplete responses diff --git a/models/doubao/doubao_bot.py b/models/doubao/doubao_bot.py index b31516ec..f8ccfff7 100644 --- a/models/doubao/doubao_bot.py +++ b/models/doubao/doubao_bot.py @@ -249,9 +249,7 @@ class DoubaoBot(Bot): request_body["tools"] = converted_tools request_body["tool_choice"] = "auto" - # Explicitly disable thinking to avoid reasoning_content issues - # in multi-turn tool calls - request_body["thinking"] = {"type": "disabled"} + request_body["thinking"] = kwargs.get("thinking", {"type": "enabled"}) logger.debug(f"[DOUBAO] API call: model={model}, " f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}") @@ -324,8 +322,17 @@ class DoubaoBot(Bot): choice = chunk["choices"][0] delta = choice.get("delta", {}) - # Skip reasoning_content (thinking) - don't log or forward if delta.get("reasoning_content"): + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "assistant", + "reasoning_content": delta["reasoning_content"] + }, + "finish_reason": None + }] + } continue # Handle text content diff --git a/models/linkai/link_ai_bot.py b/models/linkai/link_ai_bot.py index dd91f3db..212cbbd7 100644 --- a/models/linkai/link_ai_bot.py +++ b/models/linkai/link_ai_bot.py @@ -560,6 +560,10 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs): body["tools"] = tools body["tool_choice"] = kwargs.get("tool_choice", "auto") + thinking = kwargs.get("thinking") + if thinking: + body["thinking"] = thinking + # Prepare headers headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")} base_url = conf().get("linkai_api_base", "https://api.link-ai.tech") diff --git a/models/moonshot/moonshot_bot.py b/models/moonshot/moonshot_bot.py index 4d35400e..55698e45 100644 --- a/models/moonshot/moonshot_bot.py +++ b/models/moonshot/moonshot_bot.py @@ -249,10 +249,7 @@ class MoonshotBot(Bot): request_body["tools"] = converted_tools request_body["tool_choice"] = "auto" - # Explicitly disable thinking to avoid reasoning_content issues in multi-turn tool calls. - # kimi-k2.5 may enable thinking by default; without preserving reasoning_content - # in conversation history the API will reject subsequent requests. - request_body["thinking"] = {"type": "disabled"} + request_body["thinking"] = kwargs.get("thinking", {"type": "enabled"}) logger.debug(f"[MOONSHOT] API call: model={model}, " f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}") @@ -325,8 +322,17 @@ class MoonshotBot(Bot): choice = chunk["choices"][0] delta = choice.get("delta", {}) - # Skip reasoning_content (thinking) – don't log or forward if delta.get("reasoning_content"): + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "assistant", + "reasoning_content": delta["reasoning_content"] + }, + "finish_reason": None + }] + } continue # Handle text content diff --git a/run.sh b/run.sh index 74b09665..07c0753d 100755 --- a/run.sh +++ b/run.sh @@ -193,6 +193,16 @@ clone_project() { rm CowAgent.zip else local clone_ok=false + # Detect and temporarily disable invalid git proxy settings + local _git_proxy_unset=false + local _http_proxy=$(git config --global http.proxy 2>/dev/null) + local _https_proxy=$(git config --global https.proxy 2>/dev/null) + if [ -n "$_http_proxy" ] && ! curl -s --connect-timeout 3 --max-time 5 --proxy "$_http_proxy" https://github.com > /dev/null 2>&1; then + echo -e "${YELLOW}⚠️ Invalid git proxy detected: $_http_proxy, temporarily disabling...${NC}" + git config --global --unset http.proxy + [ -n "$_https_proxy" ] && git config --global --unset https.proxy + _git_proxy_unset=true + fi # Test GitHub connectivity before attempting clone if curl -sI --connect-timeout 5 --max-time 10 https://github.com > /dev/null 2>&1; then echo -e "${YELLOW}🌐 GitHub is reachable, cloning from GitHub...${NC}" @@ -204,6 +214,12 @@ clone_project() { fi if [ "$clone_ok" = false ]; then echo -e "${RED}❌ Project clone failed. Please check network connection.${NC}" + if git config --global http.proxy &> /dev/null || git config --global https.proxy &> /dev/null || [ -n "$http_proxy" ] || [ -n "$https_proxy" ] || [ -n "$HTTP_PROXY" ] || [ -n "$HTTPS_PROXY" ]; then + echo -e "${YELLOW}💡 Detected proxy settings. If proxy is misconfigured, try removing it with:${NC}" + echo -e "${YELLOW} git config --global --unset http.proxy${NC}" + echo -e "${YELLOW} git config --global --unset https.proxy${NC}" + echo -e "${YELLOW} unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY${NC}" + fi exit 1 fi fi