myagent-ai 1.30.1 → 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.
@@ -669,15 +669,15 @@ class StealthBrowserStartSkill(Skill):
669
669
  name = "stealth_browser_start"
670
670
  description = (
671
671
  "启动反检测浏览器 — 使用 DrissionPage 启动带有反检测能力的 Chromium 浏览器,"
672
- "适合需要登录的网站操作(Google/X.com/微博/抖音等)。"
673
- "每个网站使用独立 Profile,登录状态自动保持。"
672
+ "适合需要登录的网站操作。每个网站使用独立 Profile,登录状态自动保持。"
673
+ "使用 site-manage list 查看所有可用网站。"
674
674
  )
675
675
  category = "browser_stealth"
676
676
  parameters = [
677
677
  SkillParameter(
678
678
  name="profile",
679
679
  type="string",
680
- description="Profile 名称(预置: google/gmail/x_com/weibo/wechat_mp/douyin/mail139/bilibili/github)",
680
+ description="Profile 名称(使用 site-manage list 查看可用网站)",
681
681
  required=False,
682
682
  default="default",
683
683
  ),
@@ -692,7 +692,8 @@ class StealthBrowserStartSkill(Skill):
692
692
  dangerous = False
693
693
 
694
694
  async def execute(self, profile: str = "default", headless: bool = False, **kw) -> SkillResult:
695
- browser = await get_stealth_browser(profile_name=profile, headless=headless)
695
+ # Bug Fix: get_stealth_browser 是同步函数,不能用 await
696
+ browser = get_stealth_browser(profile_name=profile, headless=headless)
696
697
  return await browser.start()
697
698
 
698
699
 
@@ -708,7 +709,7 @@ class StealthBrowserNavigateSkill(Skill):
708
709
  ]
709
710
 
710
711
  async def execute(self, url: str, profile: str = "default", **kw) -> SkillResult:
711
- browser = await get_stealth_browser(profile_name=profile)
712
+ browser = get_stealth_browser(profile_name=profile)
712
713
  if not browser._started:
713
714
  r = await browser.start()
714
715
  if not r.success:
@@ -728,7 +729,7 @@ class StealthBrowserClickSkill(Skill):
728
729
  ]
729
730
 
730
731
  async def execute(self, selector: str, profile: str = "default", **kw) -> SkillResult:
731
- browser = await get_stealth_browser(profile_name=profile)
732
+ browser = get_stealth_browser(profile_name=profile)
732
733
  return await browser.click(selector)
733
734
 
734
735
 
@@ -748,7 +749,7 @@ class StealthBrowserFillSkill(Skill):
748
749
  async def execute(
749
750
  self, selector: str, value: str, clear: bool = True, profile: str = "default", **kw
750
751
  ) -> SkillResult:
751
- browser = await get_stealth_browser(profile_name=profile)
752
+ browser = get_stealth_browser(profile_name=profile)
752
753
  return await browser.fill(selector, value, clear=clear)
753
754
 
754
755
 
@@ -764,7 +765,7 @@ class StealthBrowserScreenshotSkill(Skill):
764
765
  ]
765
766
 
766
767
  async def execute(self, save_path: str = "", profile: str = "default", **kw) -> SkillResult:
767
- browser = await get_stealth_browser(profile_name=profile)
768
+ browser = get_stealth_browser(profile_name=profile)
768
769
  return await browser.screenshot(save_path=save_path)
769
770
 
770
771
 
@@ -780,7 +781,7 @@ class StealthBrowserEvalSkill(Skill):
780
781
  ]
781
782
 
782
783
  async def execute(self, script: str, profile: str = "default", **kw) -> SkillResult:
783
- browser = await get_stealth_browser(profile_name=profile)
784
+ browser = get_stealth_browser(profile_name=profile)
784
785
  return await browser.evaluate(script)
785
786
 
786
787
 
@@ -795,7 +796,7 @@ class StealthBrowserGetContentSkill(Skill):
795
796
  ]
796
797
 
797
798
  async def execute(self, profile: str = "default", **kw) -> SkillResult:
798
- browser = await get_stealth_browser(profile_name=profile)
799
+ browser = get_stealth_browser(profile_name=profile)
799
800
  return await browser.get_content()
800
801
 
801
802
 
@@ -821,7 +822,7 @@ class StealthBrowserCookieSkill(Skill):
821
822
  ]
822
823
 
823
824
  async def execute(self, action: str, profile: str = "default", **kw) -> SkillResult:
824
- browser = await get_stealth_browser(profile_name=profile)
825
+ browser = get_stealth_browser(profile_name=profile)
825
826
  if action == "save":
826
827
  return await browser.save_cookies()
827
828
  elif action == "load":
@@ -881,7 +882,7 @@ class StealthBrowserWaitManualSkill(Skill):
881
882
  async def execute(
882
883
  self, reason: str = "验证码", timeout: int = 300, profile: str = "default", **kw
883
884
  ) -> SkillResult:
884
- browser = await get_stealth_browser(profile_name=profile)
885
+ browser = get_stealth_browser(profile_name=profile)
885
886
  return await browser.wait_for_manual(reason=reason, timeout=float(timeout))
886
887
 
887
888
 
@@ -900,10 +901,149 @@ class StealthBrowserWaitSkill(Skill):
900
901
  async def execute(
901
902
  self, selector: str, timeout: int = 10, profile: str = "default", **kw
902
903
  ) -> SkillResult:
903
- browser = await get_stealth_browser(profile_name=profile)
904
+ browser = get_stealth_browser(profile_name=profile)
904
905
  return await browser.wait_for(selector, timeout=float(timeout))
905
906
 
906
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
+
907
1047
  # ── Profile 管理技能 ──────────────────────────────────────
908
1048
 
909
1049
 
@@ -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)"},
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: site-operations
3
+ description: |
4
+ 统一网站操作指南 — 使用反检测浏览器(DrissionPage)登录和操作任何网站。
5
+ 覆盖邮箱、社交媒体、云服务、AI 对话平台等 20+ 网站,支持动态扩展。
6
+ metadata:
7
+ category: site_operation
8
+ version: "1.31.0"
9
+ requires: ["stealth-browser"]
10
+ ---
11
+
12
+ # 统一网站操作指南
13
+
14
+ ## 概述
15
+
16
+ 本指南使用反检测浏览器(stealth browser)登录和操作各种网站。
17
+ 所有网站共享同一套命令接口(stealth-*),只是 Profile 名称和 URL 不同。
18
+
19
+ ## 支持的网站
20
+
21
+ 使用 `myagent-ai site-manage list` 查看所有可用网站。
22
+
23
+ ### 分类一览
24
+
25
+ | 分类 | 网站 | Profile 名称 |
26
+ |------|------|-------------|
27
+ | 邮箱 | Gmail, QQ邮箱, 139邮箱 | gmail, qq_mail, mail139 |
28
+ | 社交 | X.com, 微博, 微信网页版, 微信公众号 | x_com, weibo, wechat_web, wechat_mp |
29
+ | 云服务 | 百度网盘 | baidu_netdisk |
30
+ | AI 对话 | Gemini, DeepSeek, z.ai, 智谱清言, 豆包, Kimi, 腾讯元宝, 通义千问 | gemini, deepseek, z_ai, zhipu, doubao, kimi, tencent_yuanbao, qianwen |
31
+ | 开发 | GitHub, Google 账号 | github, google |
32
+ | 内容 | 抖音, 哔哩哔哩 | douyin, bilibili |
33
+
34
+ ## 通用操作流程
35
+
36
+ ### 1. 首次登录(通用模板)
37
+
38
+ 任何网站的首次登录流程都遵循以下模板:
39
+
40
+ ```bash
41
+ # 第1步:查看网站配置
42
+ myagent-ai site-manage show <profile_name>
43
+
44
+ # 第2步:启动浏览器(有头模式,方便观察和手动操作)
45
+ myagent-ai stealth-open <profile_name> --no-headless
46
+
47
+ # 第3步:导航到登录页
48
+ myagent-ai stealth-navigate <login_url> -p <profile_name>
49
+
50
+ # 第4步:尝试自动填写(如果页面允许)
51
+ myagent-ai stealth-screenshot -p <profile_name>
52
+ myagent-ai stealth-fill '<用户名选择器>' '用户名' -p <profile_name>
53
+ myagent-ai stealth-click '<下一步按钮选择器>' -p <profile_name>
54
+ myagent-ai stealth-wait '<密码输入框选择器>' -p <profile_name>
55
+ myagent-ai stealth-fill '<密码选择器>' '密码' -p <profile_name>
56
+ myagent-ai stealth-click '<登录按钮选择器>' -p <profile_name>
57
+
58
+ # 第5步:如果出现验证码/2FA,等待用户手动操作
59
+ myagent-ai stealth-wait-manual "验证码或安全验证" -t 300 -p <profile_name>
60
+
61
+ # 第6步:确认登录成功并保存 Cookie
62
+ myagent-ai stealth-cookies save -p <profile_name>
63
+ myagent-ai stealth-close <profile_name>
64
+ ```
65
+
66
+ ### 2. 后续操作(已登录)
67
+
68
+ ```bash
69
+ # 启动浏览器(自动恢复登录状态)
70
+ myagent-ai stealth-open <profile_name> --no-headless
71
+
72
+ # 导航到目标页面
73
+ myagent-ai stealth-navigate <目标URL> -p <profile_name>
74
+
75
+ # 等待页面加载
76
+ myagent-ai stealth-wait '<关键元素选择器>' -t 15 -p <profile_name>
77
+
78
+ # 截图确认
79
+ myagent-ai stealth-screenshot -p <profile_name>
80
+
81
+ # 获取页面内容
82
+ myagent-ai stealth-content -p <profile_name>
83
+
84
+ # 点击操作
85
+ myagent-ai stealth-click '<选择器>' -p <profile_name>
86
+
87
+ # 填写输入
88
+ myagent-ai stealth-fill '<选择器>' '内容' -p <profile_name>
89
+
90
+ # 执行 JavaScript(高级操作)
91
+ myagent-ai stealth-eval 'document.title' -p <profile_name>
92
+
93
+ # 完成后关闭
94
+ myagent-ai stealth-close <profile_name>
95
+ ```
96
+
97
+ ## 查看网站详情
98
+
99
+ 使用 `site-manage show` 查看具体网站的登录 URL、可用页面和操作提示:
100
+
101
+ ```bash
102
+ # 查看 Gmail 的配置
103
+ myagent-ai site-manage show gmail
104
+
105
+ # 查看 DeepSeek 的配置
106
+ myagent-ai site-manage show deepseek
107
+ ```
108
+
109
+ ## 动态管理网站
110
+
111
+ ```bash
112
+ # 列出所有网站
113
+ myagent-ai site-manage list
114
+
115
+ # 按分类查看
116
+ myagent-ai site-manage list --category ai_chat
117
+
118
+ # 添加自定义网站
119
+ myagent-ai site-manage add my_site --display "我的网站" --login-url "https://example.com/login" --home-url "https://example.com/dashboard"
120
+
121
+ # 更新网站配置
122
+ myagent-ai site-manage update my_site --display "新名称"
123
+
124
+ # 删除自定义网站(内置网站不可删除)
125
+ myagent-ai site-manage remove my_site
126
+
127
+ # 批量初始化所有网站的 Profile
128
+ myagent-ai site-manage init
129
+ ```
130
+
131
+ ## 选择器技巧
132
+
133
+ - CSS 选择器: `input[type="email"]`, `button.submit`, `div#main`
134
+ - 文本匹配: `button:contains("登录")`, `a:contains("下一步")`
135
+ - 属性匹配: `input[name="password"]`, `[data-testid="login-btn"]`
136
+ - XPath: `//div[@class="login-form"]//input`
137
+
138
+ 不确定选择器时,先用 `stealth-screenshot` 截图查看页面结构,再用 `stealth-eval` 测试选择器:
139
+ ```bash
140
+ myagent-ai stealth-eval "document.querySelectorAll('button').forEach((b,i) => console.log(i, b.textContent))" -p <profile>
141
+ ```
142
+
143
+ ## 重要提示
144
+
145
+ 1. **反检测模式**:stealth browser 使用 DrissionPage,自动隐藏 webdriver 标志
146
+ 2. **Profile 隔离**:每个网站独立 Profile,Cookie/Session 互不干扰
147
+ 3. **验证码处理**:遇到验证码时使用 `stealth-wait-manual`,通过 VNC/Web 手动处理
148
+ 4. **操作间隔**:建议操作间等待 2-3 秒,避免被网站检测为自动化
149
+ 5. **Cookie 持久化**:登录后执行 `stealth-cookies save`,下次自动恢复
150
+ 6. **有头模式**:登录场景建议用 `--no-headless`,方便观察和手动干预
@@ -41,54 +41,48 @@ from core.logger import get_logger
41
41
 
42
42
  logger = get_logger("myagent.core.browser_profile")
43
43
 
44
- # 预置网站 Profile 名称(用于快速创建)
45
- PRESET_PROFILES: Dict[str, Dict[str, str]] = {
46
- "google": {
47
- "display_name": "Google",
48
- "login_url": "https://accounts.google.com/",
49
- "detect_url": "https://myaccount.google.com/",
50
- },
51
- "gmail": {
52
- "display_name": "Gmail",
53
- "login_url": "https://accounts.google.com/signin",
54
- "detect_url": "https://mail.google.com/mail/u/0/",
55
- },
56
- "x_com": {
57
- "display_name": "X.com (Twitter)",
58
- "login_url": "https://x.com/i/flow/login",
59
- "detect_url": "https://x.com/home",
60
- },
61
- "weibo": {
62
- "display_name": "微博",
63
- "login_url": "https://passport.weibo.com/sso/signin",
64
- "detect_url": "https://weibo.com/",
65
- },
66
- "wechat_mp": {
67
- "display_name": "微信公众号",
68
- "login_url": "https://mp.weixin.qq.com/",
69
- "detect_url": "https://mp.weixin.qq.com/cgi-bin/home?t=home/index",
70
- },
71
- "douyin": {
72
- "display_name": "抖音",
73
- "login_url": "https://www.douyin.com/",
74
- "detect_url": "https://www.douyin.com/",
75
- },
76
- "mail139": {
77
- "display_name": "139邮箱",
78
- "login_url": "https://mail.10086.cn/",
79
- "detect_url": "https://mail.10086.cn/",
80
- },
81
- "bilibili": {
82
- "display_name": "哔哩哔哩",
83
- "login_url": "https://passport.bilibili.com/login",
84
- "detect_url": "https://www.bilibili.com/",
85
- },
86
- "github": {
87
- "display_name": "GitHub",
88
- "login_url": "https://github.com/login",
89
- "detect_url": "https://github.com/",
90
- },
91
- }
44
+
45
+ # ════════════════════════════════════════════════════════════
46
+ # 预置 Profile(兼容性保留 — 优先从 site_registry 加载)
47
+ # ════════════════════════════════════════════════════════════
48
+ # v1.31.0: PRESET_PROFILES 不再硬编码,改为从 core.site_registry 动态获取。
49
+ # 此处保留为空 dict,由 get_preset_profiles() 延迟加载。
50
+
51
+ _PRESET_PROFILES_CACHE: Optional[Dict[str, Dict[str, str]]] = None
52
+
53
+
54
+ def get_preset_profiles() -> Dict[str, Dict[str, str]]:
55
+ """获取预置 Profile 配置(从 SiteRegistry 延迟加载)。
56
+
57
+ 返回 {name: {display_name, login_url, detect_url}} 格式,
58
+ 保持与旧 PRESET_PROFILES 完全兼容。
59
+ """
60
+ global _PRESET_PROFILES_CACHE
61
+ if _PRESET_PROFILES_CACHE is not None:
62
+ return _PRESET_PROFILES_CACHE
63
+
64
+ try:
65
+ from core.site_registry import get_site_registry
66
+ reg = get_site_registry()
67
+ sites = reg.list_sites()
68
+ _PRESET_PROFILES_CACHE = {}
69
+ for s in sites:
70
+ _PRESET_PROFILES_CACHE[s["name"]] = {
71
+ "display_name": s["display_name"],
72
+ "login_url": s.get("login_url", ""),
73
+ "detect_url": s.get("detect_url", s.get("home_url", "")),
74
+ }
75
+ logger.debug(f"从 SiteRegistry 加载了 {len(_PRESET_PROFILES_CACHE)} 个预置 Profile")
76
+ return _PRESET_PROFILES_CACHE
77
+ except Exception as e:
78
+ logger.warning(f" SiteRegistry 加载失败,使用空预置: {e}")
79
+ _PRESET_PROFILES_CACHE = {}
80
+ return _PRESET_PROFILES_CACHE
81
+
82
+
83
+ # 向后兼容别名
84
+ PRESET_PROFILES = {} # 由 get_preset_profiles() 延迟填充,直接引用会得到空 dict
85
+ # 注意:BrowserProfile.__init__ 已改用 get_preset_profiles(),不再直接读 PRESET_PROFILES
92
86
 
93
87
 
94
88
  class BrowserProfile:
@@ -112,8 +106,9 @@ class BrowserProfile:
112
106
  self.cookies_file = self.profile_dir / "cookies.json"
113
107
  self.config_file = self.profile_dir / "config.json"
114
108
 
115
- # 预置配置
116
- preset = PRESET_PROFILES.get(name, {})
109
+ # 预置配置(从 SiteRegistry 延迟加载)
110
+ _presets = get_preset_profiles()
111
+ preset = _presets.get(name, {})
117
112
  self.display_name = (config or preset).get("display_name", name)
118
113
  self.login_url = (config or preset).get("login_url", "")
119
114
  self.detect_url = (config or preset).get("detect_url", "")
@@ -394,18 +389,20 @@ class BrowserProfileManager:
394
389
  return profile.delete_profile()
395
390
 
396
391
  def get_preset_names(self) -> List[str]:
397
- """获取所有预置 Profile 名称列表"""
398
- return list(PRESET_PROFILES.keys())
392
+ """获取所有预置 Profile 名称列表(从 SiteRegistry 动态获取)"""
393
+ return list(get_preset_profiles().keys())
399
394
 
400
395
  def init_presets(self) -> List[str]:
401
396
  """
402
397
  初始化所有预置 Profile(仅创建目录结构,不自动登录)。
398
+ 预置列表从 SiteRegistry 动态获取。
403
399
 
404
400
  Returns:
405
401
  已初始化的 Profile 名称列表
406
402
  """
403
+ _presets = get_preset_profiles()
407
404
  initialized = []
408
- for name, config in PRESET_PROFILES.items():
405
+ for name, config in _presets.items():
409
406
  profile = self.get_profile(name)
410
407
  if not profile.is_initialized():
411
408
  profile.ensure_dirs()