diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts
index 49beb40..3cda306 100644
--- a/static/src/stores/personalization.ts
+++ b/static/src/stores/personalization.ts
@@ -14,6 +14,7 @@ interface PersonalForm {
thinking_interval: number | null;
disabled_tool_categories: string[];
default_run_mode: RunMode | null;
+ default_model: string | null;
}
interface LiquidGlassPosition {
@@ -63,7 +64,8 @@ const defaultForm = (): PersonalForm => ({
considerations: [],
thinking_interval: null,
disabled_tool_categories: [],
- default_run_mode: null
+ default_run_mode: null,
+ default_model: 'kimi'
});
const defaultExperimentState = (): ExperimentState => ({
@@ -175,6 +177,9 @@ export const usePersonalizationStore = defineStore('personalization', {
}
},
applyPersonalizationData(data: any) {
+ // 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退为 Kimi
+ const fallbackModel =
+ (this.form && typeof this.form.default_model === 'string' ? this.form.default_model : null) || 'kimi';
this.form = {
enabled: !!data.enabled,
auto_generate_title: data.auto_generate_title !== false,
@@ -189,7 +194,8 @@ export const usePersonalizationStore = defineStore('personalization', {
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
- : null
+ : null,
+ default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel
};
this.clearFeedback();
},
@@ -350,6 +356,15 @@ export const usePersonalizationStore = defineStore('personalization', {
};
this.clearFeedback();
},
+ setDefaultModel(model: string | null) {
+ const allowed = ['deepseek', 'kimi', 'qwen3-max', 'qwen3-vl-plus'];
+ const target = typeof model === 'string' && allowed.includes(model) ? model : null;
+ this.form = {
+ ...this.form,
+ default_model: target
+ };
+ this.clearFeedback();
+ },
applyTonePreset(preset: string) {
if (!preset) {
return;
diff --git a/static/src/styles/components/panels/_resource-panel.scss b/static/src/styles/components/panels/_resource-panel.scss
index 4c4684a..9c70a15 100644
--- a/static/src/styles/components/panels/_resource-panel.scss
+++ b/static/src/styles/components/panels/_resource-panel.scss
@@ -168,22 +168,20 @@
position: absolute;
top: 12px;
left: 12px;
- width: 28px;
- height: 28px;
- border: 1px solid var(--claude-border);
+ width: 14px;
+ height: 14px;
+ border: 1px solid #e0443e;
border-radius: 50%;
- background: rgba(255, 255, 255, 0.8);
- color: var(--claude-text);
- font-size: 14px;
- display: grid;
- place-items: center;
+ background: #ff5f56;
+ display: block;
+ padding: 0;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
- box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16);
}
.token-close-btn:hover {
- background: var(--claude-surface);
+ background: #ff3b30;
transform: translateY(-1px);
}
@@ -191,6 +189,17 @@
transform: translateY(0);
}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+
.panel-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
diff --git a/sub_agent/core/web_terminal.py b/sub_agent/core/web_terminal.py
index 16dcecf..322eefe 100644
--- a/sub_agent/core/web_terminal.py
+++ b/sub_agent/core/web_terminal.py
@@ -2,6 +2,7 @@
import json
from typing import Dict, List, Optional, Callable
+import os
from core.main_terminal import MainTerminal
from utils.logger import setup_logger
try:
@@ -55,8 +56,8 @@ class WebTerminal(MainTerminal):
self.message_callback = message_callback
self.web_mode = True
- # 设置API客户端为Web模式(禁用print)
- self.api_client.web_mode = True
+ # 默认允许输出,便于排查(若需静默可设置环境变量 WEB_API_SILENT=1)
+ self.api_client.web_mode = bool(os.environ.get("WEB_API_SILENT"))
# 重新初始化终端管理器
self.terminal_manager = TerminalManager(
diff --git a/utils/api_client.py b/utils/api_client.py
index e9bcc19..1004914 100644
--- a/utils/api_client.py
+++ b/utils/api_client.py
@@ -335,6 +335,18 @@ class DeepSeekClient:
"stream": stream,
"max_tokens": max_tokens
}
+ # 部分平台(如 Qwen、DeepSeek)需要显式请求 usage 才会在流式尾包返回
+ if stream:
+ should_include_usage = False
+ if self.model_key in {"qwen3-max", "qwen3-vl-plus", "deepseek"}:
+ should_include_usage = True
+ # 兜底:根据 base_url 识别 openai 兼容的提供商
+ if api_config["base_url"]:
+ lower_url = api_config["base_url"].lower()
+ if any(keyword in lower_url for keyword in ["dashscope", "aliyuncs", "deepseek.com"]):
+ should_include_usage = True
+ if should_include_usage:
+ payload.setdefault("stream_options", {})["include_usage"] = True
# 注入模型额外参数(如 Qwen enable_thinking)
extra_params = self.thinking_extra_params if current_thinking_mode else self.fast_extra_params
if extra_params:
diff --git a/utils/context_manager.py b/utils/context_manager.py
index f288cc8..73a3dfb 100644
--- a/utils/context_manager.py
+++ b/utils/context_manager.py
@@ -448,13 +448,16 @@ class ContextManager:
return
try:
run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None
+ model_key = getattr(self.main_terminal, "model_key", None) if hasattr(self, "main_terminal") else None
self.conversation_manager.save_conversation(
conversation_id=self.current_conversation_id,
messages=self.conversation_history,
project_path=str(self.project_path),
todo_list=self.todo_list,
thinking_mode=getattr(self.main_terminal, "thinking_mode", None) if hasattr(self, "main_terminal") else None,
- run_mode=run_mode
+ run_mode=run_mode,
+ model_key=model_key,
+ has_images=self.has_images
)
# 静默保存,不输出日志
except Exception as e:
@@ -749,6 +752,14 @@ class ContextManager:
message["images"] = images
self.has_images = True
+ # 记录当前助手回复所用模型,便于回放时查看
+ if role == "assistant":
+ message.setdefault("metadata", {})
+ if "model_key" not in message["metadata"]:
+ model_key = getattr(self.main_terminal, "model_key", None) if self.main_terminal else None
+ if model_key:
+ message["metadata"]["model_key"] = model_key
+
# 如果是assistant消息且有工具调用,保存完整格式
if role == "assistant" and tool_calls:
# 确保工具调用格式完整
diff --git a/utils/conversation_manager.py b/utils/conversation_manager.py
index 6a704db..faaf6b1 100644
--- a/utils/conversation_manager.py
+++ b/utils/conversation_manager.py
@@ -469,33 +469,45 @@ class ConversationManager:
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 model_key is not None:
- existing_data["metadata"]["model_key"] = model_key
+ # 推断最新使用的模型(优先参数,其次倒序扫描助手消息)
+ inferred_model = None
+ if model_key is None:
+ for msg in reversed(messages):
+ if msg.get("role") != "assistant":
+ continue
+ msg_meta = msg.get("metadata") or {}
+ mk = msg_meta.get("model_key")
+ if mk:
+ inferred_model = mk
+ break
+ target_model = model_key if model_key is not None else inferred_model
+ if target_model is not None:
+ existing_data["metadata"]["model_key"] = target_model
elif "model_key" not in existing_data["metadata"]:
existing_data["metadata"]["model_key"] = None
if has_images is not None:
existing_data["metadata"]["has_images"] = bool(has_images)
elif "has_images" not in existing_data["metadata"]:
existing_data["metadata"]["has_images"] = False
-
+
existing_data["metadata"]["total_messages"] = len(messages)
existing_data["metadata"]["total_tools"] = self._count_tools_in_messages(messages)
# 更新待办列表
existing_data["todo_list"] = todo_list
-
+
# 确保Token统计结构存在(向后兼容)
if "token_statistics" not in existing_data:
existing_data["token_statistics"] = self._initialize_token_statistics()
else:
existing_data["token_statistics"]["updated_at"] = datetime.now().isoformat()
-
+
# 保存文件
self._save_conversation_file(conversation_id, existing_data)
-
+
# 更新索引
self._update_index(conversation_id, existing_data)
-
+
return True
except Exception as e:
print(f"⌘ 保存对话失败 {conversation_id}: {e}")
@@ -544,6 +556,21 @@ class ConversationManager:
self._save_conversation_file(conversation_id, data)
print(f"🔧 为对话 {conversation_id} 添加运行模式字段")
+ # 回填缺失的模型字段:从最近的助手消息元数据推断
+ if metadata.get("model_key") is None:
+ inferred_model = None
+ for msg in reversed(data.get("messages") or []):
+ if msg.get("role") != "assistant":
+ continue
+ mk = (msg.get("metadata") or {}).get("model_key")
+ if mk:
+ inferred_model = mk
+ break
+ if inferred_model is not None:
+ metadata["model_key"] = inferred_model
+ self._save_conversation_file(conversation_id, data)
+ print(f"🔧 为对话 {conversation_id} 回填模型字段: {inferred_model}")
+
return data
except (json.JSONDecodeError, Exception) as e:
print(f"⌘ 加载对话失败 {conversation_id}: {e}")
diff --git a/web_server.py b/web_server.py
index 4b98822..00fba88 100644
--- a/web_server.py
+++ b/web_server.py
@@ -810,11 +810,12 @@ def build_upload_error_response(exc: UploadSecurityError):
}), status
-def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[str], run_mode: Optional[str]) -> Tuple[str, bool]:
+def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[str]) -> Tuple[str, bool]:
"""确保终端加载指定对话,若无则创建新的"""
created_new = False
if not conversation_id:
- result = terminal.create_new_conversation(run_mode=run_mode)
+ # 不显式传入运行模式,优先回到个性化/默认配置
+ result = terminal.create_new_conversation()
if not result.get("success"):
raise RuntimeError(result.get("message", "创建对话失败"))
conversation_id = result["conversation_id"]
@@ -2474,7 +2475,7 @@ def handle_message(data):
requested_conversation_id = data.get('conversation_id')
try:
- conversation_id, created_new = ensure_conversation_loaded(terminal, requested_conversation_id, terminal.run_mode)
+ conversation_id, created_new = ensure_conversation_loaded(terminal, requested_conversation_id)
except RuntimeError as exc:
emit('error', {'message': str(exc)})
return
@@ -2602,8 +2603,11 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
"""创建新对话"""
try:
data = request.get_json() or {}
- thinking_mode = data.get('thinking_mode', terminal.thinking_mode)
- run_mode = data.get('mode')
+ # 前端现在期望“新建对话”回到用户配置的默认模型/模式,
+ # 只有当客户端显式要求保留当前模式时才使用传入值。
+ 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
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
@@ -3891,20 +3895,23 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
modify_result = await finalize_pending_modify(full_response, False, finish_reason="user_stop")
break
+ # 先尝试记录 usage(有些平台会在最后一个 chunk 里携带 usage 但 choices 为空)
+ usage_info = chunk.get("usage")
+ if usage_info:
+ last_usage_payload = usage_info
+
if "choices" not in chunk:
debug_log(f"Chunk {chunk_count}: 无choices字段")
continue
-
+ if not chunk.get("choices"):
+ debug_log(f"Chunk {chunk_count}: choices为空列表")
+ continue
choice = chunk["choices"][0]
delta = choice.get("delta", {})
finish_reason = choice.get("finish_reason")
if finish_reason:
last_finish_reason = finish_reason
- usage_info = choice.get("usage")
- if usage_info:
- last_usage_payload = usage_info
-
# 处理思考内容
if "reasoning_content" in delta:
reasoning_content = delta["reasoning_content"]