autoglm-gui 1.4.1__py3-none-any.whl → 1.5.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/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +43 -12
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +31 -38
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +12 -8
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +157 -240
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +67 -15
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/config.py +81 -0
- AutoGLM_GUI/config_manager.py +68 -51
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +1 -1
- AutoGLM_GUI/devices/adb_device.py +5 -10
- AutoGLM_GUI/devices/mock_device.py +4 -2
- AutoGLM_GUI/devices/remote_device.py +8 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +96 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +62 -396
- AutoGLM_GUI/platform_utils.py +26 -0
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +304 -0
- AutoGLM_GUI/schemas.py +234 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +17 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -147
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
- AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
- AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
- AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
- autoglm_gui-1.4.1.dist-info/RECORD +0 -117
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""MAI Agent 系统提示模板
|
|
2
|
+
|
|
3
|
+
基于 mai_agent/prompt.py 迁移,针对中文环境和国内应用优化。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
MAI_MOBILE_SYSTEM_PROMPT = """你是一个 GUI 自动化助手。你会收到一个任务和历史操作记录(包含多张截图),你需要分析当前屏幕状态,执行下一步操作来完成任务。
|
|
7
|
+
|
|
8
|
+
## 输出格式
|
|
9
|
+
每次操作必须包含两部分:
|
|
10
|
+
1. **思考过程**:在 <thinking></thinking> 标签中详细说明你的分析和决策
|
|
11
|
+
2. **动作指令**:在 <tool_call></tool_call> 标签中返回 JSON 格式的函数调用
|
|
12
|
+
|
|
13
|
+
示例:
|
|
14
|
+
```
|
|
15
|
+
<thinking>
|
|
16
|
+
当前屏幕显示美团首页。我需要点击顶部搜索框输入"霸王茶姬"。搜索框位于屏幕上方中央,坐标大约在 [500, 100]。
|
|
17
|
+
下一步操作:点击搜索框。
|
|
18
|
+
</thinking>
|
|
19
|
+
<tool_call>
|
|
20
|
+
{"name": "mobile_use", "arguments": {"action": "click", "coordinate": [500, 100]}}
|
|
21
|
+
</tool_call>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 动作空间(严格遵守)
|
|
25
|
+
|
|
26
|
+
### 基础操作
|
|
27
|
+
- **点击**:`{"action": "click", "coordinate": [x, y]}`
|
|
28
|
+
用于点击按钮、链接、输入框等可点击元素
|
|
29
|
+
|
|
30
|
+
- **长按**:`{"action": "long_press", "coordinate": [x, y]}`
|
|
31
|
+
用于触发长按菜单或特殊功能
|
|
32
|
+
|
|
33
|
+
- **输入文本**:`{"action": "type", "text": "要输入的文字"}`
|
|
34
|
+
必须先点击输入框聚焦,再使用此动作输入文本
|
|
35
|
+
注意:文本中的特殊字符需要转义(\\'、\\"、\\n)
|
|
36
|
+
|
|
37
|
+
### 滑动操作
|
|
38
|
+
- **滑动**:`{"action": "swipe", "direction": "up|down|left|right", "coordinate": [x, y]}`
|
|
39
|
+
direction 可选值:up(向上滑)、down(向下滑)、left(向左滑)、right(向右滑)
|
|
40
|
+
coordinate 可选:指定滑动起点坐标(用于滑动特定 UI 元素)
|
|
41
|
+
|
|
42
|
+
- **拖动**:`{"action": "drag", "start_coordinate": [x1, y1], "end_coordinate": [x2, y2]}`
|
|
43
|
+
用于拖拽元素到新位置
|
|
44
|
+
|
|
45
|
+
### 系统操作
|
|
46
|
+
- **打开应用**:`{"action": "open", "text": "应用名称"}`
|
|
47
|
+
推荐优先使用此方式打开应用(比手动点击更快)
|
|
48
|
+
|
|
49
|
+
- **系统按键**:`{"action": "system_button", "button": "back|home|menu|enter"}`
|
|
50
|
+
可选值:back(返回)、home(主页)、menu(菜单)、enter(确认)
|
|
51
|
+
|
|
52
|
+
### 任务控制
|
|
53
|
+
- **等待**:`{"action": "wait"}`
|
|
54
|
+
用于等待页面加载或动画完成(建议谨慎使用,大多数情况不需要)
|
|
55
|
+
|
|
56
|
+
- **结束任务**:`{"action": "terminate", "status": "success|fail"}`
|
|
57
|
+
任务完成或失败时必须调用此动作
|
|
58
|
+
|
|
59
|
+
- **回答问题**:`{"action": "answer", "text": "答案内容"}`
|
|
60
|
+
当用户要求你查找信息或回答问题时使用
|
|
61
|
+
|
|
62
|
+
## 坐标系统
|
|
63
|
+
- **范围**:x 和 y 都在 [0, 999] 之间
|
|
64
|
+
- **原点**:(0, 0) 是屏幕左上角
|
|
65
|
+
- **边界**:(999, 999) 是屏幕右下角
|
|
66
|
+
- **精度**:坐标是归一化的,会自动映射到实际屏幕分辨率
|
|
67
|
+
|
|
68
|
+
## 操作指南
|
|
69
|
+
|
|
70
|
+
### 思考过程建议
|
|
71
|
+
在 <thinking> 部分应包含:
|
|
72
|
+
1. **观察**:当前屏幕显示的内容和状态
|
|
73
|
+
2. **分析**:识别目标元素的位置和特征
|
|
74
|
+
3. **决策**:选择最合适的操作和参数
|
|
75
|
+
4. **总结**:用一句话明确说明下一步要做什么
|
|
76
|
+
|
|
77
|
+
### 常见应用操作技巧
|
|
78
|
+
**国内常用应用**:
|
|
79
|
+
- 外卖应用(美团、饿了么):优先使用顶部搜索框查找商家
|
|
80
|
+
- 打车应用(滴滴、高德):注意起点/终点输入框的位置区分
|
|
81
|
+
- 电商应用(淘宝、京东):搜索框通常在顶部,商品列表需要向下滑动浏览
|
|
82
|
+
- 社交应用(微信、QQ):注意顶部/底部导航栏的切换
|
|
83
|
+
|
|
84
|
+
**通用技巧**:
|
|
85
|
+
- 如果页面内容未完全显示,使用 swipe 滚动查看
|
|
86
|
+
- 输入文本前必须先 click 输入框获得焦点
|
|
87
|
+
- 遇到加载动画可以 wait 一次,但不要连续 wait
|
|
88
|
+
- 无法找到目标元素时,尝试返回上一级(system_button back)重新导航
|
|
89
|
+
|
|
90
|
+
### 常见错误避免
|
|
91
|
+
- ❌ 不要在未点击输入框的情况下直接 type
|
|
92
|
+
- ❌ 不要使用超出 [0, 999] 范围的坐标
|
|
93
|
+
- ❌ 不要遗漏 <thinking> 或 <tool_call> 标签
|
|
94
|
+
- ❌ 不要在 JSON 中使用注释或多余的字段
|
|
95
|
+
- ❌ 不要连续执行多个相同的无效操作
|
|
96
|
+
|
|
97
|
+
## 注意事项
|
|
98
|
+
- 必须严格遵循动作空间,所有动作参数必须符合上述格式
|
|
99
|
+
- 坐标必须是整数,范围在 [0, 999]
|
|
100
|
+
- 文本输入中的引号、换行等特殊字符必须转义
|
|
101
|
+
- 每次只返回一个动作,不要尝试批量操作
|
|
102
|
+
- 仔细观察截图中的 UI 元素位置,准确估算坐标
|
|
103
|
+
""".strip()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""轨迹记忆数据结构 - MAI Agent 内部实现
|
|
2
|
+
|
|
3
|
+
本模块定义了 MAI Agent 的轨迹记忆系统,用于存储和管理 Agent 执行过程中的历史信息。
|
|
4
|
+
|
|
5
|
+
设计说明:
|
|
6
|
+
- 从 mai_agent/unified_memory.py 迁移而来
|
|
7
|
+
- 适配 Python 3.10+ 类型注解
|
|
8
|
+
- 与 AutoGLM_GUI 架构集成
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TrajStep:
|
|
19
|
+
"""轨迹中的单个步骤
|
|
20
|
+
|
|
21
|
+
记录 Agent 在某一步的完整状态,包括观察、思考、动作和结果。
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
screenshot: 当前步骤的截图 (PIL Image 格式)
|
|
25
|
+
accessibility_tree: 可访问性树数据(可选,用于辅助 UI 理解)
|
|
26
|
+
prediction: 模型的原始响应文本(包含 <thinking> 和 <tool_call>)
|
|
27
|
+
action: 解析后的动作字典(如 {"action": "click", "coordinate": [0.5, 0.8]})
|
|
28
|
+
conclusion: 本步骤的结论或总结
|
|
29
|
+
thought: 模型的思考过程(从 <thinking> 标签中提取)
|
|
30
|
+
step_index: 步骤索引(从 0 开始)
|
|
31
|
+
agent_type: 生成此步骤的 Agent 类型(如 "InternalMAIAgent")
|
|
32
|
+
model_name: 使用的模型名称(如 "qwen2-vl-7b")
|
|
33
|
+
screenshot_bytes: 截图的字节数据(可选,用于序列化)
|
|
34
|
+
structured_action: 结构化的动作数据(可选,包含额外元数据)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
screenshot: Image.Image
|
|
38
|
+
accessibility_tree: dict[str, Any] | None
|
|
39
|
+
prediction: str
|
|
40
|
+
action: dict[str, Any]
|
|
41
|
+
conclusion: str
|
|
42
|
+
thought: str
|
|
43
|
+
step_index: int
|
|
44
|
+
agent_type: str
|
|
45
|
+
model_name: str
|
|
46
|
+
screenshot_bytes: bytes | None = None
|
|
47
|
+
structured_action: dict[str, Any] | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class TrajMemory:
|
|
52
|
+
"""完整任务的轨迹记忆容器
|
|
53
|
+
|
|
54
|
+
存储一个完整任务的所有步骤,提供历史查询和状态管理功能。
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
task_goal: 任务目标描述(用户的原始指令)
|
|
58
|
+
task_id: 任务唯一标识符
|
|
59
|
+
steps: 步骤列表(按执行顺序)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
task_goal: str
|
|
63
|
+
task_id: str
|
|
64
|
+
steps: list[TrajStep] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def add_step(self, step: TrajStep) -> None:
|
|
67
|
+
self.steps.append(step)
|
|
68
|
+
|
|
69
|
+
def get_history_images(self, n: int = -1) -> list[bytes]:
|
|
70
|
+
images = [step.screenshot_bytes for step in self.steps if step.screenshot_bytes]
|
|
71
|
+
if n > 0:
|
|
72
|
+
return images[-n:]
|
|
73
|
+
return images
|
|
74
|
+
|
|
75
|
+
def get_history_thoughts(self, n: int = -1) -> list[str]:
|
|
76
|
+
thoughts = [step.thought for step in self.steps if step.thought]
|
|
77
|
+
if n > 0:
|
|
78
|
+
return thoughts[-n:]
|
|
79
|
+
return thoughts
|
|
80
|
+
|
|
81
|
+
def get_history_actions(self, n: int = -1) -> list[dict[str, Any]]:
|
|
82
|
+
actions = [step.action for step in self.steps]
|
|
83
|
+
if n > 0:
|
|
84
|
+
return actions[-n:]
|
|
85
|
+
return actions
|
|
86
|
+
|
|
87
|
+
def clear(self) -> None:
|
|
88
|
+
self.steps.clear()
|
|
89
|
+
|
|
90
|
+
def __len__(self) -> int:
|
|
91
|
+
return len(self.steps)
|
AutoGLM_GUI/agents/protocols.py
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any, Protocol
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from phone_agent.agent import AgentConfig, StepResult
|
|
8
|
-
from phone_agent.model import ModelConfig
|
|
5
|
+
from AutoGLM_GUI.config import AgentConfig, ModelConfig, StepResult
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
class BaseAgent(Protocol):
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
model_config: ModelConfig
|
|
10
|
+
agent_config: AgentConfig
|
|
14
11
|
|
|
15
12
|
def run(self, task: str) -> str: ...
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
def step(self, task: str | None = None) -> StepResult: ...
|
|
15
|
+
|
|
17
16
|
def reset(self) -> None: ...
|
|
18
17
|
|
|
18
|
+
def abort(self) -> None: ...
|
|
19
|
+
|
|
19
20
|
@property
|
|
20
21
|
def step_count(self) -> int: ...
|
|
21
22
|
|
|
22
23
|
@property
|
|
23
24
|
def context(self) -> list[dict[str, Any]]: ...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_running(self) -> bool: ...
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import threading
|
|
3
|
+
import typing
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Callable, Iterator, Optional
|
|
6
|
+
|
|
7
|
+
from AutoGLM_GUI.agents.events import AgentEvent, AgentEventType
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from AutoGLM_GUI.agents.protocols import BaseAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentStepStreamer:
|
|
14
|
+
"""
|
|
15
|
+
流式 Agent 执行器(抽取可复用逻辑).
|
|
16
|
+
|
|
17
|
+
职责:
|
|
18
|
+
- 管理事件队列
|
|
19
|
+
- 协调 worker 线程
|
|
20
|
+
- 转换 StepResult 为事件
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
agent: "BaseAgent",
|
|
26
|
+
task: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._agent = agent
|
|
29
|
+
self._task = task
|
|
30
|
+
self._event_queue: queue.Queue[Optional[tuple[str, dict[str, Any]]]] = (
|
|
31
|
+
queue.Queue(maxsize=100)
|
|
32
|
+
)
|
|
33
|
+
self._stop_event = threading.Event()
|
|
34
|
+
self._worker_thread: Optional[threading.Thread] = None
|
|
35
|
+
|
|
36
|
+
def __iter__(self) -> Iterator[AgentEvent]:
|
|
37
|
+
"""返回迭代器."""
|
|
38
|
+
return self # type: ignore
|
|
39
|
+
|
|
40
|
+
def __next__(self) -> AgentEvent:
|
|
41
|
+
"""从队列获取下一个事件."""
|
|
42
|
+
try:
|
|
43
|
+
if self._worker_thread is None:
|
|
44
|
+
self._start_worker()
|
|
45
|
+
|
|
46
|
+
item = self._event_queue.get(timeout=0.1)
|
|
47
|
+
|
|
48
|
+
if item is None:
|
|
49
|
+
raise StopIteration
|
|
50
|
+
|
|
51
|
+
event_type, event_data = item
|
|
52
|
+
return AgentEvent(type=event_type, data=event_data)
|
|
53
|
+
|
|
54
|
+
except queue.Empty:
|
|
55
|
+
if self._worker_thread and self._worker_thread.is_alive():
|
|
56
|
+
return AgentEvent(
|
|
57
|
+
type=AgentEventType.STEP.value,
|
|
58
|
+
data={
|
|
59
|
+
"step": -1,
|
|
60
|
+
"thinking": "",
|
|
61
|
+
"action": None,
|
|
62
|
+
"success": True,
|
|
63
|
+
"finished": False,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
raise StopIteration
|
|
68
|
+
|
|
69
|
+
except StopIteration:
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self._stop_event.set()
|
|
74
|
+
return AgentEvent(type=AgentEventType.ERROR.value, data={"message": str(e)})
|
|
75
|
+
|
|
76
|
+
def _start_worker(self) -> None:
|
|
77
|
+
"""启动 worker 线程."""
|
|
78
|
+
|
|
79
|
+
def worker():
|
|
80
|
+
try:
|
|
81
|
+
# 检查停止事件
|
|
82
|
+
if self._stop_event.is_set():
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# 注入 thinking 回调
|
|
86
|
+
# 这是一个 hack,但为了实现 "Zero Agent Change" 目标
|
|
87
|
+
# 假设 agent 有 _thinking_callback 属性
|
|
88
|
+
original_callback = getattr(self._agent, "_thinking_callback", None)
|
|
89
|
+
|
|
90
|
+
def on_thinking(chunk: str):
|
|
91
|
+
self._event_queue.put(
|
|
92
|
+
(AgentEventType.THINKING.value, {"chunk": chunk})
|
|
93
|
+
)
|
|
94
|
+
if original_callback:
|
|
95
|
+
original_callback(chunk)
|
|
96
|
+
|
|
97
|
+
# Monkey-patch thinking callback
|
|
98
|
+
setattr(self._agent, "_thinking_callback", on_thinking)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# 执行 step 循环
|
|
102
|
+
while not self._stop_event.is_set():
|
|
103
|
+
is_first = self._agent.step_count == 0
|
|
104
|
+
result = self._agent.step(self._task if is_first else None)
|
|
105
|
+
|
|
106
|
+
# 发射 step 事件
|
|
107
|
+
self._event_queue.put(
|
|
108
|
+
(
|
|
109
|
+
AgentEventType.STEP.value,
|
|
110
|
+
{
|
|
111
|
+
"step": self._agent.step_count,
|
|
112
|
+
"thinking": result.thinking,
|
|
113
|
+
"action": result.action,
|
|
114
|
+
"success": result.success,
|
|
115
|
+
"finished": result.finished,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# 检查是否完成
|
|
121
|
+
if result.finished:
|
|
122
|
+
# 发射 done 事件
|
|
123
|
+
self._event_queue.put(
|
|
124
|
+
(
|
|
125
|
+
AgentEventType.DONE.value,
|
|
126
|
+
{
|
|
127
|
+
"message": result.message,
|
|
128
|
+
"steps": self._agent.step_count,
|
|
129
|
+
"success": result.success,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# 检查步数限制
|
|
136
|
+
if self._agent.step_count >= self._agent.agent_config.max_steps:
|
|
137
|
+
self._event_queue.put(
|
|
138
|
+
(
|
|
139
|
+
AgentEventType.DONE.value,
|
|
140
|
+
{
|
|
141
|
+
"message": "Max steps reached",
|
|
142
|
+
"steps": self._agent.step_count,
|
|
143
|
+
"success": result.success,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
break
|
|
148
|
+
finally:
|
|
149
|
+
# 恢复原始回调
|
|
150
|
+
setattr(self._agent, "_thinking_callback", original_callback)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
# 发射 error 事件
|
|
154
|
+
self._event_queue.put((AgentEventType.ERROR.value, {"message": str(e)}))
|
|
155
|
+
|
|
156
|
+
finally:
|
|
157
|
+
# 标记完成
|
|
158
|
+
self._event_queue.put(None)
|
|
159
|
+
|
|
160
|
+
self._worker_thread = threading.Thread(target=worker, daemon=True)
|
|
161
|
+
self._worker_thread.start()
|
|
162
|
+
|
|
163
|
+
@contextmanager
|
|
164
|
+
def stream_context(self) -> Iterator[Callable[[], None]]:
|
|
165
|
+
"""
|
|
166
|
+
Context manager,自动管理清理.
|
|
167
|
+
"""
|
|
168
|
+
self._stop_event.clear()
|
|
169
|
+
try:
|
|
170
|
+
yield self.abort
|
|
171
|
+
finally:
|
|
172
|
+
self._stop_event.set()
|
|
173
|
+
# 等待 worker 完成
|
|
174
|
+
if self._worker_thread and self._worker_thread.is_alive():
|
|
175
|
+
self._worker_thread.join(timeout=5.0)
|
|
176
|
+
|
|
177
|
+
# 清空队列
|
|
178
|
+
while not self._event_queue.empty():
|
|
179
|
+
try:
|
|
180
|
+
self._event_queue.get_nowait()
|
|
181
|
+
except queue.Empty:
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
def abort(self) -> None:
|
|
185
|
+
"""中止流式执行."""
|
|
186
|
+
self._stop_event.set()
|
|
187
|
+
if hasattr(self._agent, "abort"):
|
|
188
|
+
self._agent.abort()
|
AutoGLM_GUI/api/__init__.py
CHANGED
|
@@ -13,37 +13,24 @@ from fastapi.responses import FileResponse
|
|
|
13
13
|
from fastapi.staticfiles import StaticFiles
|
|
14
14
|
|
|
15
15
|
from AutoGLM_GUI.adb_plus.qr_pair import qr_pairing_manager
|
|
16
|
-
from AutoGLM_GUI.logger import logger
|
|
17
16
|
from AutoGLM_GUI.version import APP_VERSION
|
|
18
17
|
|
|
19
18
|
from . import (
|
|
20
19
|
agents,
|
|
21
20
|
control,
|
|
22
21
|
devices,
|
|
23
|
-
dual_model,
|
|
24
22
|
health,
|
|
23
|
+
history,
|
|
25
24
|
layered_agent,
|
|
26
25
|
mcp,
|
|
27
26
|
media,
|
|
28
27
|
metrics,
|
|
28
|
+
scheduled_tasks,
|
|
29
29
|
version,
|
|
30
30
|
workflows,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
# TODO:应该要支持运行时动态切换设备
|
|
35
|
-
def _maybe_inject_remote_device() -> None:
|
|
36
|
-
if remote_base_url := os.getenv("REMOTE_DEVICE_BASE_URL"):
|
|
37
|
-
from AutoGLM_GUI.device_adapter import inject_device_protocol
|
|
38
|
-
from AutoGLM_GUI.devices.remote_device import RemoteDevice
|
|
39
|
-
|
|
40
|
-
def get_remote_device(device_id: str | None):
|
|
41
|
-
return RemoteDevice(device_id or "mock_device_001", remote_base_url)
|
|
42
|
-
|
|
43
|
-
inject_device_protocol(get_remote_device)
|
|
44
|
-
logger.info(f"Remote device mode enabled: connecting to {remote_base_url}")
|
|
45
|
-
|
|
46
|
-
|
|
47
34
|
def _get_cors_origins() -> list[str]:
|
|
48
35
|
cors_origins_str = os.getenv("AUTOGLM_CORS_ORIGINS", "http://localhost:3000")
|
|
49
36
|
if cors_origins_str == "*":
|
|
@@ -58,7 +45,20 @@ def _get_static_dir() -> Path | None:
|
|
|
58
45
|
if bundled_static.exists():
|
|
59
46
|
return bundled_static
|
|
60
47
|
|
|
61
|
-
# Priority 2:
|
|
48
|
+
# Priority 2: Check filesystem directly (for Docker deployments)
|
|
49
|
+
# This handles the case where static files are copied to the package directory
|
|
50
|
+
# but not included in the Python package itself (e.g., Docker builds)
|
|
51
|
+
try:
|
|
52
|
+
from AutoGLM_GUI import __file__ as package_file
|
|
53
|
+
|
|
54
|
+
package_dir = Path(package_file).parent
|
|
55
|
+
filesystem_static = package_dir / "static"
|
|
56
|
+
if filesystem_static.exists() and filesystem_static.is_dir():
|
|
57
|
+
return filesystem_static
|
|
58
|
+
except (ImportError, AttributeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Priority 3: importlib.resources (for installed package)
|
|
62
62
|
try:
|
|
63
63
|
static_dir = files("AutoGLM_GUI").joinpath("static")
|
|
64
64
|
if hasattr(static_dir, "_path"):
|
|
@@ -77,9 +77,6 @@ def _get_static_dir() -> Path | None:
|
|
|
77
77
|
def create_app() -> FastAPI:
|
|
78
78
|
"""Build the FastAPI app with routers and static assets."""
|
|
79
79
|
|
|
80
|
-
# Inject RemoteDevice if REMOTE_DEVICE_BASE_URL is set
|
|
81
|
-
_maybe_inject_remote_device()
|
|
82
|
-
|
|
83
80
|
# Create MCP ASGI app
|
|
84
81
|
mcp_app = mcp.get_mcp_asgi_app()
|
|
85
82
|
|
|
@@ -91,15 +88,20 @@ def create_app() -> FastAPI:
|
|
|
91
88
|
asyncio.create_task(qr_pairing_manager.cleanup_expired_sessions())
|
|
92
89
|
|
|
93
90
|
from AutoGLM_GUI.device_manager import DeviceManager
|
|
91
|
+
from AutoGLM_GUI.scheduler_manager import scheduler_manager
|
|
94
92
|
|
|
95
93
|
device_manager = DeviceManager.get_instance()
|
|
96
94
|
device_manager.start_polling()
|
|
97
95
|
|
|
96
|
+
# Start scheduled task scheduler
|
|
97
|
+
scheduler_manager.start()
|
|
98
|
+
|
|
98
99
|
# Run MCP lifespan
|
|
99
100
|
async with mcp_app.lifespan(app):
|
|
100
101
|
yield
|
|
101
102
|
|
|
102
|
-
# App shutdown
|
|
103
|
+
# App shutdown
|
|
104
|
+
scheduler_manager.shutdown()
|
|
103
105
|
|
|
104
106
|
# Create FastAPI app with combined lifespan
|
|
105
107
|
app = FastAPI(
|
|
@@ -116,14 +118,15 @@ def create_app() -> FastAPI:
|
|
|
116
118
|
|
|
117
119
|
app.include_router(agents.router)
|
|
118
120
|
app.include_router(health.router)
|
|
121
|
+
app.include_router(history.router)
|
|
119
122
|
app.include_router(layered_agent.router)
|
|
120
123
|
app.include_router(devices.router)
|
|
121
124
|
app.include_router(control.router)
|
|
122
125
|
app.include_router(media.router)
|
|
123
126
|
app.include_router(metrics.router)
|
|
127
|
+
app.include_router(scheduled_tasks.router)
|
|
124
128
|
app.include_router(version.router)
|
|
125
129
|
app.include_router(workflows.router)
|
|
126
|
-
app.include_router(dual_model.router)
|
|
127
130
|
|
|
128
131
|
# Mount static files BEFORE MCP to ensure they have priority
|
|
129
132
|
# This is critical: FastAPI processes mounts in order, so static files
|
|
@@ -139,8 +142,23 @@ def create_app() -> FastAPI:
|
|
|
139
142
|
async def serve_spa(full_path: str) -> FileResponse:
|
|
140
143
|
file_path = static_dir / full_path
|
|
141
144
|
if file_path.is_file():
|
|
145
|
+
# Explicitly set media_type for common file types to avoid MIME detection issues
|
|
146
|
+
# This is critical for PyInstaller environments where mimetypes module may fail
|
|
147
|
+
media_type = None
|
|
148
|
+
suffix = file_path.suffix.lower()
|
|
149
|
+
if suffix == ".js":
|
|
150
|
+
media_type = "application/javascript"
|
|
151
|
+
elif suffix == ".css":
|
|
152
|
+
media_type = "text/css"
|
|
153
|
+
elif suffix == ".json":
|
|
154
|
+
media_type = "application/json"
|
|
155
|
+
elif suffix in (".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico"):
|
|
156
|
+
# Let FileResponse auto-detect image types (usually works)
|
|
157
|
+
media_type = None
|
|
158
|
+
|
|
142
159
|
return FileResponse(
|
|
143
160
|
file_path,
|
|
161
|
+
media_type=media_type,
|
|
144
162
|
headers={
|
|
145
163
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
146
164
|
"Pragma": "no-cache",
|
|
@@ -149,6 +167,7 @@ def create_app() -> FastAPI:
|
|
|
149
167
|
)
|
|
150
168
|
return FileResponse(
|
|
151
169
|
static_dir / "index.html",
|
|
170
|
+
media_type="text/html",
|
|
152
171
|
headers={
|
|
153
172
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
154
173
|
"Pragma": "no-cache",
|