autoglm-gui 1.2.1__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. AutoGLM_GUI/adb_plus/__init__.py +6 -6
  2. AutoGLM_GUI/api/__init__.py +54 -16
  3. AutoGLM_GUI/api/agents.py +163 -209
  4. AutoGLM_GUI/api/dual_model.py +310 -0
  5. AutoGLM_GUI/api/mcp.py +134 -0
  6. AutoGLM_GUI/api/metrics.py +36 -0
  7. AutoGLM_GUI/config_manager.py +110 -6
  8. AutoGLM_GUI/dual_model/__init__.py +53 -0
  9. AutoGLM_GUI/dual_model/decision_model.py +664 -0
  10. AutoGLM_GUI/dual_model/dual_agent.py +917 -0
  11. AutoGLM_GUI/dual_model/protocols.py +354 -0
  12. AutoGLM_GUI/dual_model/vision_model.py +442 -0
  13. AutoGLM_GUI/exceptions.py +75 -3
  14. AutoGLM_GUI/metrics.py +283 -0
  15. AutoGLM_GUI/phone_agent_manager.py +264 -14
  16. AutoGLM_GUI/prompts.py +97 -0
  17. AutoGLM_GUI/schemas.py +40 -9
  18. AutoGLM_GUI/static/assets/{about-BtBH1xKN.js → about-Cj6QXqMf.js} +1 -1
  19. AutoGLM_GUI/static/assets/chat-BJeomZgh.js +124 -0
  20. AutoGLM_GUI/static/assets/dialog-CxJlnjzH.js +45 -0
  21. AutoGLM_GUI/static/assets/{index-B_AaKuOT.js → index-C_B-Arvf.js} +1 -1
  22. AutoGLM_GUI/static/assets/index-CxJQuE4y.js +12 -0
  23. AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +1 -0
  24. AutoGLM_GUI/static/assets/{workflows-xX_QH-wI.js → workflows-BTiGCNI0.js} +1 -1
  25. AutoGLM_GUI/static/index.html +2 -2
  26. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.1.dist-info}/METADATA +90 -5
  27. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.1.dist-info}/RECORD +30 -20
  28. AutoGLM_GUI/static/assets/chat-DPzFNNGu.js +0 -124
  29. AutoGLM_GUI/static/assets/dialog-Dwuk2Hgl.js +0 -45
  30. AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
  31. AutoGLM_GUI/static/assets/index-CvQkCi2d.js +0 -11
  32. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.1.dist-info}/WHEEL +0 -0
  33. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.1.dist-info}/entry_points.txt +0 -0
  34. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,310 @@
1
+ """双模型协作API端点"""
2
+
3
+ import threading
4
+ from typing import Any, Optional
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+ from fastapi.responses import StreamingResponse
8
+ from pydantic import BaseModel
9
+
10
+ from AutoGLM_GUI.logger import logger
11
+ from AutoGLM_GUI.dual_model import (
12
+ DecisionModelConfig,
13
+ DualModelAgent,
14
+ DualModelEvent,
15
+ DualModelEventType,
16
+ )
17
+ from AutoGLM_GUI.dual_model.protocols import ThinkingMode
18
+ from phone_agent.model import ModelConfig
19
+
20
+ router = APIRouter(prefix="/api/dual", tags=["dual-model"])
21
+
22
+ # 活跃的双模型会话 (device_id -> (agent, stop_event))
23
+ _active_dual_sessions: dict[str, tuple[DualModelAgent, threading.Event]] = {}
24
+ _active_dual_sessions_lock = threading.Lock()
25
+
26
+
27
+ class DualModelInitRequest(BaseModel):
28
+ """双模型初始化请求"""
29
+
30
+ device_id: str
31
+
32
+ # 决策大模型配置
33
+ decision_base_url: str = "https://api-inference.modelscope.cn/v1"
34
+ decision_api_key: str
35
+ decision_model_name: str = "ZhipuAI/GLM-4.7"
36
+
37
+ # 视觉小模型配置(复用现有配置)
38
+ vision_base_url: Optional[str] = None
39
+ vision_api_key: Optional[str] = None
40
+ vision_model_name: Optional[str] = None
41
+
42
+ # 思考模式: fast 或 deep
43
+ thinking_mode: str = "deep"
44
+
45
+ max_steps: int = 50
46
+
47
+
48
+ class DualModelChatRequest(BaseModel):
49
+ """双模型聊天请求"""
50
+
51
+ device_id: str
52
+ message: str
53
+
54
+
55
+ class DualModelAbortRequest(BaseModel):
56
+ """中止请求"""
57
+
58
+ device_id: str
59
+
60
+
61
+ class DualModelStatusResponse(BaseModel):
62
+ """状态响应"""
63
+
64
+ active: bool
65
+ device_id: Optional[str] = None
66
+ state: Optional[dict] = None
67
+
68
+
69
+ @router.post("/init")
70
+ def init_dual_model(request: DualModelInitRequest) -> dict:
71
+ """初始化双模型Agent"""
72
+ from AutoGLM_GUI.config import config
73
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
74
+
75
+ device_id = request.device_id
76
+ thinking_mode = (
77
+ ThinkingMode.FAST if request.thinking_mode == "fast" else ThinkingMode.DEEP
78
+ )
79
+ logger.info(f"初始化双模型Agent: {device_id}, 模式: {thinking_mode.value}")
80
+
81
+ # 检查设备是否已有单模型Agent初始化
82
+ manager = PhoneAgentManager.get_instance()
83
+ if not manager.is_initialized(device_id):
84
+ raise HTTPException(
85
+ status_code=400, detail="设备尚未初始化单模型Agent,请先调用 /api/init"
86
+ )
87
+
88
+ # 获取视觉模型配置
89
+ vision_base_url = request.vision_base_url or config.base_url
90
+ vision_api_key = request.vision_api_key or config.api_key
91
+ vision_model_name = request.vision_model_name or config.model_name
92
+
93
+ if not vision_base_url:
94
+ raise HTTPException(status_code=400, detail="视觉模型base_url未配置")
95
+
96
+ # 创建配置
97
+ decision_config = DecisionModelConfig(
98
+ base_url=request.decision_base_url,
99
+ api_key=request.decision_api_key,
100
+ model_name=request.decision_model_name,
101
+ thinking_mode=thinking_mode,
102
+ )
103
+
104
+ vision_config = ModelConfig(
105
+ base_url=vision_base_url,
106
+ api_key=vision_api_key,
107
+ model_name=vision_model_name,
108
+ )
109
+
110
+ # 创建双模型Agent
111
+ try:
112
+ agent = DualModelAgent(
113
+ decision_config=decision_config,
114
+ vision_config=vision_config,
115
+ device_id=device_id,
116
+ max_steps=request.max_steps,
117
+ thinking_mode=thinking_mode,
118
+ )
119
+
120
+ # 存储到活跃会话
121
+ with _active_dual_sessions_lock:
122
+ # 清理旧会话
123
+ if device_id in _active_dual_sessions:
124
+ old_agent, old_event = _active_dual_sessions[device_id]
125
+ old_event.set()
126
+
127
+ _active_dual_sessions[device_id] = (agent, threading.Event())
128
+
129
+ logger.info(f"双模型Agent初始化成功: {device_id}")
130
+
131
+ return {
132
+ "success": True,
133
+ "device_id": device_id,
134
+ "message": "双模型Agent初始化成功",
135
+ "decision_model": request.decision_model_name,
136
+ "vision_model": vision_model_name,
137
+ "thinking_mode": thinking_mode.value,
138
+ }
139
+
140
+ except Exception as e:
141
+ logger.error(f"双模型Agent初始化失败: {e}")
142
+ raise HTTPException(status_code=500, detail=str(e))
143
+
144
+
145
+ @router.post("/chat/stream")
146
+ def dual_model_chat_stream(request: DualModelChatRequest):
147
+ """双模型聊天(SSE流式)"""
148
+ device_id = request.device_id
149
+
150
+ with _active_dual_sessions_lock:
151
+ if device_id not in _active_dual_sessions:
152
+ raise HTTPException(
153
+ status_code=400, detail="双模型Agent未初始化,请先调用 /api/dual/init"
154
+ )
155
+ agent, stop_event = _active_dual_sessions[device_id]
156
+
157
+ # 重置停止事件
158
+ stop_event.clear()
159
+
160
+ def event_generator():
161
+ """SSE事件生成器"""
162
+ try:
163
+ logger.info(f"开始双模型任务: {request.message[:50]}...")
164
+
165
+ # 在后台线程运行Agent
166
+ result_holder: list[Any] = [None]
167
+ error_holder: list[Any] = [None]
168
+
169
+ def run_agent():
170
+ try:
171
+ result = agent.run(request.message)
172
+ result_holder[0] = result
173
+ except Exception as e:
174
+ error_holder[0] = e
175
+
176
+ thread = threading.Thread(target=run_agent, daemon=True)
177
+ thread.start()
178
+
179
+ # 持续发送事件
180
+ while thread.is_alive() or not agent.event_queue.empty():
181
+ if stop_event.is_set():
182
+ agent.abort()
183
+ yield "event: aborted\n"
184
+ yield 'data: {"type": "aborted", "message": "任务被用户中断"}\n\n'
185
+ break
186
+
187
+ # 获取事件
188
+ try:
189
+ events = agent.get_events(timeout=0.1)
190
+ for event in events:
191
+ yield event.to_sse()
192
+
193
+ # 如果是完成或错误事件,结束循环
194
+ if event.type in [
195
+ DualModelEventType.TASK_COMPLETE,
196
+ DualModelEventType.ERROR,
197
+ ]:
198
+ return
199
+ except Exception:
200
+ continue
201
+
202
+ # 等待线程完成
203
+ thread.join(timeout=5)
204
+
205
+ # 检查错误
206
+ if error_holder[0]:
207
+ error_event = DualModelEvent(
208
+ type=DualModelEventType.ERROR,
209
+ data={"message": str(error_holder[0])},
210
+ )
211
+ yield error_event.to_sse()
212
+
213
+ # 如果没有发送完成事件,发送一个
214
+ if result_holder[0] and not stop_event.is_set():
215
+ result = result_holder[0]
216
+ if isinstance(result, dict):
217
+ done_event = DualModelEvent(
218
+ type=DualModelEventType.TASK_COMPLETE,
219
+ data={
220
+ "success": result.get("success", False),
221
+ "message": result.get("message", ""),
222
+ "steps": result.get("steps", 0),
223
+ },
224
+ )
225
+ yield done_event.to_sse()
226
+
227
+ except Exception as e:
228
+ logger.exception(f"双模型任务异常: {e}")
229
+ error_event = DualModelEvent(
230
+ type=DualModelEventType.ERROR,
231
+ data={"message": str(e)},
232
+ )
233
+ yield error_event.to_sse()
234
+
235
+ return StreamingResponse(
236
+ event_generator(),
237
+ media_type="text/event-stream",
238
+ headers={
239
+ "Cache-Control": "no-cache",
240
+ "Connection": "keep-alive",
241
+ "X-Accel-Buffering": "no",
242
+ },
243
+ )
244
+
245
+
246
+ @router.post("/chat/abort")
247
+ def abort_dual_model_chat(request: DualModelAbortRequest) -> dict:
248
+ """中止双模型聊天"""
249
+ device_id = request.device_id
250
+
251
+ with _active_dual_sessions_lock:
252
+ if device_id in _active_dual_sessions:
253
+ agent, stop_event = _active_dual_sessions[device_id]
254
+ stop_event.set()
255
+ agent.abort()
256
+ logger.info(f"双模型任务已中止: {device_id}")
257
+ return {"success": True, "message": "已发送中止信号"}
258
+ else:
259
+ return {"success": False, "message": "未找到活跃的双模型会话"}
260
+
261
+
262
+ @router.get("/status")
263
+ def get_dual_model_status(device_id: Optional[str] = None) -> DualModelStatusResponse:
264
+ """获取双模型状态"""
265
+ with _active_dual_sessions_lock:
266
+ if device_id:
267
+ if device_id in _active_dual_sessions:
268
+ agent, _ = _active_dual_sessions[device_id]
269
+ return DualModelStatusResponse(
270
+ active=True,
271
+ device_id=device_id,
272
+ state=agent.get_state(),
273
+ )
274
+ else:
275
+ return DualModelStatusResponse(active=False, device_id=device_id)
276
+ else:
277
+ # 返回所有活跃会话
278
+ return DualModelStatusResponse(
279
+ active=len(_active_dual_sessions) > 0,
280
+ state={"active_devices": list(_active_dual_sessions.keys())},
281
+ )
282
+
283
+
284
+ @router.post("/reset")
285
+ def reset_dual_model(request: DualModelAbortRequest) -> dict:
286
+ """重置双模型Agent"""
287
+ device_id = request.device_id
288
+
289
+ with _active_dual_sessions_lock:
290
+ if device_id in _active_dual_sessions:
291
+ agent, stop_event = _active_dual_sessions[device_id]
292
+ stop_event.set()
293
+ agent.reset()
294
+ logger.info(f"双模型Agent已重置: {device_id}")
295
+ return {"success": True, "message": "双模型Agent已重置"}
296
+ else:
297
+ return {"success": False, "message": "未找到双模型会话"}
298
+
299
+
300
+ @router.delete("/session/{device_id}")
301
+ def delete_dual_model_session(device_id: str) -> dict:
302
+ """删除双模型会话"""
303
+ with _active_dual_sessions_lock:
304
+ if device_id in _active_dual_sessions:
305
+ agent, stop_event = _active_dual_sessions.pop(device_id)
306
+ stop_event.set()
307
+ logger.info(f"双模型会话已删除: {device_id}")
308
+ return {"success": True, "message": "双模型会话已删除"}
309
+ else:
310
+ return {"success": False, "message": "未找到双模型会话"}
AutoGLM_GUI/api/mcp.py ADDED
@@ -0,0 +1,134 @@
1
+ """MCP (Model Context Protocol) tools for AutoGLM-GUI."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from fastmcp import FastMCP
6
+
7
+ from AutoGLM_GUI.logger import logger
8
+ from AutoGLM_GUI.prompts import MCP_SYSTEM_PROMPT_ZH
9
+
10
+ # 创建 MCP 服务器实例
11
+ mcp = FastMCP("AutoGLM-GUI MCP Server")
12
+
13
+ # MCP-specific step limit
14
+ MCP_MAX_STEPS = 5
15
+
16
+
17
+ @mcp.tool()
18
+ def chat(device_id: str, message: str) -> Dict[str, Any]:
19
+ """
20
+ Send a task to the AutoGLM Phone Agent for execution.
21
+
22
+ The agent will be automatically initialized with global configuration
23
+ if not already initialized. MCP calls use a specialized Fail-Fast prompt
24
+ optimized for atomic operations within 5 steps.
25
+
26
+ Args:
27
+ device_id: Device identifier (e.g., "192.168.1.100:5555" or serial)
28
+ message: Natural language task (e.g., "打开微信", "发送消息")
29
+
30
+ Returns:
31
+ {
32
+ "result": str, # Task execution result
33
+ "steps": int, # Number of steps taken
34
+ "success": bool # Success flag
35
+ }
36
+ """
37
+ from AutoGLM_GUI.exceptions import DeviceBusyError
38
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
39
+
40
+ logger.info(f"[MCP] chat tool called: device_id={device_id}")
41
+
42
+ manager = PhoneAgentManager.get_instance()
43
+
44
+ # 使用上下文管理器获取 agent(自动管理锁,自动初始化)
45
+ try:
46
+ with manager.use_agent(device_id, timeout=None) as agent:
47
+ # Temporarily override config for MCP (thread-safe within device lock)
48
+ original_max_steps = agent.agent_config.max_steps
49
+ original_system_prompt = agent.agent_config.system_prompt
50
+
51
+ agent.agent_config.max_steps = MCP_MAX_STEPS
52
+ agent.agent_config.system_prompt = MCP_SYSTEM_PROMPT_ZH
53
+
54
+ try:
55
+ # Reset agent before each chat to ensure clean state
56
+ agent.reset()
57
+
58
+ result = agent.run(message)
59
+ steps = agent.step_count
60
+
61
+ # Check if MCP step limit was reached
62
+ if steps >= MCP_MAX_STEPS and result == "Max steps reached":
63
+ return {
64
+ "result": (
65
+ f"已达到 MCP 最大步数限制({MCP_MAX_STEPS}步)。任务可能未完成,"
66
+ "建议将任务拆分为更小的子任务。"
67
+ ),
68
+ "steps": MCP_MAX_STEPS,
69
+ "success": False,
70
+ }
71
+
72
+ return {"result": result, "steps": steps, "success": True}
73
+
74
+ finally:
75
+ # Restore original config
76
+ agent.agent_config.max_steps = original_max_steps
77
+ agent.agent_config.system_prompt = original_system_prompt
78
+
79
+ except DeviceBusyError:
80
+ raise RuntimeError(f"Device {device_id} is busy. Please wait.")
81
+ except Exception as e:
82
+ logger.error(f"[MCP] chat tool error: {e}")
83
+ return {"result": str(e), "steps": 0, "success": False}
84
+
85
+
86
+ @mcp.tool()
87
+ def list_devices() -> List[Dict[str, Any]]:
88
+ """
89
+ List all connected ADB devices and their agent status.
90
+
91
+ Returns:
92
+ List of devices, each containing:
93
+ - id: Device identifier for API calls
94
+ - serial: Hardware serial number
95
+ - model: Device model name
96
+ - status: Connection status
97
+ - connection_type: "usb" | "wifi" | "remote"
98
+ - state: "online" | "offline" | "disconnected"
99
+ - agent: Agent status (if initialized)
100
+ """
101
+ from AutoGLM_GUI.api.devices import _build_device_response_with_agent
102
+ from AutoGLM_GUI.device_manager import DeviceManager
103
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
104
+
105
+ logger.info("[MCP] list_devices tool called")
106
+
107
+ device_manager = DeviceManager.get_instance()
108
+ agent_manager = PhoneAgentManager.get_instance()
109
+
110
+ # Fallback: 如果轮询未启动,执行同步刷新
111
+ if not device_manager._poll_thread or not device_manager._poll_thread.is_alive():
112
+ logger.warning("Polling not started, performing sync refresh")
113
+ device_manager.force_refresh()
114
+
115
+ managed_devices = device_manager.get_devices()
116
+
117
+ # 重用现有的聚合逻辑
118
+ devices_with_agents = [
119
+ _build_device_response_with_agent(d, agent_manager) for d in managed_devices
120
+ ]
121
+
122
+ return devices_with_agents
123
+
124
+
125
+ def get_mcp_asgi_app():
126
+ """
127
+ Get the MCP server's ASGI app for mounting in FastAPI.
128
+
129
+ Returns:
130
+ ASGI app that handles MCP protocol requests
131
+ """
132
+ # 创建 MCP HTTP app with /mcp path prefix
133
+ # This will create routes under /mcp when mounted at root
134
+ return mcp.http_app(path="/mcp")
@@ -0,0 +1,36 @@
1
+ """Prometheus metrics endpoint."""
2
+
3
+ from fastapi import APIRouter, Response
4
+ from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
5
+
6
+ from AutoGLM_GUI.metrics import get_metrics_registry
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/api/metrics")
12
+ def metrics_endpoint() -> Response:
13
+ """
14
+ Prometheus metrics endpoint.
15
+
16
+ Returns metrics in Prometheus text exposition format.
17
+ This endpoint should be scraped by Prometheus at regular intervals.
18
+
19
+ Example Prometheus scrape config:
20
+ scrape_configs:
21
+ - job_name: 'autoglm-gui'
22
+ scrape_interval: 15s
23
+ static_configs:
24
+ - targets: ['localhost:8000']
25
+ metrics_path: '/api/metrics'
26
+
27
+ Returns:
28
+ Response: Prometheus-compatible text format
29
+ """
30
+ registry = get_metrics_registry()
31
+ data = generate_latest(registry)
32
+
33
+ return Response(
34
+ content=data,
35
+ media_type=CONTENT_TYPE_LATEST,
36
+ )
@@ -38,6 +38,13 @@ class ConfigSource(str, Enum):
38
38
  # ==================== 类型安全配置模型 ====================
39
39
 
40
40
 
41
+ class ThinkingMode(str, Enum):
42
+ """思考模式枚举."""
43
+
44
+ FAST = "fast" # 快速响应模式 - 减少思考时间
45
+ DEEP = "deep" # 深度思考模式 - 完整思考过程
46
+
47
+
41
48
  class ConfigModel(BaseModel):
42
49
  """类型安全的配置模型,使用 Pydantic 进行验证."""
43
50
 
@@ -45,6 +52,15 @@ class ConfigModel(BaseModel):
45
52
  model_name: str = "autoglm-phone-9b"
46
53
  api_key: str = "EMPTY"
47
54
 
55
+ # 双模型配置
56
+ dual_model_enabled: bool = False
57
+ decision_base_url: str = "https://api-inference.modelscope.cn/v1"
58
+ decision_model_name: str = "ZhipuAI/GLM-4.7"
59
+ decision_api_key: str = ""
60
+
61
+ # 思考模式配置
62
+ thinking_mode: str = "deep" # "fast" 或 "deep"
63
+
48
64
  @field_validator("base_url")
49
65
  @classmethod
50
66
  def validate_base_url(cls, v: str) -> str:
@@ -61,6 +77,22 @@ class ConfigModel(BaseModel):
61
77
  raise ValueError("model_name cannot be empty")
62
78
  return v.strip()
63
79
 
80
+ @field_validator("decision_base_url")
81
+ @classmethod
82
+ def validate_decision_base_url(cls, v: str) -> str:
83
+ """验证 decision_base_url 格式."""
84
+ if v and not v.startswith(("http://", "https://")):
85
+ raise ValueError("decision_base_url must start with http:// or https://")
86
+ return v.rstrip("/") # 去除尾部斜杠
87
+
88
+ @field_validator("thinking_mode")
89
+ @classmethod
90
+ def validate_thinking_mode(cls, v: str) -> str:
91
+ """验证思考模式."""
92
+ if v not in ("fast", "deep"):
93
+ raise ValueError("thinking_mode must be 'fast' or 'deep'")
94
+ return v
95
+
64
96
 
65
97
  # ==================== 配置层数据类 ====================
66
98
 
@@ -72,6 +104,14 @@ class ConfigLayer:
72
104
  base_url: Optional[str] = None
73
105
  model_name: Optional[str] = None
74
106
  api_key: Optional[str] = None
107
+ # 双模型配置
108
+ dual_model_enabled: Optional[bool] = None
109
+ decision_base_url: Optional[str] = None
110
+ decision_model_name: Optional[str] = None
111
+ decision_api_key: Optional[str] = None
112
+ # 思考模式配置
113
+ thinking_mode: Optional[str] = None
114
+
75
115
  source: ConfigSource = ConfigSource.DEFAULT
76
116
 
77
117
  def has_value(self, key: str) -> bool:
@@ -98,6 +138,11 @@ class ConfigLayer:
98
138
  "base_url": self.base_url,
99
139
  "model_name": self.model_name,
100
140
  "api_key": self.api_key,
141
+ "dual_model_enabled": self.dual_model_enabled,
142
+ "decision_base_url": self.decision_base_url,
143
+ "decision_model_name": self.decision_model_name,
144
+ "decision_api_key": self.decision_api_key,
145
+ "thinking_mode": self.thinking_mode,
101
146
  }.items()
102
147
  if v is not None
103
148
  }
@@ -265,6 +310,11 @@ class UnifiedConfigManager:
265
310
  base_url=config_data.get("base_url"),
266
311
  model_name=config_data.get("model_name"),
267
312
  api_key=config_data.get("api_key"),
313
+ dual_model_enabled=config_data.get("dual_model_enabled"),
314
+ decision_base_url=config_data.get("decision_base_url"),
315
+ decision_model_name=config_data.get("decision_model_name"),
316
+ decision_api_key=config_data.get("decision_api_key"),
317
+ thinking_mode=config_data.get("thinking_mode"),
268
318
  source=ConfigSource.FILE,
269
319
  )
270
320
  self._effective_config = None # 清除缓存
@@ -292,6 +342,11 @@ class UnifiedConfigManager:
292
342
  base_url: str,
293
343
  model_name: str,
294
344
  api_key: Optional[str] = None,
345
+ dual_model_enabled: Optional[bool] = None,
346
+ decision_base_url: Optional[str] = None,
347
+ decision_model_name: Optional[str] = None,
348
+ decision_api_key: Optional[str] = None,
349
+ thinking_mode: Optional[str] = None,
295
350
  merge_mode: bool = True,
296
351
  ) -> bool:
297
352
  """
@@ -301,6 +356,11 @@ class UnifiedConfigManager:
301
356
  base_url: Base URL
302
357
  model_name: 模型名称
303
358
  api_key: API key(可选)
359
+ dual_model_enabled: 是否启用双模型
360
+ decision_base_url: 决策模型 Base URL
361
+ decision_model_name: 决策模型名称
362
+ decision_api_key: 决策模型 API key
363
+ thinking_mode: 思考模式 (fast/deep)
304
364
  merge_mode: 是否合并现有配置(True: 保留未提供的字段)
305
365
 
306
366
  Returns:
@@ -318,6 +378,16 @@ class UnifiedConfigManager:
318
378
 
319
379
  if api_key:
320
380
  new_config["api_key"] = api_key
381
+ if dual_model_enabled is not None:
382
+ new_config["dual_model_enabled"] = dual_model_enabled
383
+ if decision_base_url:
384
+ new_config["decision_base_url"] = decision_base_url
385
+ if decision_model_name:
386
+ new_config["decision_model_name"] = decision_model_name
387
+ if decision_api_key:
388
+ new_config["decision_api_key"] = decision_api_key
389
+ if thinking_mode:
390
+ new_config["thinking_mode"] = thinking_mode
321
391
 
322
392
  # 合并模式:保留现有文件中未提供的字段
323
393
  if merge_mode and self._config_path.exists():
@@ -325,9 +395,18 @@ class UnifiedConfigManager:
325
395
  with open(self._config_path, "r", encoding="utf-8") as f:
326
396
  existing = json.load(f)
327
397
 
328
- # 保留 api_key(如果新配置未提供)
329
- if not api_key and "api_key" in existing:
330
- new_config["api_key"] = existing["api_key"]
398
+ # 保留未提供的字段
399
+ preserve_keys = [
400
+ "api_key",
401
+ "dual_model_enabled",
402
+ "decision_base_url",
403
+ "decision_model_name",
404
+ "decision_api_key",
405
+ "thinking_mode",
406
+ ]
407
+ for key in preserve_keys:
408
+ if key not in new_config and key in existing:
409
+ new_config[key] = existing[key]
331
410
 
332
411
  except (json.JSONDecodeError, Exception) as e:
333
412
  logger.warning(f"Could not merge with existing config: {e}")
@@ -387,6 +466,11 @@ class UnifiedConfigManager:
387
466
  Returns:
388
467
  ConfigModel: 验证后的配置对象
389
468
  """
469
+ # 首次加载:如果文件层为空且配置文件存在,自动加载
470
+ if not self._file_layer.to_dict() and self._config_path.exists():
471
+ logger.debug("Auto-loading config file on first access")
472
+ self.load_file_config()
473
+
390
474
  # 重新加载文件(热重载支持)
391
475
  if reload_file:
392
476
  self.load_file_config(force_reload=True)
@@ -398,7 +482,19 @@ class UnifiedConfigManager:
398
482
  # 按优先级合并配置
399
483
  merged = {}
400
484
 
401
- for key in ["base_url", "model_name", "api_key"]:
485
+ # 所有配置字段
486
+ config_keys = [
487
+ "base_url",
488
+ "model_name",
489
+ "api_key",
490
+ "dual_model_enabled",
491
+ "decision_base_url",
492
+ "decision_model_name",
493
+ "decision_api_key",
494
+ "thinking_mode",
495
+ ]
496
+
497
+ for key in config_keys:
402
498
  # 1. CLI 优先
403
499
  if self._cli_layer.has_value(key):
404
500
  merged[key] = getattr(self._cli_layer, key)
@@ -408,8 +504,11 @@ class UnifiedConfigManager:
408
504
  # 3. 配置文件
409
505
  elif self._file_layer.has_value(key):
410
506
  merged[key] = getattr(self._file_layer, key)
411
- # 4. 默认值
412
- else:
507
+ # 4. 默认值(只对 base_url, model_name, api_key 有效)
508
+ elif (
509
+ hasattr(self._default_layer, key)
510
+ and getattr(self._default_layer, key, None) is not None
511
+ ):
413
512
  merged[key] = getattr(self._default_layer, key)
414
513
 
415
514
  # 验证并缓存
@@ -555,6 +654,11 @@ class UnifiedConfigManager:
555
654
  "base_url": config.base_url,
556
655
  "model_name": config.model_name,
557
656
  "api_key": config.api_key,
657
+ "dual_model_enabled": config.dual_model_enabled,
658
+ "decision_base_url": config.decision_base_url,
659
+ "decision_model_name": config.decision_model_name,
660
+ "decision_api_key": config.decision_api_key,
661
+ "thinking_mode": config.thinking_mode,
558
662
  }
559
663
 
560
664