autoglm-gui 1.2.0__py3-none-any.whl → 1.3.0__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.
- AutoGLM_GUI/adb_plus/__init__.py +6 -6
- AutoGLM_GUI/api/__init__.py +49 -15
- AutoGLM_GUI/api/agents.py +163 -209
- AutoGLM_GUI/api/dual_model.py +310 -0
- AutoGLM_GUI/api/mcp.py +134 -0
- AutoGLM_GUI/api/metrics.py +36 -0
- AutoGLM_GUI/config_manager.py +110 -6
- AutoGLM_GUI/dual_model/__init__.py +53 -0
- AutoGLM_GUI/dual_model/decision_model.py +664 -0
- AutoGLM_GUI/dual_model/dual_agent.py +917 -0
- AutoGLM_GUI/dual_model/protocols.py +354 -0
- AutoGLM_GUI/dual_model/vision_model.py +442 -0
- AutoGLM_GUI/exceptions.py +75 -3
- AutoGLM_GUI/metrics.py +283 -0
- AutoGLM_GUI/phone_agent_manager.py +264 -14
- AutoGLM_GUI/prompts.py +97 -0
- AutoGLM_GUI/schemas.py +40 -9
- AutoGLM_GUI/static/assets/{about-PcGX7dIG.js → about-CrBXGOgB.js} +1 -1
- AutoGLM_GUI/static/assets/chat-Di2fwu8V.js +124 -0
- AutoGLM_GUI/static/assets/dialog-CHJSPLHJ.js +45 -0
- AutoGLM_GUI/static/assets/{index-DOt5XNhh.js → index-9IaIXvyy.js} +1 -1
- AutoGLM_GUI/static/assets/index-Dt7cVkfR.js +12 -0
- AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +1 -0
- AutoGLM_GUI/static/assets/{workflows-B1hgBC_O.js → workflows-DHadKApI.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/METADATA +11 -4
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/RECORD +30 -20
- AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +0 -124
- AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +0 -45
- AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
- AutoGLM_GUI/static/assets/index-CnEYDOXp.js +0 -11
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.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
|
+
)
|
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -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
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|