myagent-ai 1.28.3 → 1.29.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 +924 -0
- package/aiskills/registry.py +41 -9
- package/aiskills/stealth_browser/SKILL.md +133 -0
- package/core/browser_profile.py +433 -0
- package/package.json +1 -1
- package/requirements.txt +5 -3
- package/scripts/cli.py +277 -1
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aiskills/browser_stealth.py - DrissionPage 反检测浏览器技能
|
|
3
|
+
==========================================================
|
|
4
|
+
基于 DrissionPage 的反检测浏览器自动化,专用于需要登录的网站操作。
|
|
5
|
+
|
|
6
|
+
核心能力:
|
|
7
|
+
- 反自动化检测: 自动隐藏 webdriver 标志、CDP 痕迹等
|
|
8
|
+
- Profile 持久化: 每个网站独立 Profile,登录一次永久保持
|
|
9
|
+
- Cookie 管理: 保存/恢复/清除 Cookie
|
|
10
|
+
- 人机协作: 遇到验证码/2FA 时暂停,等待用户手动操作
|
|
11
|
+
- 完整操作: 导航、点击、填写、截图、执行 JS、等待元素等
|
|
12
|
+
|
|
13
|
+
与 chromedev_mcp.py 的区别:
|
|
14
|
+
- chromedev_mcp: 标准 CDP 协议,功能全面,但会被网站检测为自动化
|
|
15
|
+
- browser_stealth: 反检测优先,适合登录敏感网站(Google/X/微博/抖音等)
|
|
16
|
+
|
|
17
|
+
架构:
|
|
18
|
+
DrissionPage (Python) ──CDP(隐藏)──> Chromium/Chrome
|
|
19
|
+
↑
|
|
20
|
+
User Data Directory (Profile)
|
|
21
|
+
Cookie 持久化
|
|
22
|
+
|
|
23
|
+
依赖:
|
|
24
|
+
- DrissionPage >= 4.1.0
|
|
25
|
+
- Chromium/Chrome (系统已有,或自动检测)
|
|
26
|
+
|
|
27
|
+
参考:
|
|
28
|
+
https://drissionpage.cn/
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import shutil
|
|
36
|
+
import time
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional
|
|
39
|
+
|
|
40
|
+
from core.logger import get_logger
|
|
41
|
+
from aiskills.base import Skill, SkillResult, SkillParameter
|
|
42
|
+
|
|
43
|
+
logger = get_logger("myagent.skills.browser_stealth")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── 反检测浏览器管理器 ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StealthBrowser:
|
|
50
|
+
"""
|
|
51
|
+
基于 DrissionPage 的反检测浏览器实例。
|
|
52
|
+
|
|
53
|
+
用法:
|
|
54
|
+
browser = StealthBrowser(profile_name="google")
|
|
55
|
+
await browser.start()
|
|
56
|
+
await browser.navigate("https://accounts.google.com/")
|
|
57
|
+
await browser.fill('input[type="email"]', "user@gmail.com")
|
|
58
|
+
await browser.click('button:contains("下一步")')
|
|
59
|
+
screenshot = await browser.screenshot()
|
|
60
|
+
await browser.close()
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
profile_name: str = "default",
|
|
66
|
+
headless: bool = False,
|
|
67
|
+
user_data_dir: Optional[str] = None,
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Args:
|
|
71
|
+
profile_name: Profile 名称(对应 BrowserProfileManager 中的 profile)
|
|
72
|
+
headless: 是否无头模式(登录场景建议 False)
|
|
73
|
+
user_data_dir: 自定义用户数据目录(覆盖 Profile 管理)
|
|
74
|
+
"""
|
|
75
|
+
self.profile_name = profile_name
|
|
76
|
+
self._headless = headless
|
|
77
|
+
self._custom_user_data_dir = user_data_dir
|
|
78
|
+
self._page = None # DrissionPage ChromiumPage 实例
|
|
79
|
+
self._browser = None # DrissionPage Chromium 实例
|
|
80
|
+
self._started = False
|
|
81
|
+
self._user_data_dir = ""
|
|
82
|
+
|
|
83
|
+
def _get_user_data_dir(self) -> str:
|
|
84
|
+
"""获取用户数据目录路径"""
|
|
85
|
+
if self._custom_user_data_dir:
|
|
86
|
+
return self._custom_user_data_dir
|
|
87
|
+
|
|
88
|
+
from core.browser_profile import get_browser_profile_manager
|
|
89
|
+
mgr = get_browser_profile_manager()
|
|
90
|
+
profile = mgr.get_profile(self.profile_name)
|
|
91
|
+
profile.ensure_dirs()
|
|
92
|
+
self._user_data_dir = str(profile.user_data_dir)
|
|
93
|
+
return self._user_data_dir
|
|
94
|
+
|
|
95
|
+
async def start(self) -> SkillResult:
|
|
96
|
+
"""启动反检测浏览器"""
|
|
97
|
+
try:
|
|
98
|
+
from DrissionPage import Chromium, ChromiumOptions
|
|
99
|
+
except ImportError:
|
|
100
|
+
return SkillResult(
|
|
101
|
+
success=False,
|
|
102
|
+
error="DrissionPage 未安装。请运行: pip install DrissionPage",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
user_data = self._get_user_data_dir()
|
|
107
|
+
co = ChromiumOptions()
|
|
108
|
+
|
|
109
|
+
# 设置用户数据目录(Profile 持久化)
|
|
110
|
+
if user_data:
|
|
111
|
+
co.set_user_data_path(user_data)
|
|
112
|
+
co.set_argument(f"--user-data-dir={user_data}")
|
|
113
|
+
|
|
114
|
+
# 反检测核心参数
|
|
115
|
+
co.set_argument("--no-first-run")
|
|
116
|
+
co.set_argument("--no-default-browser-check")
|
|
117
|
+
co.set_argument("--disable-blink-features=AutomationControlled")
|
|
118
|
+
|
|
119
|
+
# 禁用自动化相关标志
|
|
120
|
+
co.set_argument("--disable-infobars")
|
|
121
|
+
co.set_argument("--disable-extensions")
|
|
122
|
+
|
|
123
|
+
# VNC/容器环境适配
|
|
124
|
+
if os.environ.get("DISPLAY"):
|
|
125
|
+
co.set_argument(f"--display={os.environ['DISPLAY']}")
|
|
126
|
+
|
|
127
|
+
# 无头模式
|
|
128
|
+
if self._headless:
|
|
129
|
+
co.headless()
|
|
130
|
+
|
|
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
|
+
# 自动检测浏览器路径
|
|
138
|
+
browser_path = self._detect_browser()
|
|
139
|
+
if browser_path:
|
|
140
|
+
co.set_browser_path(browser_path)
|
|
141
|
+
|
|
142
|
+
# 设置窗口大小
|
|
143
|
+
co.set_argument("--window-size=1920,1080")
|
|
144
|
+
|
|
145
|
+
# 创建浏览器实例
|
|
146
|
+
self._browser = Chromium(co)
|
|
147
|
+
self._page = self._browser.latest_tab
|
|
148
|
+
|
|
149
|
+
# 隐藏 webdriver 标志(额外注入)
|
|
150
|
+
try:
|
|
151
|
+
self._page.run_js("""
|
|
152
|
+
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
|
153
|
+
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
|
|
154
|
+
Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh', 'en']});
|
|
155
|
+
window.chrome = {runtime: {}};
|
|
156
|
+
""")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.debug(f"反检测 JS 注入提示: {e}")
|
|
159
|
+
|
|
160
|
+
self._started = True
|
|
161
|
+
logger.info(
|
|
162
|
+
f"反检测浏览器已启动 (profile={self.profile_name}, "
|
|
163
|
+
f"headless={self._headless})"
|
|
164
|
+
)
|
|
165
|
+
return SkillResult(
|
|
166
|
+
success=True,
|
|
167
|
+
message=f"反检测浏览器已启动 (Profile: {self.profile_name})",
|
|
168
|
+
data={"profile": self.profile_name, "headless": self._headless},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"启动反检测浏览器失败: {e}")
|
|
173
|
+
return SkillResult(
|
|
174
|
+
success=False,
|
|
175
|
+
error=f"启动反检测浏览器失败: {e}",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def close(self) -> SkillResult:
|
|
179
|
+
"""关闭浏览器"""
|
|
180
|
+
try:
|
|
181
|
+
if self._browser:
|
|
182
|
+
self._browser.quit()
|
|
183
|
+
self._browser = None
|
|
184
|
+
self._page = None
|
|
185
|
+
self._started = False
|
|
186
|
+
logger.info("反检测浏览器已关闭")
|
|
187
|
+
return SkillResult(success=True, message="浏览器已关闭")
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"关闭浏览器异常: {e}")
|
|
190
|
+
self._started = False
|
|
191
|
+
return SkillResult(success=True, message="浏览器已关闭")
|
|
192
|
+
|
|
193
|
+
async def navigate(self, url: str, wait: float = 2.0) -> SkillResult:
|
|
194
|
+
"""导航到指定 URL"""
|
|
195
|
+
if not self._ensure_page():
|
|
196
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
self._page.get(url)
|
|
200
|
+
if wait > 0:
|
|
201
|
+
time.sleep(wait)
|
|
202
|
+
|
|
203
|
+
title = self._page.title or ""
|
|
204
|
+
current_url = self._page.url or ""
|
|
205
|
+
|
|
206
|
+
return SkillResult(
|
|
207
|
+
success=True,
|
|
208
|
+
message=f"已导航到: {url}",
|
|
209
|
+
data={"title": title, "url": current_url},
|
|
210
|
+
)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return SkillResult(success=False, error=f"导航失败: {e}")
|
|
213
|
+
|
|
214
|
+
async def click(self, selector: str, wait: float = 1.0) -> SkillResult:
|
|
215
|
+
"""点击页面元素"""
|
|
216
|
+
if not self._ensure_page():
|
|
217
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
ele = self._page.ele(selector, timeout=10)
|
|
221
|
+
if not ele:
|
|
222
|
+
return SkillResult(
|
|
223
|
+
success=False,
|
|
224
|
+
error=f"未找到元素: {selector}",
|
|
225
|
+
)
|
|
226
|
+
ele.click()
|
|
227
|
+
if wait > 0:
|
|
228
|
+
time.sleep(wait)
|
|
229
|
+
return SkillResult(success=True, message=f"已点击: {selector}")
|
|
230
|
+
except Exception as e:
|
|
231
|
+
return SkillResult(success=False, error=f"点击失败: {e}")
|
|
232
|
+
|
|
233
|
+
async def fill(
|
|
234
|
+
self, selector: str, value: str, clear: bool = True, wait: float = 0.5
|
|
235
|
+
) -> SkillResult:
|
|
236
|
+
"""填写输入框"""
|
|
237
|
+
if not self._ensure_page():
|
|
238
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
ele = self._page.ele(selector, timeout=10)
|
|
242
|
+
if not ele:
|
|
243
|
+
return SkillResult(
|
|
244
|
+
success=False,
|
|
245
|
+
error=f"未找到元素: {selector}",
|
|
246
|
+
)
|
|
247
|
+
if clear:
|
|
248
|
+
ele.clear()
|
|
249
|
+
ele.input(value)
|
|
250
|
+
if wait > 0:
|
|
251
|
+
time.sleep(wait)
|
|
252
|
+
return SkillResult(
|
|
253
|
+
success=True,
|
|
254
|
+
message=f"已填写 {selector}: {value[:50]}{'...' if len(value) > 50 else ''}",
|
|
255
|
+
)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
return SkillResult(success=False, error=f"填写失败: {e}")
|
|
258
|
+
|
|
259
|
+
async def screenshot(self, save_path: str = "") -> SkillResult:
|
|
260
|
+
"""页面截图"""
|
|
261
|
+
if not self._ensure_page():
|
|
262
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
if not save_path:
|
|
266
|
+
# 自动生成路径
|
|
267
|
+
from core.browser_profile import get_browser_profile_manager
|
|
268
|
+
save_dir = get_browser_profile_manager().base_dir / "screenshots"
|
|
269
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
save_path = str(
|
|
271
|
+
save_dir
|
|
272
|
+
/ f"{self.profile_name}_{int(time.time())}.png"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self._page.get_screenshot(path=save_path, full_page=False)
|
|
276
|
+
|
|
277
|
+
return SkillResult(
|
|
278
|
+
success=True,
|
|
279
|
+
message=f"截图已保存: {save_path}",
|
|
280
|
+
files=[save_path],
|
|
281
|
+
)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return SkillResult(success=False, error=f"截图失败: {e}")
|
|
284
|
+
|
|
285
|
+
async def evaluate(self, script: str) -> SkillResult:
|
|
286
|
+
"""执行 JavaScript"""
|
|
287
|
+
if not self._ensure_page():
|
|
288
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
result = self._page.run_js(script)
|
|
292
|
+
return SkillResult(
|
|
293
|
+
success=True,
|
|
294
|
+
message="JS 执行完成",
|
|
295
|
+
data={"result": str(result) if result is not None else ""},
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
return SkillResult(success=False, error=f"JS 执行失败: {e}")
|
|
299
|
+
|
|
300
|
+
async def get_content(self) -> SkillResult:
|
|
301
|
+
"""获取页面文本内容"""
|
|
302
|
+
if not self._ensure_page():
|
|
303
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
text = self._page.text or ""
|
|
307
|
+
title = self._page.title or ""
|
|
308
|
+
url = self._page.url or ""
|
|
309
|
+
|
|
310
|
+
# 限制长度避免返回过多数据
|
|
311
|
+
output_text = text[:10000] if len(text) > 10000 else text
|
|
312
|
+
|
|
313
|
+
return SkillResult(
|
|
314
|
+
success=True,
|
|
315
|
+
message=f"页面内容: {title}",
|
|
316
|
+
data={
|
|
317
|
+
"title": title,
|
|
318
|
+
"url": url,
|
|
319
|
+
"text": output_text,
|
|
320
|
+
"text_length": len(text),
|
|
321
|
+
"truncated": len(text) > 10000,
|
|
322
|
+
},
|
|
323
|
+
output=output_text,
|
|
324
|
+
)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
return SkillResult(success=False, error=f"获取页面内容失败: {e}")
|
|
327
|
+
|
|
328
|
+
async def get_html(self) -> SkillResult:
|
|
329
|
+
"""获取页面 HTML"""
|
|
330
|
+
if not self._ensure_page():
|
|
331
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
html = self._page.html or ""
|
|
335
|
+
output_html = html[:50000] if len(html) > 50000 else html
|
|
336
|
+
|
|
337
|
+
return SkillResult(
|
|
338
|
+
success=True,
|
|
339
|
+
message=f"HTML 获取完成 ({len(html)} 字符)",
|
|
340
|
+
data={
|
|
341
|
+
"url": self._page.url or "",
|
|
342
|
+
"html_length": len(html),
|
|
343
|
+
"html": output_html,
|
|
344
|
+
},
|
|
345
|
+
output=output_html,
|
|
346
|
+
)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
return SkillResult(success=False, error=f"获取 HTML 失败: {e}")
|
|
349
|
+
|
|
350
|
+
async def wait_for(
|
|
351
|
+
self, selector: str, timeout: float = 10.0
|
|
352
|
+
) -> SkillResult:
|
|
353
|
+
"""等待元素出现"""
|
|
354
|
+
if not self._ensure_page():
|
|
355
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
ele = self._page.ele(selector, timeout=timeout)
|
|
359
|
+
if ele:
|
|
360
|
+
return SkillResult(
|
|
361
|
+
success=True,
|
|
362
|
+
message=f"元素已出现: {selector}",
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
return SkillResult(
|
|
366
|
+
success=False,
|
|
367
|
+
error=f"等待超时: {selector} ({timeout}s)",
|
|
368
|
+
)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
return SkillResult(success=False, error=f"等待元素失败: {e}")
|
|
371
|
+
|
|
372
|
+
async def get_cookies(self) -> SkillResult:
|
|
373
|
+
"""获取当前页面的 Cookie"""
|
|
374
|
+
if not self._ensure_page():
|
|
375
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
cookies = self._page.cookies()
|
|
379
|
+
cookie_list = []
|
|
380
|
+
if cookies:
|
|
381
|
+
for c in cookies:
|
|
382
|
+
cookie_list.append({
|
|
383
|
+
"name": c.get("name", ""),
|
|
384
|
+
"value": c.get("value", ""),
|
|
385
|
+
"domain": c.get("domain", ""),
|
|
386
|
+
"path": c.get("path", "/"),
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
return SkillResult(
|
|
390
|
+
success=True,
|
|
391
|
+
message=f"获取到 {len(cookie_list)} 个 Cookie",
|
|
392
|
+
data={"cookies": cookie_list, "count": len(cookie_list)},
|
|
393
|
+
)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
return SkillResult(success=False, error=f"获取 Cookie 失败: {e}")
|
|
396
|
+
|
|
397
|
+
async def save_cookies(self) -> SkillResult:
|
|
398
|
+
"""保存当前 Cookie 到 Profile"""
|
|
399
|
+
if not self._ensure_page():
|
|
400
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
cookies = self._page.cookies()
|
|
404
|
+
cookie_list = []
|
|
405
|
+
if cookies:
|
|
406
|
+
for c in cookies:
|
|
407
|
+
cookie_list.append(dict(c))
|
|
408
|
+
|
|
409
|
+
from core.browser_profile import get_browser_profile_manager
|
|
410
|
+
mgr = get_browser_profile_manager()
|
|
411
|
+
profile = mgr.get_profile(self.profile_name)
|
|
412
|
+
profile.save_cookies(cookie_list)
|
|
413
|
+
|
|
414
|
+
return SkillResult(
|
|
415
|
+
success=True,
|
|
416
|
+
message=f"已保存 {len(cookie_list)} 个 Cookie 到 Profile: {self.profile_name}",
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
return SkillResult(success=False, error=f"保存 Cookie 失败: {e}")
|
|
420
|
+
|
|
421
|
+
async def load_cookies(self) -> SkillResult:
|
|
422
|
+
"""从 Profile 加载 Cookie"""
|
|
423
|
+
if not self._ensure_page():
|
|
424
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
from core.browser_profile import get_browser_profile_manager
|
|
428
|
+
mgr = get_browser_profile_manager()
|
|
429
|
+
profile = mgr.get_profile(self.profile_name)
|
|
430
|
+
cookies = profile.load_cookies()
|
|
431
|
+
|
|
432
|
+
if cookies:
|
|
433
|
+
self._page.set.cookies(cookies)
|
|
434
|
+
return SkillResult(
|
|
435
|
+
success=True,
|
|
436
|
+
message=f"已加载 {len(cookies)} 个 Cookie",
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
return SkillResult(
|
|
440
|
+
success=True,
|
|
441
|
+
message="Profile 中没有保存的 Cookie",
|
|
442
|
+
)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
return SkillResult(success=False, error=f"加载 Cookie 失败: {e}")
|
|
445
|
+
|
|
446
|
+
async def clear_cookies(self) -> SkillResult:
|
|
447
|
+
"""清除当前页面和 Profile 中的 Cookie"""
|
|
448
|
+
if not self._ensure_page():
|
|
449
|
+
return SkillResult(success=False, error="浏览器未启动")
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
self._page.clear_cookies()
|
|
453
|
+
from core.browser_profile import get_browser_profile_manager
|
|
454
|
+
mgr = get_browser_profile_manager()
|
|
455
|
+
profile = mgr.get_profile(self.profile_name)
|
|
456
|
+
profile.clear_cookies()
|
|
457
|
+
|
|
458
|
+
return SkillResult(success=True, message="Cookie 已清除")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
return SkillResult(success=False, error=f"清除 Cookie 失败: {e}")
|
|
461
|
+
|
|
462
|
+
async def wait_for_manual(
|
|
463
|
+
self, reason: str = "验证码", timeout: float = 300.0
|
|
464
|
+
) -> SkillResult:
|
|
465
|
+
"""
|
|
466
|
+
等待用户手动操作(验证码/2FA 等)。
|
|
467
|
+
|
|
468
|
+
暂停自动化流程,等待指定时间让用户通过 VNC 或 Web Control
|
|
469
|
+
手动完成验证码、短信验证等操作。
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
reason: 等待原因
|
|
473
|
+
timeout: 最长等待时间(秒),默认 5 分钟
|
|
474
|
+
"""
|
|
475
|
+
message = (
|
|
476
|
+
f"⏸ 需要用户手动操作: {reason}\n"
|
|
477
|
+
f"请通过 VNC 远程桌面或 Web Control 面板手动完成操作。\n"
|
|
478
|
+
f"最多等待 {int(timeout)} 秒..."
|
|
479
|
+
)
|
|
480
|
+
logger.info(f"[{self.profile_name}] 等待用户手动操作: {reason}")
|
|
481
|
+
|
|
482
|
+
# 简单的等待机制:轮询页面变化
|
|
483
|
+
start = time.time()
|
|
484
|
+
last_url = self._page.url if self._page else ""
|
|
485
|
+
while time.time() - start < timeout:
|
|
486
|
+
time.sleep(3)
|
|
487
|
+
if self._page:
|
|
488
|
+
current_url = self._page.url or ""
|
|
489
|
+
# 如果 URL 发生变化,说明用户完成了操作
|
|
490
|
+
if current_url != last_url and current_url:
|
|
491
|
+
elapsed = int(time.time() - start)
|
|
492
|
+
return SkillResult(
|
|
493
|
+
success=True,
|
|
494
|
+
message=f"用户已完成手动操作 ({reason}),耗时 {elapsed} 秒",
|
|
495
|
+
data={"elapsed": elapsed, "new_url": current_url},
|
|
496
|
+
)
|
|
497
|
+
last_url = current_url
|
|
498
|
+
|
|
499
|
+
return SkillResult(
|
|
500
|
+
success=False,
|
|
501
|
+
error=f"等待用户操作超时 ({int(timeout)} 秒): {reason}",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def _ensure_page(self) -> bool:
|
|
505
|
+
"""确保浏览器和页面可用"""
|
|
506
|
+
if not self._started or not self._page:
|
|
507
|
+
return False
|
|
508
|
+
try:
|
|
509
|
+
# 检查页面是否仍然有效
|
|
510
|
+
_ = self._page.url
|
|
511
|
+
return True
|
|
512
|
+
except Exception:
|
|
513
|
+
self._started = False
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _detect_browser() -> Optional[str]:
|
|
518
|
+
"""自动检测 Chromium/Chrome 浏览器路径"""
|
|
519
|
+
# 1. 环境变量
|
|
520
|
+
for key in ("CHROME_PATH", "BROWSER_PATH", "CHROMIUM_PATH"):
|
|
521
|
+
val = os.environ.get(key, "").strip()
|
|
522
|
+
if val and os.path.isfile(val):
|
|
523
|
+
return val
|
|
524
|
+
|
|
525
|
+
# 2. PATH 中的浏览器命令
|
|
526
|
+
for cmd in (
|
|
527
|
+
"google-chrome", "google-chrome-stable", "chromium-browser",
|
|
528
|
+
"chromium", "brave-browser", "microsoft-edge",
|
|
529
|
+
):
|
|
530
|
+
found = shutil.which(cmd)
|
|
531
|
+
if found:
|
|
532
|
+
return found
|
|
533
|
+
|
|
534
|
+
# 3. Linux 常见路径
|
|
535
|
+
for p in (
|
|
536
|
+
"/usr/bin/google-chrome", "/usr/bin/google-chrome-stable",
|
|
537
|
+
"/usr/bin/chromium-browser", "/usr/bin/chromium",
|
|
538
|
+
"/snap/bin/chromium",
|
|
539
|
+
):
|
|
540
|
+
if os.path.isfile(p) and os.access(p, os.X_OK):
|
|
541
|
+
return p
|
|
542
|
+
|
|
543
|
+
# 4. Puppeteer 缓存
|
|
544
|
+
home = os.path.expanduser("~")
|
|
545
|
+
for cache in (
|
|
546
|
+
os.path.join(home, ".cache", "puppeteer", "chrome"),
|
|
547
|
+
os.path.join(home, ".cache", "ms-playwright"),
|
|
548
|
+
):
|
|
549
|
+
if os.path.isdir(cache):
|
|
550
|
+
for root, _, files in os.walk(cache):
|
|
551
|
+
for fname in files:
|
|
552
|
+
if fname in ("chrome", "chromium", "headless_shell"):
|
|
553
|
+
full = os.path.join(root, fname)
|
|
554
|
+
if os.access(full, os.X_OK):
|
|
555
|
+
return full
|
|
556
|
+
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ── 全局浏览器实例管理 ──────────────────────────────────────
|
|
561
|
+
|
|
562
|
+
_browsers: Dict[str, StealthBrowser] = {}
|
|
563
|
+
_browser_lock = asyncio.Lock()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
async def get_stealth_browser(
|
|
567
|
+
profile_name: str = "default",
|
|
568
|
+
headless: bool = False,
|
|
569
|
+
) -> StealthBrowser:
|
|
570
|
+
"""
|
|
571
|
+
获取反检测浏览器实例(按 profile_name 复用)。
|
|
572
|
+
|
|
573
|
+
同一 profile 名称返回相同实例,避免重复启动。
|
|
574
|
+
"""
|
|
575
|
+
async with _browser_lock:
|
|
576
|
+
if profile_name in _browsers:
|
|
577
|
+
browser = _browsers[profile_name]
|
|
578
|
+
if browser._started and browser._ensure_page():
|
|
579
|
+
return browser
|
|
580
|
+
|
|
581
|
+
browser = StealthBrowser(profile_name=profile_name, headless=headless)
|
|
582
|
+
_browsers[profile_name] = browser
|
|
583
|
+
return browser
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
async def close_stealth_browser(profile_name: str = "") -> None:
|
|
587
|
+
"""关闭浏览器实例"""
|
|
588
|
+
async with _browser_lock:
|
|
589
|
+
if profile_name and profile_name in _browsers:
|
|
590
|
+
await _browsers[profile_name].close()
|
|
591
|
+
del _browsers[profile_name]
|
|
592
|
+
elif not profile_name:
|
|
593
|
+
for name, browser in _browsers.items():
|
|
594
|
+
try:
|
|
595
|
+
await browser.close()
|
|
596
|
+
except Exception:
|
|
597
|
+
pass
|
|
598
|
+
_browsers.clear()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# ── 技能类(供 SkillRegistry 自动发现)─────────────────────
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class StealthBrowserStartSkill(Skill):
|
|
605
|
+
"""启动反检测浏览器"""
|
|
606
|
+
|
|
607
|
+
name = "stealth_browser_start"
|
|
608
|
+
description = (
|
|
609
|
+
"启动反检测浏览器 — 使用 DrissionPage 启动带有反检测能力的 Chromium 浏览器,"
|
|
610
|
+
"适合需要登录的网站操作(Google/X.com/微博/抖音等)。"
|
|
611
|
+
"每个网站使用独立 Profile,登录状态自动保持。"
|
|
612
|
+
)
|
|
613
|
+
category = "browser_stealth"
|
|
614
|
+
parameters = [
|
|
615
|
+
SkillParameter(
|
|
616
|
+
name="profile",
|
|
617
|
+
type="string",
|
|
618
|
+
description="Profile 名称(预置: google/gmail/x_com/weibo/wechat_mp/douyin/mail139/bilibili/github)",
|
|
619
|
+
required=False,
|
|
620
|
+
default="default",
|
|
621
|
+
),
|
|
622
|
+
SkillParameter(
|
|
623
|
+
name="headless",
|
|
624
|
+
type="boolean",
|
|
625
|
+
description="是否无头模式(登录场景建议 false,默认 false)",
|
|
626
|
+
required=False,
|
|
627
|
+
default=False,
|
|
628
|
+
),
|
|
629
|
+
]
|
|
630
|
+
dangerous = False
|
|
631
|
+
|
|
632
|
+
async def execute(self, profile: str = "default", headless: bool = False, **kw) -> SkillResult:
|
|
633
|
+
browser = await get_stealth_browser(profile_name=profile, headless=headless)
|
|
634
|
+
return await browser.start()
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class StealthBrowserNavigateSkill(Skill):
|
|
638
|
+
"""反检测浏览器导航"""
|
|
639
|
+
|
|
640
|
+
name = "stealth_browser_navigate"
|
|
641
|
+
description = "反检测浏览器导航 — 导航到指定 URL"
|
|
642
|
+
category = "browser_stealth"
|
|
643
|
+
parameters = [
|
|
644
|
+
SkillParameter(name="url", type="string", description="目标 URL", required=True),
|
|
645
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
646
|
+
]
|
|
647
|
+
|
|
648
|
+
async def execute(self, url: str, profile: str = "default", **kw) -> SkillResult:
|
|
649
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
650
|
+
if not browser._started:
|
|
651
|
+
r = await browser.start()
|
|
652
|
+
if not r.success:
|
|
653
|
+
return r
|
|
654
|
+
return await browser.navigate(url)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class StealthBrowserClickSkill(Skill):
|
|
658
|
+
"""反检测浏览器点击"""
|
|
659
|
+
|
|
660
|
+
name = "stealth_browser_click"
|
|
661
|
+
description = "反检测浏览器点击 — 点击页面元素(支持 CSS 选择器、文本匹配等)"
|
|
662
|
+
category = "browser_stealth"
|
|
663
|
+
parameters = [
|
|
664
|
+
SkillParameter(name="selector", type="string", description="元素选择器 (CSS/文本/XPath)", required=True),
|
|
665
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
async def execute(self, selector: str, profile: str = "default", **kw) -> SkillResult:
|
|
669
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
670
|
+
return await browser.click(selector)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class StealthBrowserFillSkill(Skill):
|
|
674
|
+
"""反检测浏览器填写"""
|
|
675
|
+
|
|
676
|
+
name = "stealth_browser_fill"
|
|
677
|
+
description = "反检测浏览器填写 — 填写输入框(支持 CSS 选择器定位)"
|
|
678
|
+
category = "browser_stealth"
|
|
679
|
+
parameters = [
|
|
680
|
+
SkillParameter(name="selector", type="string", description="输入框选择器", required=True),
|
|
681
|
+
SkillParameter(name="value", type="string", description="要输入的文本", required=True),
|
|
682
|
+
SkillParameter(name="clear", type="boolean", description="是否先清空", required=False, default=True),
|
|
683
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
684
|
+
]
|
|
685
|
+
|
|
686
|
+
async def execute(
|
|
687
|
+
self, selector: str, value: str, clear: bool = True, profile: str = "default", **kw
|
|
688
|
+
) -> SkillResult:
|
|
689
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
690
|
+
return await browser.fill(selector, value, clear=clear)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class StealthBrowserScreenshotSkill(Skill):
|
|
694
|
+
"""反检测浏览器截图"""
|
|
695
|
+
|
|
696
|
+
name = "stealth_browser_screenshot"
|
|
697
|
+
description = "反检测浏览器截图 — 截取当前页面截图并返回文件路径"
|
|
698
|
+
category = "browser_stealth"
|
|
699
|
+
parameters = [
|
|
700
|
+
SkillParameter(name="save_path", type="string", description="保存路径(空=自动)", required=False, default=""),
|
|
701
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
702
|
+
]
|
|
703
|
+
|
|
704
|
+
async def execute(self, save_path: str = "", profile: str = "default", **kw) -> SkillResult:
|
|
705
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
706
|
+
return await browser.screenshot(save_path=save_path)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class StealthBrowserEvalSkill(Skill):
|
|
710
|
+
"""反检测浏览器执行 JS"""
|
|
711
|
+
|
|
712
|
+
name = "stealth_browser_eval"
|
|
713
|
+
description = "反检测浏览器执行 JavaScript — 在页面中执行自定义 JS 代码"
|
|
714
|
+
category = "browser_stealth"
|
|
715
|
+
parameters = [
|
|
716
|
+
SkillParameter(name="script", type="string", description="JavaScript 代码", required=True),
|
|
717
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
async def execute(self, script: str, profile: str = "default", **kw) -> SkillResult:
|
|
721
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
722
|
+
return await browser.evaluate(script)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class StealthBrowserGetContentSkill(Skill):
|
|
726
|
+
"""反检测浏览器获取内容"""
|
|
727
|
+
|
|
728
|
+
name = "stealth_browser_content"
|
|
729
|
+
description = "反检测浏览器获取页面内容 — 获取当前页面的文本内容(用于读取邮件/帖子等)"
|
|
730
|
+
category = "browser_stealth"
|
|
731
|
+
parameters = [
|
|
732
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
async def execute(self, profile: str = "default", **kw) -> SkillResult:
|
|
736
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
737
|
+
return await browser.get_content()
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class StealthBrowserCookieSkill(Skill):
|
|
741
|
+
"""反检测浏览器 Cookie 管理"""
|
|
742
|
+
|
|
743
|
+
name = "stealth_browser_cookies"
|
|
744
|
+
description = (
|
|
745
|
+
"反检测浏览器 Cookie 管理 — 保存/加载/获取/清除 Cookie。"
|
|
746
|
+
"save=保存当前Cookie到Profile, load=从Profile加载Cookie, "
|
|
747
|
+
"get=获取当前Cookie列表, clear=清除Cookie"
|
|
748
|
+
)
|
|
749
|
+
category = "browser_stealth"
|
|
750
|
+
parameters = [
|
|
751
|
+
SkillParameter(
|
|
752
|
+
name="action",
|
|
753
|
+
type="string",
|
|
754
|
+
description="操作: save/load/get/clear",
|
|
755
|
+
required=True,
|
|
756
|
+
enum=["save", "load", "get", "clear"],
|
|
757
|
+
),
|
|
758
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
async def execute(self, action: str, profile: str = "default", **kw) -> SkillResult:
|
|
762
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
763
|
+
if action == "save":
|
|
764
|
+
return await browser.save_cookies()
|
|
765
|
+
elif action == "load":
|
|
766
|
+
return await browser.load_cookies()
|
|
767
|
+
elif action == "get":
|
|
768
|
+
return await browser.get_cookies()
|
|
769
|
+
elif action == "clear":
|
|
770
|
+
return await browser.clear_cookies()
|
|
771
|
+
else:
|
|
772
|
+
return SkillResult(success=False, error=f"未知操作: {action}")
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class StealthBrowserCloseSkill(Skill):
|
|
776
|
+
"""关闭反检测浏览器"""
|
|
777
|
+
|
|
778
|
+
name = "stealth_browser_close"
|
|
779
|
+
description = "关闭反检测浏览器 — 关闭指定 Profile 的浏览器实例"
|
|
780
|
+
category = "browser_stealth"
|
|
781
|
+
parameters = [
|
|
782
|
+
SkillParameter(name="profile", type="string", description="Profile 名称(空=关闭所有)", required=False, default=""),
|
|
783
|
+
]
|
|
784
|
+
|
|
785
|
+
async def execute(self, profile: str = "", **kw) -> SkillResult:
|
|
786
|
+
await close_stealth_browser(profile_name=profile)
|
|
787
|
+
return SkillResult(success=True, message="浏览器已关闭")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class StealthBrowserWaitManualSkill(Skill):
|
|
791
|
+
"""等待用户手动操作(验证码/2FA)"""
|
|
792
|
+
|
|
793
|
+
name = "stealth_browser_wait_manual"
|
|
794
|
+
description = (
|
|
795
|
+
"等待用户手动操作 — 遇到验证码/短信验证/2FA 等需要人工干预的场景时调用。"
|
|
796
|
+
"暂停自动化流程,等待用户通过 VNC 或 Web Control 面板手动完成操作。"
|
|
797
|
+
"检测到 URL 变化后自动恢复。"
|
|
798
|
+
)
|
|
799
|
+
category = "browser_stealth"
|
|
800
|
+
parameters = [
|
|
801
|
+
SkillParameter(
|
|
802
|
+
name="reason",
|
|
803
|
+
type="string",
|
|
804
|
+
description="等待原因(如: 验证码/短信验证/2FA)",
|
|
805
|
+
required=False,
|
|
806
|
+
default="验证码",
|
|
807
|
+
),
|
|
808
|
+
SkillParameter(
|
|
809
|
+
name="timeout",
|
|
810
|
+
type="integer",
|
|
811
|
+
description="最长等待秒数(默认 300)",
|
|
812
|
+
required=False,
|
|
813
|
+
default=300,
|
|
814
|
+
),
|
|
815
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
816
|
+
]
|
|
817
|
+
|
|
818
|
+
async def execute(
|
|
819
|
+
self, reason: str = "验证码", timeout: int = 300, profile: str = "default", **kw
|
|
820
|
+
) -> SkillResult:
|
|
821
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
822
|
+
return await browser.wait_for_manual(reason=reason, timeout=float(timeout))
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class StealthBrowserWaitSkill(Skill):
|
|
826
|
+
"""等待元素出现"""
|
|
827
|
+
|
|
828
|
+
name = "stealth_browser_wait"
|
|
829
|
+
description = "反检测浏览器等待 — 等待页面元素出现"
|
|
830
|
+
category = "browser_stealth"
|
|
831
|
+
parameters = [
|
|
832
|
+
SkillParameter(name="selector", type="string", description="元素选择器", required=True),
|
|
833
|
+
SkillParameter(name="timeout", type="integer", description="超时秒数(默认 10)", required=False, default=10),
|
|
834
|
+
SkillParameter(name="profile", type="string", description="Profile 名称", required=False, default="default"),
|
|
835
|
+
]
|
|
836
|
+
|
|
837
|
+
async def execute(
|
|
838
|
+
self, selector: str, timeout: int = 10, profile: str = "default", **kw
|
|
839
|
+
) -> SkillResult:
|
|
840
|
+
browser = await get_stealth_browser(profile_name=profile)
|
|
841
|
+
return await browser.wait_for(selector, timeout=float(timeout))
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# ── Profile 管理技能 ──────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class BrowserProfileListSkill(Skill):
|
|
848
|
+
"""列出所有浏览器 Profile"""
|
|
849
|
+
|
|
850
|
+
name = "browser_profile_list"
|
|
851
|
+
description = "列出所有浏览器 Profile — 显示已创建的网站 Profile 及其状态"
|
|
852
|
+
category = "browser_stealth"
|
|
853
|
+
parameters = []
|
|
854
|
+
|
|
855
|
+
async def execute(self, **kw) -> SkillResult:
|
|
856
|
+
from core.browser_profile import get_browser_profile_manager
|
|
857
|
+
mgr = get_browser_profile_manager()
|
|
858
|
+
profiles = mgr.list_profiles()
|
|
859
|
+
presets = mgr.get_preset_names()
|
|
860
|
+
uninitialized = [p for p in presets if not any(x["name"] == p for x in profiles)]
|
|
861
|
+
|
|
862
|
+
info = {
|
|
863
|
+
"profiles": profiles,
|
|
864
|
+
"available_presets": presets,
|
|
865
|
+
"uninitialized_presets": uninitialized,
|
|
866
|
+
"total_size_mb": round(mgr.get_total_size_mb(), 2),
|
|
867
|
+
}
|
|
868
|
+
return SkillResult(
|
|
869
|
+
success=True,
|
|
870
|
+
message=f"共 {len(profiles)} 个 Profile",
|
|
871
|
+
data=info,
|
|
872
|
+
output=json.dumps(info, ensure_ascii=False, indent=2),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
class BrowserProfileCreateSkill(Skill):
|
|
877
|
+
"""创建浏览器 Profile"""
|
|
878
|
+
|
|
879
|
+
name = "browser_profile_create"
|
|
880
|
+
description = (
|
|
881
|
+
"创建浏览器 Profile — 为网站创建独立的浏览器 Profile,"
|
|
882
|
+
"每个 Profile 有独立的 Cookie 和登录状态。"
|
|
883
|
+
)
|
|
884
|
+
category = "browser_stealth"
|
|
885
|
+
parameters = [
|
|
886
|
+
SkillParameter(name="name", type="string", description="Profile 名称", required=True),
|
|
887
|
+
SkillParameter(name="display_name", type="string", description="显示名称", required=False, default=""),
|
|
888
|
+
SkillParameter(name="login_url", type="string", description="登录页 URL", required=False, default=""),
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
async def execute(
|
|
892
|
+
self, name: str, display_name: str = "", login_url: str = "", **kw
|
|
893
|
+
) -> SkillResult:
|
|
894
|
+
from core.browser_profile import get_browser_profile_manager
|
|
895
|
+
mgr = get_browser_profile_manager()
|
|
896
|
+
profile = mgr.create_profile(
|
|
897
|
+
name=name,
|
|
898
|
+
display_name=display_name,
|
|
899
|
+
login_url=login_url,
|
|
900
|
+
)
|
|
901
|
+
return SkillResult(
|
|
902
|
+
success=True,
|
|
903
|
+
message=f"Profile 已创建: {name}",
|
|
904
|
+
data=profile.to_dict(),
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
class BrowserProfileDeleteSkill(Skill):
|
|
909
|
+
"""删除浏览器 Profile"""
|
|
910
|
+
|
|
911
|
+
name = "browser_profile_delete"
|
|
912
|
+
description = "删除浏览器 Profile — 删除指定网站的 Profile(包括 Cookie 和用户数据)"
|
|
913
|
+
category = "browser_stealth"
|
|
914
|
+
parameters = [
|
|
915
|
+
SkillParameter(name="name", type="string", description="Profile 名称", required=True),
|
|
916
|
+
]
|
|
917
|
+
|
|
918
|
+
async def execute(self, name: str, **kw) -> SkillResult:
|
|
919
|
+
from core.browser_profile import get_browser_profile_manager
|
|
920
|
+
mgr = get_browser_profile_manager()
|
|
921
|
+
ok = mgr.delete_profile(name)
|
|
922
|
+
if ok:
|
|
923
|
+
return SkillResult(success=True, message=f"Profile 已删除: {name}")
|
|
924
|
+
return SkillResult(success=False, error=f"删除失败: {name}")
|