myagent-ai 1.2.2 → 1.3.1

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.
@@ -428,6 +428,13 @@ class MainAgent(BaseAgent):
428
428
  # 浏览器技能需要网络
429
429
  if skill_name in ("browser_open", "browser_click", "browser_fill"):
430
430
  return "network"
431
+ # 浏览器扩展技能 - 截图/JS执行/导航/关闭
432
+ if skill_name in ("browser_screenshot", "browser_eval", "browser_navigate", "browser_close"):
433
+ return "network"
434
+ # 桌面 GUI 自动化技能 - 涉及系统级执行权限
435
+ if skill_name in ("screenshot", "mouse_click", "mouse_drag", "type_text",
436
+ "hotkey", "window_list", "window_focus", "screen_element"):
437
+ return "execution"
431
438
  # 其他技能不需要特殊权限检查
432
439
  return None
433
440
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,473 @@
1
+ """
2
+ core/deps_checker.py - 自动依赖检测与安装
3
+ ==========================================
4
+ 开箱即用:启动时自动检测缺失依赖并安装,确保所有技能可直接使用。
5
+
6
+ 设计原则:
7
+ - 轻量化:仅检测实际需要的模块,不做全量 pip check
8
+ - 高效率:使用 importlib.util.find_spec 快速检测,0网络开销
9
+ - 自动修复:缺失时自动 pip install,无需用户干预
10
+ - 幂等安全:已安装的模块直接跳过,重复调用无副作用
11
+ - 平台适配:自动区分 Windows/macOS/Linux,跳过不适用的依赖
12
+
13
+ 依赖映射:
14
+ 核心功能: openai, aiohttp, requests
15
+ 搜索技能: duckduckgo_search, bs4, lxml
16
+ 系统技能: psutil
17
+ 托盘功能: pystray, PIL
18
+ 语音合成: edge_tts
19
+ 浏览器自动化: playwright (+ chromium 浏览器二进制)
20
+ 桌面GUI自动化: mss, pynput, pygetwindow
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import importlib.util
25
+ import subprocess
26
+ import sys
27
+ import threading
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Dict, List, Optional, Set, Tuple
31
+
32
+ from core.logger import get_logger
33
+
34
+ logger = get_logger("myagent.deps")
35
+
36
+ # 平台检测
37
+ IS_MACOS = sys.platform == "darwin"
38
+ IS_WINDOWS = sys.platform == "win32"
39
+ IS_LINUX = sys.platform.startswith("linux")
40
+
41
+
42
+ # ── 依赖定义 ──────────────────────────────────────────────
43
+
44
+ @dataclass
45
+ class DepInfo:
46
+ """单个依赖的信息"""
47
+ import_name: str # Python import 名
48
+ pip_name: str # pip install 名
49
+ min_version: str = "" # 最低版本(可选)
50
+ category: str = "core" # 分类:core/browser/gui/tts/tray/optional
51
+ platform: str = "all" # 适用平台:all/windows/macos/linux
52
+ note: str = "" # 安装说明(失败时展示给用户)
53
+
54
+
55
+ # 所有需要自动管理的依赖
56
+ DEPENDENCIES: List[DepInfo] = [
57
+ # ── 核心依赖 ──
58
+ DepInfo("openai", "openai", "1.12.0", "core", "all"),
59
+ DepInfo("aiohttp", "aiohttp", "3.9.0", "core", "all"),
60
+ DepInfo("requests", "requests", "2.31.0", "core", "all"),
61
+
62
+ # ── 搜索技能 ──
63
+ DepInfo("duckduckgo_search", "duckduckgo-search", "6.0.0", "search", "all"),
64
+ DepInfo("bs4", "beautifulsoup4", "4.12.0", "search", "all"),
65
+ DepInfo("lxml", "lxml", "5.0.0", "search", "all"),
66
+
67
+ # ── 系统技能 ──
68
+ DepInfo("psutil", "psutil", "5.9.0", "system", "all"),
69
+
70
+ # ── 系统托盘 ──
71
+ DepInfo("pystray", "pystray", "0.19.5", "tray", "all"),
72
+ DepInfo("PIL", "Pillow", "10.0.0", "tray", "all"),
73
+
74
+ # ── 语音合成 ──
75
+ DepInfo("edge_tts", "edge-tts", "6.1.0", "tts", "all"),
76
+
77
+ # ── 浏览器自动化 (Playwright) ──
78
+ DepInfo("playwright", "playwright", "1.41.0", "browser", "all",
79
+ note="安装后还需运行: playwright install chromium"),
80
+
81
+ # ── 桌面 GUI 自动化 ──
82
+ DepInfo("mss", "mss", "9.0.0", "gui", "all",
83
+ note="跨平台屏幕截图库 (~1MB)"),
84
+ DepInfo("pynput", "pynput", "1.7.6", "gui", "all",
85
+ note="鼠标/键盘控制 (~500KB)"),
86
+ DepInfo("pygetwindow", "pygetwindow", "0.0.9", "gui", "windows_macos",
87
+ note="窗口管理 (仅 Windows/macOS)"),
88
+ ]
89
+
90
+
91
+ # ── 检测与安装 ────────────────────────────────────────────
92
+
93
+ def _is_platform_match(dep: DepInfo) -> bool:
94
+ """检查依赖是否适用于当前平台"""
95
+ if dep.platform == "all":
96
+ return True
97
+ if dep.platform == "windows" and IS_WINDOWS:
98
+ return True
99
+ if dep.platform == "macos" and IS_MACOS:
100
+ return True
101
+ if dep.platform == "linux" and IS_LINUX:
102
+ return True
103
+ if dep.platform == "windows_macos" and (IS_WINDOWS or IS_MACOS):
104
+ return True
105
+ return False
106
+
107
+
108
+ def _is_module_available(import_name: str) -> bool:
109
+ """快速检测 Python 模块是否可导入(不实际导入,无副作用)"""
110
+ spec = importlib.util.find_spec(import_name)
111
+ return spec is not None
112
+
113
+
114
+ def _get_python_executable() -> str:
115
+ """获取当前 Python 解释器路径"""
116
+ return sys.executable
117
+
118
+
119
+ def _pip_install(pip_names: List[str], category: str = "") -> Tuple[bool, str]:
120
+ """
121
+ 使用当前 Python 解释器执行 pip install。
122
+
123
+ Returns:
124
+ (success, message)
125
+ """
126
+ python = _get_python_executable()
127
+ packages = " ".join(pip_names)
128
+
129
+ # 构建安装命令
130
+ cmd = [python, "-m", "pip", "install", "--quiet"] + pip_names
131
+
132
+ # 处理 PEP668 (externally-managed-environment)
133
+ # 尝试: 1) 直接安装 2) --break-system-packages 3) 用户空间 --user
134
+ install_attempts = [
135
+ [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check"] + pip_names,
136
+ [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
137
+ "--break-system-packages"] + pip_names,
138
+ [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
139
+ "--user"] + pip_names,
140
+ ]
141
+
142
+ last_error = ""
143
+ for attempt_cmd in install_attempts:
144
+ try:
145
+ result = subprocess.run(
146
+ attempt_cmd,
147
+ capture_output=True,
148
+ text=True,
149
+ timeout=120, # 2分钟超时
150
+ )
151
+ if result.returncode == 0:
152
+ return True, f"已安装: {packages}"
153
+ last_error = result.stderr.strip()
154
+ # 如果已经安装(满足要求),也算成功
155
+ if "Requirement already satisfied" in result.stdout or \
156
+ "Requirement already satisfied" in result.stderr:
157
+ return True, f"已就绪: {packages}"
158
+ except subprocess.TimeoutExpired:
159
+ last_error = "安装超时(120秒)"
160
+ continue
161
+ except Exception as e:
162
+ last_error = str(e)
163
+ continue
164
+
165
+ return False, f"安装失败: {packages} - {last_error}"
166
+
167
+
168
+ def _install_playwright_browsers() -> Tuple[bool, str]:
169
+ """
170
+ 安装 Playwright 浏览器二进制文件(Chromium)。
171
+ 这是一个独立的步骤,因为 pip install playwright 只安装 Python 包,
172
+ 浏览器二进制需要单独下载。
173
+ """
174
+ python = _get_python_executable()
175
+ try:
176
+ result = subprocess.run(
177
+ [python, "-m", "playwright", "install", "chromium"],
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=300, # 5分钟超时(浏览器较大)
181
+ )
182
+ if result.returncode == 0:
183
+ return True, "Chromium 浏览器已安装"
184
+ # 检查是否已经安装
185
+ if "Chromium" in result.stdout and "already" in result.stdout.lower():
186
+ return True, "Chromium 浏览器已就绪"
187
+ return False, f"Chromium 安装失败: {result.stderr[:200]}"
188
+ except subprocess.TimeoutExpired:
189
+ return False, "Chromium 安装超时(5分钟),请手动运行: playwright install chromium"
190
+ except Exception as e:
191
+ return False, f"Chromium 安装异常: {e}"
192
+
193
+
194
+ def _check_version(import_name: str, min_version: str) -> bool:
195
+ """检查模块版本是否满足最低要求"""
196
+ if not min_version:
197
+ return True
198
+ try:
199
+ spec = importlib.util.find_spec(import_name)
200
+ if spec is None:
201
+ return False
202
+ mod = importlib.import_module(import_name)
203
+ ver = getattr(mod, "__version__", "0.0.0")
204
+ # 简单版本比较
205
+ from packaging.version import parse as parse_ver
206
+ return parse_ver(ver) >= parse_ver(min_version)
207
+ except Exception:
208
+ return True # 无法获取版本时保守通过
209
+
210
+
211
+ # ── 主流程 ────────────────────────────────────────────────
212
+
213
+ def check_and_install_deps(
214
+ categories: Optional[Set[str]] = None,
215
+ auto_fix: bool = True,
216
+ silent: bool = False,
217
+ ) -> Dict[str, any]:
218
+ """
219
+ 检测依赖并在缺失时自动安装。
220
+
221
+ Args:
222
+ categories: 要检查的依赖分类集合。
223
+ None = 检查所有分类。
224
+ 例如: {"browser", "gui"} 只检查浏览器和GUI依赖。
225
+ auto_fix: 是否自动安装缺失的依赖(默认 True)。
226
+ silent: 是否静默模式,不打印日志(默认 False)。
227
+
228
+ Returns:
229
+ {
230
+ "checked": int, # 检查的依赖数量
231
+ "available": int, # 已安装的数量
232
+ "missing": int, # 缺失的数量
233
+ "installed": int, # 本次新安装的数量
234
+ "failed": int, # 安装失败的数量
235
+ "skipped_platform": int, # 因平台不匹配而跳过的数量
236
+ "details": {...}, # 每个依赖的状态
237
+ "playwright_browser": str, # Chromium 状态
238
+ }
239
+ """
240
+ stats = {
241
+ "checked": 0,
242
+ "available": 0,
243
+ "missing": 0,
244
+ "installed": 0,
245
+ "failed": 0,
246
+ "skipped_platform": 0,
247
+ "details": {},
248
+ "playwright_browser": "not_checked",
249
+ }
250
+
251
+ # 按分类收集缺失的依赖,批量安装以减少 pip 调用次数
252
+ missing_by_category: Dict[str, List[str]] = {} # category -> [pip_name, ...]
253
+ dep_pip_map: Dict[str, DepInfo] = {} # pip_name -> DepInfo (用于记录安装状态)
254
+
255
+ # 第一遍:检测所有依赖
256
+ for dep in DEPENDENCIES:
257
+ # 平台过滤
258
+ if not _is_platform_match(dep):
259
+ stats["skipped_platform"] += 1
260
+ stats["details"][dep.import_name] = {
261
+ "status": "skipped",
262
+ "reason": f"平台不匹配 (需要: {dep.platform}, 当前: {sys.platform})",
263
+ }
264
+ continue
265
+
266
+ # 分类过滤
267
+ if categories and dep.category not in categories:
268
+ continue
269
+
270
+ stats["checked"] += 1
271
+
272
+ # 检测模块是否可用
273
+ if _is_module_available(dep.import_name):
274
+ stats["available"] += 1
275
+ stats["details"][dep.import_name] = {"status": "available"}
276
+ if not silent:
277
+ logger.debug(f"依赖已就绪: {dep.import_name}")
278
+ continue
279
+
280
+ # 缺失
281
+ stats["missing"] += 1
282
+ stats["details"][dep.import_name] = {
283
+ "status": "missing",
284
+ "pip_name": dep.pip_name,
285
+ "category": dep.category,
286
+ }
287
+
288
+ if dep.pip_name not in dep_pip_map:
289
+ dep_pip_map[dep.pip_name] = dep
290
+ missing_by_category.setdefault(dep.category, []).append(dep.pip_name)
291
+
292
+ if not silent:
293
+ logger.info(f"依赖缺失: {dep.import_name} (pip: {dep.pip_name}, 分类: {dep.category})")
294
+
295
+ if not auto_fix or stats["missing"] == 0:
296
+ return stats
297
+
298
+ # 第二遍:批量安装缺失的依赖(按分类分批)
299
+ if not silent:
300
+ logger.info(f"开始自动安装 {stats['missing']} 个缺失依赖...")
301
+
302
+ total_installed = 0
303
+ total_failed = 0
304
+
305
+ for category, pip_names in missing_by_category.items():
306
+ success, message = _pip_install(pip_names, category)
307
+
308
+ # 更新所有相关依赖的状态
309
+ for pip_name in pip_names:
310
+ dep = dep_pip_map[pip_name]
311
+ if success:
312
+ stats["installed"] += 1
313
+ stats["details"][dep.import_name]["status"] = "installed"
314
+ total_installed += 1
315
+ if not silent:
316
+ logger.info(f" ✓ 已安装: {dep.import_name} ({dep.category})")
317
+ else:
318
+ stats["failed"] += 1
319
+ stats["details"][dep.import_name]["status"] = "install_failed"
320
+ stats["details"][dep.import_name]["error"] = message
321
+ total_failed += 1
322
+ if not silent:
323
+ logger.warning(f" ✗ 安装失败: {dep.import_name} - {message}")
324
+
325
+ # 第三遍:如果 playwright 安装成功,还需要安装 Chromium 浏览器二进制
326
+ playwright_dep = next((d for d in DEPENDENCIES if d.import_name == "playwright"), None)
327
+ if playwright_dep and stats["details"].get("playwright", {}).get("status") == "installed":
328
+ if not silent:
329
+ logger.info("正在安装 Chromium 浏览器二进制...")
330
+ success, message = _install_playwright_browsers()
331
+ stats["playwright_browser"] = "installed" if success else "failed"
332
+ if not silent:
333
+ if success:
334
+ logger.info(f" ✓ {message}")
335
+ else:
336
+ logger.warning(f" ✗ {message}")
337
+ elif playwright_dep and stats["details"].get("playwright", {}).get("status") == "available":
338
+ # playwright 已安装,检查 chromium 是否已安装
339
+ try:
340
+ result = subprocess.run(
341
+ [_get_python_executable(), "-m", "playwright", "install", "--dry-run", "chromium"],
342
+ capture_output=True, text=True, timeout=10,
343
+ )
344
+ if result.returncode == 0:
345
+ stats["playwright_browser"] = "ready"
346
+ else:
347
+ # 尝试安装
348
+ if not silent:
349
+ logger.info("正在安装 Chromium 浏览器二进制...")
350
+ success, message = _install_playwright_browsers()
351
+ stats["playwright_browser"] = "installed" if success else "failed"
352
+ except Exception:
353
+ stats["playwright_browser"] = "unknown"
354
+
355
+ # 汇总日志
356
+ if not silent and (total_installed > 0 or total_failed > 0):
357
+ logger.info(
358
+ f"依赖检查完成: {stats['available']}/{stats['checked']} 已就绪, "
359
+ f"{total_installed} 新安装, {total_failed} 失败"
360
+ )
361
+
362
+ return stats
363
+
364
+
365
+ def ensure_skill_deps(skill_category: str) -> bool:
366
+ """
367
+ 确保指定技能分类的依赖已安装。
368
+ 在技能执行前调用,提供懒加载安装能力。
369
+
370
+ Args:
371
+ skill_category: 技能分类名称
372
+ "browser" - 浏览器自动化 (playwright)
373
+ "gui" - 桌面GUI自动化 (mss, pynput, pygetwindow)
374
+ "search" - 搜索技能
375
+ "tts" - 语音合成
376
+
377
+ Returns:
378
+ True 如果所有依赖已就绪(包括刚安装的)
379
+ """
380
+ category_map = {
381
+ "browser": {"browser"},
382
+ "gui": {"gui"},
383
+ "search": {"search"},
384
+ "tts": {"tts"},
385
+ "tray": {"tray"},
386
+ }
387
+
388
+ cats = category_map.get(skill_category, {skill_category})
389
+ result = check_and_install_deps(categories=cats, auto_fix=True, silent=True)
390
+
391
+ return result["failed"] == 0 and result["missing"] == result["installed"]
392
+
393
+
394
+ def get_deps_status() -> Dict[str, Dict]:
395
+ """
396
+ 获取所有依赖的当前状态(不自动安装)。
397
+ 供 API 和 UI 展示使用。
398
+
399
+ Returns:
400
+ {import_name: {"status": "available"|"missing", "version": "...", "category": "..."}}
401
+ """
402
+ status = {}
403
+ for dep in DEPENDENCIES:
404
+ if not _is_platform_match(dep):
405
+ continue
406
+
407
+ available = _is_module_available(dep.import_name)
408
+ version = ""
409
+ if available:
410
+ try:
411
+ mod = importlib.import_module(dep.import_name)
412
+ version = getattr(mod, "__version__", "")
413
+ except Exception:
414
+ pass
415
+
416
+ status[dep.import_name] = {
417
+ "status": "available" if available else "missing",
418
+ "version": version,
419
+ "category": dep.category,
420
+ "pip_name": dep.pip_name,
421
+ }
422
+
423
+ return status
424
+
425
+
426
+ # ── 后台预安装(非阻塞) ──────────────────────────────────
427
+
428
+ _background_check_done = False
429
+ _background_check_running = False
430
+
431
+
432
+ def start_background_check():
433
+ """
434
+ 在后台线程中执行依赖检查和安装(非阻塞)。
435
+ 适用于启动时不想等待安装完成的场景。
436
+ """
437
+ global _background_check_done, _background_check_running
438
+
439
+ if _background_check_done or _background_check_running:
440
+ return
441
+
442
+ _background_check_running = True
443
+
444
+ def _do_check():
445
+ global _background_check_done, _background_check_running
446
+ try:
447
+ check_and_install_deps(auto_fix=True, silent=True)
448
+ except Exception as e:
449
+ logger.warning(f"后台依赖检查失败: {e}")
450
+ finally:
451
+ _background_check_done = True
452
+ _background_check_running = False
453
+
454
+ t = threading.Thread(target=_do_check, daemon=True, name="deps-checker")
455
+ t.start()
456
+ logger.info("后台依赖检查已启动")
457
+
458
+
459
+ def wait_background_check(timeout: float = 30.0) -> bool:
460
+ """
461
+ 等待后台依赖检查完成。
462
+
463
+ Args:
464
+ timeout: 最长等待时间(秒)
465
+
466
+ Returns:
467
+ True 如果检查已完成,False 如果超时
468
+ """
469
+ import time
470
+ deadline = time.monotonic() + timeout
471
+ while _background_check_running and time.monotonic() < deadline:
472
+ time.sleep(0.5)
473
+ return _background_check_done
File without changes
File without changes
File without changes
package/core/version.py CHANGED
@@ -11,7 +11,7 @@ import subprocess
11
11
  from pathlib import Path
12
12
 
13
13
  # ── 基线版本(与 setup.py / package.json 保持一致) ──
14
- BASE_VERSION = "1.2.2"
14
+ BASE_VERSION = "1.3.1"
15
15
 
16
16
 
17
17
  def _version_from_git() -> str:
File without changes
File without changes
File without changes
package/groups/manager.py CHANGED
File without changes