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.
Files changed (104) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  15. AutoGLM_GUI/adb_plus/serial.py +38 -20
  16. AutoGLM_GUI/adb_plus/touch.py +4 -9
  17. AutoGLM_GUI/agents/__init__.py +43 -12
  18. AutoGLM_GUI/agents/events.py +19 -0
  19. AutoGLM_GUI/agents/factory.py +31 -38
  20. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  21. AutoGLM_GUI/agents/glm/agent.py +292 -0
  22. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  23. AutoGLM_GUI/agents/glm/parser.py +110 -0
  24. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  25. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  26. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  27. AutoGLM_GUI/agents/mai/agent.py +405 -0
  28. AutoGLM_GUI/agents/mai/parser.py +254 -0
  29. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  30. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  31. AutoGLM_GUI/agents/protocols.py +12 -8
  32. AutoGLM_GUI/agents/stream_runner.py +188 -0
  33. AutoGLM_GUI/api/__init__.py +40 -21
  34. AutoGLM_GUI/api/agents.py +157 -240
  35. AutoGLM_GUI/api/control.py +9 -6
  36. AutoGLM_GUI/api/devices.py +102 -12
  37. AutoGLM_GUI/api/history.py +78 -0
  38. AutoGLM_GUI/api/layered_agent.py +67 -15
  39. AutoGLM_GUI/api/media.py +64 -1
  40. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  41. AutoGLM_GUI/config.py +81 -0
  42. AutoGLM_GUI/config_manager.py +68 -51
  43. AutoGLM_GUI/device_manager.py +248 -29
  44. AutoGLM_GUI/device_protocol.py +1 -1
  45. AutoGLM_GUI/devices/adb_device.py +5 -10
  46. AutoGLM_GUI/devices/mock_device.py +4 -2
  47. AutoGLM_GUI/devices/remote_device.py +8 -3
  48. AutoGLM_GUI/history_manager.py +164 -0
  49. AutoGLM_GUI/i18n.py +81 -0
  50. AutoGLM_GUI/model/__init__.py +5 -0
  51. AutoGLM_GUI/model/message_builder.py +69 -0
  52. AutoGLM_GUI/model/types.py +24 -0
  53. AutoGLM_GUI/models/__init__.py +10 -0
  54. AutoGLM_GUI/models/history.py +96 -0
  55. AutoGLM_GUI/models/scheduled_task.py +71 -0
  56. AutoGLM_GUI/parsers/__init__.py +22 -0
  57. AutoGLM_GUI/parsers/base.py +50 -0
  58. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  59. AutoGLM_GUI/phone_agent_manager.py +62 -396
  60. AutoGLM_GUI/platform_utils.py +26 -0
  61. AutoGLM_GUI/prompt_config.py +15 -0
  62. AutoGLM_GUI/prompts/__init__.py +32 -0
  63. AutoGLM_GUI/scheduler_manager.py +304 -0
  64. AutoGLM_GUI/schemas.py +234 -72
  65. AutoGLM_GUI/scrcpy_stream.py +142 -24
  66. AutoGLM_GUI/socketio_server.py +100 -27
  67. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
  68. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  69. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  70. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  71. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  72. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  73. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  74. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
  75. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  76. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  77. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  78. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  79. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  80. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  81. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  82. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  83. AutoGLM_GUI/static/index.html +2 -2
  84. AutoGLM_GUI/types.py +17 -0
  85. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
  86. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  87. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  88. AutoGLM_GUI/api/dual_model.py +0 -317
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  103. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  104. {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)
@@ -1,23 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Protocol
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
- agent_config: "AgentConfig"
13
- model_config: "ModelConfig"
9
+ model_config: ModelConfig
10
+ agent_config: AgentConfig
14
11
 
15
12
  def run(self, task: str) -> str: ...
16
- def step(self, task: str | None = None) -> "StepResult": ...
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()
@@ -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: importlib.resources (for installed package)
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 (if needed in the future)
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",