From 2f4ea590a810ed8226f2500d7cf4202d329c013c Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 30 Nov 2025 13:39:50 +0800 Subject: [PATCH] feat: add deep thinking mode --- core/main_terminal.py | 28 ++++- modules/personalization_manager.py | 17 +++ static/src/App.vue | 9 +- static/src/app.ts | 105 ++++++++++++++++-- static/src/components/input/InputComposer.vue | 14 ++- static/src/components/input/QuickMenu.vue | 59 +++++++++- static/src/components/panels/LeftPanel.vue | 29 ++++- .../personalization/PersonalizationDrawer.vue | 22 +++- static/src/composables/useLegacySocket.ts | 6 +- static/src/stores/connection.ts | 24 +++- static/src/stores/input.ts | 13 ++- static/src/stores/personalization.ts | 52 ++++++++- .../styles/components/input/_composer.scss | 14 +++ .../styles/components/overlays/_overlays.scss | 61 ++++++++++ .../styles/components/panels/_left-panel.scss | 10 ++ utils/api_client.py | 4 + web_server.py | 66 ++++++++--- 17 files changed, 480 insertions(+), 53 deletions(-) diff --git a/core/main_terminal.py b/core/main_terminal.py index 1898618..065d4e4 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -53,6 +53,7 @@ from modules.easter_egg_manager import EasterEggManager from modules.personalization_manager import ( load_personalization_config, build_personalization_prompt, + THINKING_MODE_OPTIONS, ) try: from config.limits import THINKING_FAST_INTERVAL @@ -86,6 +87,11 @@ class MainTerminal: # 初始化组件 self.api_client = DeepSeekClient(thinking_mode=thinking_mode) + self.thinking_mode_option = "smart" if thinking_mode else "fast" + self.default_thinking_mode_option = self.thinking_mode_option + self.deep_thinking_mode = self.thinking_mode_option == "deep" + self.api_client.deep_thinking_mode = self.deep_thinking_mode + self.set_thinking_mode_option(self.thinking_mode_option) self.context_manager = ContextManager(project_path, data_dir=str(self.data_dir)) self.context_manager.main_terminal = self self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace" @@ -205,6 +211,17 @@ class MainTerminal: except Exception: pass + def set_thinking_mode_option(self, option: str): + previous_option = getattr(self, "thinking_mode_option", "fast") + normalized = option if option in THINKING_MODE_OPTIONS else "fast" + self.thinking_mode_option = normalized + self.thinking_mode = normalized in ("smart", "deep") + self.deep_thinking_mode = normalized == "deep" + self.api_client.thinking_mode = self.thinking_mode + self.api_client.deep_thinking_mode = self.deep_thinking_mode + if previous_option != normalized: + self.api_client.start_new_task() + def update_container_session(self, session: Optional["ContainerHandle"]): self._apply_container_session(session) if getattr(self, "terminal_manager", None): @@ -352,6 +369,10 @@ class MainTerminal: for key, category in TOOL_CATEGORIES.items(): self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled self._refresh_disabled_tools() + default_mode = effective_config.get("default_mode") + if isinstance(default_mode, str) and default_mode in THINKING_MODE_OPTIONS: + self.default_thinking_mode_option = default_mode + self.set_thinking_mode_option(default_mode) def _handle_read_tool(self, arguments: Dict) -> Dict: @@ -700,7 +721,7 @@ class MainTerminal: assistant_content_parts = [] # 添加思考内容 - if final_thinking: + if final_thinking and not self.deep_thinking_mode: assistant_content_parts.append(f"\n{final_thinking}\n") # 添加回复内容 @@ -714,7 +735,8 @@ class MainTerminal: self.context_manager.add_conversation( "assistant", assistant_content, - collected_tool_calls if collected_tool_calls else None + collected_tool_calls if collected_tool_calls else None, + reasoning_content=final_thinking if self.deep_thinking_mode else None ) # 3. 保存独立的tool消息 @@ -2360,6 +2382,8 @@ class MainTerminal: tool_calls = conv.get("tool_calls") or [] if tool_calls and self._tool_calls_followed_by_tools(conversation, idx, tool_calls): message["tool_calls"] = tool_calls + if conv.get("reasoning_content") and self.thinking_mode_option == "deep": + message["reasoning_content"] = conv["reasoning_content"] messages.append(message) elif conv["role"] == "tool": diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index 3555e3a..a464e15 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -21,6 +21,7 @@ MAX_CONSIDERATION_ITEMS = 10 TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"] THINKING_INTERVAL_MIN = 1 THINKING_INTERVAL_MAX = 50 +THINKING_MODE_OPTIONS = ("fast", "smart", "deep") DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "enabled": False, @@ -31,6 +32,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "considerations": [], "thinking_interval": None, "disabled_tool_categories": [], + "default_mode": "fast", } __all__ = [ @@ -38,6 +40,9 @@ __all__ = [ "DEFAULT_PERSONALIZATION_CONFIG", "TONE_PRESETS", "MAX_CONSIDERATION_ITEMS", + "THINKING_INTERVAL_MIN", + "THINKING_INTERVAL_MAX", + "THINKING_MODE_OPTIONS", "load_personalization_config", "save_personalization_config", "ensure_personalization_config", @@ -123,6 +128,10 @@ def sanitize_personalization_payload( base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories) else: base["disabled_tool_categories"] = _sanitize_tool_categories(base.get("disabled_tool_categories"), allowed_tool_categories) + if "default_mode" in data: + base["default_mode"] = _sanitize_default_mode(data.get("default_mode")) + else: + base["default_mode"] = _sanitize_default_mode(base.get("default_mode")) return base @@ -222,3 +231,11 @@ def _sanitize_tool_categories(value: Any, allowed: set) -> list: if candidate not in result: result.append(candidate) return result + + +def _sanitize_default_mode(value: Any) -> str: + if isinstance(value, str): + normalized = value.strip() + if normalized in THINKING_MODE_OPTIONS: + return normalized + return DEFAULT_PERSONALIZATION_CONFIG["default_mode"] diff --git a/static/src/App.vue b/static/src/App.vue index c3e2901..682f6f6 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -63,6 +63,7 @@ :icon-style="iconStyle" :agent-version="agentVersion" :thinking-mode="thinkingMode" + :thinking-mode-option="thinkingModeOption" :is-connected="isConnected" :panel-menu-open="panelMenuOpen" :panel-mode="panelMode" @@ -147,12 +148,14 @@ :is-connected="isConnected" :streaming-message="streamingMessage" :uploading="uploading" - :thinking-mode="thinkingMode" :quick-menu-open="quickMenuOpen" :tool-menu-open="toolMenuOpen" :tool-settings="toolSettings" :tool-settings-loading="toolSettingsLoading" :settings-open="settingsOpen" + :mode-menu-open="modeMenuOpen" + :thinking-mode-option="thinkingModeOption" + :mode-options="modeOptions" :compressing="compressing" :current-conversation-id="currentConversationId" :icon-style="iconStyle" @@ -166,7 +169,8 @@ @send-or-stop="handleSendOrStop" @quick-upload="handleQuickUpload" @toggle-tool-menu="toggleToolMenu" - @quick-mode-toggle="handleQuickModeToggle" + @toggle-mode-menu="toggleModeMenu" + @select-mode="selectThinkingMode" @toggle-settings="toggleSettings" @update-tool-category="updateToolCategory" @realtime-terminal="handleRealtimeTerminalClick" @@ -280,6 +284,7 @@ :icon-style="iconStyle" :agent-version="agentVersion" :thinking-mode="thinkingMode" + :thinking-mode-option="thinkingModeOption" :is-connected="isConnected" :panel-menu-open="panelMenuOpen" :panel-mode="panelMode" diff --git a/static/src/app.ts b/static/src/app.ts index 05ef727..f5611e9 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -97,6 +97,11 @@ if (window.visualViewport) { } const ENABLE_APP_DEBUG_LOGS = false; +const MODE_OPTIONS = [ + { value: 'fast', label: '快速模式', description: '即时响应', icon: 'zap' }, + { value: 'smart', label: '思考模式', description: '智能调度', icon: 'brain' }, + { value: 'deep', label: '深度思考模式', description: '全程推理', icon: 'brain' } +]; function debugLog(...args) { if (!ENABLE_APP_DEBUG_LOGS) { return; @@ -135,7 +140,8 @@ const appOptions = { // 工具控制菜单 icons: ICONS, - toolCategoryIcons: TOOL_CATEGORY_ICON_MAP + toolCategoryIcons: TOOL_CATEGORY_ICON_MAP, + modeOptions: MODE_OPTIONS } }, @@ -199,7 +205,8 @@ const appOptions = { 'stopRequested', 'projectPath', 'agentVersion', - 'thinkingMode' + 'thinkingMode', + 'thinkingModeOption' ]), ...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']), ...mapWritableState(useUiStore, [ @@ -250,7 +257,8 @@ const appOptions = { 'inputIsFocused', 'quickMenuOpen', 'toolMenuOpen', - 'settingsOpen' + 'settingsOpen', + 'modeMenuOpen' ]), ...mapWritableState(useToolStore, [ 'preparingTools', @@ -483,6 +491,8 @@ const appOptions = { inputSetToolMenuOpen: 'setToolMenuOpen', inputToggleSettingsMenu: 'toggleSettingsMenu', inputSetSettingsOpen: 'setSettingsOpen', + inputToggleModeMenu: 'toggleModeMenu', + inputSetModeMenuOpen: 'setModeMenuOpen', inputSetMessage: 'setInputMessage', inputClearMessage: 'clearInputMessage', inputSetLineCount: 'setInputLineCount', @@ -1042,7 +1052,7 @@ const appOptions = { const statusData = await statusResponse.json(); this.projectPath = statusData.project_path || ''; this.agentVersion = statusData.version || this.agentVersion; - this.thinkingMode = !!statusData.thinking_mode; + this.applyThinkingModeSnapshot(statusData); this.applyStatusSnapshot(statusData); await this.fetchUsageQuota(); @@ -1817,20 +1827,28 @@ const appOptions = { return this.subAgentFetch(); }, - async toggleThinkingMode() { - const nextMode = !this.thinkingMode; + async setThinkingModeOption(option) { + if (!this.isConnected || this.streamingMessage) { + return; + } + if (!this.modeOptions.some((item) => item.value === option)) { + return; + } try { const response = await fetch('/api/thinking-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ thinking_mode: nextMode }) + body: JSON.stringify({ mode: option }) }); const data = await response.json(); if (response.ok && data.success) { - const actual = typeof data.data === 'boolean' ? data.data : nextMode; - this.thinkingMode = actual; + const payload = data.data || {}; + this.applyThinkingModeSnapshot({ + thinking_mode_option: payload.mode || option, + thinking_mode: typeof payload.thinking_mode === 'boolean' ? payload.thinking_mode : undefined + }); return; } throw new Error(data.message || data.error || '切换失败'); @@ -1838,12 +1856,40 @@ const appOptions = { console.error('切换思考模式失败:', error); this.uiPushToast({ title: '切换思考模式失败', - message: error.message || '请稍后重试', + message: error?.message || '请稍后重试', type: 'error' }); } }, + applyThinkingModeSnapshot(payload) { + const normalized = this.normalizeModeOption( + payload && payload.thinking_mode_option, + Object.prototype.hasOwnProperty.call(payload || {}, 'thinking_mode') ? payload!.thinking_mode : undefined, + Object.prototype.hasOwnProperty.call(payload || {}, 'deep_thinking_mode') ? payload!.deep_thinking_mode : undefined + ); + this.thinkingModeOption = normalized; + this.thinkingMode = normalized !== 'fast'; + }, + + normalizeModeOption(option, fallback, deepFlag) { + if (typeof deepFlag === 'boolean') { + if (deepFlag) { + return 'deep'; + } + if (!deepFlag && option === 'deep') { + return fallback ? 'smart' : 'fast'; + } + } + if (typeof option === 'string' && this.modeOptions.some((item) => item.value === option)) { + return option; + } + if (typeof fallback === 'boolean') { + return fallback ? 'smart' : 'fast'; + } + return 'fast'; + }, + triggerFileUpload() { if (this.uploading) { return; @@ -1994,6 +2040,7 @@ const appOptions = { const nextState = this.inputToggleToolMenu(); if (nextState) { this.inputSetSettingsOpen(false); + this.inputSetModeMenuOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } @@ -2003,6 +2050,28 @@ const appOptions = { } }, + toggleModeMenu() { + if (!this.isConnected) { + return; + } + const nextState = this.inputToggleModeMenu(); + if (nextState) { + this.inputSetToolMenuOpen(false); + this.inputSetSettingsOpen(false); + if (!this.quickMenuOpen) { + this.inputOpenQuickMenu(); + } + } + }, + + selectThinkingMode(option) { + if (!option) { + return; + } + this.setThinkingModeOption(option); + this.inputSetModeMenuOpen(false); + }, + toggleQuickMenu() { if (!this.isConnected) { return; @@ -2025,7 +2094,20 @@ const appOptions = { if (!this.isConnected || this.streamingMessage) { return; } - this.toggleThinkingMode(); + const next = this.nextModeOption(); + this.setThinkingModeOption(next); + }, + + nextModeOption() { + const order = this.modeOptions.map((item) => item.value); + if (!order.length) { + return 'fast'; + } + const currentIndex = order.indexOf(this.thinkingModeOption); + if (currentIndex === -1) { + return order[0]; + } + return order[(currentIndex + 1) % order.length] || order[0]; }, handleInputChange() { @@ -2217,6 +2299,7 @@ const appOptions = { const nextState = this.inputToggleSettingsMenu(); if (nextState) { this.inputSetToolMenuOpen(false); + this.inputSetModeMenuOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index 11f314f..2b1585f 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -41,18 +41,21 @@ :is-connected="isConnected" :uploading="uploading" :streaming-message="streamingMessage" - :thinking-mode="thinkingMode" :tool-menu-open="toolMenuOpen" :tool-settings="toolSettings" :tool-settings-loading="toolSettingsLoading" :settings-open="settingsOpen" + :mode-menu-open="modeMenuOpen" + :thinking-mode-option="thinkingModeOption" + :mode-options="modeOptions" :compressing="compressing" :current-conversation-id="currentConversationId" :icon-style="iconStyle" :tool-category-icon="toolCategoryIcon" @quick-upload="triggerQuickUpload" @toggle-tool-menu="$emit('toggle-tool-menu')" - @quick-mode-toggle="$emit('quick-mode-toggle')" + @toggle-mode-menu="$emit('toggle-mode-menu')" + @select-mode="value => $emit('select-mode', value)" @toggle-settings="$emit('toggle-settings')" @update-tool-category="(id, enabled) => $emit('update-tool-category', id, enabled)" @realtime-terminal="$emit('realtime-terminal')" @@ -81,7 +84,8 @@ const emit = defineEmits([ 'send-or-stop', 'quick-upload', 'toggle-tool-menu', - 'quick-mode-toggle', + 'toggle-mode-menu', + 'select-mode', 'toggle-settings', 'update-tool-category', 'realtime-terminal', @@ -98,16 +102,18 @@ const props = defineProps<{ isConnected: boolean; streamingMessage: boolean; uploading: boolean; - thinkingMode: boolean; quickMenuOpen: boolean; toolMenuOpen: boolean; toolSettings: Array<{ id: string; label: string; enabled: boolean }>; toolSettingsLoading: boolean; settingsOpen: boolean; + modeMenuOpen: boolean; compressing: boolean; currentConversationId: string | null; iconStyle: (key: string) => Record; toolCategoryIcon: (categoryId: string) => string; + thinkingModeOption: string; + modeOptions: Array<{ value: string; label: string; description: string; icon: string }>; }>(); const inputStore = useInputStore(); diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index 28f1496..b810a9f 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -15,11 +15,14 @@ + + + diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue index e4cdf91..10993b1 100644 --- a/static/src/components/panels/LeftPanel.vue +++ b/static/src/components/panels/LeftPanel.vue @@ -14,15 +14,15 @@ + +
@@ -297,7 +316,8 @@ const { toggleUpdating, toolCategories, thinkingIntervalDefault, - thinkingIntervalRange + thinkingIntervalRange, + modeOptions } = storeToRefs(personalization); const activeTab = ref<'preferences' | 'behavior'>('preferences'); diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 961d756..3f793ce 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -529,7 +529,7 @@ export async function initializeLegacySocket(ctx: any) { ctx.socket.on('system_ready', (data) => { ctx.projectPath = data.project_path || ''; ctx.agentVersion = data.version || ctx.agentVersion; - ctx.thinkingMode = !!data.thinking_mode; + ctx.applyThinkingModeSnapshot(data); socketLog('系统就绪:', data); // 系统就绪后立即加载对话列表 @@ -630,9 +630,7 @@ export async function initializeLegacySocket(ctx: any) { if (status.conversation && status.conversation.current_id) { ctx.currentConversationId = status.conversation.current_id; } - if (typeof status.thinking_mode !== 'undefined') { - ctx.thinkingMode = !!status.thinking_mode; - } + ctx.applyThinkingModeSnapshot(status); }); // AI消息开始 diff --git a/static/src/stores/connection.ts b/static/src/stores/connection.ts index 31a4812..1f1ff83 100644 --- a/static/src/stores/connection.ts +++ b/static/src/stores/connection.ts @@ -8,6 +8,7 @@ interface ConnectionState { projectPath: string; agentVersion: string; thinkingMode: boolean; + thinkingModeOption: 'fast' | 'smart' | 'deep'; } export const useConnectionStore = defineStore('connection', { @@ -17,7 +18,8 @@ export const useConnectionStore = defineStore('connection', { stopRequested: false, projectPath: '', agentVersion: '', - thinkingMode: true + thinkingMode: false, + thinkingModeOption: 'fast' }), actions: { setSocket(socket: Socket | null) { @@ -43,9 +45,27 @@ export const useConnectionStore = defineStore('connection', { }, setThinkingMode(value: boolean) { this.thinkingMode = !!value; + if (!value) { + this.thinkingModeOption = 'fast'; + } else if (this.thinkingModeOption === 'fast') { + this.thinkingModeOption = 'smart'; + } + }, + setThinkingModeOption(option: string) { + if (!['fast', 'smart', 'deep'].includes(option)) { + return; + } + this.thinkingModeOption = option as ConnectionState['thinkingModeOption']; + this.thinkingMode = option !== 'fast'; }, toggleThinkingMode() { - this.thinkingMode = !this.thinkingMode; + if (this.thinkingModeOption === 'fast') { + this.setThinkingModeOption('smart'); + } else if (this.thinkingModeOption === 'smart') { + this.setThinkingModeOption('deep'); + } else { + this.setThinkingModeOption('fast'); + } } } }); diff --git a/static/src/stores/input.ts b/static/src/stores/input.ts index 8de5f0e..d54f689 100644 --- a/static/src/stores/input.ts +++ b/static/src/stores/input.ts @@ -8,6 +8,7 @@ interface InputState { quickMenuOpen: boolean; toolMenuOpen: boolean; settingsOpen: boolean; + modeMenuOpen: boolean; } export const useInputStore = defineStore('input', { @@ -18,7 +19,8 @@ export const useInputStore = defineStore('input', { inputIsFocused: false, quickMenuOpen: false, toolMenuOpen: false, - settingsOpen: false + settingsOpen: false, + modeMenuOpen: false }), actions: { setInputMessage(value: string) { @@ -44,6 +46,7 @@ export const useInputStore = defineStore('input', { if (!open) { this.toolMenuOpen = false; this.settingsOpen = false; + this.modeMenuOpen = false; } }, toggleQuickMenu() { @@ -55,6 +58,7 @@ export const useInputStore = defineStore('input', { this.quickMenuOpen = false; this.toolMenuOpen = false; this.settingsOpen = false; + this.modeMenuOpen = false; }, toggleToolMenu() { this.toolMenuOpen = !this.toolMenuOpen; @@ -69,6 +73,13 @@ export const useInputStore = defineStore('input', { }, setSettingsOpen(open: boolean) { this.settingsOpen = open; + }, + toggleModeMenu() { + this.modeMenuOpen = !this.modeMenuOpen; + return this.modeMenuOpen; + }, + setModeMenuOpen(open: boolean) { + this.modeMenuOpen = open; } } }); diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 893629a..6d1a199 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -9,6 +9,7 @@ interface PersonalForm { considerations: string[]; thinking_interval: number | null; disabled_tool_categories: string[]; + default_mode: string; } interface PersonalizationState { @@ -28,10 +29,16 @@ interface PersonalizationState { toolCategories: Array<{ id: string; label: string }>; thinkingIntervalDefault: number; thinkingIntervalRange: { min: number; max: number }; + modeOptions: Array<{ value: string; label: string; description: string }>; } const DEFAULT_INTERVAL = 10; const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 }; +const DEFAULT_MODE_OPTIONS = [ + { value: 'fast', label: '快速模式', description: '即时响应' }, + { value: 'smart', label: '思考模式', description: '智能调度' }, + { value: 'deep', label: '深度思考模式', description: '全程推理' } +]; const defaultForm = (): PersonalForm => ({ enabled: false, @@ -41,7 +48,8 @@ const defaultForm = (): PersonalForm => ({ tone: '', considerations: [], thinking_interval: null, - disabled_tool_categories: [] + disabled_tool_categories: [], + default_mode: 'fast' }); export const usePersonalizationStore = defineStore('personalization', { @@ -61,7 +69,8 @@ export const usePersonalizationStore = defineStore('personalization', { form: defaultForm(), toolCategories: [], thinkingIntervalDefault: DEFAULT_INTERVAL, - thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE } + thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE }, + modeOptions: [...DEFAULT_MODE_OPTIONS] }), actions: { async openDrawer() { @@ -121,7 +130,10 @@ export const usePersonalizationStore = defineStore('personalization', { tone: data.tone || '', considerations: Array.isArray(data.considerations) ? [...data.considerations] : [], 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') : [] + disabled_tool_categories: Array.isArray(data.disabled_tool_categories) + ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') + : [], + default_mode: typeof data.default_mode === 'string' ? data.default_mode : 'fast' }; this.clearFeedback(); }, @@ -150,6 +162,26 @@ export const usePersonalizationStore = defineStore('personalization', { } else { this.toolCategories = []; } + if (payload && Array.isArray(payload.thinking_mode_options) && payload.thinking_mode_options.length) { + this.modeOptions = payload.thinking_mode_options + .map((value: string) => { + const preset = DEFAULT_MODE_OPTIONS.find((option) => option.value === value); + if (preset) { + return preset; + } + return { value, label: value, description: '' }; + }) + .filter((item: { value: string }) => !!item.value); + } else { + this.modeOptions = [...DEFAULT_MODE_OPTIONS]; + } + if (!this.modeOptions.some((option) => option.value === this.form.default_mode)) { + const fallback = this.modeOptions[0]?.value || 'fast'; + this.form = { + ...this.form, + default_mode: fallback + }; + } }, clearFeedback() { this.status = ''; @@ -281,6 +313,20 @@ export const usePersonalizationStore = defineStore('personalization', { }; this.clearFeedback(); }, + setDefaultMode(mode: string) { + if (!mode) { + return; + } + const valid = this.modeOptions.some((option) => option.value === mode); + if (!valid) { + return; + } + this.form = { + ...this.form, + default_mode: mode + }; + this.clearFeedback(); + }, updateNewConsideration(value: string) { this.newConsideration = value; this.clearFeedback(); diff --git a/static/src/styles/components/input/_composer.scss b/static/src/styles/components/input/_composer.scss index b3c4fef..2180fd6 100644 --- a/static/src/styles/components/input/_composer.scss +++ b/static/src/styles/components/input/_composer.scss @@ -259,6 +259,11 @@ bottom: 0; } +.quick-submenu.mode-submenu { + top: auto; + bottom: 0; +} + .menu-entry.submenu-entry { width: 100%; justify-content: space-between; @@ -268,6 +273,15 @@ color: var(--claude-text-secondary); } +.menu-entry.submenu-entry.active { + background: rgba(189, 93, 58, 0.1); + border: 1px solid rgba(189, 93, 58, 0.3); +} + +.menu-entry.submenu-entry.active .entry-arrow { + color: var(--claude-accent); +} + .menu-entry.disabled { opacity: 0.5; } diff --git a/static/src/styles/components/overlays/_overlays.scss b/static/src/styles/components/overlays/_overlays.scss index b30666f..ca35dcd 100644 --- a/static/src/styles/components/overlays/_overlays.scss +++ b/static/src/styles/components/overlays/_overlays.scss @@ -170,6 +170,67 @@ margin-bottom: 18px; } +.mode-preference { + margin-bottom: 18px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.mode-preference-header { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mode-preference-title { + font-weight: 600; + color: var(--claude-text); +} + +.mode-preference-hint { + font-size: 13px; + color: var(--claude-text-secondary); +} + +.mode-option-grid { + display: flex; + flex-direction: column; + gap: 8px; +} + +.mode-option-chip { + border: 1px solid rgba(118, 103, 84, 0.3); + border-radius: 16px; + padding: 10px 14px; + background: rgba(255, 255, 255, 0.9); + text-align: left; + display: flex; + flex-direction: column; + gap: 4px; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.mode-option-chip:hover { + border-color: var(--claude-accent); +} + +.mode-option-chip.active { + border-color: var(--claude-accent); + background: rgba(189, 93, 58, 0.08); + box-shadow: 0 6px 20px rgba(189, 93, 58, 0.15); +} + +.mode-option-label { + font-weight: 600; +} + +.mode-option-desc { + font-size: 13px; + color: var(--claude-text-secondary); +} + .personalization-layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); diff --git a/static/src/styles/components/panels/_left-panel.scss b/static/src/styles/components/panels/_left-panel.scss index 8c62142..67b0d91 100644 --- a/static/src/styles/components/panels/_left-panel.scss +++ b/static/src/styles/components/panels/_left-panel.scss @@ -137,6 +137,16 @@ box-shadow: 0 8px 20px rgba(255, 204, 77, 0.35); } +.mode-indicator.smart { + background: var(--claude-accent); + box-shadow: 0 8px 20px rgba(189, 93, 58, 0.25); +} + +.mode-indicator.deep { + background: linear-gradient(135deg, #7c5dfa, #4b32c3); + box-shadow: 0 8px 20px rgba(76, 50, 195, 0.35); +} + .mode-indicator .icon { --icon-size: 18px; color: inherit; diff --git a/utils/api_client.py b/utils/api_client.py index e90b6ac..f8ff8c5 100644 --- a/utils/api_client.py +++ b/utils/api_client.py @@ -46,6 +46,7 @@ class DeepSeekClient: "model_id": THINKING_MODEL_ID or MODEL_ID } self.thinking_mode = thinking_mode # True=智能思考模式, False=快速模式 + self.deep_thinking_mode = False self.web_mode = web_mode # Web模式标志,用于禁用print输出 # 兼容旧代码路径 self.api_base_url = self.fast_api_config["base_url"] @@ -57,6 +58,7 @@ class DeepSeekClient: self.force_thinking_next_call = False # 单次强制思考 self.skip_thinking_next_call = False # 单次强制快速 self.last_call_used_thinking = False # 最近一次调用是否使用思考模型 + self.deep_thinking_mode = False def _print(self, message: str, end: str = "\n", flush: bool = False): """安全的打印函数,在Web模式下不输出""" @@ -154,6 +156,8 @@ class DeepSeekClient: def get_current_thinking_mode(self) -> bool: """获取当前应该使用的思考模式""" + if self.deep_thinking_mode: + return True if not self.thinking_mode: return False if self.force_thinking_next_call: diff --git a/web_server.py b/web_server.py index 53951c0..a3d88c7 100644 --- a/web_server.py +++ b/web_server.py @@ -57,6 +57,7 @@ from modules.personalization_manager import ( save_personalization_config, THINKING_INTERVAL_MIN, THINKING_INTERVAL_MAX, + THINKING_MODE_OPTIONS, ) from modules.user_container_manager import UserContainerManager from modules.usage_tracker import UsageTracker @@ -484,6 +485,13 @@ def with_terminal(func): return jsonify({"error": str(exc), "code": "resource_busy"}), 503 if not terminal or not workspace: return jsonify({"error": "System not initialized"}), 503 + preferred_mode = session.get('thinking_mode_option') + if not preferred_mode: + preferred_mode = getattr(terminal, "default_thinking_mode_option", "fast") + session['thinking_mode_option'] = preferred_mode + if preferred_mode != getattr(terminal, "thinking_mode_option", None): + terminal.set_thinking_mode_option(preferred_mode) + session['thinking_mode'] = terminal.thinking_mode kwargs.update({ 'terminal': terminal, 'workspace': workspace, @@ -661,6 +669,11 @@ def apply_thinking_schedule(terminal: WebTerminal): client.skip_thinking_next_call = False return state = get_thinking_state(terminal) + if getattr(terminal, "deep_thinking_mode", False): + client.force_thinking_next_call = False + client.skip_thinking_next_call = False + state["fast_streak"] = 0 + return awaiting_writes = getattr(terminal, "pending_append_request", None) or getattr(terminal, "pending_modify_request", None) if awaiting_writes: client.skip_thinking_next_call = True @@ -699,6 +712,9 @@ def update_thinking_after_call(terminal: WebTerminal): if not getattr(terminal, "thinking_mode", False): return state = get_thinking_state(terminal) + if getattr(terminal, "deep_thinking_mode", False): + state["fast_streak"] = 0 + return if terminal.api_client.last_call_used_thinking: state["fast_streak"] = 0 else: @@ -831,7 +847,9 @@ def login(): session['logged_in'] = True session['username'] = record.username - session['thinking_mode'] = app.config.get('DEFAULT_THINKING_MODE', False) + default_mode_option = app.config.get('DEFAULT_THINKING_MODE_OPTION', 'fast') + session['thinking_mode_option'] = default_mode_option if default_mode_option in THINKING_MODE_OPTIONS else 'fast' + session['thinking_mode'] = session['thinking_mode_option'] != 'fast' session.permanent = True clear_failures("login", identifier=client_ip) workspace = user_manager.ensure_user_workspace(record.username) @@ -979,6 +997,8 @@ def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str): print(f"[Status] 获取当前对话信息失败: {e}") status['project_path'] = str(workspace.project_path) + status['thinking_mode_option'] = getattr(terminal, "thinking_mode_option", "fast") + status['deep_thinking_mode'] = getattr(terminal, "deep_thinking_mode", False) try: status['container'] = container_manager.get_container_status(username) except Exception as exc: @@ -1041,11 +1061,16 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna """切换思考模式""" try: data = request.get_json() or {} - desired_mode = bool(data.get('thinking_mode')) - terminal.thinking_mode = desired_mode - terminal.api_client.thinking_mode = desired_mode + mode_option = data.get('mode') + if isinstance(mode_option, str) and mode_option not in THINKING_MODE_OPTIONS: + mode_option = None + if not mode_option: + desired_mode = bool(data.get('thinking_mode')) + mode_option = "smart" if desired_mode else "fast" + terminal.set_thinking_mode_option(mode_option) terminal.api_client.start_new_task() - session['thinking_mode'] = desired_mode + session['thinking_mode_option'] = mode_option + session['thinking_mode'] = terminal.thinking_mode # 更新当前对话的元数据 ctx = terminal.context_manager if ctx.current_conversation_id: @@ -1055,17 +1080,21 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna messages=ctx.conversation_history, project_path=str(ctx.project_path), todo_list=ctx.todo_list, - thinking_mode=desired_mode + thinking_mode=terminal.thinking_mode ) 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": status.get("thinking_mode") + "data": { + "thinking_mode": terminal.thinking_mode, + "mode": terminal.thinking_mode_option, + "deep": terminal.deep_thinking_mode + } }) except Exception as exc: print(f"[API] 切换思考模式失败: {exc}") @@ -1091,7 +1120,8 @@ def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace "thinking_interval_range": { "min": THINKING_INTERVAL_MIN, "max": THINKING_INTERVAL_MAX - } + }, + "thinking_mode_options": THINKING_MODE_OPTIONS }) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @@ -1108,6 +1138,8 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp config = save_personalization_config(workspace.data_dir, payload) try: terminal.apply_personalization_preferences(config) + session['thinking_mode_option'] = getattr(terminal, "thinking_mode_option", session.get('thinking_mode_option', 'fast')) + session['thinking_mode'] = terminal.thinking_mode except Exception as exc: debug_log(f"应用个性化偏好失败: {exc}") return jsonify({ @@ -1118,7 +1150,8 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp "thinking_interval_range": { "min": THINKING_INTERVAL_MIN, "max": THINKING_INTERVAL_MAX - } + }, + "thinking_mode_options": THINKING_MODE_OPTIONS }) except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 @@ -3791,14 +3824,17 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "content": assistant_content, "tool_calls": tool_calls } - + reasoning_payload = current_thinking if getattr(web_terminal, "deep_thinking_mode", False) else None + if reasoning_payload: + assistant_message["reasoning_content"] = reasoning_payload + messages.append(assistant_message) - if assistant_content or current_thinking or tool_calls: + if assistant_content or reasoning_payload or tool_calls: web_terminal.context_manager.add_conversation( "assistant", assistant_content, tool_calls=tool_calls if tool_calls else None, - reasoning_content=current_thinking or None + reasoning_content=reasoning_payload ) # 为下一轮迭代重置流状态标志,但保留 full_response 供上面保存使用 @@ -4312,7 +4348,9 @@ def initialize_system(path: str, thinking_mode: bool = False): print(f"[Init] 自动修复: {'开启' if AUTO_FIX_TOOL_CALL else '关闭'}") print(f"[Init] 调试日志: {DEBUG_LOG_FILE}") + default_option = "smart" if thinking_mode else "fast" app.config['DEFAULT_THINKING_MODE'] = thinking_mode + app.config['DEFAULT_THINKING_MODE_OPTION'] = default_option print(f"{OUTPUT_FORMATS['success']} Web系统初始化完成(多用户模式)") def run_server(path: str, thinking_mode: bool = False, port: int = DEFAULT_PORT, debug: bool = False):