feat: 移除实验性间隔思考模式,推理强度前置到前端

- 彻底移除「每 N 次请求强制思考」的间隔调度机制(思考/非思考交替导致缓存命中率极低)
- 运行模式收敛为 fast/thinking:原 deep 改名 thinking,历史数据读取时映射
- 新增推理强度五档滑块(low/medium/high/xhigh/max + 默认不传参),置于模型/模式弹窗
- 模型配置新增 reasoning_effort 开关;档位会话级持久化到对话 meta,随对话保存/恢复
- 个人空间新增「默认推理强度」;新用户默认运行模式改为 thinking
- 滑块交互:JS 插值驱动(点击/拖动/吸附全程平滑),首帧档位语义定位无闪烁
- 保存链路:前端防抖 600ms + conversation_id 兜底,修复 /new 发消息丢档位、切对话恢复错误
This commit is contained in:
JOJO 2026-07-21 13:47:53 +08:00
parent 496bbe4f97
commit c9c3eedd82
78 changed files with 1718 additions and 862 deletions

View File

@ -8,6 +8,7 @@
"apikey": "${API_KEY_KIMI_OFFICIAL}",
"multimodal": "image,video",
"reasoning_capability": "fast,thinking",
"reasoning_effort": true,
"context_window": 256000,
"max_output_tokens": 32768,
"thinkmode_status": {

View File

@ -20,7 +20,9 @@ MAX_ITERATIONS_PER_TASK = None
MAX_CONSECUTIVE_SAME_TOOL = None
MAX_TOTAL_TOOL_CALLS = None
TOOL_CALL_COOLDOWN = 0.5
THINKING_FAST_INTERVAL = 10
# 推理强度档位(模型配置 supports_reasoning_effort 时思考模式下可选None 表示默认不传参)
REASONING_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
# 工具字符/体积限制
MAX_READ_FILE_CHARS = 50000
@ -94,7 +96,7 @@ __all__ = [
"MAX_CONSECUTIVE_SAME_TOOL",
"MAX_TOTAL_TOOL_CALLS",
"TOOL_CALL_COOLDOWN",
"THINKING_FAST_INTERVAL",
"REASONING_EFFORT_LEVELS",
"MAX_READ_FILE_CHARS",
"MAX_FOCUS_FILE_CHARS",
"MAX_RUN_COMMAND_CHARS",

View File

@ -58,10 +58,10 @@ def _to_optional_int(value: Any) -> Optional[int]:
def _reasoning_flags(capability: str) -> Dict[str, bool]:
normalized = str(capability or "fast").replace(" ", "").lower()
if normalized == "thinking":
return {"supports_thinking": True, "fast_only": False, "deep_only": True}
return {"supports_thinking": True, "fast_only": False, "thinking_only": True}
if normalized == "fast,thinking":
return {"supports_thinking": True, "fast_only": False, "deep_only": False}
return {"supports_thinking": False, "fast_only": True, "deep_only": False}
return {"supports_thinking": True, "fast_only": False, "thinking_only": False}
return {"supports_thinking": False, "fast_only": True, "thinking_only": False}
def _build_custom_profile(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
@ -131,8 +131,11 @@ def _build_custom_profile(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"supports_thinking": bool(flags["supports_thinking"]),
"fast_only": bool(flags["fast_only"]),
}
if flags["deep_only"]:
profile["deep_only"] = True
if flags["thinking_only"]:
profile["thinking_only"] = True
# 推理强度开关:配置为真值(如 "reasoning_effort": true思考模式下前端可选档位
if bool(item.get("reasoning_effort")):
profile["supports_reasoning_effort"] = True
if flags["supports_thinking"]:
profile["thinking"] = {
"base_url": base_url.rstrip("/"),
@ -231,7 +234,8 @@ def get_model_capabilities(key: str) -> Dict[str, Any]:
"multimodal": multimodal,
"supports_thinking": bool(profile.get("supports_thinking", False)),
"fast_only": bool(profile.get("fast_only", False)),
"deep_only": bool(profile.get("deep_only", False)),
"thinking_only": bool(profile.get("thinking_only", False)),
"supports_reasoning_effort": bool(profile.get("supports_reasoning_effort", False)),
}

View File

@ -57,10 +57,6 @@ from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
from modules.mcp_client_manager import MCPClientManager
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient
@ -90,20 +86,22 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
usage_tracker: Optional[object] = None,
):
self.project_path = project_path
allowed_modes = {"fast", "thinking", "deep"}
allowed_modes = {"fast", "thinking"}
if run_mode == "deep": # 旧版标识符映射
run_mode = "thinking"
initial_mode = run_mode if run_mode in allowed_modes else ("thinking" if thinking_mode else "fast")
self.run_mode = initial_mode
self.thinking_mode = initial_mode != "fast" # False=快速模式, True=思考模式
self.deep_thinking_mode = initial_mode == "deep"
# 记录因 fast-only 模型被强制切到 fast 前的模式,便于切回可思考模型时恢复
self._mode_before_fast_only: Optional[str] = None
# 会话级推理强度None=默认,不传参)
self.reasoning_effort: Optional[str] = None
self.data_dir = Path(data_dir).expanduser().resolve() if data_dir else Path(DATA_DIR).resolve()
self.usage_tracker = usage_tracker
# 初始化组件
self.api_client = DeepSeekClient(thinking_mode=self.thinking_mode)
self.api_client.project_path = project_path
self.api_client.set_deep_thinking_mode(self.deep_thinking_mode)
self.model_key = get_default_model_key()
self.model_profile = get_model_profile(self.model_key)
self.apply_model_profile(self.model_profile)
@ -200,7 +198,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
self.disabled_tools: Set[str] = set()
self.disabled_notice_tools: Set[str] = set()
self._refresh_disabled_tools()
self.thinking_fast_interval = THINKING_FAST_INTERVAL
self.default_disabled_tool_categories: List[str] = []
# 个性化工具意图开关
self.tool_intent_enabled: bool = False

View File

@ -65,9 +65,9 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
from config.limits import REASONING_EFFORT_LEVELS
except ImportError:
THINKING_FAST_INTERVAL = 10
REASONING_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
@ -156,11 +156,6 @@ class MainTerminalCommandMixin:
async def handle_task(self, user_input: str):
"""处理用户任务(完全修复版:彻底解决对话记录重复问题)"""
try:
# 如果是思考模式,每个新任务重置状态
# 注意:这里重置的是当前任务的第一次调用标志,确保新用户请求重新思考
if self.thinking_mode:
self.api_client.start_new_task(force_deep=self.deep_thinking_mode)
# 新增:开始新的任务会话
self.current_session_id += 1
@ -302,9 +297,12 @@ class MainTerminalCommandMixin:
# 保存响应内容
final_response = response
# 获取思考内容(如果有的话)
if self.api_client.current_task_thinking:
final_thinking = self.api_client.current_task_thinking
# 获取思考内容(如果有的话):取最后一条 assistant 消息的 reasoning_content
if not final_thinking:
for _msg in reversed(messages):
if _msg.get("role") == "assistant" and _msg.get("reasoning_content"):
final_thinking = _msg["reasoning_content"]
break
# ===== 统一保存到对话历史(关键修复) =====
@ -449,10 +447,6 @@ class MainTerminalCommandMixin:
print(f"{OUTPUT_FORMATS['success']} 对话已加载: {conversation_id}")
print(f"{OUTPUT_FORMATS['info']} 消息数量: {len(self.context_manager.conversation_history)}")
# 如果是思考模式,重置状态(下次任务会重新思考)
if self.thinking_mode:
self.api_client.start_new_task(force_deep=self.deep_thinking_mode)
self.current_session_id += 1
else:
@ -471,10 +465,6 @@ class MainTerminalCommandMixin:
print(f"{OUTPUT_FORMATS['success']} 已创建新对话: {conversation_id}")
# 重置相关状态
if self.thinking_mode:
self.api_client.start_new_task(force_deep=self.deep_thinking_mode)
self.current_session_id += 1
except Exception as e:
@ -523,8 +513,6 @@ class MainTerminalCommandMixin:
# 思考模式状态
thinking_status = self.get_run_mode_label()
if self.thinking_mode:
thinking_status += f" ({'等待新任务' if self.api_client.current_task_first_call else '任务进行中'})"
# 新增:对话统计
conversation_stats = self.context_manager.get_conversation_statistics()
@ -761,32 +749,36 @@ class MainTerminalCommandMixin:
print(f"\n总计: {structure['total_files']} 个文件, {structure['total_size'] / 1024 / 1024:.2f} MB")
def set_run_mode(self, mode: str) -> str:
"""统一设置运行模式"""
allowed = ["fast", "thinking", "deep"]
"""统一设置运行模式fast / thinking旧值 deep 映射为 thinking"""
normalized = mode.lower()
if normalized == "deep": # 旧版“深度思考”标识符,统一映射为思考模式
normalized = "thinking"
allowed = ["fast", "thinking"]
if normalized not in allowed:
raise ValueError(f"不支持的模式: {mode}")
# 仅深度思考模型限制
if getattr(self, "model_profile", {}).get("deep_only") and normalized != "deep":
raise ValueError("当前模型仅支持深度思考模式")
# 仅思考模型限制
if getattr(self, "model_profile", {}).get("thinking_only") and normalized != "thinking":
raise ValueError("当前模型仅支持思考模式")
# fast-only 模型限制
if getattr(self, "model_profile", {}).get("fast_only") and normalized != "fast":
raise ValueError("当前模型仅支持快速模式")
previous_mode = getattr(self, "run_mode", "fast")
self.run_mode = normalized
self.thinking_mode = normalized != "fast"
self.deep_thinking_mode = normalized == "deep"
self.api_client.thinking_mode = self.thinking_mode
self.api_client.set_deep_thinking_mode(self.deep_thinking_mode)
if self.deep_thinking_mode:
self.api_client.force_thinking_next_call = False
self.api_client.skip_thinking_next_call = False
if not self.thinking_mode:
self.api_client.start_new_task()
elif previous_mode == "deep" and normalized != "deep":
self.api_client.start_new_task()
return self.run_mode
def set_reasoning_effort(self, effort: Optional[str]) -> Optional[str]:
"""设置会话级推理强度None 表示默认(请求中不传该参数)。"""
if effort is None or str(effort).strip() == "":
self.reasoning_effort = None
else:
normalized = str(effort).strip().lower()
if normalized not in REASONING_EFFORT_LEVELS:
raise ValueError(f"不支持的推理强度: {effort}")
self.reasoning_effort = normalized
self.api_client.reasoning_effort = self.reasoning_effort
return self.reasoning_effort
def apply_model_profile(self, profile: dict):
"""将模型配置应用到 API 客户端"""
if not profile:
@ -810,12 +802,12 @@ class MainTerminalCommandMixin:
# fast-only 模型强制快速模式
if profile.get("fast_only") and self.run_mode != "fast":
# 记住被强制前的模式,切回支持思考的模型时可恢复
if previous_mode in {"thinking", "deep"}:
if previous_mode == "thinking":
self._mode_before_fast_only = previous_mode
self.set_run_mode("fast")
# 仅深度思考模型强制 deep
if profile.get("deep_only") and self.run_mode != "deep":
self.set_run_mode("deep")
# 仅思考模型强制思考模式
if profile.get("thinking_only") and self.run_mode != "thinking":
self.set_run_mode("thinking")
# 从 fast-only 模型切回支持思考的模型时,恢复先前模式(若可用)
if (
not profile.get("fast_only")
@ -823,7 +815,7 @@ class MainTerminalCommandMixin:
and profile.get("supports_thinking")
):
restore_mode = getattr(self, "_mode_before_fast_only", None)
if restore_mode in {"thinking", "deep"}:
if restore_mode == "thinking":
try:
self.set_run_mode(restore_mode)
except ValueError:
@ -832,25 +824,22 @@ class MainTerminalCommandMixin:
self._mode_before_fast_only = None
try:
logger.info(
"[ModelSwitch] model=%s run_mode=%s thinking=%s deep=%s fast_only=%s deep_only=%s",
"[ModelSwitch] model=%s run_mode=%s thinking=%s fast_only=%s thinking_only=%s",
self.model_key,
self.run_mode,
self.thinking_mode,
self.deep_thinking_mode,
bool(profile.get("fast_only")),
bool(profile.get("deep_only")),
bool(profile.get("thinking_only")),
)
except Exception:
pass
# 如果模型支持思考,但当前 run_mode 为 thinking/deep则保持否则无需调整
self.api_client.start_new_task(force_deep=self.deep_thinking_mode)
# 如果模型支持思考,则保持当前 run_mode否则无需调整
return self.model_key
def get_run_mode_label(self) -> str:
labels = {
"fast": "快速模式(无思考)",
"thinking": "思考模式(首次调用使用思考模型)",
"deep": "深度思考模式(整轮使用思考模型)"
"thinking": "思考模式(使用思考模型)"
}
return labels.get(self.run_mode, "快速模式(无思考)")

View File

@ -67,10 +67,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -67,10 +67,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -69,10 +69,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -68,10 +68,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -67,10 +67,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -67,10 +67,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -65,10 +65,6 @@ from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -92,10 +92,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -67,9 +67,9 @@ from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
from config.limits import REASONING_EFFORT_LEVELS
except ImportError:
THINKING_FAST_INTERVAL = 10
REASONING_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
@ -116,8 +116,13 @@ class MainTerminalToolsPolicyMixin:
continue
self.tool_category_states[key] = bool(value)
def apply_personalization_preferences(self, config: Optional[Dict[str, Any]] = None, *, apply_default_model: bool = True):
"""Apply persisted personalization settings that affect runtime behavior."""
def apply_personalization_preferences(self, config: Optional[Dict[str, Any]] = None, *, apply_default_model: bool = True, apply_default_modes: bool = True):
"""Apply persisted personalization settings that affect runtime behavior.
apply_default_modes=False 时跳过 default_model / default_run_mode /
default_reasoning_effort 三项默认值应用对话加载链路必须传 False
三者以对话 meta 为权威默认值仅用于创建空对话
"""
try:
effective_config = config or load_personalization_config(self.data_dir)
except Exception:
@ -147,12 +152,6 @@ class MainTerminalToolsPolicyMixin:
except Exception:
self.enabled_skill_ids = set()
interval = effective_config.get("thinking_interval")
if isinstance(interval, int) and interval > 0:
self.thinking_fast_interval = interval
else:
self.thinking_fast_interval = THINKING_FAST_INTERVAL
disabled_categories = []
raw_disabled = effective_config.get("disabled_tool_categories")
if isinstance(raw_disabled, list):
@ -176,21 +175,37 @@ class MainTerminalToolsPolicyMixin:
# 默认模型偏好(优先应用,再处理运行模式)
preferred_model = effective_config.get("default_model")
if apply_default_model and isinstance(preferred_model, str) and preferred_model != self.model_key:
if apply_default_modes and apply_default_model and isinstance(preferred_model, str) and preferred_model != self.model_key:
try:
self.set_model(preferred_model)
except Exception as exc:
logger.warning("忽略无效默认模型: %s (%s)", preferred_model, exc)
preferred_mode = effective_config.get("default_run_mode")
if isinstance(preferred_mode, str):
if apply_default_modes and isinstance(preferred_mode, str):
normalized_mode = preferred_mode.strip().lower()
if normalized_mode in {"fast", "thinking", "deep"} and normalized_mode != self.run_mode:
if normalized_mode == "deep": # 旧版标识符映射
normalized_mode = "thinking"
if normalized_mode in {"fast", "thinking"} and normalized_mode != self.run_mode:
try:
self.set_run_mode(normalized_mode)
except ValueError:
logger.warning("忽略无效默认运行模式: %s", preferred_mode)
# 默认推理强度None=默认,不传参)
if apply_default_modes:
preferred_effort = effective_config.get("default_reasoning_effort")
if isinstance(preferred_effort, str):
preferred_effort = preferred_effort.strip().lower() or None
if preferred_effort not in REASONING_EFFORT_LEVELS:
preferred_effort = None
else:
preferred_effort = None
try:
self.set_reasoning_effort(preferred_effort)
except (ValueError, AttributeError):
pass
# 静默禁用工具提示
self.silent_tool_disable = bool(effective_config.get("silent_tool_disable"))
permission_mode = effective_config.get("default_permission_mode")

View File

@ -69,10 +69,6 @@ from modules.skills_manager import (
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES

View File

@ -11,14 +11,14 @@ from modules.personalization_manager import load_personalization_config
from modules.versioning_manager import ConversationVersioningManager, VersioningError
from utils.perf_log import perf_log, PerfTimer
try:
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE, REASONING_EFFORT_LEVELS
except ImportError:
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parents[1]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE, REASONING_EFFORT_LEVELS
from modules.terminal_manager import TerminalManager
if TYPE_CHECKING:
@ -173,6 +173,13 @@ class WebTerminal(MainTerminal):
preferred_model = prefs.get("default_model")
preferred_mode = prefs.get("default_run_mode")
preferred_permission_mode = prefs.get("default_permission_mode") or None
preferred_effort = prefs.get("default_reasoning_effort")
if isinstance(preferred_effort, str):
preferred_effort = preferred_effort.strip().lower() or None
if preferred_effort not in REASONING_EFFORT_LEVELS:
preferred_effort = None
else:
preferred_effort = None
if preferred_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
try:
preferred_permission_mode = self.get_permission_mode()
@ -180,14 +187,21 @@ class WebTerminal(MainTerminal):
preferred_permission_mode = None
if not isinstance(preferred_permission_mode, str) or not preferred_permission_mode.strip():
preferred_permission_mode = "unrestricted"
if isinstance(preferred_mode, str) and preferred_mode.lower() in {"fast", "thinking", "deep"}:
candidate_mode = preferred_mode.lower() if isinstance(preferred_mode, str) else None
if candidate_mode == "deep": # 旧版标识符映射
candidate_mode = "thinking"
if candidate_mode in {"fast", "thinking"}:
try:
self.set_run_mode(preferred_mode.lower())
self.set_run_mode(candidate_mode)
except ValueError as exc:
logger.warning("忽略无效默认运行模式 %s: %s", preferred_mode, exc)
else:
# 未配置默认模式时回到快速模式
self.set_run_mode("fast")
try:
self.set_reasoning_effort(preferred_effort)
except ValueError:
self.set_reasoning_effort(None)
try:
self.set_permission_mode(preferred_permission_mode, persist=False)
except Exception:
@ -217,6 +231,9 @@ class WebTerminal(MainTerminal):
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
"pending_permission_mode": None,
"pending_execution_mode": None,
# 推理强度随创建写入一次prefer_defaults 时 self 已应用个性化默认值;
# 显式模式(/new 页发消息)时 self 即用户当前档位,两者都直接沿用
"reasoning_effort": getattr(self, "reasoning_effort", None),
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
}
if isinstance(metadata_overrides, dict):
@ -266,10 +283,6 @@ class WebTerminal(MainTerminal):
logger.warning("新对话应用默认版本控制失败: %s", exc)
perf_log("create_new_conversation after default versioning", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": conversation_id})
# 重置相关状态
if self.thinking_mode:
self.api_client.start_new_task()
self.current_session_id += 1
perf_log("create_new_conversation done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": conversation_id})
@ -361,15 +374,25 @@ class WebTerminal(MainTerminal):
try:
success = self.context_manager.load_conversation_by_id(conversation_id)
if success:
# 根据对话元数据同步思考模式
# 根据对话元数据同步运行模式与推理强度
try:
target_manager = self.context_manager._get_conversation_manager_for_id(conversation_id)
conv_data = target_manager.load_conversation(conversation_id) or {}
meta = conv_data.get("metadata", {}) or {}
mode = bool(meta.get("thinking_mode", self.thinking_mode))
self.thinking_mode = mode
self.api_client.thinking_mode = mode
self.api_client.start_new_task()
saved_mode = str(meta.get("run_mode") or "").strip().lower()
if saved_mode == "deep": # 旧版标识符映射
saved_mode = "thinking"
if saved_mode in {"fast", "thinking"}:
self.run_mode = saved_mode
self.thinking_mode = saved_mode != "fast"
else:
self.thinking_mode = bool(meta.get("thinking_mode", self.thinking_mode))
self.run_mode = "thinking" if self.thinking_mode else "fast"
self.api_client.thinking_mode = self.thinking_mode
try:
self.set_reasoning_effort(meta.get("reasoning_effort"))
except ValueError:
self.set_reasoning_effort(None)
permission_mode = str(meta.get("permission_mode") or "").strip().lower()
if permission_mode:
try:
@ -525,6 +548,7 @@ class WebTerminal(MainTerminal):
"thinking_mode": self.thinking_mode,
"thinking_status": self.get_thinking_mode_status(),
"run_mode": self.run_mode,
"reasoning_effort": getattr(self, "reasoning_effort", None),
"model_key": getattr(self, "model_key", None),
"permission_mode": self.get_permission_mode() if hasattr(self, "get_permission_mode") else "unrestricted",
"execution_mode": self.get_execution_mode_state() if hasattr(self, "get_execution_mode_state") else {"mode": "sandbox"},

View File

@ -0,0 +1,473 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>推理强度选择器 Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--page-bg: #1a1a1c;
--card-bg: #2c2c2e;
--track-bg: #3a3a3c;
--text-primary: #f2f2f4;
--text-secondary: #9c9ca0;
--knob-bg: #ffffff;
--dot-on: rgba(255, 255, 255, 0.55);
--dot-off: rgba(0, 0, 0, 0.28);
}
html, body {
height: 100%;
background: var(--page-bg);
font-family: -apple-system, "PingFang SC", "Helvetica Neue", sans-serif;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 24px;
}
.demo-title {
color: var(--text-secondary);
font-size: 13px;
letter-spacing: 0.5px;
}
/* ========== 卡片(模拟弹窗内的一块区域) ========== */
.effort-card {
width: 460px;
background: var(--card-bg);
border-radius: 18px;
padding: 22px 26px 20px;
user-select: none;
-webkit-user-select: none;
}
/* ========== 顶部:两种状态文字叠在一起,淡入淡出切换 ========== */
.effort-header {
position: relative;
height: 26px;
margin-bottom: 18px;
}
.header-state {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.header-idle { opacity: 1; }
.header-drag {
opacity: 0;
transform: translateY(4px);
pointer-events: none;
color: var(--text-secondary);
font-size: 16px;
}
.effort-card.live-drag .header-idle {
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
}
.effort-card.live-drag .header-drag {
opacity: 1;
transform: translateY(0);
}
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
/* ========== 「默认」打勾框:个人空间 fancy-check 缩小版 ========== */
.default-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.default-label {
font-size: 13px;
color: var(--text-secondary);
}
.default-toggle .fancy-check {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.default-toggle .fancy-check svg {
width: 16px;
height: 16px;
overflow: visible;
}
.fancy-path {
fill: none;
stroke: var(--text-secondary);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
transition:
stroke-dasharray 0.5s ease,
stroke-dashoffset 0.5s ease,
stroke 0.2s ease;
stroke-dasharray: 241 9999999;
stroke-dashoffset: 0;
}
.default-toggle.checked .fancy-path {
stroke-dasharray: 70.5096664428711 9999999;
stroke-dashoffset: -262.2723388671875;
}
/* ========== 滑块区域 ========== */
.slider-zone {
position: relative;
transition: filter 0.25s ease, opacity 0.25s ease;
}
/* 默认勾选时:整体置灰,且停掉所有动画 */
.effort-card.is-default .slider-zone {
filter: grayscale(1) brightness(0.72);
opacity: 0.55;
}
.effort-card.is-default .fill-layer {
animation: none;
}
.effort-card.is-default .fill-layer::after {
animation: none;
}
.track {
position: relative;
height: 34px;
border-radius: 17px;
background: var(--track-bg);
cursor: pointer;
touch-action: none;
}
/* 填充5 层叠放,当前档淡入、其余淡出 —— 任何颜色切换都是平滑 crossfade */
.fill-stack {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 23px;
border-radius: 17px;
overflow: hidden;
}
.fill-layer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
opacity: 0;
transition: opacity 0.35s ease;
}
.fill-layer.on { opacity: 1; }
.fill-layer.lv-0 { background: #f5c542; }
.fill-layer.lv-1 { background: #3fd07c; }
.fill-layer.lv-2 { background: #4f8ef7; }
.fill-layer.lv-3 { background: #a55df0; }
/* xhigh白光扫过90deg 整列渐变无硬边;位移动画两端完全出画后回绕 → 无缝) */
.fill-layer.lv-3::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -50%;
width: 45%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.55) 50%,
transparent 100%
);
animation: shine-sweep 1.8s linear infinite;
}
@keyframes shine-sweep {
from { left: -50%; }
to { left: 130%; }
}
/* max彩虹流动200% 宽 + 位移整数个循环单元 → 真无缝) */
.fill-layer.lv-4 {
width: 200%;
background: repeating-linear-gradient(
90deg,
#ff3b5c 0%,
#ff9f2e 7.15%,
#ffe93b 14.3%,
#3ddc68 21.45%,
#2fd4ff 28.6%,
#7a5cff 35.75%,
#ff4fd8 42.9%,
#ff3b5c 50%
);
animation: rainbow-flow 2.4s linear infinite;
}
/* 位移方向:色块随条从左向右流动;位移为整数个循环单元 → 真无缝 */
@keyframes rainbow-flow {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
/* 圆钮与填充transition 常驻 —— 点击也平滑滑动;仅真实拖动时关闭跟随 */
.knob, .fill-stack {
transition: left 0.2s ease, width 0.2s ease;
}
.effort-card.live-drag .knob,
.effort-card.live-drag .fill-stack {
transition: none;
}
/* 档位圆点 */
.dot {
position: absolute;
top: 50%;
width: 7px;
height: 7px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: var(--dot-off);
transition: background 0.2s ease;
pointer-events: none;
}
.dot.covered { background: var(--dot-on); }
/* 拖柄 */
.knob {
position: absolute;
top: 50%;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--knob-bg);
transform: translate(-50%, -50%);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
cursor: grab;
z-index: 2;
}
.effort-card.live-drag .knob { cursor: grabbing; }
/* ========== 档位文字 ========== */
.level-labels {
position: relative;
height: 20px;
margin-top: 12px;
}
.level-label {
position: absolute;
transform: translateX(-50%);
font-size: 12px;
color: var(--text-secondary);
transition: color 0.2s ease, font-weight 0.2s ease;
white-space: nowrap;
}
.level-label.active {
color: var(--text-primary);
font-weight: 600;
}
/* demo 辅助:当前状态输出 */
.demo-state {
color: var(--text-secondary);
font-size: 12px;
font-family: ui-monospace, monospace;
}
</style>
</head>
<body>
<div class="demo-title">推理强度选择器 Demo · 拖动滑块 / 点击档位 / 勾选「默认」试试</div>
<div class="effort-card" id="card">
<!-- 顶部:非拖动 = 推理强度 + 默认;拖动中 = 更高效 / 更智能 -->
<div class="effort-header">
<div class="header-state header-idle">
<span class="header-title">推理强度</span>
<span class="default-toggle checked" id="defaultToggle">
<span class="default-label">默认</span>
<span class="fancy-check" aria-hidden="true">
<svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path>
</svg>
</span>
</span>
</div>
<div class="header-state header-drag">
<span>更高效</span>
<span>更智能</span>
</div>
</div>
<!-- 滑块 -->
<div class="slider-zone">
<div class="track" id="track">
<div class="fill-stack" id="fillStack">
<div class="fill-layer lv-0"></div>
<div class="fill-layer lv-1"></div>
<div class="fill-layer lv-2"></div>
<div class="fill-layer lv-3"></div>
<div class="fill-layer lv-4"></div>
</div>
<!-- dots 由 JS 生成 -->
<div class="knob" id="knob"></div>
</div>
<div class="level-labels" id="labels"></div>
</div>
</div>
<div class="demo-state" id="stateOut"></div>
<script>
(function () {
const LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
// 轨道内两端档位的圆钮中心 inset比圆钮半径(23)小 3px
// 让圆钮在两端时外凸 3px完整盖住填充条端头避免相切处露出杂边
const EDGE = 20;
const card = document.getElementById('card');
const track = document.getElementById('track');
const fillStack = document.getElementById('fillStack');
const layers = Array.from(fillStack.querySelectorAll('.fill-layer'));
const knob = document.getElementById('knob');
const labelsBox = document.getElementById('labels');
const defaultToggle = document.getElementById('defaultToggle');
const stateOut = document.getElementById('stateOut');
let levelIndex = 2; // 当前档位(记忆值,勾选默认时也保留)
let isDefault = true; // 是否勾选「默认」
let dragging = false; // 按下
let liveDrag = false; // 按下且已移动(真实拖动中)
let downX = 0;
const dots = [];
const labelEls = [];
// 生成 5 个档位点 + 5 个文字标签
LEVELS.forEach((name) => {
const dot = document.createElement('div');
dot.className = 'dot';
track.insertBefore(dot, knob);
dots.push(dot);
const label = document.createElement('span');
label.className = 'level-label';
label.textContent = name;
label.style.left = posPct(dots.length - 1);
labelsBox.appendChild(label);
labelEls.push(label);
});
// 档位 i 在轨道内的位置(百分比字符串,供标签/圆点用)
function posPct(i) {
const ratio = i / (LEVELS.length - 1);
return `calc(${EDGE}px + (100% - ${EDGE * 2}px) * ${ratio})`;
}
// 档位 i 在轨道内的像素 x
function posPx(i) {
const w = track.clientWidth;
return EDGE + (w - EDGE * 2) * (i / (LEVELS.length - 1));
}
// 像素 x → 最近档位
function pxToLevel(x) {
const w = track.clientWidth;
const ratio = (x - EDGE) / (w - EDGE * 2);
return Math.max(0, Math.min(LEVELS.length - 1, Math.round(ratio * (LEVELS.length - 1))));
}
function render(previewX) {
// 拖动时用连续像素位置否则吸附到档位点CSS transition 负责平滑)
const x = typeof previewX === 'number' ? previewX : posPx(levelIndex);
const activeLv = typeof previewX === 'number' ? pxToLevel(previewX) : levelIndex;
fillStack.style.width = x + 'px';
knob.style.left = x + 'px';
layers.forEach((layer, i) => layer.classList.toggle('on', i === activeLv));
dots.forEach((dot, i) => {
dot.style.left = posPct(i);
dot.classList.toggle('covered', posPx(i) <= x);
});
labelEls.forEach((el, i) => el.classList.toggle('active', i === activeLv));
card.classList.toggle('is-default', isDefault);
card.classList.toggle('live-drag', liveDrag);
defaultToggle.classList.toggle('checked', isDefault);
stateOut.textContent =
`level = ${isDefault ? '(默认,不传参)' : LEVELS[levelIndex]} | dragging = ${liveDrag}`;
}
// ---- 交互 ----
function onDown(e) {
dragging = true;
liveDrag = false;
downX = e.clientX;
track.setPointerCapture(e.pointerId);
// 点击含置灰状态解除默认并平滑滑到目标档transition 生效)
if (isDefault) isDefault = false;
levelIndex = pxToLevel(clampX(e));
render();
}
function onMove(e) {
if (!dragging) return;
if (!liveDrag && Math.abs(e.clientX - downX) > 3) liveDrag = true;
if (liveDrag) render(clampX(e)); // 真实拖动:无 transition 连续跟随
}
function onUp(e) {
if (!dragging) return;
dragging = false;
if (liveDrag) {
// 拖动结束吸附最近档。先用松手点的连续位置渲染knob 不动),
// 同时移除 live-drag 恢复 transition下一帧再吸附到档位点
// 避免 transition 恢复与位置变化同帧导致瞬间跳变
const x = clampX(e);
levelIndex = pxToLevel(x);
liveDrag = false;
render(x);
requestAnimationFrame(() => render());
return;
}
render();
}
function clampX(e) {
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
return Math.max(EDGE, Math.min(rect.width - EDGE, x));
}
track.addEventListener('pointerdown', onDown);
track.addEventListener('pointermove', onMove);
track.addEventListener('pointerup', onUp);
track.addEventListener('pointercancel', onUp);
// 勾选/取消「默认」
defaultToggle.addEventListener('click', () => {
isDefault = !isDefault;
render();
});
window.addEventListener('resize', () => render());
render();
})();
</script>
</body>
</html>

View File

@ -8,14 +8,14 @@ from pathlib import Path
from typing import Any, Dict, Optional, Union
try:
from config.limits import THINKING_FAST_INTERVAL
from config.limits import REASONING_EFFORT_LEVELS
except ImportError:
THINKING_FAST_INTERVAL = 10
REASONING_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
from core.tool_config import TOOL_CATEGORIES
from config.model_profiles import get_default_model_key, get_registered_model_keys
ALLOWED_RUN_MODES = {"fast", "thinking", "deep"}
ALLOWED_RUN_MODES = {"fast", "thinking"}
ALLOWED_PERMISSION_MODES = {"readonly", "approval", "auto_approval", "unrestricted"}
ALLOWED_THEMES = {"classic", "light", "dark"}
ALLOWED_COMMUNICATION_STYLES = {"default", "human_like", "auto"}
@ -34,8 +34,6 @@ MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_TEXT_LENGTH = 2000
MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
THINKING_INTERVAL_MIN = 1
THINKING_INTERVAL_MAX = 50
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN = 1
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX = 30
RECENT_CONVERSATIONS_PROMPT_LIMIT_DEFAULT = 10
@ -66,11 +64,11 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"profession": "",
"tone": "",
"considerations": [],
"thinking_interval": None,
"disabled_tool_categories": [],
"enabled_skills": None,
"skills_catalog_snapshot": None,
"default_run_mode": "deep",
"default_run_mode": "thinking",
"default_reasoning_effort": None, # 默认推理强度None=默认(不传参)
"default_permission_mode": "unrestricted",
"auto_generate_title": True,
"recent_conversations_prompt_enabled": False,
@ -254,11 +252,6 @@ def sanitize_personalization_payload(
else:
base["considerations"] = _sanitize_considerations(base.get("considerations", []))
if "thinking_interval" in data:
base["thinking_interval"] = _sanitize_thinking_interval(data.get("thinking_interval"))
else:
base["thinking_interval"] = _sanitize_thinking_interval(base.get("thinking_interval"))
# 工具意图提示开关
if "tool_intent_enabled" in data:
base["tool_intent_enabled"] = bool(data.get("tool_intent_enabled"))
@ -316,6 +309,11 @@ def sanitize_personalization_payload(
else:
base["default_run_mode"] = _sanitize_run_mode(base.get("default_run_mode"))
if "default_reasoning_effort" in data:
base["default_reasoning_effort"] = _sanitize_reasoning_effort(data.get("default_reasoning_effort"))
else:
base["default_reasoning_effort"] = _sanitize_reasoning_effort(base.get("default_reasoning_effort"))
permission_mode = data.get("default_permission_mode", base.get("default_permission_mode"))
if isinstance(permission_mode, str) and permission_mode in ALLOWED_PERMISSION_MODES:
base["default_permission_mode"] = permission_mode
@ -844,19 +842,6 @@ def _sanitize_considerations(value: Any) -> str:
return ""
def _sanitize_thinking_interval(value: Any) -> Optional[int]:
if value is None or value == "":
return None
try:
interval = int(value)
except (TypeError, ValueError):
return None
interval = max(THINKING_INTERVAL_MIN, min(THINKING_INTERVAL_MAX, interval))
if interval == THINKING_FAST_INTERVAL:
return None
return interval
def _sanitize_tool_categories(value: Any, allowed: set) -> list:
if not isinstance(value, list):
return []
@ -881,6 +866,16 @@ def _sanitize_run_mode(value: Any) -> Optional[str]:
return None
if isinstance(value, str):
candidate = value.strip().lower()
if candidate == "deep": # 旧版标识符,映射为思考模式
candidate = "thinking"
if candidate in ALLOWED_RUN_MODES:
return candidate
return None
def _sanitize_reasoning_effort(value: Any) -> Optional[str]:
"""推理强度:合法档位原样返回,其余(含空)一律 None=默认不传参。"""
if not isinstance(value, str):
return None
candidate = value.strip().lower()
return candidate if candidate in REASONING_EFFORT_LEVELS else None

View File

@ -542,9 +542,6 @@ class SubAgentTask:
client = DeepSeekClient(thinking_mode=(self.thinking_mode == "thinking"), web_mode=True)
client.model_key = chosen_key
client.project_path = str(self.manager.project_path)
if self.thinking_mode == "thinking":
# 子智能体的 thinking 模式应全程使用思考模型
client.deep_thinking_session = True
client.apply_profile(model_map[chosen_key])
return client, chosen_key

View File

@ -277,9 +277,13 @@ def send_message_api(workspace_id: str):
"personalization_name": personalization_name,
"workspace_id": ws.workspace_id,
})
# 立即应用个性化偏好(确保禁用工具分类生效)
# 立即应用个性化偏好(确保禁用工具分类生效);
# 对话加载链路不应用默认 模型/模式/推理强度(以对话 meta 为权威)
try:
terminal.apply_personalization_preferences(terminal.context_manager.custom_personalization_config)
terminal.apply_personalization_preferences(
terminal.context_manager.custom_personalization_config,
apply_default_modes=False,
)
except Exception as exc:
debug_log(f"[api_v1] 应用个性化失败: {exc}")
except Exception as exc:
@ -688,7 +692,8 @@ def list_models_api():
"visible": not bool(profile.get("hidden")),
"supports_thinking": profile.get("supports_thinking", False),
"fast_only": profile.get("fast_only", False),
"deep_only": profile.get("deep_only", False),
"thinking_only": profile.get("thinking_only", False),
"supports_reasoning_effort": profile.get("supports_reasoning_effort", False),
"multimodal": multimodal,
"context_window": profile.get("context_window"),
"max_output_tokens": (profile.get("fast") or {}).get("max_tokens"),

View File

@ -225,7 +225,6 @@ from config import (
DEFAULT_PROJECT_PATH,
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
MAX_ACTIVE_USER_CONTAINERS,
PROJECT_MAX_STORAGE_MB,
PROJECT_MAX_STORAGE_BYTES,
@ -239,8 +238,6 @@ from modules.upload_security import UploadQuarantineManager, UploadSecurityError
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
)
from modules.user_container_manager import UserContainerManager
from modules.usage_tracker import UsageTracker, QUOTA_DEFAULTS
@ -332,7 +329,6 @@ RECENT_UPLOAD_EVENT_LIMIT = 150
RECENT_UPLOAD_FEED_LIMIT = 60
DEFAULT_PORT = WEB_SERVER_PORT
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
CSRF_HEADER_NAME = state.CSRF_HEADER_NAME
CSRF_SESSION_KEY = state.CSRF_SESSION_KEY
CSRF_SAFE_METHODS = state.CSRF_SAFE_METHODS
@ -1098,10 +1094,6 @@ def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[
terminal.set_run_mode("thinking")
else:
terminal.set_run_mode("fast")
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
else:
terminal.api_client.start_new_task()
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
except Exception:
@ -1114,11 +1106,7 @@ def reset_system_state(terminal: Optional[WebTerminal]):
return
try:
# 1. 重置API客户端状态
if hasattr(terminal, 'api_client') and terminal.api_client:
debug_log("重置API客户端状态")
terminal.api_client.start_new_task(force_deep=getattr(terminal, "deep_thinking_mode", False))
# 1. 重置API客户端状态思考状态已简化无需额外重置
# 2. 重置主终端会话状态
if hasattr(terminal, 'current_session_id'):
terminal.current_session_id += 1 # 开始新会话
@ -1189,101 +1177,6 @@ def log_goal_mode_debug_entry(data: Dict[str, Any]):
_write_log(GOAL_MODE_DEBUG_LOG_FILE, serialized)
def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]:
"""获取(或初始化)思考调度状态。"""
state = getattr(terminal, "_thinking_state", None)
if not state:
state = {"fast_streak": 0, "force_next": False, "suppress_next": False}
terminal._thinking_state = state
return state
def mark_force_thinking(terminal: WebTerminal, reason: str = ""):
"""标记下一次API调用必须使用思考模型。"""
if getattr(terminal, "deep_thinking_mode", False):
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
state["force_next"] = True
if reason:
debug_log(f"[Thinking] 下次强制思考,原因: {reason}")
def mark_suppress_thinking(terminal: WebTerminal):
"""标记下一次API调用必须跳过思考模型例如写入窗口"""
if getattr(terminal, "deep_thinking_mode", False):
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
state["suppress_next"] = True
def apply_thinking_schedule(terminal: WebTerminal):
"""根据当前状态配置API客户端的思考/快速模式。"""
client = terminal.api_client
if getattr(terminal, "deep_thinking_mode", False):
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
return
if not getattr(terminal, "thinking_mode", False):
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
return
state = get_thinking_state(terminal)
if state.get("suppress_next"):
client.skip_thinking_next_call = True
state["suppress_next"] = False
debug_log("[Thinking] 已标记跳过思考模式。")
return
if state.get("force_next"):
client.force_thinking_next_call = True
state["force_next"] = False
state["fast_streak"] = 0
debug_log("[Thinking] 响应失败,下一次强制思考。")
return
custom_interval = getattr(terminal, "thinking_fast_interval", THINKING_FAST_INTERVAL)
interval = max(0, custom_interval or 0)
if interval > 0:
allowed_fast = max(0, interval - 1)
if state.get("fast_streak", 0) >= allowed_fast:
client.force_thinking_next_call = True
state["fast_streak"] = 0
if allowed_fast == 0:
debug_log("[Thinking] 频率=1持续思考。")
else:
debug_log(f"[Thinking] 快速模式已连续 {allowed_fast} 次,下一次强制思考。")
return
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
def update_thinking_after_call(terminal: WebTerminal):
"""一次API调用完成后更新快速计数。"""
if getattr(terminal, "deep_thinking_mode", False):
state = get_thinking_state(terminal)
state["fast_streak"] = 0
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
if terminal.api_client.last_call_used_thinking:
state["fast_streak"] = 0
else:
state["fast_streak"] = state.get("fast_streak", 0) + 1
debug_log(f"[Thinking] 快速模式计数: {state['fast_streak']}")
def maybe_mark_failure_from_message(terminal: WebTerminal, content: Optional[str]):
"""根据system消息内容判断是否需要强制思考。"""
if not content:
return
normalized = content.lower()
if any(keyword.lower() in normalized for keyword in THINKING_FAILURE_KEYWORDS):
mark_force_thinking(terminal, reason="system_message")
def detect_tool_failure(result_data: Any) -> bool:
"""识别工具返回结果是否代表失败。"""
if not isinstance(result_data, dict):

View File

@ -167,7 +167,9 @@ def login():
candidate_mode = (personal_config or {}).get('default_run_mode')
if isinstance(candidate_mode, str):
normalized_mode = candidate_mode.lower()
if normalized_mode in {"fast", "thinking", "deep"}:
if normalized_mode == "deep": # 旧版标识符映射
normalized_mode = "thinking"
if normalized_mode in {"fast", "thinking"}:
preferred_run_mode = normalized_mode
except Exception as exc:
debug_log(f"加载个性化偏好失败: {exc}")

View File

@ -13,13 +13,11 @@ from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import secrets
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -39,7 +37,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot

View File

@ -14,13 +14,11 @@ from werkzeug.exceptions import RequestEntityTooLarge
import secrets
import mimetypes
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -40,7 +38,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot

View File

@ -13,13 +13,11 @@ from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import secrets
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -39,7 +37,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot

View File

@ -18,13 +18,11 @@ from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import secrets
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -49,7 +47,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker, get_user_resources
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot

View File

@ -13,13 +13,11 @@ from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import secrets
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -39,7 +37,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot
@ -82,21 +80,19 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st
@with_terminal
@rate_limited("thinking_mode_toggle", 15, 60, scope="user")
def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""切换思考模式"""
"""切换运行模式fast / thinking旧值 deep 自动映射为 thinking"""
try:
data = request.get_json() or {}
requested_mode = data.get('mode')
if requested_mode in {"fast", "thinking", "deep"}:
if isinstance(requested_mode, str) and requested_mode.lower() == 'deep':
requested_mode = 'thinking'
if requested_mode in {"fast", "thinking"}:
target_mode = requested_mode
elif 'thinking_mode' in data:
target_mode = "thinking" if bool(data.get('thinking_mode')) else "fast"
else:
target_mode = terminal.run_mode
terminal.set_run_mode(target_mode)
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
else:
terminal.api_client.start_new_task()
session['thinking_mode'] = terminal.thinking_mode
session['run_mode'] = terminal.run_mode
# 更新当前对话的元数据
@ -136,6 +132,72 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna
"message": "切换思考模式时发生异常"
}), code
@chat_bp.route('/api/reasoning-effort', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("reasoning_effort_toggle", 20, 60, scope="user")
def update_reasoning_effort(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""设置会话级推理强度effort 为 null/空 表示默认(请求中不传该参数)。"""
try:
data = request.get_json() or {}
effort = data.get('effort')
if isinstance(effort, str) and not effort.strip():
effort = None
# 前端防抖序列开始时记录的所属对话:指定后把值写入该对话 meta
# terminal 当前状态仅当保存目标就是当前对话时才更新,避免串写
ctx = terminal.context_manager
target_conversation_id = str(data.get('conversation_id') or '').strip() or None
is_current_target = not target_conversation_id or target_conversation_id == ctx.current_conversation_id
if is_current_target:
terminal.set_reasoning_effort(effort)
save_conversation_id = target_conversation_id or ctx.current_conversation_id
# 更新对话的元数据
if save_conversation_id:
try:
if is_current_target:
ctx.conversation_manager.save_conversation(
conversation_id=save_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
reasoning_effort=effort,
model_key=getattr(terminal, "model_key", None),
has_images=getattr(ctx, "has_images", False),
has_videos=getattr(ctx, "has_videos", False)
)
else:
# 所属对话已不是当前对话:只更新推理强度 meta。
# 重新读出目标对话消息回传merge 语义下保持不变),
# 避免把当前对话的消息串写过去
existing = ctx.conversation_manager.load_conversation(save_conversation_id) or {}
ctx.conversation_manager.save_conversation(
conversation_id=save_conversation_id,
messages=existing.get("messages") or [],
reasoning_effort=effort,
)
except Exception as exc:
print(f"[API] 保存推理强度到对话失败: {exc}")
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
return jsonify({
"success": True,
"data": {
"reasoning_effort": terminal.reasoning_effort
}
})
except Exception as exc:
print(f"[API] 设置推理强度失败: {exc}")
code = 400 if isinstance(exc, ValueError) else 500
return jsonify({
"success": False,
"error": str(exc),
"message": "设置推理强度时发生异常"
}), code
@chat_bp.route('/api/model', methods=['POST'])
@api_login_required
@with_terminal
@ -185,6 +247,7 @@ def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str)
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
reasoning_effort=terminal.reasoning_effort,
model_key=terminal.model_key,
has_images=getattr(ctx, "has_images", False),
has_videos=getattr(ctx, "has_videos", False)
@ -249,11 +312,6 @@ def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace
int(model_window or compression_settings.get("deep_trigger_tokens") or 0),
),
},
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
},
"recent_conversations_prompt_limit_range": {
"min": RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
"max": RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
@ -293,7 +351,12 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp
except Exception as sync_exc:
debug_log(f"[Skills] 同步失败: {sync_exc}")
try:
terminal.apply_personalization_preferences(config)
# 对话级 terminal 的 模型/模式/推理强度 以对话 meta 为权威,
# 保存个性化默认值不得回写当前对话;工作区级(/new则立即生效
terminal.apply_personalization_preferences(
config,
apply_default_modes=not bool(getattr(terminal, '_bound_conversation_id', None)),
)
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
ctx = getattr(terminal, 'context_manager', None)
@ -335,11 +398,6 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp
int(model_window or compression_settings.get("deep_trigger_tokens") or 0),
),
},
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
},
"recent_conversations_prompt_limit_range": {
"min": RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
"max": RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,

View File

@ -13,13 +13,11 @@ from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import secrets
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
from modules.personalization_manager import (
load_personalization_config,
resolve_context_compression_settings,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
)
@ -39,7 +37,7 @@ from server.auth_helpers import api_login_required, resolve_admin_policy, get_cu
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
from server.security import rate_limited, prune_socket_tokens
from server.utils_common import debug_log
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot

View File

@ -35,7 +35,6 @@ from config import (
DEFAULT_PROJECT_PATH,
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
PROJECT_MAX_STORAGE_MB,
PROJECT_MAX_STORAGE_BYTES,
UPLOAD_SCAN_LOG_SUBDIR,
@ -43,8 +42,6 @@ from config import (
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
)
from modules.sub_agent.state import TERMINAL_STATUSES as SUB_AGENT_TERMINAL_STATUSES
from modules.upload_security import UploadSecurityError
@ -88,7 +85,6 @@ from .state import (
PROJECT_STORAGE_CACHE_TTL_SECONDS,
RECENT_UPLOAD_EVENT_LIMIT,
RECENT_UPLOAD_FEED_LIMIT,
THINKING_FAILURE_KEYWORDS,
TITLE_PROMPT_PATH,
get_last_active_ts,
user_manager,
@ -105,12 +101,6 @@ from .state import (
from .chat_flow_helpers import (
detect_malformed_tool_call as _detect_malformed_tool_call,
detect_tool_failure,
get_thinking_state,
mark_force_thinking as _mark_force_thinking,
mark_suppress_thinking,
apply_thinking_schedule as _apply_thinking_schedule,
update_thinking_after_call as _update_thinking_after_call,
maybe_mark_failure_from_message as _maybe_mark_failure_from_message,
generate_conversation_title_background as _generate_conversation_title_background,
)
from .chat_flow_runner import handle_task_with_sender
@ -132,27 +122,6 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
)
def mark_force_thinking(terminal: WebTerminal, reason: str = ""):
return _mark_force_thinking(terminal, reason=reason, debug_logger=debug_log)
def apply_thinking_schedule(terminal: WebTerminal):
return _apply_thinking_schedule(terminal, default_interval=THINKING_FAST_INTERVAL, debug_logger=debug_log)
def update_thinking_after_call(terminal: WebTerminal):
return _update_thinking_after_call(terminal, debug_logger=debug_log)
def maybe_mark_failure_from_message(terminal: WebTerminal, content: Optional[str]):
return _maybe_mark_failure_from_message(
terminal,
content,
failure_keywords=THINKING_FAILURE_KEYWORDS,
debug_logger=debug_log,
)
def detect_malformed_tool_call(text):
return _detect_malformed_tool_call(text)

View File

@ -183,110 +183,6 @@ def generate_conversation_title_background(
_title_debug_log("title_background_runner_exception", error=str(exc), conversation_id=conversation_id, username=username)
def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]:
"""获取(或初始化)思考调度状态。"""
state = getattr(terminal, "_thinking_state", None)
if not state:
state = {"fast_streak": 0, "force_next": False, "suppress_next": False}
terminal._thinking_state = state
return state
def mark_force_thinking(terminal: WebTerminal, reason: str = "", debug_logger=None):
"""标记下一次API调用必须使用思考模型。"""
if getattr(terminal, "deep_thinking_mode", False):
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
state["force_next"] = True
if reason and callable(debug_logger):
debug_logger(f"[Thinking] 下次强制思考,原因: {reason}")
def mark_suppress_thinking(terminal: WebTerminal):
"""标记下一次API调用必须跳过思考模型例如写入窗口"""
if getattr(terminal, "deep_thinking_mode", False):
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
state["suppress_next"] = True
def apply_thinking_schedule(terminal: WebTerminal, default_interval: int, debug_logger):
"""根据当前状态配置API客户端的思考/快速模式。"""
client = terminal.api_client
if getattr(terminal, "deep_thinking_mode", False):
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
return
if not getattr(terminal, "thinking_mode", False):
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
return
state = get_thinking_state(terminal)
if state.get("suppress_next"):
client.skip_thinking_next_call = True
state["suppress_next"] = False
debug_logger("[Thinking] 已标记跳过思考模式。")
return
if state.get("force_next"):
client.force_thinking_next_call = True
state["force_next"] = False
state["fast_streak"] = 0
debug_logger("[Thinking] 响应失败,下一次强制思考。")
return
custom_interval = getattr(terminal, "thinking_fast_interval", default_interval)
interval = max(0, custom_interval or 0)
if interval > 0:
allowed_fast = max(0, interval - 1)
if state.get("fast_streak", 0) >= allowed_fast:
client.force_thinking_next_call = True
state["fast_streak"] = 0
if allowed_fast == 0:
debug_logger("[Thinking] 频率=1持续思考。")
else:
debug_logger(f"[Thinking] 快速模式已连续 {allowed_fast} 次,下一次强制思考。")
return
client.force_thinking_next_call = False
client.skip_thinking_next_call = False
def update_thinking_after_call(terminal: WebTerminal, debug_logger):
"""一次API调用完成后更新快速计数。"""
if getattr(terminal, "deep_thinking_mode", False):
state = get_thinking_state(terminal)
state["fast_streak"] = 0
return
if not getattr(terminal, "thinking_mode", False):
return
state = get_thinking_state(terminal)
if terminal.api_client.last_call_used_thinking:
state["fast_streak"] = 0
else:
state["fast_streak"] = state.get("fast_streak", 0) + 1
debug_logger(f"[Thinking] 快速模式计数: {state['fast_streak']}")
def maybe_mark_failure_from_message(
terminal: WebTerminal,
content: Optional[str],
failure_keywords,
debug_logger,
):
"""根据system消息内容判断是否需要强制思考。"""
if not content:
return
normalized = content.lower()
if any(keyword.lower() in normalized for keyword in failure_keywords):
mark_force_thinking(terminal, reason="system_message", debug_logger=debug_logger)
def detect_tool_failure(result_data: Any) -> bool:
"""识别工具返回结果是否代表失败。"""
if not isinstance(result_data, dict):

View File

@ -2,20 +2,12 @@ from __future__ import annotations
from .chat_flow_runtime import (
generate_conversation_title_background,
mark_force_thinking,
apply_thinking_schedule,
update_thinking_after_call,
maybe_mark_failure_from_message,
detect_malformed_tool_call,
)
from .chat_flow_task_runner import handle_task_with_sender
__all__ = [
"generate_conversation_title_background",
"mark_force_thinking",
"apply_thinking_schedule",
"update_thinking_after_call",
"maybe_mark_failure_from_message",
"detect_malformed_tool_call",
"handle_task_with_sender",
]

View File

@ -28,7 +28,6 @@ from config import (
DEFAULT_PROJECT_PATH,
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
PROJECT_MAX_STORAGE_MB,
PROJECT_MAX_STORAGE_BYTES,
UPLOAD_SCAN_LOG_SUBDIR,
@ -36,8 +35,6 @@ from config import (
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
)
from modules.upload_security import UploadSecurityError
from modules.user_manager import UserWorkspace
@ -79,7 +76,6 @@ from .state import (
PROJECT_STORAGE_CACHE_TTL_SECONDS,
RECENT_UPLOAD_EVENT_LIMIT,
RECENT_UPLOAD_FEED_LIMIT,
THINKING_FAILURE_KEYWORDS,
TITLE_PROMPT_PATH,
get_last_active_ts,
user_manager,
@ -96,12 +92,6 @@ from .state import (
from .chat_flow_helpers import (
detect_malformed_tool_call as _detect_malformed_tool_call,
detect_tool_failure,
get_thinking_state,
mark_force_thinking as _mark_force_thinking,
mark_suppress_thinking,
apply_thinking_schedule as _apply_thinking_schedule,
update_thinking_after_call as _update_thinking_after_call,
maybe_mark_failure_from_message as _maybe_mark_failure_from_message,
generate_conversation_title_background as _generate_conversation_title_background,
)
@ -125,22 +115,5 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
debug_logger=debug_log,
)
def mark_force_thinking(terminal: WebTerminal, reason: str = ""):
return _mark_force_thinking(terminal, reason=reason, debug_logger=debug_log)
def apply_thinking_schedule(terminal: WebTerminal):
return _apply_thinking_schedule(terminal, default_interval=THINKING_FAST_INTERVAL, debug_logger=debug_log)
def update_thinking_after_call(terminal: WebTerminal):
return _update_thinking_after_call(terminal, debug_logger=debug_log)
def maybe_mark_failure_from_message(terminal: WebTerminal, content: Optional[str]):
return _maybe_mark_failure_from_message(
terminal,
content,
failure_keywords=THINKING_FAILURE_KEYWORDS,
debug_logger=debug_log,
)
def detect_malformed_tool_call(text):
return _detect_malformed_tool_call(text)

View File

@ -28,7 +28,6 @@ from config import (
DEFAULT_PROJECT_PATH,
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
PROJECT_MAX_STORAGE_MB,
PROJECT_MAX_STORAGE_BYTES,
UPLOAD_SCAN_LOG_SUBDIR,
@ -36,8 +35,6 @@ from config import (
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
resolve_context_compression_settings,
)
from modules.skill_hint_manager import SkillHintManager
@ -86,7 +83,6 @@ from .state import (
PROJECT_STORAGE_CACHE_TTL_SECONDS,
RECENT_UPLOAD_EVENT_LIMIT,
RECENT_UPLOAD_FEED_LIMIT,
THINKING_FAILURE_KEYWORDS,
TITLE_PROMPT_PATH,
get_last_active_ts,
user_manager,
@ -104,12 +100,6 @@ from .state import (
from .chat_flow_helpers import (
detect_malformed_tool_call as _detect_malformed_tool_call,
detect_tool_failure,
get_thinking_state,
mark_force_thinking as _mark_force_thinking,
mark_suppress_thinking,
apply_thinking_schedule as _apply_thinking_schedule,
update_thinking_after_call as _update_thinking_after_call,
maybe_mark_failure_from_message as _maybe_mark_failure_from_message,
generate_conversation_title_background as _generate_conversation_title_background,
)
@ -124,10 +114,6 @@ from .chat_flow_runner_helpers import (
from .chat_flow_runtime import (
generate_conversation_title_background,
mark_force_thinking,
apply_thinking_schedule,
update_thinking_after_call,
maybe_mark_failure_from_message,
detect_malformed_tool_call,
)
@ -1390,14 +1376,6 @@ async def handle_task_with_sender(
raw_sender(event_type, payload)
# 如果是思考模式,重置状态
if web_terminal.thinking_mode:
web_terminal.api_client.start_new_task(force_deep=web_terminal.deep_thinking_mode)
state = get_thinking_state(web_terminal)
state["fast_streak"] = 0
state["force_next"] = False
state["suppress_next"] = False
# 添加到对话历史
user_work_started_at = datetime.now().isoformat()
user_message_index = -1
@ -1885,11 +1863,8 @@ async def handle_task_with_sender(
sender('system_message', {
'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。'
})
mark_force_thinking(web_terminal, reason="tool_limit")
break
apply_thinking_schedule(web_terminal)
full_response = ""
tool_calls = []
current_thinking = ""
@ -2025,12 +2000,6 @@ async def handle_task_with_sender(
if full_response.strip():
debug_log(f"流式文本内容长度: {len(full_response)} 字符")
if web_terminal.api_client.last_call_used_thinking and current_thinking:
web_terminal.api_client.current_task_thinking = current_thinking or ""
if web_terminal.api_client.current_task_first_call:
web_terminal.api_client.current_task_first_call = False
update_thinking_after_call(web_terminal)
# 检测是否有格式错误的工具调用
if not tool_calls and full_response and AUTO_FIX_TOOL_CALL:
if detect_malformed_tool_call(full_response):
@ -2044,7 +2013,6 @@ async def handle_task_with_sender(
sender('system_message', {
'content': f'⚠️ 自动修复: {fix_message}'
})
maybe_mark_failure_from_message(web_terminal, f'⚠️ 自动修复: {fix_message}')
messages.append({
"role": "user",
@ -2058,7 +2026,6 @@ async def handle_task_with_sender(
sender('system_message', {
'content': f'⌘ 工具调用格式错误,自动修复失败。请手动检查并重试。'
})
maybe_mark_failure_from_message(web_terminal, '⌘ 工具调用格式错误,自动修复失败。请手动检查并重试。')
break
# 构建助手消息用于API继续对话
@ -2166,14 +2133,12 @@ async def handle_task_with_sender(
sender('system_message', {
'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。'
})
maybe_mark_failure_from_message(web_terminal, f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。')
if consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL + 2:
debug_log(f"终止: 工具 {tool_name} 调用次数过多")
sender('system_message', {
'content': f'⌘ 工具 {tool_name} 重复调用过多,任务终止。'
})
maybe_mark_failure_from_message(web_terminal, f'⌘ 工具 {tool_name} 重复调用过多,任务终止。')
break
else:
consecutive_same_tool.clear()
@ -2208,8 +2173,6 @@ async def handle_task_with_sender(
process_sub_agent_updates=process_sub_agent_updates,
process_background_command_updates=process_background_command_updates,
process_multi_agent_master_messages=process_multi_agent_master_messages,
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
mark_force_thinking=mark_force_thinking,
get_stop_flag=get_stop_flag,
clear_stop_flag=clear_stop_flag,
workspace=workspace,

View File

@ -150,7 +150,7 @@ def inject_runtime_user_message(
return formatted
async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log):
"""轮询子智能体任务并通知前端,并把结果插入当前对话上下文。"""
manager = getattr(web_terminal, "sub_agent_manager", None)
if not manager:
@ -263,10 +263,9 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
if inline:
web_terminal._inline_sub_agent_notified.add(inline_key)
debug_log(f"[SubAgent] 插入子智能体通知 after_tool_call_id={after_tool_call_id} inline={inline}")
maybe_mark_failure_from_message(web_terminal, message)
async def process_background_command_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
async def process_background_command_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log):
"""轮询后台 run_command 完成状态并发送 system 消息通知。"""
manager = getattr(web_terminal, "background_command_manager", None)
if not manager:
@ -313,7 +312,6 @@ async def process_background_command_updates(*, messages: List[Dict], inline: bo
except Exception:
debug_log(f"[BgCmdDebug] mark_notified failed command_id={command_id}")
pass
maybe_mark_failure_from_message(web_terminal, message)

View File

@ -261,7 +261,7 @@ async def _wait_for_user_questions(*, question_ids: List[str], username: str, ti
return answered
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, process_background_command_updates, process_multi_agent_master_messages=process_multi_agent_master_messages, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag, workspace=None):
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, process_background_command_updates, process_multi_agent_master_messages=process_multi_agent_master_messages, get_stop_flag, clear_stop_flag, workspace=None):
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
web_terminal._tool_loop_active = True
allowed_tool_names = set()
@ -1240,7 +1240,6 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
inline=False,
extra_metadata={"task_id": result_data.get("task_id")},
)
maybe_mark_failure_from_message(web_terminal, system_message)
# 自动深层压缩(工具调用后触发)
current_context_tokens = web_terminal.context_manager.get_current_context_tokens(conversation_id)
@ -1263,9 +1262,6 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
await asyncio.sleep(0.2)
if tool_failed:
mark_force_thinking(web_terminal, reason=f"{function_name}_failed")
# 自动深层压缩必须等待同一轮全部 tool_call 的 tool 消息都已写入 messages/历史后再触发。
if deep_compression_pending and not web_terminal.context_manager.is_compression_in_progress():
web_terminal.context_manager._set_meta_flag("is_ultra_long_conversation", True)
@ -1308,7 +1304,6 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
web_terminal=web_terminal,
sender=sender,
debug_log=debug_log,
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
)
debug_log(
"[BgCmdDebug] after all tools finished -> poll background command updates "
@ -1321,7 +1316,6 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
web_terminal=web_terminal,
sender=sender,
debug_log=debug_log,
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
)
await process_multi_agent_master_messages(
messages=messages,

View File

@ -145,12 +145,32 @@ def _apply_workspace_personalization_preferences(terminal: WebTerminal, workspac
and not bool(session_model)
and not bool(getattr(terminal, "_workspace_default_model_applied", False))
)
terminal.apply_personalization_preferences(config, apply_default_model=apply_default_model)
# 对话级 terminal 的 模型/思考模式/推理强度 以对话 meta 为权威,
# 此函数在每次 /api/status、任务创建、加载资源时都会触发
# 不得在对话加载后反复用 prefs 默认值覆盖 meta 恢复值。
# 工作区级 terminal/new 页)同理:三项 modes 默认值仅首次应用一次,
# 之后用户在 /new 手动调整的 模式/档位 必须稳定存活到创建对话时,
# 不能被 status 轮询反复重置prefs 更新走 settings 保存路径显式应用,
# 新建空对话走 create_new_conversation 的 prefer_defaults 路径重置)。
apply_default_modes = (
not is_conversation_bound
and not bool(getattr(terminal, "_workspace_default_modes_applied", False))
)
terminal.apply_personalization_preferences(
config,
apply_default_model=apply_default_model,
apply_default_modes=apply_default_modes,
)
if apply_default_model:
try:
terminal._workspace_default_model_applied = True
except Exception:
pass
if apply_default_modes:
try:
terminal._workspace_default_modes_applied = True
except Exception:
pass
if has_request_context() and update_session:
session["run_mode"] = getattr(terminal, "run_mode", session.get("run_mode"))
session["thinking_mode"] = getattr(terminal, "thinking_mode", session.get("thinking_mode"))
@ -631,9 +651,13 @@ def apply_conversation_overrides(terminal: WebTerminal, workspace, conversation_
else:
terminal.context_manager.custom_personalization_config = None
# 应用个性化偏好(含禁用工具分类)到当前终端
# 应用个性化偏好(含禁用工具分类)到当前终端;
# 对话加载链路不应用默认 模型/模式/推理强度(以对话 meta 为权威)
try:
terminal.apply_personalization_preferences(terminal.context_manager.custom_personalization_config)
terminal.apply_personalization_preferences(
terminal.context_manager.custom_personalization_config,
apply_default_modes=False,
)
except Exception as exc:
debug_log(f"[apply_overrides] 应用个性化失败: {exc}")
except Exception as exc:
@ -745,10 +769,10 @@ def ensure_conversation_loaded(
terminal.set_run_mode("thinking")
else:
terminal.set_run_mode("fast")
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
else:
terminal.api_client.start_new_task()
try:
terminal.set_reasoning_effort(meta.get("reasoning_effort"))
except (ValueError, AttributeError):
pass
if has_request_context():
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
@ -790,9 +814,6 @@ def reset_system_state(terminal: Optional[WebTerminal]):
if not terminal:
return
try:
if hasattr(terminal, 'api_client') and terminal.api_client:
debug_log("重置API客户端状态")
terminal.api_client.start_new_task(force_deep=getattr(terminal, "deep_thinking_mode", False))
if hasattr(terminal, 'current_session_id'):
terminal.current_session_id += 1
debug_log(f"重置会话ID为: {terminal.current_session_id}")

View File

@ -33,16 +33,14 @@ from config import (
DEFAULT_PROJECT_PATH,
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
PROJECT_MAX_STORAGE_MB,
PROJECT_MAX_STORAGE_BYTES,
UPLOAD_SCAN_LOG_SUBDIR,
REASONING_EFFORT_LEVELS,
)
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
)
from modules.upload_security import UploadSecurityError
from modules.user_manager import UserWorkspace
@ -189,6 +187,8 @@ def _build_safe_load_result(terminal: WebTerminal, conversation_id: str) -> Dict
}
meta = data.get("metadata", {}) or {}
run_mode = meta.get("run_mode") or getattr(terminal, "run_mode", "fast")
if run_mode == "deep": # 旧版标识符映射
run_mode = "thinking"
thinking_mode = bool(meta.get("thinking_mode", run_mode != "fast"))
return {
"success": True,
@ -197,6 +197,7 @@ def _build_safe_load_result(terminal: WebTerminal, conversation_id: str) -> Dict
"messages_count": len(data.get("messages", []) or []),
"run_mode": run_mode,
"thinking_mode": thinking_mode,
"reasoning_effort": meta.get("reasoning_effort"),
"model_key": meta.get("model_key") or getattr(terminal, "model_key", None),
"message": f"对话已加载: {normalized_id}",
"safe_navigation": True,
@ -601,6 +602,22 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
target_workspace_id = (data.get('workspace_id') or '').strip()
multi_agent_mode = bool(data.get('multi_agent_mode'))
# /new 页发消息/新建对话时前端随 body 传入当前生效的推理强度,
# 权威写入新对话 meta未提供时回落 terminal 当前档位 / 个性化默认值
body_effort_provided = 'reasoning_effort' in data
body_effort = None
if body_effort_provided:
raw_effort = data.get('reasoning_effort')
if raw_effort is None:
body_effort = None # 显式默认档(不传参)
elif isinstance(raw_effort, str):
candidate_effort = raw_effort.strip().lower()
if candidate_effort in REASONING_EFFORT_LEVELS:
body_effort = candidate_effort
else:
body_effort_provided = False # 非法值视为未提供
else:
body_effort_provided = False
if target_workspace_id:
try:
@ -626,15 +643,34 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
prefs = load_personalization_config(workspace.data_dir)
except Exception:
prefs = {}
safe_run_mode = run_mode
if safe_run_mode not in {"fast", "thinking", "deep"}:
candidate = (prefs or {}).get("default_run_mode")
safe_run_mode = candidate if candidate in {"fast", "thinking", "deep"} else "fast"
safe_run_mode = str(run_mode or "").strip().lower()
if safe_run_mode == "deep": # 旧版标识符映射
safe_run_mode = "thinking"
if safe_run_mode not in {"fast", "thinking"}:
candidate = str((prefs or {}).get("default_run_mode") or "").strip().lower()
if candidate == "deep":
candidate = "thinking"
safe_run_mode = candidate if candidate in {"fast", "thinking"} else "fast"
safe_thinking = bool(thinking_mode) if thinking_mode is not None else safe_run_mode != "fast"
previous_cm_current = getattr(cm, "current_conversation_id", None)
default_permission_mode = (prefs or {}).get("default_permission_mode")
if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
default_permission_mode = None
# 推理强度优先级body 显式值 > terminal 当前档位 > 个性化默认值
if body_effort_provided:
default_effort = body_effort
else:
terminal_effort = getattr(terminal, "reasoning_effort", None)
if isinstance(terminal_effort, str) and terminal_effort.strip().lower() in REASONING_EFFORT_LEVELS:
default_effort = terminal_effort.strip().lower()
else:
default_effort = (prefs or {}).get("default_reasoning_effort")
if isinstance(default_effort, str):
default_effort = default_effort.strip().lower() or None
if default_effort not in REASONING_EFFORT_LEVELS:
default_effort = None
else:
default_effort = None
conversation_id = cm.create_conversation(
project_path=str(workspace.project_path),
thinking_mode=safe_thinking,
@ -645,6 +681,7 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
"multi_agent_mode": bool(multi_agent_mode),
"reasoning_effort": default_effort,
},
)
try:
@ -665,10 +702,15 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
"safe_navigation": True,
}
else:
# body 显式携带档位时权威覆盖prefer_defaults 路径内部会应用 prefs 默认值,
# 必须以用户实际选择为准)
create_meta_overrides = {"multi_agent_mode": bool(multi_agent_mode)}
if body_effort_provided:
create_meta_overrides["reasoning_effort"] = body_effort
result = terminal.create_new_conversation(
thinking_mode=thinking_mode,
run_mode=run_mode,
metadata_overrides={"multi_agent_mode": bool(multi_agent_mode)},
metadata_overrides=create_meta_overrides,
)
# 同步 terminal 级别的多智能体开关,避免新对话继承旧状态
terminal.multi_agent_mode = bool(multi_agent_mode)
@ -1366,6 +1408,7 @@ def _restore_checkpoint_to_conversation(
restore_project_path = restore_meta.get("project_path") or str(workspace.project_path)
restore_thinking_mode = bool(restore_meta.get("thinking_mode", False))
restore_run_mode = restore_meta.get("run_mode") or ("thinking" if restore_thinking_mode else "fast")
restore_reasoning_effort = restore_meta.get("reasoning_effort")
restore_model_key = restore_meta.get("model_key")
restore_has_images = bool(restore_meta.get("has_images", False))
restore_has_videos = bool(restore_meta.get("has_videos", False))
@ -1376,6 +1419,7 @@ def _restore_checkpoint_to_conversation(
project_path=restore_project_path,
thinking_mode=restore_thinking_mode,
run_mode=restore_run_mode,
reasoning_effort=restore_reasoning_effort,
model_key=restore_model_key,
has_images=restore_has_images,
has_videos=restore_has_videos,
@ -2276,8 +2320,6 @@ def _execute_system_command(terminal: WebTerminal, command: str) -> Dict[str, An
if cmd == "clear":
terminal.context_manager.conversation_history.clear()
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
return {
'command': cmd,
'success': True,

View File

@ -171,6 +171,8 @@ def bootstrap_conversation(conversation_id: str):
raw_meta = conversation_data.get("metadata", {}) or {}
messages = conversation_data.get("messages", []) or []
run_mode = raw_meta.get("run_mode") or ("thinking" if raw_meta.get("thinking_mode") else "fast")
if run_mode == "deep": # 旧版标识符映射
run_mode = "thinking"
# 运行状态聚合(对齐 tasks/api.py running-status主 task + 三类后台)
main_rec = _find_active_main_task(username, normalized_id)
@ -209,6 +211,7 @@ def bootstrap_conversation(conversation_id: str):
"title": conversation_data.get("title", "未知对话"),
"run_mode": run_mode,
"thinking_mode": bool(raw_meta.get("thinking_mode", run_mode != "fast")),
"reasoning_effort": raw_meta.get("reasoning_effort"),
"model_key": raw_meta.get("model_key"),
"multi_agent_mode": bool(raw_meta.get("multi_agent_mode", False)),
"permission_mode": raw_meta.get("permission_mode") or "",

View File

@ -26,6 +26,7 @@ from modules.multi_agent.role_store import (
sync_preset_roles,
)
from config.paths import CUSTOM_ROLES_DIR, WEB_PRESET_ROLES_DIR
from config.limits import REASONING_EFFORT_LEVELS
multi_agent_bp = Blueprint("multi_agent", __name__)
@ -285,6 +286,21 @@ def create_multi_agent_conversation():
preserve_mode = bool(data.get("preserve_mode"))
thinking_mode = data.get("thinking_mode") if preserve_mode and "thinking_mode" in data else None
run_mode = data.get("mode") if preserve_mode and "mode" in data else None
# 前端随 body 传入当前生效的推理强度,权威写入新对话 meta
body_effort_provided = "reasoning_effort" in data
body_effort = None
if body_effort_provided:
raw_effort = data.get("reasoning_effort")
if raw_effort is None:
body_effort = None
elif isinstance(raw_effort, str):
candidate_effort = raw_effort.strip().lower()
if candidate_effort in REASONING_EFFORT_LEVELS:
body_effort = candidate_effort
else:
body_effort_provided = False
else:
body_effort_provided = False
effective_workspace_id = target_workspace_id or session.get("workspace_id") or "default"
active_task = _get_active_workspace_task(username=username, workspace_id=effective_workspace_id)
@ -301,10 +317,14 @@ def create_multi_agent_conversation():
if not cm:
return jsonify({"success": False, "error": "对话管理器未初始化"}), 500
safe_run_mode = run_mode
if safe_run_mode not in {"fast", "thinking", "deep"}:
candidate = (prefs or {}).get("default_run_mode")
safe_run_mode = candidate if candidate in {"fast", "thinking", "deep"} else "fast"
safe_run_mode = str(run_mode or "").strip().lower()
if safe_run_mode == "deep": # 旧版标识符映射
safe_run_mode = "thinking"
if safe_run_mode not in {"fast", "thinking"}:
candidate = str((prefs or {}).get("default_run_mode") or "").strip().lower()
if candidate == "deep":
candidate = "thinking"
safe_run_mode = candidate if candidate in {"fast", "thinking"} else "fast"
safe_thinking = bool(thinking_mode) if thinking_mode is not None else safe_run_mode != "fast"
default_permission_mode = (prefs or {}).get("default_permission_mode")
if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
@ -312,6 +332,22 @@ def create_multi_agent_conversation():
previous_cm_current = getattr(ctx_manager, "current_conversation_id", None)
# 推理强度优先级body 显式值 > terminal 当前档位 > 个性化默认值
if body_effort_provided:
ma_effort = body_effort
else:
terminal_effort = getattr(terminal, "reasoning_effort", None)
if isinstance(terminal_effort, str) and terminal_effort.strip().lower() in REASONING_EFFORT_LEVELS:
ma_effort = terminal_effort.strip().lower()
else:
ma_effort = (prefs or {}).get("default_reasoning_effort")
if isinstance(ma_effort, str):
ma_effort = ma_effort.strip().lower() or None
if ma_effort not in REASONING_EFFORT_LEVELS:
ma_effort = None
else:
ma_effort = None
conversation_id = cm.create_conversation(
project_path=str(workspace.project_path),
thinking_mode=safe_thinking,
@ -322,6 +358,7 @@ def create_multi_agent_conversation():
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
"multi_agent_mode": True,
"reasoning_effort": ma_effort,
},
)
# 恢复 context_manager 的当前对话(不要影响普通对话的 current_conversation_id

View File

@ -61,7 +61,6 @@ PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_
# 其他配置
DEFAULT_PORT = WEB_SERVER_PORT
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
CSRF_HEADER_NAME = "X-CSRF-Token"
CSRF_SESSION_KEY = "_csrf_token"
CSRF_SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
@ -113,7 +112,6 @@ __all__ = [
"PROJECT_STORAGE_CACHE",
"PROJECT_STORAGE_CACHE_TTL_SECONDS",
"DEFAULT_PORT",
"THINKING_FAILURE_KEYWORDS",
"CSRF_HEADER_NAME",
"CSRF_SESSION_KEY",
"CSRF_SAFE_METHODS",

View File

@ -788,7 +788,7 @@ class TaskManager:
f"model_key={getattr(terminal, 'model_key', None)!r} "
f"run_mode={getattr(terminal, 'run_mode', None)!r} "
f"thinking_mode={getattr(terminal, 'thinking_mode', None)!r} "
f"deep_mode={getattr(terminal, 'deep_thinking_mode', None)!r}"
f"reasoning_effort={getattr(terminal, 'reasoning_effort', None)!r}"
)
except Exception:
pass
@ -809,7 +809,7 @@ class TaskManager:
f"model_key={getattr(terminal, 'model_key', None)!r} "
f"run_mode={getattr(terminal, 'run_mode', None)!r} "
f"thinking_mode={getattr(terminal, 'thinking_mode', None)!r} "
f"deep_mode={getattr(terminal, 'deep_thinking_mode', None)!r}"
f"reasoning_effort={getattr(terminal, 'reasoning_effort', None)!r}"
)
except Exception:
pass

View File

@ -225,6 +225,12 @@
<div class="item-desc">{{ option.desc }}</div>
<span v-if="option.value === resolvedRunMode" class="item-check"></span>
</button>
<div v-if="showEffortSlider" class="dropdown-effort">
<EffortSlider
:model-value="reasoningEffort"
@update:model-value="setReasoningEffort"
/>
</div>
</div>
</div>
</transition>
@ -751,6 +757,12 @@
<div class="item-label">{{ option.label }}</div>
<span v-if="option.value === resolvedRunMode" class="item-check"></span>
</button>
<div v-if="showEffortSlider" class="dropdown-effort">
<EffortSlider
:model-value="reasoningEffort"
@update:model-value="setReasoningEffort"
/>
</div>
</div>
</div>
</transition>

View File

@ -8,6 +8,7 @@ import TerminalPanel from '../components/panels/TerminalPanel.vue';
import TokenDrawer from '../components/token/TokenDrawer.vue';
import QuickMenu from '../components/input/QuickMenu.vue';
import InputComposer from '../components/input/InputComposer.vue';
import EffortSlider from '../components/input/EffortSlider.vue';
import AppShell from '../components/shell/AppShell.vue';
import StatusAvatar from '../components/avatar/StatusAvatar.vue';
@ -57,6 +58,7 @@ export const appComponents = {
PersonalizationDrawer,
QuickMenu,
InputComposer,
EffortSlider,
AppShell,
StatusAvatar,
ImagePicker,

View File

@ -61,7 +61,8 @@ export const computed = {
'projectPath',
'agentVersion',
'thinkingMode',
'runMode'
'runMode',
'reasoningEffort'
]),
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
...mapWritableState(useUiStore, [
@ -138,23 +139,29 @@ export const computed = {
'goalDialogOpen'
]),
resolvedRunMode() {
const allowed = ['fast', 'thinking', 'deep'];
if (allowed.includes(this.runMode)) {
return this.runMode;
// 历史值 deep 映射为 thinking
const raw = (this.runMode as string) === 'deep' ? 'thinking' : this.runMode;
const allowed = ['fast', 'thinking'];
if (allowed.includes(raw)) {
return raw;
}
return this.thinkingMode ? 'thinking' : 'fast';
},
headerRunModeOptions() {
return [
{ value: 'fast', label: '快速模式', desc: '低思考,响应更快' },
{ value: 'thinking', label: '思考模式', desc: '更长思考,综合回答' },
{ value: 'deep', label: '深度思考', desc: '持续推理,适合复杂任务' }
{ value: 'thinking', label: '思考模式', desc: '持续推理,适合复杂任务' }
];
},
headerRunModeLabel() {
const current = this.headerRunModeOptions.find((o) => o.value === this.resolvedRunMode);
return current ? current.label : '快速模式';
},
// 推理强度滑块:仅思考模式 + 当前模型支持推理强度参数时显示
showEffortSlider() {
const modelStore = useModelStore();
return this.resolvedRunMode === 'thinking' && !!modelStore.currentModel?.supportsReasoningEffort;
},
currentModelLabel() {
const modelStore = useModelStore();
return modelStore.currentModel?.label || '未选择模型';

View File

@ -54,6 +54,9 @@ export const actionMethods = {
});
this.logMessageState('createNewConversation:start');
// 新建对话前把 debounce 中的推理强度保存立即落盘(存到原对话)
this.flushReasoningEffortSave?.();
await this.persistComposerDraftNow({
reason: 'create-new-conversation',
force: true,
@ -67,12 +70,15 @@ export const actionMethods = {
if (personalizationStore.loaded) {
const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model;
const defaultReasoningEffort = personalizationStore.form.default_reasoning_effort;
if (defaultRunMode) {
this.runMode = defaultRunMode;
debugLog('应用默认运行模式:', defaultRunMode);
}
this.reasoningEffort = defaultReasoningEffort;
if (defaultModel) {
this.currentModelKey = defaultModel;
debugLog('应用默认模型:', defaultModel);
@ -124,9 +130,11 @@ export const actionMethods = {
const isMultiAgent = Boolean(this.multiAgentMode);
const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations';
// reasoning_effort 随创建权威写入新对话 meta此处 this.reasoningEffort 已被上方
// 重置为个性化默认值,语义与“创建空对话写入默认值一次”一致)
const createBody = isMultiAgent
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode })
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode });
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode, reasoning_effort: this.reasoningEffort })
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode, reasoning_effort: this.reasoningEffort });
const response = await fetch(createUrl, {
method: 'POST',
headers: {

View File

@ -49,7 +49,8 @@ export const bootstrapMethods = {
// 1. 应用模式/模型(与旧 PUT load 响应处理对齐)
if (typeof meta.run_mode === 'string') {
this.runMode = meta.run_mode;
// 历史值 deep 映射为 thinking
this.runMode = (meta.run_mode === 'deep' ? 'thinking' : meta.run_mode) as 'fast' | 'thinking';
this.thinkingMode =
typeof meta.thinking_mode === 'boolean' ? meta.thinking_mode : meta.run_mode !== 'fast';
} else if (typeof meta.thinking_mode === 'boolean') {
@ -59,6 +60,8 @@ export const bootstrapMethods = {
if (typeof meta.model_key === 'string' && meta.model_key) {
this.modelSet(meta.model_key);
}
// 恢复会话级推理强度档位null = 默认,不传参)
this.reasoningEffort = typeof meta.reasoning_effort === 'string' ? meta.reasoning_effort : null;
if (typeof meta.multi_agent_mode === 'boolean') {
this.multiAgentMode = meta.multi_agent_mode;
}

View File

@ -99,6 +99,10 @@ export const loadMethods = {
return;
}
// 切换对话前把 debounce 中的推理强度保存立即落盘(随请求带原对话 id
// 不会串到新对话fire-and-forget不阻塞加载
this.flushReasoningEffortSave?.();
if ((this.compressionInProgress || this.compressing) && !force) {
const confirmed = await this.confirmAction({
title: '压缩进行中',

View File

@ -243,9 +243,11 @@ export const sendMethods = {
const isMultiAgent = Boolean(this.multiAgentMode);
const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations';
// reasoning_effort 随创建权威写入新对话 meta/new 页面用户可能已手动调整档位,
// 不能依赖后端 terminal 值(会被 status 轮询的 prefs 应用覆盖)
const createBody = isMultiAgent
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode })
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode });
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode, reasoning_effort: this.reasoningEffort })
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode, reasoning_effort: this.reasoningEffort });
const createResp = await fetch(createUrl, {
method: 'POST',
headers: {

View File

@ -130,16 +130,30 @@ export const resourceMethods = {
const prevExecutionMode = this.currentExecutionMode;
const prevDirectUntil = this.executionModeDirectUntil;
this.resourceApplyStatusSnapshot(status);
if (status && typeof status.thinking_mode !== 'undefined') {
this.thinkingMode = !!status.thinking_mode;
}
if (status && typeof status.run_mode === 'string') {
this.runMode = status.run_mode;
} else if (status && typeof status.thinking_mode !== 'undefined') {
this.runMode = status.thinking_mode ? 'thinking' : 'fast';
}
if (status && typeof status.model_key === 'string') {
this.modelSet(status.model_key);
// 模型/思考模式/推理强度的权威来源规则:
// - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复),
// status 快照不得覆盖terminal 状态可能滞后于对话加载)
// - 空对话/首页terminal 状态即权威,从 status 同步
const convId = this.currentConversationId;
const hasConversation =
typeof convId === 'string' && convId.length > 0 && !convId.startsWith('temp_');
if (!hasConversation) {
if (status && typeof status.thinking_mode !== 'undefined') {
this.thinkingMode = !!status.thinking_mode;
}
if (status && typeof status.run_mode === 'string') {
// 历史值 deep 映射为 thinking
this.runMode = status.run_mode === 'deep' ? 'thinking' : status.run_mode;
} else if (status && typeof status.thinking_mode !== 'undefined') {
this.runMode = status.thinking_mode ? 'thinking' : 'fast';
}
if (status && Object.prototype.hasOwnProperty.call(status, 'reasoning_effort')) {
this.reasoningEffort =
typeof status.reasoning_effort === 'string' ? status.reasoning_effort : null;
}
if (status && typeof status.model_key === 'string') {
this.modelSet(status.model_key);
}
}
if (status && typeof status.permission_mode === 'string') {
this.currentPermissionMode = status.permission_mode;

View File

@ -23,6 +23,16 @@ import {
parseSystemNoticeLabel,
} from './shared';
// 推理强度滑块是高频交互(后端限制 20 次/分):档位变化只乐观更新 UI
// 停止操作 600ms 后才把最终值发一次请求;序列开始时的对话 id 随请求带上,
// 即使 debounce 期间切换对话也能存到正确对话
const effortSave = {
timer: null as ReturnType<typeof setTimeout> | null,
rollback: undefined as string | null | undefined, // undefined = 无 pending
latest: undefined as string | null | undefined,
conversationId: null as string | null
};
export const modeMethods = {
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
@ -106,7 +116,7 @@ export const modeMethods = {
this.closeHeaderMenu();
},
async handleCycleRunMode() {
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
const modes: Array<'fast' | 'thinking'> = ['fast', 'thinking'];
const currentMode = this.resolvedRunMode;
const currentIndex = modes.indexOf(currentMode);
const nextMode = modes[(currentIndex + 1) % modes.length];
@ -117,9 +127,13 @@ export const modeMethods = {
this.modeMenuOpen = false;
return;
}
// 历史值 deep 映射为 thinking
if (mode === 'deep') {
mode = 'thinking';
}
const modelStore = useModelStore();
const fastOnly = modelStore.currentModel?.fastOnly;
const deepOnly = modelStore.currentModel?.deepOnly;
const thinkingOnly = modelStore.currentModel?.thinkingOnly;
if (fastOnly && mode !== 'fast') {
if (!options.suppressToast) {
this.uiPushToast({
@ -132,11 +146,11 @@ export const modeMethods = {
this.inputCloseMenus();
return;
}
if (deepOnly && mode !== 'deep') {
if (thinkingOnly && mode !== 'thinking') {
if (!options.suppressToast) {
this.uiPushToast({
title: '模式不可用',
message: '当前模型仅支持深度思考模式',
message: '当前模型仅支持思考模式',
type: 'warning'
});
}
@ -181,6 +195,70 @@ export const modeMethods = {
async toggleThinkingMode() {
await this.handleCycleRunMode();
},
async setReasoningEffort(effort) {
if (effortSave.rollback === undefined) {
// 序列开始:记录回滚目标与所属对话
effortSave.rollback = this.reasoningEffort;
effortSave.conversationId = this.currentConversationId || null;
}
effortSave.latest = effort;
// 乐观更新UI 与后续创建对话 body 都读这个值,立即生效)
this.reasoningEffort = effort;
if (effortSave.timer) clearTimeout(effortSave.timer);
effortSave.timer = setTimeout(() => {
effortSave.timer = null;
this.flushReasoningEffortSave();
}, 600);
},
// 立即执行 pending 的推理强度保存debounce 兜底:切换/新建对话前调用)
async flushReasoningEffortSave() {
if (effortSave.timer) {
clearTimeout(effortSave.timer);
effortSave.timer = null;
}
if (effortSave.rollback === undefined) return;
const effort = effortSave.latest;
const rollback = effortSave.rollback;
const targetConversationId = effortSave.conversationId;
effortSave.rollback = undefined;
effortSave.latest = undefined;
effortSave.conversationId = null;
try {
const response = await fetch('/api/reasoning-effort', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ effort, conversation_id: targetConversationId })
});
const payload = await response.json();
if (!response.ok || !payload.success) {
throw new Error(payload.message || payload.error || '设置失败');
}
const data = payload.data || {};
// 仅当 UI 仍停留在该序列的值时才用服务端回写确认
//(切对话后 UI 已恢复为新对话的值,不得覆盖)
if (
Object.prototype.hasOwnProperty.call(data, 'reasoning_effort') &&
this.reasoningEffort === effort
) {
this.reasoningEffort =
typeof data.reasoning_effort === 'string' ? data.reasoning_effort : null;
}
} catch (error) {
// 仅当 UI 仍停留在该序列的值时才回滚
if (this.reasoningEffort === effort) {
this.reasoningEffort = rollback;
}
console.error('设置推理强度失败:', error);
const message = error instanceof Error ? error.message : String(error || '未知错误');
this.uiPushToast({
title: '设置推理强度失败',
message: message || '请稍后重试',
type: 'error'
});
}
},
async handleStopAllSubAgents() {
const isMultiAgent = !!this.multiAgentMode;
const mode = isMultiAgent ? 'soft_stop' : 'terminate';

View File

@ -95,8 +95,8 @@ export const modelMethods = {
} else {
// 前端兼容策略:根据模型特性自动调整运行模式
const currentModel = modelStore.currentModel;
if (currentModel?.deepOnly) {
this.runMode = 'deep';
if (currentModel?.thinkingOnly) {
this.runMode = 'thinking';
this.thinkingMode = true;
} else if (currentModel?.fastOnly) {
this.runMode = 'fast';

View File

@ -237,7 +237,6 @@ export const socketMethods = {
this.socket = null;
this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = !!statusData.thinking_mode;
this.applyStatusSnapshot(statusData);
this.fetchPermissionMode();
this.fetchExecutionMode();
@ -276,12 +275,18 @@ export const socketMethods = {
if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) {
const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model;
const defaultReasoningEffort = personalizationStore.form.default_reasoning_effort;
if (defaultRunMode) {
this.runMode = defaultRunMode;
debugLog('应用默认运行模式:', defaultRunMode);
}
this.reasoningEffort = defaultReasoningEffort;
if (defaultReasoningEffort) {
debugLog('应用默认推理强度:', defaultReasoningEffort);
}
if (defaultModel) {
modelStore.setModel(defaultModel);
this.currentModelKey = modelStore.currentModelKey;

View File

@ -0,0 +1,518 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import type { ReasoningEffort } from '../../stores/personalization';
const LEVELS: ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
// inset(23) 3px
// 3px
const EDGE = 20;
const props = defineProps<{
modelValue: ReasoningEffort | null;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: ReasoningEffort | null): void;
}>();
const trackRef = ref<HTMLElement | null>(null);
const trackWidth = ref(0);
//
const levelIndex = ref(2);
// /
const knobX = ref(0);
// /JS rAF /
// covered
// /
//
const knobXVisual = ref(0);
const activeLevel = ref(2);
const dragging = ref(false);
const liveDrag = ref(false);
let downX = 0;
let animFrame = 0;
const isDefault = computed(() => props.modelValue === null);
const posRatio = (i: number) => i / (LEVELS.length - 1);
const posPct = (i: number) => `calc(${EDGE}px + (100% - ${EDGE * 2}px) * ${posRatio(i)})`;
const posPx = (i: number) => EDGE + (trackWidth.value - EDGE * 2) * posRatio(i);
const pxToLevel = (x: number) => {
const w = trackWidth.value;
if (w <= EDGE * 2) return 0;
const ratio = (x - EDGE) / (w - EDGE * 2);
return Math.max(0, Math.min(LEVELS.length - 1, Math.round(ratio * (LEVELS.length - 1))));
};
const clampEventX = (clientX: number) => {
const el = trackRef.value;
if (!el) return EDGE;
const rect = el.getBoundingClientRect();
const x = clientX - rect.left;
return Math.max(EDGE, Math.min(rect.width - EDGE, x));
};
const KNOB_ANIM_MS = 200;
// CSS ease easeInOutQuad
const easeInOut = (t: number) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2);
const cancelKnobAnim = () => {
if (animFrame) {
cancelAnimationFrame(animFrame);
animFrame = 0;
}
};
const setKnobInstant = (x: number) => {
cancelKnobAnim();
knobXVisual.value = x;
};
//
const animateKnobTo = (target: number) => {
const start = knobXVisual.value;
if (start === target) return;
cancelKnobAnim();
const t0 = performance.now();
const step = (now: number) => {
const p = Math.min(1, (now - t0) / KNOB_ANIM_MS);
knobXVisual.value = start + (target - start) * easeInOut(p);
if (p < 1) {
animFrame = requestAnimationFrame(step);
} else {
animFrame = 0;
}
};
animFrame = requestAnimationFrame(step);
};
// animate=true/false/resize/
const syncToLevel = (animate = false) => {
const target = posPx(levelIndex.value);
knobX.value = target;
activeLevel.value = levelIndex.value;
if (animate && trackWidth.value > EDGE * 2) {
animateKnobTo(target);
} else {
setKnobInstant(target);
}
};
// trackWidth=0
// /退
// covered 退
const knobLeftStyle = computed(() =>
trackWidth.value > EDGE * 2 ? `${knobXVisual.value}px` : posPct(levelIndex.value)
);
const fillWidthStyle = computed(() =>
trackWidth.value > EDGE * 2 ? `${knobXVisual.value}px` : posPct(levelIndex.value)
);
const dotCovered = (i: number) =>
trackWidth.value > EDGE * 2 ? posPx(i) <= knobXVisual.value : i <= levelIndex.value;
const commitLevel = () => {
emit('update:modelValue', LEVELS[levelIndex.value]);
};
const onPointerDown = (e: PointerEvent) => {
dragging.value = true;
liveDrag.value = false;
downX = e.clientX;
trackRef.value?.setPointerCapture(e.pointerId);
// JS
levelIndex.value = pxToLevel(clampEventX(e.clientX));
commitLevel();
syncToLevel(true);
};
const onPointerMove = (e: PointerEvent) => {
if (!dragging.value) return;
if (!liveDrag.value && Math.abs(e.clientX - downX) > 3) liveDrag.value = true;
if (liveDrag.value) {
//
const x = clampEventX(e.clientX);
cancelKnobAnim();
knobX.value = x;
knobXVisual.value = x;
activeLevel.value = pxToLevel(x);
}
};
const onPointerUp = (e: PointerEvent) => {
if (!dragging.value) return;
dragging.value = false;
if (liveDrag.value) {
//
//
liveDrag.value = false;
levelIndex.value = pxToLevel(clampEventX(e.clientX));
commitLevel();
syncToLevel(true);
return;
}
syncToLevel(true);
};
const toggleDefault = () => {
emit('update:modelValue', isDefault.value ? LEVELS[levelIndex.value] : null);
};
const measure = () => {
trackWidth.value = trackRef.value?.clientWidth ?? 0;
syncToLevel();
};
watch(
() => props.modelValue,
(value) => {
if (value === null) return; //
const i = LEVELS.indexOf(value);
if (i < 0 || i === levelIndex.value) return; // emit
//
levelIndex.value = i;
syncToLevel(false);
},
// immediate modelValue
// high
{ immediate: true }
);
onMounted(() => {
measure();
window.addEventListener('resize', measure);
});
onBeforeUnmount(() => {
cancelKnobAnim();
window.removeEventListener('resize', measure);
});
</script>
<template>
<div
class="effort-slider"
:class="{ 'is-default': isDefault, 'live-drag': liveDrag }"
>
<!-- 顶部非拖动 = 推理强度 + 默认拖动中 = 更高效 / 更智能 -->
<div class="effort-header">
<div class="header-state header-idle">
<span class="header-title">推理强度</span>
<span class="default-toggle" :class="{ checked: isDefault }" @click.stop="toggleDefault">
<span class="default-label">默认</span>
<span class="fancy-check" aria-hidden="true">
<svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path>
</svg>
</span>
</span>
</div>
<div class="header-state header-drag">
<span>更高效</span>
<span>更智能</span>
</div>
</div>
<!-- 滑块 -->
<div class="slider-zone">
<div
ref="trackRef"
class="track"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointercancel="onPointerUp"
>
<div class="fill-stack" :style="{ width: fillWidthStyle }">
<div
v-for="(_, i) in LEVELS"
:key="`lv-${i}`"
class="fill-layer"
:class="[`lv-${i}`, { on: i === activeLevel }]"
></div>
</div>
<div
v-for="(_, i) in LEVELS"
:key="`dot-${i}`"
class="dot"
:class="{ covered: dotCovered(i) }"
:style="{ left: posPct(i) }"
></div>
<div class="knob" :style="{ left: knobLeftStyle }"></div>
</div>
<div class="level-labels">
<span
v-for="(name, i) in LEVELS"
:key="`label-${i}`"
class="level-label"
:class="{ active: i === activeLevel }"
:style="{ left: posPct(i) }"
>{{ name }}</span
>
</div>
</div>
</div>
</template>
<style scoped>
.effort-slider {
width: 100%;
padding: 4px 2px 0;
user-select: none;
-webkit-user-select: none;
}
/* ========== 顶部:两种状态文字叠在一起,淡入淡出切换 ========== */
.effort-header {
position: relative;
height: 24px;
margin-bottom: 14px;
}
.header-state {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
transition:
opacity 0.18s ease,
transform 0.18s ease;
}
.header-idle {
opacity: 1;
}
.header-drag {
opacity: 0;
transform: translateY(4px);
pointer-events: none;
color: var(--text-secondary);
font-size: 14px;
}
.effort-slider.live-drag .header-idle {
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
}
.effort-slider.live-drag .header-drag {
opacity: 1;
transform: translateY(0);
}
.header-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
/* ========== 「默认」打勾框:个人空间 fancy-check 缩小版 ========== */
.default-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.default-label {
font-size: 12px;
color: var(--text-secondary);
}
.default-toggle .fancy-check {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.default-toggle .fancy-check svg {
width: 14px;
height: 14px;
overflow: visible;
}
.fancy-path {
fill: none;
stroke: var(--text-secondary);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
transition:
stroke-dasharray 0.5s ease,
stroke-dashoffset 0.5s ease,
stroke 0.2s ease;
stroke-dasharray: 241 9999999;
stroke-dashoffset: 0;
}
.default-toggle.checked .fancy-path {
stroke-dasharray: 70.5096664428711 9999999;
stroke-dashoffset: -262.2723388671875;
}
/* ========== 滑块区域 ========== */
.slider-zone {
position: relative;
transition:
filter 0.25s ease,
opacity 0.25s ease;
}
/* 默认勾选时:整体置灰,且停掉所有动画 */
.effort-slider.is-default .slider-zone {
filter: grayscale(1) brightness(0.72);
opacity: 0.55;
}
.effort-slider.is-default .fill-layer,
.effort-slider.is-default .fill-layer::after {
animation: none;
}
.track {
position: relative;
height: 34px;
border-radius: 17px;
background: var(--effort-track-bg);
cursor: pointer;
touch-action: none;
}
/* 填充5 层叠放,当前档淡入、其余淡出 —— 任何颜色切换都是平滑 crossfade */
.fill-stack {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 23px;
border-radius: 17px;
overflow: hidden;
}
.fill-layer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
opacity: 0;
transition: opacity 0.35s ease;
}
.fill-layer.on {
opacity: 1;
}
.fill-layer.lv-0 {
background: var(--effort-low);
}
.fill-layer.lv-1 {
background: var(--effort-medium);
}
.fill-layer.lv-2 {
background: var(--effort-high);
}
.fill-layer.lv-3 {
background: var(--effort-xhigh);
}
/* xhigh白光扫过90deg 整列渐变无硬边;位移动画两端完全出画后回绕 → 无缝) */
.fill-layer.lv-3::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -50%;
width: 45%;
background: linear-gradient(
90deg,
transparent 0%,
var(--effort-xhigh-shine) 50%,
transparent 100%
);
animation: shine-sweep 1.8s linear infinite;
}
@keyframes shine-sweep {
from {
left: -50%;
}
to {
left: 130%;
}
}
/* max彩虹流动200% 宽 + 位移整数个循环单元 → 真无缝,方向从左到右) */
.fill-layer.lv-4 {
width: 200%;
background: repeating-linear-gradient(
90deg,
var(--effort-max-red) 0%,
var(--effort-max-orange) 7.15%,
var(--effort-max-yellow) 14.3%,
var(--effort-max-green) 21.45%,
var(--effort-max-cyan) 28.6%,
var(--effort-max-purple) 35.75%,
var(--effort-max-pink) 42.9%,
var(--effort-max-red) 50%
);
animation: rainbow-flow 2.4s linear infinite;
}
@keyframes rainbow-flow {
from {
transform: translateX(-50%);
}
to {
transform: translateX(0);
}
}
/* JS rAF script/
拖动连续跟随圆点变色时机与圆钮视觉位置严格同步 不用 CSS transition */
/* 档位圆点 */
.dot {
position: absolute;
top: 50%;
width: 7px;
height: 7px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: var(--effort-dot-uncovered);
transition: background 0.2s ease;
pointer-events: none;
}
.dot.covered {
background: var(--effort-dot-covered);
}
/* 拖柄 */
.knob {
position: absolute;
top: 50%;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--effort-knob-bg);
border: 1px solid var(--border-default);
transform: translate(-50%, -50%);
box-shadow: 0 1px 4px var(--effort-knob-shadow);
cursor: grab;
z-index: 2;
}
.effort-slider.live-drag .knob {
cursor: grabbing;
}
/* ========== 档位文字 ========== */
.level-labels {
position: relative;
height: 18px;
margin-top: 10px;
}
.level-label {
position: absolute;
transform: translateX(-50%);
font-size: 11px;
color: var(--text-secondary);
transition:
color 0.2s ease,
font-weight 0.2s ease;
white-space: nowrap;
}
.level-label.active {
color: var(--text-primary);
font-weight: 600;
}
</style>

View File

@ -570,7 +570,7 @@ const props = defineProps<{
inputLocked: boolean;
uploading: boolean;
thinkingMode: boolean;
runMode: 'fast' | 'thinking' | 'deep';
runMode: 'fast' | 'thinking';
quickMenuOpen: boolean;
toolMenuOpen: boolean;
modeMenuOpen: boolean;
@ -1497,8 +1497,7 @@ const modelSlashMenuItems = computed<SlashMenuItem[]>(() => {
const runModeSlashMenuItems = computed<SlashMenuItem[]>(() => {
const modes: Array<{ value: string; label: string }> = [
{ value: 'fast', label: '快速' },
{ value: 'thinking', label: '思考' },
{ value: 'deep', label: '深度' }
{ value: 'thinking', label: '思考' }
];
const current = String(props.runMode || '');
return modes.map((mode) => ({

View File

@ -192,7 +192,7 @@ const props = defineProps<{
iconStyle?: (key: string) => Record<string, string>;
toolCategoryIcon: (categoryId: string) => string;
modeMenuOpen: boolean;
runMode?: 'fast' | 'thinking' | 'deep';
runMode?: 'fast' | 'thinking';
modelMenuOpen: boolean;
modelOptions: Array<{
key: string;
@ -228,7 +228,7 @@ defineEmits<{
(event: 'compress-conversation'): void;
(event: 'toggle-approval-panel'): void;
(event: 'toggle-mode-menu'): void;
(event: 'select-run-mode', mode: 'fast' | 'thinking' | 'deep'): void;
(event: 'select-run-mode', mode: 'fast' | 'thinking'): void;
(event: 'toggle-model-menu'): void;
(event: 'select-model', key: string): void;
(event: 'open-review'): void;

View File

@ -301,7 +301,7 @@ const props = defineProps<{
isConnected: boolean;
panelMenuOpen: boolean;
panelMode: 'files' | 'todo' | 'subAgents' | 'backgroundCommands';
runMode: 'fast' | 'thinking' | 'deep';
runMode: 'fast' | 'thinking';
fileManagerDisabled?: boolean;
hostWorkspaceEnabled?: boolean;
hostWorkspaces?: Array<{
@ -341,17 +341,19 @@ const panelStyle = computed(() => {
});
const resolveRunMode = () => {
if (props.runMode === 'deep' || props.runMode === 'thinking' || props.runMode === 'fast') {
return props.runMode;
// deep thinking
const rawMode = props.runMode as string;
if (rawMode === 'deep' || rawMode === 'thinking') {
return 'thinking';
}
if (rawMode === 'fast') {
return 'fast';
}
return props.thinkingMode ? 'thinking' : 'fast';
};
const modeIndicatorClass = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return 'deep';
}
if (mode === 'thinking') {
return 'thinking';
}
@ -360,9 +362,6 @@ const modeIndicatorClass = computed(() => {
const modeIndicatorIcon = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return 'brainCog';
}
if (mode === 'thinking') {
return 'brain';
}
@ -371,9 +370,6 @@ const modeIndicatorIcon = computed(() => {
const modeIndicatorTitle = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return '深度思考模式(点击切换)';
}
if (mode === 'thinking') {
return '思考模式(点击切换)';
}

View File

@ -536,37 +536,41 @@
</div>
</div>
</div>
<div class="settings-list-row tall">
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">思考频率</span
><span class="settings-row-title">默认推理强度</span
><span class="settings-row-desc"
>连续快速模式运行多少轮后系统强制切换到思考模型</span
>思考模式下请求模型时附带的推理强度参数</span
></span
>
<div class="settings-input-stack wide">
<div class="settings-chip-row right">
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'reasoning-effort' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('reasoning-effort')"
>
{{ reasoningEffortLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="preset in thinkingPresets"
:key="preset.id"
v-for="option in reasoningEffortOptions"
:key="option.id"
type="button"
:class="{ active: isPresetActive(preset.value) }"
@click.prevent="applyThinkingPreset(preset.value)"
class="settings-menu-option"
:class="{ selected: isEffortActive(option.value) }"
@click="selectDefaultReasoningEffort(option.value)"
>
{{ preset.label }} · {{ preset.value }}
</button>
</div>
<div class="settings-number-row">
<input
type="number"
:min="thinkingIntervalRange.min"
:max="thinkingIntervalRange.max"
:placeholder="`默认 ${thinkingIntervalDefault} 轮`"
:value="form.thinking_interval ?? ''"
@input="handleThinkingInput"
@focus="personalization.clearFeedback()"
/>
<button type="button" @click.prevent="restoreThinkingInterval">
恢复默认
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
@ -1875,7 +1879,7 @@
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">思考模式</span>
<span class="settings-row-desc">fast = 快速响应thinking = 深度推理</span>
<span class="settings-row-desc">fast = 快速响应thinking = 思考推理</span>
</span>
<div
class="settings-select-wrap"
@ -1908,7 +1912,7 @@
:class="{ selected: roleForm.thinking_mode === 'thinking' }"
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
>
<strong>thinking</strong><span>深度推理模式</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
<strong>thinking</strong><span>思考推理模式</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
@ -2091,8 +2095,6 @@ const {
toggleUpdating,
toolCategories,
skillsCatalog,
thinkingIntervalDefault,
thinkingIntervalRange,
recentConversationsPromptLimitRange,
experiments
} = storeToRefs(personalization);
@ -2374,7 +2376,8 @@ onBeforeUnmount(() => {
window.removeEventListener('scroll', updateFloatingMenuPosition, true);
});
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
type RunModeValue = 'fast' | 'thinking' | null;
type ReasoningEffortValue = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;
type PermissionModeValue = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
type CompressionField =
| 'shallow_compress_trigger_tokens'
@ -2392,8 +2395,21 @@ const runModeOptions: Array<{
badge?: string;
}> = [
{ id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' },
{ id: 'thinking', label: '思考模式', desc: '首轮回复会先输出思考过程', value: 'thinking' },
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
{ id: 'thinking', label: '思考模式', desc: '整轮对话都使用思考模型', value: 'thinking' }
];
const reasoningEffortOptions: Array<{
id: string;
label: string;
desc: string;
value: ReasoningEffortValue;
}> = [
{ id: 'default', label: '默认', desc: '不指定强度,使用 API 默认行为', value: null },
{ id: 'low', label: 'low', desc: '最低推理,响应最快', value: 'low' },
{ id: 'medium', label: 'medium', desc: '较低推理', value: 'medium' },
{ id: 'high', label: 'high', desc: '均衡推理', value: 'high' },
{ id: 'xhigh', label: 'xhigh', desc: '更高推理', value: 'xhigh' },
{ id: 'max', label: 'max', desc: '最高推理,适合最复杂任务', value: 'max' }
];
const permissionModeOptions: Array<{
@ -2422,20 +2438,14 @@ const filteredModelOptions = computed(() =>
value: opt.key,
label: opt.label,
desc: opt.description || '',
badge: multimodal === 'image,video' ? '图文' : opt.deepOnly ? '深度思考' : undefined,
deepOnly: !!opt.deepOnly,
badge: multimodal === 'image,video' ? '图文' : opt.thinkingOnly ? '思考' : undefined,
thinkingOnly: !!opt.thinkingOnly,
fastOnly: !!opt.fastOnly,
disabled: policyStore.disabledModelSet.has(opt.key)
};
})
);
const thinkingPresets = [
{ id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 },
{ id: 'high', label: '高', value: 3 }
];
const imageCompressionOptions = [
{ id: 'original', label: '原图', desc: '不压缩' },
{ id: '1080p', label: '1080p', desc: '最长边不超过 1080p 等比缩放' },
@ -2454,6 +2464,24 @@ const runModeLabel = computed(() => {
return runModeOptions.find((option) => isRunModeActive(option.value))?.label || '未设置';
});
const isEffortActive = (value: ReasoningEffortValue) => {
if (value === null) {
return !form.value.default_reasoning_effort;
}
return form.value.default_reasoning_effort === value;
};
const reasoningEffortLabel = computed(() => {
return (
reasoningEffortOptions.find((option) => isEffortActive(option.value))?.label || '默认'
);
});
const selectDefaultReasoningEffort = (value: ReasoningEffortValue) => {
personalization.setDefaultReasoningEffort(value);
closeDropdown();
};
const permissionModeLabel = computed(() => {
return (
permissionModeOptions.find((option) => option.id === form.value.default_permission_mode)
@ -2965,10 +2993,6 @@ const startTutorial = () => {
}, 320);
};
const applyThinkingPreset = (value: number) => {
personalization.setThinkingInterval(value);
};
const isRunModeActive = (value: RunModeValue) => {
if (value === null) {
return !form.value.default_run_mode;
@ -3053,8 +3077,8 @@ const selectBlockDisplayMode = (mode: 'traditional' | 'stacked' | 'minimal') =>
const checkModeModelConflict = (mode: RunModeValue, model: string | null): boolean => {
const found = (filteredModelOptions.value || []).find((item: any) => item.value === model);
const warnings: string[] = [];
if (found?.deepOnly && mode && mode !== 'deep') {
warnings.push(`${found.label} 仅支持深度思考模式,已保持原设置。`);
if (found?.thinkingOnly && mode && mode !== 'thinking') {
warnings.push(`${found.label} 仅支持思考模式,已保持原设置。`);
}
if (found?.fastOnly && mode && mode !== 'fast') {
warnings.push(`${found.label} 仅支持快速模式,已保持原设置。`);
@ -3071,20 +3095,6 @@ const checkModeModelConflict = (mode: RunModeValue, model: string | null): boole
return false;
};
const handleThinkingInput = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.value) {
personalization.setThinkingInterval(null);
return;
}
const parsed = Number(target.value);
personalization.setThinkingInterval(Number.isNaN(parsed) ? null : parsed);
};
const restoreThinkingInterval = () => {
personalization.setThinkingInterval(null);
};
const handleRecentConversationsPromptLimitInput = (event: Event) => {
const target = event.target as HTMLInputElement | null;
if (!target) {
@ -3132,16 +3142,6 @@ const restoreCompressionDefaults = () => {
personalization.updateField({ key: 'deep_compress_trigger_tokens', value: null });
};
const isPresetActive = (value: number) => {
if (
form.value.thinking_interval === null ||
typeof form.value.thinking_interval === 'undefined'
) {
return value === thinkingIntervalDefault.value;
}
return form.value.thinking_interval === value;
};
const toggleCategory = (categoryId: string) => {
personalization.toggleDefaultToolCategory(categoryId);
};

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import type { Socket } from 'socket.io-client';
import type { ReasoningEffort } from './personalization';
interface ConnectionState {
isConnected: boolean;
@ -8,7 +9,8 @@ interface ConnectionState {
projectPath: string;
agentVersion: string;
thinkingMode: boolean;
runMode: 'fast' | 'thinking' | 'deep';
runMode: 'fast' | 'thinking';
reasoningEffort: ReasoningEffort | null;
}
export const useConnectionStore = defineStore('connection', {
@ -19,7 +21,8 @@ export const useConnectionStore = defineStore('connection', {
projectPath: '',
agentVersion: '',
thinkingMode: true,
runMode: 'thinking'
runMode: 'thinking',
reasoningEffort: null
}),
actions: {
setSocket(socket: Socket | null) {
@ -49,9 +52,12 @@ export const useConnectionStore = defineStore('connection', {
toggleThinkingMode() {
this.thinkingMode = !this.thinkingMode;
},
setRunMode(mode: 'fast' | 'thinking' | 'deep') {
setRunMode(mode: 'fast' | 'thinking') {
this.runMode = mode;
this.thinkingMode = mode !== 'fast';
},
setReasoningEffort(effort: ReasoningEffort | null) {
this.reasoningEffort = effort;
}
}
});

View File

@ -14,7 +14,8 @@ export interface ModelOption {
maxOutputTokens?: number | null;
fastOnly: boolean;
supportsThinking: boolean;
deepOnly?: boolean;
thinkingOnly?: boolean;
supportsReasoningEffort?: boolean;
}
interface ModelState {
@ -89,7 +90,8 @@ export const useModelStore = defineStore('model', {
typeof item.max_output_tokens === 'number' ? item.max_output_tokens : null,
fastOnly: !!item.fast_only,
supportsThinking: !!item.supports_thinking,
deepOnly: !!item.deep_only
thinkingOnly: !!item.thinking_only,
supportsReasoningEffort: !!item.supports_reasoning_effort
};
});
this.setModels(mapped);

View File

@ -4,7 +4,8 @@ import { persistDefaultHideWorkspace, useUiStore } from './ui';
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
export type CompactMessageDisplay = 'full' | 'brief';
type RunMode = 'fast' | 'thinking' | 'deep';
type RunMode = 'fast' | 'thinking';
export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
type PermissionMode = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
type CommunicationStyle = 'default' | 'human_like' | 'auto';
type ConversationContinuity = 'low' | 'medium' | 'high';
@ -40,9 +41,9 @@ interface PersonalForm {
profession: string;
tone: string;
considerations: string;
thinking_interval: number | null;
disabled_tool_categories: string[];
default_run_mode: RunMode | null;
default_reasoning_effort: ReasoningEffort | null;
default_permission_mode: PermissionMode;
versioning_enabled_by_default: boolean;
versioning_backup_mode: VersioningBackupMode;
@ -89,19 +90,16 @@ interface PersonalizationState {
form: PersonalForm;
toolCategories: Array<{ id: string; label: string }>;
skillsCatalog: Array<{ id: string; label: string; description?: string }>;
thinkingIntervalDefault: number;
thinkingIntervalRange: { min: number; max: number };
recentConversationsPromptLimitRange: { min: number; max: number };
experiments: ExperimentState;
}
const DEFAULT_INTERVAL = 10;
const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 };
const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT = 10;
const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE = { min: 1, max: 30 };
const DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS = 80000;
const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000;
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking'];
const REASONING_EFFORT_OPTIONS: ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
const THEME_STORAGE_KEY = 'agents_ui_theme';
const STACKED_HIDE_BORDERS_STORAGE_KEY = 'agents_stacked_hide_borders';
@ -204,9 +202,9 @@ const defaultForm = (): PersonalForm => ({
profession: '',
tone: '',
considerations: '',
thinking_interval: null,
disabled_tool_categories: [],
default_run_mode: null,
default_reasoning_effort: null,
default_permission_mode: 'unrestricted',
versioning_enabled_by_default: true,
versioning_backup_mode: 'shallow',
@ -299,8 +297,6 @@ export const usePersonalizationStore = defineStore('personalization', {
form: defaultForm(),
toolCategories: [],
skillsCatalog: [],
thinkingIntervalDefault: DEFAULT_INTERVAL,
thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE },
recentConversationsPromptLimitRange: { ...DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE },
experiments: loadExperimentState()
}),
@ -414,15 +410,20 @@ export const usePersonalizationStore = defineStore('personalization', {
: Array.isArray(data.considerations)
? data.considerations.filter((item: unknown) => typeof item === 'string').join('\n')
: '',
thinking_interval:
typeof data.thinking_interval === 'number' ? data.thinking_interval : null,
disabled_tool_categories: Array.isArray(data.disabled_tool_categories)
? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string')
: [],
default_run_mode:
typeof data.default_run_mode === 'string' &&
RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode)
? (data.default_run_mode as RunMode)
default_run_mode: (() => {
// 历史值 deep 映射为 thinking
const saved = data.default_run_mode === 'deep' ? 'thinking' : data.default_run_mode;
return typeof saved === 'string' && RUN_MODE_OPTIONS.includes(saved as RunMode)
? (saved as RunMode)
: null;
})(),
default_reasoning_effort:
typeof data.default_reasoning_effort === 'string' &&
REASONING_EFFORT_OPTIONS.includes(data.default_reasoning_effort as ReasoningEffort)
? (data.default_reasoning_effort as ReasoningEffort)
: null,
default_permission_mode:
data.default_permission_mode === 'readonly' ||
@ -553,20 +554,6 @@ export const usePersonalizationStore = defineStore('personalization', {
return Math.max(min, Math.min(max, Math.round(parsed)));
},
applyPersonalizationMeta(payload: any) {
if (payload && typeof payload.thinking_interval_default === 'number') {
this.thinkingIntervalDefault = payload.thinking_interval_default;
} else {
this.thinkingIntervalDefault = DEFAULT_INTERVAL;
}
if (payload && payload.thinking_interval_range) {
const { min, max } = payload.thinking_interval_range;
this.thinkingIntervalRange = {
min: typeof min === 'number' ? min : DEFAULT_INTERVAL_RANGE.min,
max: typeof max === 'number' ? max : DEFAULT_INTERVAL_RANGE.max
};
} else {
this.thinkingIntervalRange = { ...DEFAULT_INTERVAL_RANGE };
}
if (payload && payload.recent_conversations_prompt_limit_range) {
const { min, max } = payload.recent_conversations_prompt_limit_range;
this.recentConversationsPromptLimitRange = {
@ -736,28 +723,6 @@ export const usePersonalizationStore = defineStore('personalization', {
this.saving = false;
}
},
setThinkingInterval(value: number | null) {
let target: number | null = value;
if (typeof target === 'number') {
if (Number.isNaN(target)) {
target = null;
} else {
const rounded = Math.round(target);
const min = this.thinkingIntervalRange.min ?? DEFAULT_INTERVAL_RANGE.min;
const max = this.thinkingIntervalRange.max ?? DEFAULT_INTERVAL_RANGE.max;
target = Math.max(min, Math.min(max, rounded));
if (target === this.thinkingIntervalDefault) {
target = null;
}
}
}
this.form = {
...this.form,
thinking_interval: target
};
this.clearFeedback();
this.scheduleAutoSave();
},
setRecentConversationsPromptLimit(value: number | null) {
const target =
value === null || typeof value === 'undefined'
@ -816,6 +781,18 @@ export const usePersonalizationStore = defineStore('personalization', {
this.clearFeedback();
this.scheduleAutoSave();
},
setDefaultReasoningEffort(effort: ReasoningEffort | null) {
let target: ReasoningEffort | null = null;
if (typeof effort === 'string' && REASONING_EFFORT_OPTIONS.includes(effort as ReasoningEffort)) {
target = effort as ReasoningEffort;
}
this.form = {
...this.form,
default_reasoning_effort: target
};
this.clearFeedback();
this.scheduleAutoSave();
},
setDefaultPermissionMode(mode: PermissionMode) {
const allowed: PermissionMode[] = ['readonly', 'approval', 'auto_approval', 'unrestricted'];
const target = allowed.includes(mode) ? mode : 'unrestricted';

View File

@ -66,7 +66,7 @@ export const useTaskStore = defineStore('task', {
conversationId: string | null = null,
options: {
model_key?: string | null;
run_mode?: 'fast' | 'thinking' | 'deep' | null;
run_mode?: 'fast' | 'thinking' | null;
thinking_mode?: boolean | null;
message_source?: string | null;
goal_mode?: boolean | null;

View File

@ -104,7 +104,7 @@ const DEFAULT_STEPS: TutorialStep[] = [
{
id: 'workspace-mode-indicator',
title: '思考模式',
description: '点击切换快速 / 思考 / 深度思考。',
description: '点击切换快速 / 思考。',
target: '[data-tutorial="run-mode-indicator"]',
mode: 'info',
placement: 'right',
@ -141,7 +141,7 @@ const DEFAULT_STEPS: TutorialStep[] = [
{
id: 'header-runmode-options',
title: '运行模式列表',
description: '这里可切换快速 / 思考 / 深度思考。',
description: '这里可切换快速 / 思考。',
target: '[data-tutorial="header-runmode-options"]',
mode: 'info',
placement: 'bottom',
@ -278,7 +278,7 @@ const DEFAULT_STEPS: TutorialStep[] = [
{
id: 'mobile-runmode-options',
title: '思考模式列表',
description: '这里可切换快速 / 思考 / 深度思考。',
description: '这里可切换快速 / 思考。',
target: '[data-tutorial="header-runmode-options"]',
mode: 'info',
placement: 'bottom',

View File

@ -28,6 +28,24 @@
/* 拖拽预览浮层投影(主题无关,固定黑投影) */
--drag-preview-shadow: rgba(0, 0, 0, 0.35);
/* 推理强度档位色主题无关固定功能指示色EffortSlider 滑块填充与动效 */
--effort-low: #f5c542;
--effort-medium: #3fd07c;
--effort-high: #4f8ef7;
--effort-xhigh: #a55df0;
--effort-xhigh-shine: rgba(255, 255, 255, 0.55);
--effort-max-red: #ff3b5c;
--effort-max-orange: #ff9f2e;
--effort-max-yellow: #ffe93b;
--effort-max-green: #3ddc68;
--effort-max-cyan: #2fd4ff;
--effort-max-purple: #7a5cff;
--effort-max-pink: #ff4fd8;
--effort-knob-bg: #ffffff;
--effort-knob-shadow: rgba(0, 0, 0, 0.28);
--effort-dot-covered: rgba(255, 255, 255, 0.55);
--effort-dot-uncovered: rgba(0, 0, 0, 0.28);
/* === 兼容别名(旧 token 名 → 新语义 token迁移完成后删除 === */
--claude-bg: var(--surface-base);
--claude-panel: var(--surface-panel);
@ -76,6 +94,7 @@
:root[data-theme='classic'] {
color-scheme: light;
/* Surface 表面层级 —— Claude 亮色暖奶油盘canvas<soft<card<cream-strongraised纯白浮起 */
--effort-track-bg: var(--surface-muted);
--surface-base: #faf9f5;
--surface-panel: #faf9f5;
--surface-rail: #f5f0e8;
@ -136,6 +155,7 @@
:root[data-theme='light'] {
color-scheme: light;
--effort-track-bg: var(--surface-muted);
/* Surface 表面层级 —— ChatGPT 亮色冷白盘 */
--surface-base: #ffffff;
--surface-panel: #ffffff;
@ -197,6 +217,8 @@
:root[data-theme='dark'] {
color-scheme: dark;
/* 推理强度滑槽(深色下需可见灰,不用近黑 surface-muted */
--effort-track-bg: #3a3a3c;
/* Surface 表面层级 */
--surface-base: #1a1a1a;
--surface-panel: #1a1a1a;
@ -258,6 +280,7 @@
/* 首屏JS 设置 data-theme 之前)回退到 classic 取值 */
:root:not([data-theme]) {
--effort-track-bg: var(--surface-muted);
/* Surface 表面层级 —— 同 classicClaude 亮色暖奶油盘) */
--surface-base: #faf9f5;
--surface-panel: #faf9f5;

View File

@ -298,14 +298,14 @@
top: 48px;
left: 16px;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(160px, 0.65fr);
grid-template-columns: minmax(0, 300px) minmax(220px, 1fr);
gap: 12px;
padding: 12px;
border-radius: 14px;
background: var(--surface-soft);
box-shadow: none;
pointer-events: auto;
width: min(640px, calc(100vw - 32px));
width: min(720px, calc(100vw - 32px));
max-width: calc(100vw - 32px);
z-index: 50;
}
@ -337,6 +337,13 @@
gap: 6px;
}
/* 运行模式列内的推理强度滑块:与上方选项用分隔线区分 */
.dropdown-effort {
margin-top: 8px;
padding: 10px 4px 2px;
border-top: 1px solid var(--border-default);
}
.dropdown-list {
display: flex;
flex-direction: column;

View File

@ -16,23 +16,6 @@ class ServerRefactorSmokeTest(unittest.TestCase):
def test_chat_flow_helper_functions(self):
import server.chat_flow_helpers as helpers
terminal = SimpleNamespace(
deep_thinking_mode=False,
thinking_mode=True,
api_client=SimpleNamespace(
force_thinking_next_call=False,
skip_thinking_next_call=False,
last_call_used_thinking=False,
),
thinking_fast_interval=2,
)
helpers.apply_thinking_schedule(terminal, default_interval=2, debug_logger=lambda _: None)
helpers.update_thinking_after_call(terminal, debug_logger=lambda _: None)
state = helpers.get_thinking_state(terminal)
self.assertIsInstance(state, dict)
self.assertIn("fast_streak", state)
self.assertTrue(helpers.detect_tool_failure({"success": False}))
self.assertFalse(helpers.detect_tool_failure({"success": True}))

View File

@ -58,9 +58,7 @@ class DeepSeekClientBaseMixin:
self.current_context_tokens: int = 0
self.max_context_tokens: Optional[int] = None
self.default_context_window: Optional[int] = None
self.thinking_mode = thinking_mode # True=智能思考模式, False=快速模式
self.deep_thinking_mode = False # 深度思考模式:整轮都使用思考模型
self.deep_thinking_session = False # 当前任务是否处于深度思考会话
self.thinking_mode = thinking_mode # True=思考模式(每次请求都用思考配置), False=快速模式
self.web_mode = web_mode # Web模式标志用于禁用print输出
# 兼容旧代码路径
self.api_base_url = self.fast_api_config["base_url"]
@ -69,12 +67,9 @@ class DeepSeekClientBaseMixin:
self.model_key = None # 由宿主终端注入,便于做模型兼容处理
self.model_multimodal = "none" # 由模型 profile 注入model_key 不可用时用于能力兜底
self.project_path: Optional[str] = None
# 每个任务的独立状态
self.current_task_first_call = True # 当前任务是否是第一次调用
self.current_task_thinking = "" # 当前任务的思考内容
self.force_thinking_next_call = False # 单次强制思考
self.skip_thinking_next_call = False # 单次强制快速
self.last_call_used_thinking = False # 最近一次调用是否使用思考模型
# 推理强度reasoning effortNone=默认(不传参);由会话级设置注入
self.reasoning_effort: Optional[str] = None
self.supports_reasoning_effort = False # 当前模型是否支持推理强度,由 apply_profile 注入
# 最近一次API错误详情
self.last_error_info: Optional[Dict[str, Any]] = None
# 请求体落盘目录(跟随 LOGS_DIR默认 ~/.astrion/<mode>/logs/api_requests

View File

@ -59,22 +59,11 @@ class DeepSeekClientChatMixin:
self._print(f"{OUTPUT_FORMATS['error']} API密钥未配置请检查模型配置")
return
# 决定是否使用思考模式
# 决定是否使用思考模式(思考模式下每次请求都用思考配置)
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 current_thinking_mode and self.current_task_thinking:
self._print(f"{OUTPUT_FORMATS['info']} [任务内快速模式] 使用本次任务的思考继续处理...")
# 记录本次调用的模式
self.last_call_used_thinking = current_thinking_mode
if current_thinking_mode and self.force_thinking_next_call:
self.force_thinking_next_call = False
if not current_thinking_mode and self.skip_thinking_next_call:
self.skip_thinking_next_call = False
try:
override_max = self.thinking_max_tokens if current_thinking_mode else self.fast_max_tokens
if override_max is not None:
@ -110,6 +99,9 @@ class DeepSeekClientChatMixin:
extra_params = self.thinking_extra_params if current_thinking_mode else self.fast_extra_params
if extra_params:
payload.update(extra_params)
# 推理强度:思考模式下且模型支持时注入(与额外参数同级的平铺参数)
if current_thinking_mode and self.supports_reasoning_effort and self.reasoning_effort:
payload["reasoning_effort"] = self.reasoning_effort
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
@ -120,9 +112,8 @@ class DeepSeekClientChatMixin:
"event": "request_prepare",
"model_key": self.model_key,
"thinking_mode_flag": bool(self.thinking_mode),
"deep_thinking_mode": bool(self.deep_thinking_mode),
"deep_thinking_session": bool(self.deep_thinking_session),
"current_call_use_thinking": bool(current_thinking_mode),
"reasoning_effort": self.reasoning_effort,
"api_base_url": api_config.get("base_url"),
"api_model_id": api_config.get("model_id"),
"payload_model": payload.get("model"),
@ -451,12 +442,6 @@ class DeepSeekClientChatMixin:
if in_thinking:
self._print("\n💭 [思考结束]\n")
# 记录思考内容并更新调用状态
if self.last_call_used_thinking and current_thinking:
self.current_task_thinking = current_thinking
if self.current_task_first_call:
self.current_task_first_call = False # 标记当前任务的第一次调用已完成
# 如果没有工具调用,说明完成了
if not tool_calls:
if full_response: # 有正常回复,任务完成
@ -602,24 +587,6 @@ class DeepSeekClientChatMixin:
thinking_content = ""
in_thinking = False
# 如果思考模式且已有本任务的思考内容,补充到上下文,确保多次调用时思考不割裂
if (
self.thinking_mode
and not self.current_task_first_call
and self.current_task_thinking
):
thinking_context = (
"\n=== 📋 本次任务的思考 ===\n"
f"{self.current_task_thinking}\n"
"=== 思考结束 ===\n"
"提示:以上是本轮任务先前的思考,请在此基础上继续。"
)
messages.append({
"role": "system",
"content": thinking_context
})
thinking_context_injected = True
try:
async for chunk in self.chat(messages, tools=None, stream=True):
if chunk.get("error"):
@ -660,11 +627,6 @@ class DeepSeekClientChatMixin:
if in_thinking:
self._print("\n💭 [思考结束]\n")
if self.last_call_used_thinking and thinking_content:
self.current_task_thinking = thinking_content
if self.current_task_first_call:
self.current_task_first_call = False
# 如果没有收到任何响应
if not full_response and not thinking_content:
self._print(f"{OUTPUT_FORMATS['error']} API未返回任何内容请检查API密钥和模型ID")

View File

@ -66,6 +66,7 @@ class DeepSeekClientProfileMixin:
self.thinking_max_tokens = thinking.get("max_tokens")
self.fast_extra_params = fast.get("extra_params") or {}
self.thinking_extra_params = thinking.get("extra_params") or {}
self.supports_reasoning_effort = bool(profile.get("supports_reasoning_effort"))
self.model_multimodal = self._normalize_multimodal_capability(profile.get("multimodal"))
self.default_context_window = profile.get("context_window") or fast.get("context_window")
# 同步旧字段
@ -101,31 +102,8 @@ class DeepSeekClientProfileMixin:
self.max_context_tokens = None
def get_current_thinking_mode(self) -> bool:
"""获取当前应该使用的思考模式"""
if self.deep_thinking_session:
return True
if not self.thinking_mode:
return False
if self.force_thinking_next_call:
return True
if self.skip_thinking_next_call:
return False
return self.current_task_first_call
def set_deep_thinking_mode(self, enabled: bool):
"""配置深度思考模式(持续使用思考模型)。"""
self.deep_thinking_mode = bool(enabled)
if not enabled:
self.deep_thinking_session = False
def start_new_task(self, force_deep: bool = False):
"""开始新任务(重置任务级别的状态)"""
self.current_task_first_call = True
self.current_task_thinking = ""
self.force_thinking_next_call = False
self.skip_thinking_next_call = False
self.last_call_used_thinking = False
self.deep_thinking_session = bool(force_deep) or bool(self.deep_thinking_mode)
"""获取当前应该使用的思考模式:思考模式下每次请求都用思考配置。"""
return bool(self.thinking_mode)
def _build_headers(self, api_key: str) -> Dict[str, str]:
return {

View File

@ -30,6 +30,10 @@ except Exception:
_CONV_SAVE_LOG_DIR = None
# save_conversation 专用哨兵:区分「不更新该字段」与「显式清除为 None」
_KEEP = object()
def _debug_conversation_save_trace(conversation_id: str, file_path, data: Dict):
"""[临时调试] 记录每次对话文件写入,重点捕获「非空被空覆盖」的调用栈。
@ -362,6 +366,7 @@ class CrudMixin:
project_path: str = None,
thinking_mode: bool = None,
run_mode: Optional[str] = None,
reasoning_effort: Any = _KEEP,
todo_list: Optional[Dict] = None,
model_key: Optional[str] = None,
has_images: Optional[bool] = None,
@ -436,12 +441,23 @@ class CrudMixin:
if thinking_mode is not None:
existing_data["metadata"]["thinking_mode"] = thinking_mode
if run_mode:
normalized_mode = run_mode if run_mode in {"fast", "thinking", "deep"} else (
"thinking" if existing_data["metadata"].get("thinking_mode") else "fast"
)
normalized_mode = str(run_mode).lower()
if normalized_mode == "deep": # 旧版标识符映射
normalized_mode = "thinking"
if normalized_mode not in {"fast", "thinking"}:
normalized_mode = "thinking" if existing_data["metadata"].get("thinking_mode") else "fast"
existing_data["metadata"]["run_mode"] = normalized_mode
elif "run_mode" not in existing_data["metadata"]:
existing_data["metadata"]["run_mode"] = "thinking" if existing_data["metadata"].get("thinking_mode") else "fast"
if reasoning_effort is not _KEEP:
# 显式传入None/空 = 清除为默认(不传参);否则写入档位
if reasoning_effort is None:
existing_data["metadata"]["reasoning_effort"] = None
else:
normalized_effort = str(reasoning_effort).strip().lower()
existing_data["metadata"]["reasoning_effort"] = normalized_effort or None
elif "reasoning_effort" not in existing_data["metadata"]:
existing_data["metadata"]["reasoning_effort"] = None
# 推断最新使用的模型(优先参数,其次倒序扫描助手消息)
inferred_model = None
if model_key is None: