myagent-ai 1.30.0 → 1.31.0
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.
- package/aiskills/browser_stealth.py +261 -58
- package/aiskills/registry.py +3 -0
- package/aiskills/site-operations/SKILL.md +150 -0
- package/core/browser_profile.py +58 -55
- package/core/site_registry.py +773 -0
- package/main.py +28 -20
- package/package.json +1 -1
- package/scripts/cli.py +181 -3
- package/worklog.md +25 -0
|
@@ -33,6 +33,7 @@ import asyncio
|
|
|
33
33
|
import json
|
|
34
34
|
import os
|
|
35
35
|
import shutil
|
|
36
|
+
import threading
|
|
36
37
|
import time
|
|
37
38
|
from pathlib import Path
|
|
38
39
|
from typing import Any, Dict, List, Optional
|
|
@@ -89,7 +90,9 @@ class StealthBrowser:
|
|
|
89
90
|
mgr = get_browser_profile_manager()
|
|
90
91
|
profile = mgr.get_profile(self.profile_name)
|
|
91
92
|
profile.ensure_dirs()
|
|
92
|
-
|
|
93
|
+
# DrissionPage 的 set_user_data_path 会自动在路径下创建 Default 目录
|
|
94
|
+
# 我们直接用 profile_dir 作为 user-data-dir,避免嵌套
|
|
95
|
+
self._user_data_dir = str(profile.profile_dir)
|
|
93
96
|
return self._user_data_dir
|
|
94
97
|
|
|
95
98
|
async def start(self) -> SkillResult:
|
|
@@ -107,8 +110,9 @@ class StealthBrowser:
|
|
|
107
110
|
co = ChromiumOptions()
|
|
108
111
|
|
|
109
112
|
# 设置用户数据目录(Profile 持久化)
|
|
113
|
+
# 注意:只用 set_argument,不用 set_user_data_path,因为后者会
|
|
114
|
+
# 在路径下再套一层 Default/ 子目录,导致路径混乱
|
|
110
115
|
if user_data:
|
|
111
|
-
co.set_user_data_path(user_data)
|
|
112
116
|
co.set_argument(f"--user-data-dir={user_data}")
|
|
113
117
|
|
|
114
118
|
# 反检测核心参数
|
|
@@ -120,20 +124,16 @@ class StealthBrowser:
|
|
|
120
124
|
co.set_argument("--disable-infobars")
|
|
121
125
|
co.set_argument("--disable-extensions")
|
|
122
126
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
# 容器环境(无论有无 DISPLAY 都加上,避免切换环境后出错)
|
|
128
|
+
co.set_argument("--no-sandbox")
|
|
129
|
+
co.set_argument("--disable-setuid-sandbox")
|
|
130
|
+
co.set_argument("--disable-dev-shm-usage")
|
|
131
|
+
co.set_argument("--disable-gpu")
|
|
126
132
|
|
|
127
133
|
# 无头模式
|
|
128
134
|
if self._headless:
|
|
129
135
|
co.headless()
|
|
130
136
|
|
|
131
|
-
# 容器环境
|
|
132
|
-
if not os.environ.get("DISPLAY"):
|
|
133
|
-
co.set_argument("--no-sandbox")
|
|
134
|
-
co.set_argument("--disable-setuid-sandbox")
|
|
135
|
-
co.set_argument("--disable-gpu")
|
|
136
|
-
|
|
137
137
|
# 自动检测浏览器路径
|
|
138
138
|
browser_path = self._detect_browser()
|
|
139
139
|
if browser_path:
|
|
@@ -146,17 +146,27 @@ class StealthBrowser:
|
|
|
146
146
|
self._browser = Chromium(co)
|
|
147
147
|
self._page = self._browser.latest_tab
|
|
148
148
|
|
|
149
|
+
# 如果浏览器已崩溃或无法获取 tab
|
|
150
|
+
if not self._page:
|
|
151
|
+
return SkillResult(
|
|
152
|
+
success=False,
|
|
153
|
+
error="启动浏览器后无法获取页面标签(Chrome 可能未正确启动)",
|
|
154
|
+
)
|
|
155
|
+
|
|
149
156
|
# 隐藏 webdriver 标志(额外注入)
|
|
150
157
|
try:
|
|
151
|
-
self._page.run_js(
|
|
152
|
-
Object.defineProperty(navigator,
|
|
153
|
-
Object.defineProperty(navigator,
|
|
154
|
-
Object.defineProperty(navigator,
|
|
155
|
-
window.chrome
|
|
156
|
-
|
|
158
|
+
self._page.run_js(
|
|
159
|
+
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined});"
|
|
160
|
+
"Object.defineProperty(navigator,'plugins',{get:()=>[1,2,3,4,5]});"
|
|
161
|
+
"Object.defineProperty(navigator,'languages',{get:()=>['zh-CN','zh','en']});"
|
|
162
|
+
"window.chrome={runtime:{}};"
|
|
163
|
+
)
|
|
157
164
|
except Exception as e:
|
|
158
165
|
logger.debug(f"反检测 JS 注入提示: {e}")
|
|
159
166
|
|
|
167
|
+
# 等待浏览器完全就绪
|
|
168
|
+
time.sleep(1)
|
|
169
|
+
|
|
160
170
|
self._started = True
|
|
161
171
|
logger.info(
|
|
162
172
|
f"反检测浏览器已启动 (profile={self.profile_name}, "
|
|
@@ -177,17 +187,18 @@ class StealthBrowser:
|
|
|
177
187
|
|
|
178
188
|
async def close(self) -> SkillResult:
|
|
179
189
|
"""关闭浏览器"""
|
|
190
|
+
self._started = False
|
|
180
191
|
try:
|
|
181
192
|
if self._browser:
|
|
182
193
|
self._browser.quit()
|
|
183
194
|
self._browser = None
|
|
184
195
|
self._page = None
|
|
185
|
-
self._started = False
|
|
186
196
|
logger.info("反检测浏览器已关闭")
|
|
187
197
|
return SkillResult(success=True, message="浏览器已关闭")
|
|
188
198
|
except Exception as e:
|
|
189
199
|
logger.error(f"关闭浏览器异常: {e}")
|
|
190
|
-
|
|
200
|
+
self._browser = None
|
|
201
|
+
self._page = None
|
|
191
202
|
return SkillResult(success=True, message="浏览器已关闭")
|
|
192
203
|
|
|
193
204
|
async def navigate(self, url: str, wait: float = 2.0) -> SkillResult:
|
|
@@ -198,7 +209,7 @@ class StealthBrowser:
|
|
|
198
209
|
try:
|
|
199
210
|
self._page.get(url)
|
|
200
211
|
if wait > 0:
|
|
201
|
-
|
|
212
|
+
await asyncio.sleep(wait)
|
|
202
213
|
|
|
203
214
|
title = self._page.title or ""
|
|
204
215
|
current_url = self._page.url or ""
|
|
@@ -225,7 +236,7 @@ class StealthBrowser:
|
|
|
225
236
|
)
|
|
226
237
|
ele.click()
|
|
227
238
|
if wait > 0:
|
|
228
|
-
|
|
239
|
+
await asyncio.sleep(wait)
|
|
229
240
|
return SkillResult(success=True, message=f"已点击: {selector}")
|
|
230
241
|
except Exception as e:
|
|
231
242
|
return SkillResult(success=False, error=f"点击失败: {e}")
|
|
@@ -248,7 +259,7 @@ class StealthBrowser:
|
|
|
248
259
|
ele.clear()
|
|
249
260
|
ele.input(value)
|
|
250
261
|
if wait > 0:
|
|
251
|
-
|
|
262
|
+
await asyncio.sleep(wait)
|
|
252
263
|
return SkillResult(
|
|
253
264
|
success=True,
|
|
254
265
|
message=f"已填写 {selector}: {value[:50]}{'...' if len(value) > 50 else ''}",
|
|
@@ -303,7 +314,17 @@ class StealthBrowser:
|
|
|
303
314
|
return SkillResult(success=False, error="浏览器未启动")
|
|
304
315
|
|
|
305
316
|
try:
|
|
306
|
-
|
|
317
|
+
# Bug Fix: DrissionPage 没有 page.text 属性
|
|
318
|
+
# 需要通过 page.ele('tag:html').text 获取页面文本
|
|
319
|
+
try:
|
|
320
|
+
text = self._page.ele('tag:html').text or ""
|
|
321
|
+
except Exception:
|
|
322
|
+
# 降级:用 BS4 从 html 提取文本
|
|
323
|
+
try:
|
|
324
|
+
from bs4 import BeautifulSoup
|
|
325
|
+
text = BeautifulSoup(self._page.html or "", "html.parser").get_text(strip=True, separator="\n")
|
|
326
|
+
except Exception:
|
|
327
|
+
text = ""
|
|
307
328
|
title = self._page.title or ""
|
|
308
329
|
url = self._page.url or ""
|
|
309
330
|
|
|
@@ -375,16 +396,21 @@ class StealthBrowser:
|
|
|
375
396
|
return SkillResult(success=False, error="浏览器未启动")
|
|
376
397
|
|
|
377
398
|
try:
|
|
399
|
+
# DrissionPage cookies() 返回 CookiesList(list 子类),每项是 dict
|
|
378
400
|
cookies = self._page.cookies()
|
|
379
401
|
cookie_list = []
|
|
380
402
|
if cookies:
|
|
381
403
|
for c in cookies:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
404
|
+
# CookiesList 中的每项是 dict,但 key 可能不全
|
|
405
|
+
if isinstance(c, dict):
|
|
406
|
+
cookie_list.append({
|
|
407
|
+
"name": c.get("name", ""),
|
|
408
|
+
"value": c.get("value", ""),
|
|
409
|
+
"domain": c.get("domain", ""),
|
|
410
|
+
"path": c.get("path", "/"),
|
|
411
|
+
})
|
|
412
|
+
else:
|
|
413
|
+
cookie_list.append(str(c))
|
|
388
414
|
|
|
389
415
|
return SkillResult(
|
|
390
416
|
success=True,
|
|
@@ -404,10 +430,15 @@ class StealthBrowser:
|
|
|
404
430
|
cookie_list = []
|
|
405
431
|
if cookies:
|
|
406
432
|
for c in cookies:
|
|
407
|
-
|
|
433
|
+
# CookiesList 中每项是 dict,确保可序列化
|
|
434
|
+
if isinstance(c, dict):
|
|
435
|
+
cookie_list.append(dict(c))
|
|
436
|
+
else:
|
|
437
|
+
cookie_list.append(str(c))
|
|
408
438
|
|
|
409
439
|
from core.browser_profile import get_browser_profile_manager
|
|
410
440
|
mgr = get_browser_profile_manager()
|
|
441
|
+
# 保存时用 profile_name 对应的原始 BrowserProfile(不加 user_data 子路径)
|
|
411
442
|
profile = mgr.get_profile(self.profile_name)
|
|
412
443
|
profile.save_cookies(cookie_list)
|
|
413
444
|
|
|
@@ -430,6 +461,8 @@ class StealthBrowser:
|
|
|
430
461
|
cookies = profile.load_cookies()
|
|
431
462
|
|
|
432
463
|
if cookies:
|
|
464
|
+
# Bug Fix: page.set.cookies 是 CookiesSetter 对象(可调用)
|
|
465
|
+
# 直接调用 page.set.cookies(cookies_list) 即可
|
|
433
466
|
self._page.set.cookies(cookies)
|
|
434
467
|
return SkillResult(
|
|
435
468
|
success=True,
|
|
@@ -449,7 +482,13 @@ class StealthBrowser:
|
|
|
449
482
|
return SkillResult(success=False, error="浏览器未启动")
|
|
450
483
|
|
|
451
484
|
try:
|
|
452
|
-
|
|
485
|
+
# Bug Fix: DrissionPage 没有 page.clear_cookies() 方法
|
|
486
|
+
# 正确方式是 page.set.cookies.clear() 或 page.clear_cache()
|
|
487
|
+
try:
|
|
488
|
+
self._page.set.cookies.clear()
|
|
489
|
+
except Exception:
|
|
490
|
+
self._page.clear_cache(cookies=True)
|
|
491
|
+
|
|
453
492
|
from core.browser_profile import get_browser_profile_manager
|
|
454
493
|
mgr = get_browser_profile_manager()
|
|
455
494
|
profile = mgr.get_profile(self.profile_name)
|
|
@@ -479,13 +518,17 @@ class StealthBrowser:
|
|
|
479
518
|
)
|
|
480
519
|
logger.info(f"[{self.profile_name}] 等待用户手动操作: {reason}")
|
|
481
520
|
|
|
482
|
-
#
|
|
521
|
+
# Bug Fix: 使用 asyncio.sleep 避免阻塞事件循环
|
|
483
522
|
start = time.time()
|
|
484
523
|
last_url = self._page.url if self._page else ""
|
|
524
|
+
poll_interval = 3
|
|
485
525
|
while time.time() - start < timeout:
|
|
486
|
-
|
|
526
|
+
await asyncio.sleep(poll_interval)
|
|
487
527
|
if self._page:
|
|
488
|
-
|
|
528
|
+
try:
|
|
529
|
+
current_url = self._page.url or ""
|
|
530
|
+
except Exception:
|
|
531
|
+
break # 浏览器已关闭
|
|
489
532
|
# 如果 URL 发生变化,说明用户完成了操作
|
|
490
533
|
if current_url != last_url and current_url:
|
|
491
534
|
elapsed = int(time.time() - start)
|
|
@@ -506,11 +549,13 @@ class StealthBrowser:
|
|
|
506
549
|
if not self._started or not self._page:
|
|
507
550
|
return False
|
|
508
551
|
try:
|
|
509
|
-
#
|
|
552
|
+
# 检查页面是否仍然有效(try 访问 url,如果浏览器已崩溃会抛异常)
|
|
510
553
|
_ = self._page.url
|
|
511
554
|
return True
|
|
512
555
|
except Exception:
|
|
513
|
-
|
|
556
|
+
# 不要在这里重置 _started,由 close() 方法负责
|
|
557
|
+
# 避免在异步上下文中误判
|
|
558
|
+
logger.debug(f"页面检查失败 (profile={self.profile_name}),浏览器可能已关闭")
|
|
514
559
|
return False
|
|
515
560
|
|
|
516
561
|
@staticmethod
|
|
@@ -560,10 +605,10 @@ class StealthBrowser:
|
|
|
560
605
|
# ── 全局浏览器实例管理 ──────────────────────────────────────
|
|
561
606
|
|
|
562
607
|
_browsers: Dict[str, StealthBrowser] = {}
|
|
563
|
-
_browser_lock =
|
|
608
|
+
_browser_lock = threading.Lock()
|
|
564
609
|
|
|
565
610
|
|
|
566
|
-
|
|
611
|
+
def get_stealth_browser(
|
|
567
612
|
profile_name: str = "default",
|
|
568
613
|
headless: bool = False,
|
|
569
614
|
) -> StealthBrowser:
|
|
@@ -571,8 +616,9 @@ async def get_stealth_browser(
|
|
|
571
616
|
获取反检测浏览器实例(按 profile_name 复用)。
|
|
572
617
|
|
|
573
618
|
同一 profile 名称返回相同实例,避免重复启动。
|
|
619
|
+
Bug Fix: 改用同步锁 + 同步返回,避免在非 async 上下文中死锁。
|
|
574
620
|
"""
|
|
575
|
-
|
|
621
|
+
with _browser_lock:
|
|
576
622
|
if profile_name in _browsers:
|
|
577
623
|
browser = _browsers[profile_name]
|
|
578
624
|
if browser._started and browser._ensure_page():
|
|
@@ -583,21 +629,37 @@ async def get_stealth_browser(
|
|
|
583
629
|
return browser
|
|
584
630
|
|
|
585
631
|
|
|
586
|
-
|
|
587
|
-
"""
|
|
588
|
-
|
|
632
|
+
def close_stealth_browser(profile_name: str = "") -> None:
|
|
633
|
+
"""关闭浏览器实例(同步版本)"""
|
|
634
|
+
with _browser_lock:
|
|
589
635
|
if profile_name and profile_name in _browsers:
|
|
590
|
-
|
|
636
|
+
browser = _browsers[profile_name]
|
|
637
|
+
# 同步调用内部关闭逻辑
|
|
638
|
+
browser._started = False
|
|
639
|
+
try:
|
|
640
|
+
if browser._browser:
|
|
641
|
+
browser._browser.quit()
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
browser._browser = None
|
|
645
|
+
browser._page = None
|
|
591
646
|
del _browsers[profile_name]
|
|
592
647
|
elif not profile_name:
|
|
593
648
|
for name, browser in _browsers.items():
|
|
594
649
|
try:
|
|
595
|
-
|
|
650
|
+
browser._started = False
|
|
651
|
+
if browser._browser:
|
|
652
|
+
browser._browser.quit()
|
|
596
653
|
except Exception:
|
|
597
654
|
pass
|
|
598
655
|
_browsers.clear()
|
|
599
656
|
|
|
600
657
|
|
|
658
|
+
async def close_stealth_browser_async(profile_name: str = "") -> None:
|
|
659
|
+
"""关闭浏览器实例(异步版本,供 async 上下文调用)"""
|
|
660
|
+
close_stealth_browser(profile_name)
|
|
661
|
+
|
|
662
|
+
|
|
601
663
|
# ── 技能类(供 SkillRegistry 自动发现)─────────────────────
|
|
602
664
|
|
|
603
665
|
|
|
@@ -607,15 +669,15 @@ class StealthBrowserStartSkill(Skill):
|
|
|
607
669
|
name = "stealth_browser_start"
|
|
608
670
|
description = (
|
|
609
671
|
"启动反检测浏览器 — 使用 DrissionPage 启动带有反检测能力的 Chromium 浏览器,"
|
|
610
|
-
"
|
|
611
|
-
"
|
|
672
|
+
"适合需要登录的网站操作。每个网站使用独立 Profile,登录状态自动保持。"
|
|
673
|
+
"使用 site-manage list 查看所有可用网站。"
|
|
612
674
|
)
|
|
613
675
|
category = "browser_stealth"
|
|
614
676
|
parameters = [
|
|
615
677
|
SkillParameter(
|
|
616
678
|
name="profile",
|
|
617
679
|
type="string",
|
|
618
|
-
description="Profile
|
|
680
|
+
description="Profile 名称(使用 site-manage list 查看可用网站)",
|
|
619
681
|
required=False,
|
|
620
682
|
default="default",
|
|
621
683
|
),
|
|
@@ -630,7 +692,8 @@ class StealthBrowserStartSkill(Skill):
|
|
|
630
692
|
dangerous = False
|
|
631
693
|
|
|
632
694
|
async def execute(self, profile: str = "default", headless: bool = False, **kw) -> SkillResult:
|
|
633
|
-
|
|
695
|
+
# Bug Fix: get_stealth_browser 是同步函数,不能用 await
|
|
696
|
+
browser = get_stealth_browser(profile_name=profile, headless=headless)
|
|
634
697
|
return await browser.start()
|
|
635
698
|
|
|
636
699
|
|
|
@@ -646,7 +709,7 @@ class StealthBrowserNavigateSkill(Skill):
|
|
|
646
709
|
]
|
|
647
710
|
|
|
648
711
|
async def execute(self, url: str, profile: str = "default", **kw) -> SkillResult:
|
|
649
|
-
browser =
|
|
712
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
650
713
|
if not browser._started:
|
|
651
714
|
r = await browser.start()
|
|
652
715
|
if not r.success:
|
|
@@ -666,7 +729,7 @@ class StealthBrowserClickSkill(Skill):
|
|
|
666
729
|
]
|
|
667
730
|
|
|
668
731
|
async def execute(self, selector: str, profile: str = "default", **kw) -> SkillResult:
|
|
669
|
-
browser =
|
|
732
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
670
733
|
return await browser.click(selector)
|
|
671
734
|
|
|
672
735
|
|
|
@@ -686,7 +749,7 @@ class StealthBrowserFillSkill(Skill):
|
|
|
686
749
|
async def execute(
|
|
687
750
|
self, selector: str, value: str, clear: bool = True, profile: str = "default", **kw
|
|
688
751
|
) -> SkillResult:
|
|
689
|
-
browser =
|
|
752
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
690
753
|
return await browser.fill(selector, value, clear=clear)
|
|
691
754
|
|
|
692
755
|
|
|
@@ -702,7 +765,7 @@ class StealthBrowserScreenshotSkill(Skill):
|
|
|
702
765
|
]
|
|
703
766
|
|
|
704
767
|
async def execute(self, save_path: str = "", profile: str = "default", **kw) -> SkillResult:
|
|
705
|
-
browser =
|
|
768
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
706
769
|
return await browser.screenshot(save_path=save_path)
|
|
707
770
|
|
|
708
771
|
|
|
@@ -718,7 +781,7 @@ class StealthBrowserEvalSkill(Skill):
|
|
|
718
781
|
]
|
|
719
782
|
|
|
720
783
|
async def execute(self, script: str, profile: str = "default", **kw) -> SkillResult:
|
|
721
|
-
browser =
|
|
784
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
722
785
|
return await browser.evaluate(script)
|
|
723
786
|
|
|
724
787
|
|
|
@@ -733,7 +796,7 @@ class StealthBrowserGetContentSkill(Skill):
|
|
|
733
796
|
]
|
|
734
797
|
|
|
735
798
|
async def execute(self, profile: str = "default", **kw) -> SkillResult:
|
|
736
|
-
browser =
|
|
799
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
737
800
|
return await browser.get_content()
|
|
738
801
|
|
|
739
802
|
|
|
@@ -759,7 +822,7 @@ class StealthBrowserCookieSkill(Skill):
|
|
|
759
822
|
]
|
|
760
823
|
|
|
761
824
|
async def execute(self, action: str, profile: str = "default", **kw) -> SkillResult:
|
|
762
|
-
browser =
|
|
825
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
763
826
|
if action == "save":
|
|
764
827
|
return await browser.save_cookies()
|
|
765
828
|
elif action == "load":
|
|
@@ -783,7 +846,8 @@ class StealthBrowserCloseSkill(Skill):
|
|
|
783
846
|
]
|
|
784
847
|
|
|
785
848
|
async def execute(self, profile: str = "", **kw) -> SkillResult:
|
|
786
|
-
|
|
849
|
+
# close_stealth_browser 现在是同步函数,直接调用
|
|
850
|
+
close_stealth_browser(profile_name=profile)
|
|
787
851
|
return SkillResult(success=True, message="浏览器已关闭")
|
|
788
852
|
|
|
789
853
|
|
|
@@ -818,7 +882,7 @@ class StealthBrowserWaitManualSkill(Skill):
|
|
|
818
882
|
async def execute(
|
|
819
883
|
self, reason: str = "验证码", timeout: int = 300, profile: str = "default", **kw
|
|
820
884
|
) -> SkillResult:
|
|
821
|
-
browser =
|
|
885
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
822
886
|
return await browser.wait_for_manual(reason=reason, timeout=float(timeout))
|
|
823
887
|
|
|
824
888
|
|
|
@@ -837,10 +901,149 @@ class StealthBrowserWaitSkill(Skill):
|
|
|
837
901
|
async def execute(
|
|
838
902
|
self, selector: str, timeout: int = 10, profile: str = "default", **kw
|
|
839
903
|
) -> SkillResult:
|
|
840
|
-
browser =
|
|
904
|
+
browser = get_stealth_browser(profile_name=profile)
|
|
841
905
|
return await browser.wait_for(selector, timeout=float(timeout))
|
|
842
906
|
|
|
843
907
|
|
|
908
|
+
# ── Site Registry 管理技能 (v1.31.0) ────────────────────
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
class SiteManageListSkill(Skill):
|
|
912
|
+
"""列出所有已注册网站"""
|
|
913
|
+
|
|
914
|
+
name = "site_manage_list"
|
|
915
|
+
description = "列出所有已注册网站 — 显示网站名称、分类、登录 URL 和 Profile 状态"
|
|
916
|
+
category = "browser_stealth"
|
|
917
|
+
parameters = [
|
|
918
|
+
SkillParameter(
|
|
919
|
+
name="category",
|
|
920
|
+
type="string",
|
|
921
|
+
description="按分类筛选(空=全部)",
|
|
922
|
+
required=False,
|
|
923
|
+
default="",
|
|
924
|
+
),
|
|
925
|
+
]
|
|
926
|
+
|
|
927
|
+
async def execute(self, category: str = "", **kw) -> SkillResult:
|
|
928
|
+
from core.site_registry import get_site_registry
|
|
929
|
+
reg = get_site_registry()
|
|
930
|
+
sites = reg.list_sites(category=category)
|
|
931
|
+
summary = reg.to_summary()
|
|
932
|
+
|
|
933
|
+
return SkillResult(
|
|
934
|
+
success=True,
|
|
935
|
+
message=f"共 {len(sites)} 个网站" + (f" (分类: {category})" if category else ""),
|
|
936
|
+
data={"sites": sites, "summary": {k: v for k, v in summary.items() if k != "sites"}},
|
|
937
|
+
output=json.dumps(sites, ensure_ascii=False, indent=2),
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class SiteManageShowSkill(Skill):
|
|
942
|
+
"""显示网站详细配置"""
|
|
943
|
+
|
|
944
|
+
name = "site_manage_show"
|
|
945
|
+
description = "显示网站详细配置 — 查看网站的登录 URL、可用页面和操作提示"
|
|
946
|
+
category = "browser_stealth"
|
|
947
|
+
parameters = [
|
|
948
|
+
SkillParameter(name="name", type="string", description="网站名称", required=True),
|
|
949
|
+
]
|
|
950
|
+
|
|
951
|
+
async def execute(self, name: str, **kw) -> SkillResult:
|
|
952
|
+
from core.site_registry import get_site_registry
|
|
953
|
+
reg = get_site_registry()
|
|
954
|
+
site = reg.get_site(name)
|
|
955
|
+
if not site:
|
|
956
|
+
return SkillResult(success=False, error=f"网站不存在: {name}")
|
|
957
|
+
|
|
958
|
+
return SkillResult(
|
|
959
|
+
success=True,
|
|
960
|
+
message=f"网站: {site.get('display_name', name)}",
|
|
961
|
+
data=site,
|
|
962
|
+
output=json.dumps(site, ensure_ascii=False, indent=2),
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
class SiteManageAddSkill(Skill):
|
|
967
|
+
"""添加自定义网站"""
|
|
968
|
+
|
|
969
|
+
name = "site_manage_add"
|
|
970
|
+
description = (
|
|
971
|
+
"添加自定义网站 — 为新网站创建配置,后续可用 stealth-open 登录。"
|
|
972
|
+
"需提供 name(英文标识)、login_url(登录页)和 display_name(显示名称)。"
|
|
973
|
+
)
|
|
974
|
+
category = "browser_stealth"
|
|
975
|
+
parameters = [
|
|
976
|
+
SkillParameter(name="name", type="string", description="网站名称(英文,如 my_site)", required=True),
|
|
977
|
+
SkillParameter(name="display_name", type="string", description="显示名称", required=False, default=""),
|
|
978
|
+
SkillParameter(name="login_url", type="string", description="登录页 URL", required=False, default=""),
|
|
979
|
+
SkillParameter(name="home_url", type="string", description="首页 URL", required=False, default=""),
|
|
980
|
+
SkillParameter(name="category", type="string", description="分类", required=False, default="custom"),
|
|
981
|
+
]
|
|
982
|
+
|
|
983
|
+
async def execute(
|
|
984
|
+
self, name: str, display_name: str = "", login_url: str = "",
|
|
985
|
+
home_url: str = "", category: str = "custom", **kw
|
|
986
|
+
) -> SkillResult:
|
|
987
|
+
from core.site_registry import get_site_registry
|
|
988
|
+
reg = get_site_registry()
|
|
989
|
+
try:
|
|
990
|
+
site = reg.add_site({
|
|
991
|
+
"name": name,
|
|
992
|
+
"display_name": display_name or name,
|
|
993
|
+
"login_url": login_url,
|
|
994
|
+
"home_url": home_url,
|
|
995
|
+
"category": category,
|
|
996
|
+
})
|
|
997
|
+
return SkillResult(
|
|
998
|
+
success=True,
|
|
999
|
+
message=f"已添加网站: {name}",
|
|
1000
|
+
data=site,
|
|
1001
|
+
)
|
|
1002
|
+
except ValueError as e:
|
|
1003
|
+
return SkillResult(success=False, error=str(e))
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class SiteManageRemoveSkill(Skill):
|
|
1007
|
+
"""删除自定义网站"""
|
|
1008
|
+
|
|
1009
|
+
name = "site_manage_remove"
|
|
1010
|
+
description = "删除自定义网站(内置网站不可删除)"
|
|
1011
|
+
category = "browser_stealth"
|
|
1012
|
+
parameters = [
|
|
1013
|
+
SkillParameter(name="name", type="string", description="网站名称", required=True),
|
|
1014
|
+
]
|
|
1015
|
+
|
|
1016
|
+
async def execute(self, name: str, **kw) -> SkillResult:
|
|
1017
|
+
from core.site_registry import get_site_registry
|
|
1018
|
+
reg = get_site_registry()
|
|
1019
|
+
ok = reg.remove_site(name)
|
|
1020
|
+
if ok:
|
|
1021
|
+
return SkillResult(success=True, message=f"已删除网站: {name}")
|
|
1022
|
+
return SkillResult(
|
|
1023
|
+
success=False,
|
|
1024
|
+
error=f"删除失败: {name}(内置网站不可删除,或网站不存在)",
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
class SiteManageInitSkill(Skill):
|
|
1029
|
+
"""批量初始化网站 Profile"""
|
|
1030
|
+
|
|
1031
|
+
name = "site_manage_init"
|
|
1032
|
+
description = "批量初始化所有网站的浏览器 Profile(仅创建目录,不自动登录)"
|
|
1033
|
+
category = "browser_stealth"
|
|
1034
|
+
parameters = []
|
|
1035
|
+
|
|
1036
|
+
async def execute(self, **kw) -> SkillResult:
|
|
1037
|
+
from core.site_registry import get_site_registry
|
|
1038
|
+
reg = get_site_registry()
|
|
1039
|
+
initialized = reg.init_profiles()
|
|
1040
|
+
return SkillResult(
|
|
1041
|
+
success=True,
|
|
1042
|
+
message=f"已初始化 {len(initialized)} 个网站的 Profile",
|
|
1043
|
+
data={"initialized": initialized, "count": len(initialized)},
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
|
|
844
1047
|
# ── Profile 管理技能 ──────────────────────────────────────
|
|
845
1048
|
|
|
846
1049
|
|
package/aiskills/registry.py
CHANGED
|
@@ -206,6 +206,9 @@ CLI_COMMANDS: List[Dict[str, Any]] = [
|
|
|
206
206
|
"description": "创建浏览器 Profile"},
|
|
207
207
|
{"name": "profile-delete", "category": "stealth", "cli": "myagent-ai profile-delete <name>",
|
|
208
208
|
"description": "删除浏览器 Profile"},
|
|
209
|
+
# ── Site 管理 (v1.31.0) ──
|
|
210
|
+
{"name": "site-manage", "category": "stealth", "cli": "myagent-ai site-manage <list|show|add|remove|init> [args]",
|
|
211
|
+
"description": "网站注册管理 — 列出/查看/添加/删除/初始化网站配置(支持 20+ 内置网站和动态扩展)"},
|
|
209
212
|
# ── GUI 桌面 ──
|
|
210
213
|
{"name": "screenshot", "category": "gui", "cli": "myagent-ai screenshot [region] [-m monitor]",
|
|
211
214
|
"description": "屏幕截图(仅 Windows/macOS)"},
|