myagent-ai 1.31.1 → 1.31.3
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/package.json +1 -1
- package/web/api_server.py +309 -68
- package/web/ui/admin/admin-core.js +1 -1
- package/web/ui/admin/admin-sites.js +341 -0
- package/web/ui/admin/admin-skills.js +4 -0
- package/web/ui/index.html +2 -0
- package/worklog.md +24 -0
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -224,6 +224,9 @@ class ApiServer:
|
|
|
224
224
|
self._model_chain_lock = asyncio.Lock()
|
|
225
225
|
self._setup_routes()
|
|
226
226
|
self._runner: Optional[web.AppRunner] = None
|
|
227
|
+
# [v1.31.1] Agent 初始化锁:防止并发请求重复创建全权Agent和配置助手
|
|
228
|
+
self._agents_initialized = False
|
|
229
|
+
self._agents_init_lock = asyncio.Lock()
|
|
227
230
|
|
|
228
231
|
# ── 统一热更新辅助方法 ──
|
|
229
232
|
def _hot_reload_llm(self):
|
|
@@ -492,6 +495,18 @@ class ApiServer:
|
|
|
492
495
|
r.add_get(r"/api/departments/{path:[\w%/-]+}", self.handle_get_dept)
|
|
493
496
|
r.add_put(r"/api/departments/{path:[\w%/-]+}", self.handle_update_dept)
|
|
494
497
|
r.add_delete(r"/api/departments/{path:[\w%/-]+}", self.handle_delete_dept)
|
|
498
|
+
# ── 网站管理 (SiteRegistry) ──
|
|
499
|
+
r.add_get("/api/sites", self.handle_list_sites)
|
|
500
|
+
r.add_get("/api/sites/categories", self.handle_site_categories)
|
|
501
|
+
r.add_get("/api/sites/{name}", self.handle_get_site)
|
|
502
|
+
r.add_post("/api/sites", self.handle_add_site)
|
|
503
|
+
r.add_put("/api/sites/{name}", self.handle_update_site)
|
|
504
|
+
r.add_delete("/api/sites/{name}", self.handle_remove_site)
|
|
505
|
+
r.add_post("/api/sites/{name}/init-profile", self.handle_init_site_profile)
|
|
506
|
+
r.add_get("/api/sites/profiles", self.handle_list_profiles)
|
|
507
|
+
r.add_delete("/api/sites/profiles/{name}", self.handle_delete_profile)
|
|
508
|
+
r.add_post("/api/sites/{name}/open-browser", self.handle_open_site_browser)
|
|
509
|
+
r.add_post("/api/sites/{name}/close-browser", self.handle_close_site_browser)
|
|
495
510
|
# ── 任务持久化 ──
|
|
496
511
|
r.add_get("/api/tasks", self.handle_list_tasks)
|
|
497
512
|
r.add_post("/api/tasks/{task_id}/retry", self.handle_retry_task)
|
|
@@ -3169,8 +3184,26 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3169
3184
|
"""根据 agent ID 返回目录 (aid 如 '1', '2')"""
|
|
3170
3185
|
return self._agents_dir() / aid
|
|
3171
3186
|
|
|
3187
|
+
async def _ensure_agents_initialized(self):
|
|
3188
|
+
"""[v1.31.1] 异步安全地确保系统 Agent 只创建一次。
|
|
3189
|
+
|
|
3190
|
+
使用 asyncio.Lock 防止并发请求导致重复创建。
|
|
3191
|
+
"""
|
|
3192
|
+
if self._agents_initialized:
|
|
3193
|
+
return
|
|
3194
|
+
async with self._agents_init_lock:
|
|
3195
|
+
if self._agents_initialized:
|
|
3196
|
+
return
|
|
3197
|
+
self._ensure_default_agent()
|
|
3198
|
+
self._agents_initialized = True
|
|
3199
|
+
logger.info("系统 Agent 初始化完成(全权Agent + 配置助手)")
|
|
3200
|
+
|
|
3172
3201
|
def _ensure_default_agent(self):
|
|
3173
|
-
"""确保默认 agent 存在(ID=1,名为「全权Agent」,目录名=1)
|
|
3202
|
+
"""确保默认 agent 存在(ID=1,名为「全权Agent」,目录名=1)
|
|
3203
|
+
|
|
3204
|
+
[v1.31.1] 改为内部方法,由 _ensure_agents_initialized 统一调用。
|
|
3205
|
+
不再由各个 API handler 单独调用,防止并发重复创建。
|
|
3206
|
+
"""
|
|
3174
3207
|
aid = "1"
|
|
3175
3208
|
ad = self._agent_dir(aid)
|
|
3176
3209
|
if not (ad / "config.json").exists():
|
|
@@ -3417,7 +3450,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3417
3450
|
|
|
3418
3451
|
async def handle_list_agents(self, request):
|
|
3419
3452
|
"""GET /api/agents - 返回扁平 agent 列表(系统 agent 排在最前)"""
|
|
3420
|
-
self.
|
|
3453
|
+
await self._ensure_agents_initialized()
|
|
3421
3454
|
agents = self._scan_agents_flat()
|
|
3422
3455
|
# 统计会话数
|
|
3423
3456
|
if self.core.memory:
|
|
@@ -3437,7 +3470,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3437
3470
|
|
|
3438
3471
|
async def handle_agents_tree(self, request):
|
|
3439
3472
|
"""GET /api/agents/tree - 返回树形结构(系统 agent 排在最前)"""
|
|
3440
|
-
self.
|
|
3473
|
+
await self._ensure_agents_initialized()
|
|
3441
3474
|
agents_flat = self._scan_agents_flat()
|
|
3442
3475
|
# 统计会话数
|
|
3443
3476
|
if self.core.memory:
|
|
@@ -3461,7 +3494,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3461
3494
|
|
|
3462
3495
|
async def handle_create_agent(self, request):
|
|
3463
3496
|
"""POST /api/agents - 创建顶级 agent"""
|
|
3464
|
-
self.
|
|
3497
|
+
await self._ensure_agents_initialized()
|
|
3465
3498
|
data = await request.json()
|
|
3466
3499
|
name = data.get("name", "").strip()
|
|
3467
3500
|
if not name:
|
|
@@ -3534,7 +3567,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3534
3567
|
async def handle_create_child(self, request):
|
|
3535
3568
|
"""POST /api/agents/{parent}/children - 创建子 agent"""
|
|
3536
3569
|
parent_path = request.match_info["name"]
|
|
3537
|
-
self.
|
|
3570
|
+
await self._ensure_agents_initialized()
|
|
3538
3571
|
|
|
3539
3572
|
parent_cfg = self._read_agent_config(parent_path)
|
|
3540
3573
|
if not parent_cfg:
|
|
@@ -4926,10 +4959,14 @@ window.addEventListener('beforeunload', function() {{
|
|
|
4926
4959
|
return web.json_response({"builtin_tools": [], "cli_commands": [], "python_skills": [], "skill_guides": []})
|
|
4927
4960
|
|
|
4928
4961
|
view = request.query.get("view", "")
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4962
|
+
try:
|
|
4963
|
+
if view == "categorized":
|
|
4964
|
+
return web.json_response(self.core.skill_registry.list_skills_by_category())
|
|
4965
|
+
# 默认:向后兼容的扁平列表
|
|
4966
|
+
return web.json_response(self.core.skill_registry.list_skills_info())
|
|
4967
|
+
except Exception as e:
|
|
4968
|
+
logger.error(f"list_skills 失败: {e}", exc_info=True)
|
|
4969
|
+
return web.json_response({"error": f"获取技能列表失败: {e}"}, status=500)
|
|
4933
4970
|
|
|
4934
4971
|
async def handle_get_skill(self, request):
|
|
4935
4972
|
"""GET /api/skills/{name} - 获取单个技能/工具详情
|
|
@@ -4938,44 +4975,48 @@ window.addEventListener('beforeunload', function() {{
|
|
|
4938
4975
|
"""
|
|
4939
4976
|
name = request.match_info["name"]
|
|
4940
4977
|
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4978
|
+
try:
|
|
4979
|
+
# 1. 从 SkillRegistry 查找(Python 技能 + Markdown 技能指南 + 旧版存根)
|
|
4980
|
+
if self.core.skill_registry:
|
|
4981
|
+
s = self.core.skill_registry._skills.get(name)
|
|
4982
|
+
if s:
|
|
4983
|
+
info = s.to_openclaw_format()
|
|
4984
|
+
info["disabled"] = self.core.skill_registry._is_disabled(name)
|
|
4985
|
+
return web.json_response(info)
|
|
4986
|
+
|
|
4987
|
+
# 2. 从 BUILTIN_TOOLS 元数据查找(内置平台工具)
|
|
4988
|
+
from aiskills.registry import BUILTIN_TOOLS, CLI_COMMANDS
|
|
4989
|
+
for bt in BUILTIN_TOOLS:
|
|
4990
|
+
if bt["name"] == name:
|
|
4991
|
+
return web.json_response({
|
|
4992
|
+
"name": bt["name"],
|
|
4993
|
+
"description": bt["description"],
|
|
4994
|
+
"category": bt["category"],
|
|
4995
|
+
"parameters": bt["parameters"],
|
|
4996
|
+
"dangerous": False,
|
|
4997
|
+
"disabled": False,
|
|
4998
|
+
"skill_type": "builtin_platform",
|
|
4999
|
+
"note": f"由 {bt['handler']} 处理,不可禁用",
|
|
5000
|
+
})
|
|
4963
5001
|
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5002
|
+
# 3. [v1.23.0] 从 CLI_COMMANDS 查找
|
|
5003
|
+
for cmd in CLI_COMMANDS:
|
|
5004
|
+
if cmd["name"] == name or name in cmd.get("aliases", []):
|
|
5005
|
+
return web.json_response({
|
|
5006
|
+
"name": cmd["name"],
|
|
5007
|
+
"description": cmd["description"],
|
|
5008
|
+
"category": cmd["category"],
|
|
5009
|
+
"cli": cmd.get("cli", ""),
|
|
5010
|
+
"aliases": cmd.get("aliases", []),
|
|
5011
|
+
"disabled": False,
|
|
5012
|
+
"skill_type": "cli_command",
|
|
5013
|
+
"note": "通过 command 工具调用: myagent-ai <name>",
|
|
5014
|
+
})
|
|
4977
5015
|
|
|
4978
|
-
|
|
5016
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
5017
|
+
except Exception as e:
|
|
5018
|
+
logger.error(f"get_skill 失败 ({name}): {e}", exc_info=True)
|
|
5019
|
+
return web.json_response({"error": f"获取技能 '{name}' 失败: {e}"}, status=500)
|
|
4979
5020
|
|
|
4980
5021
|
async def handle_toggle_skill(self, request):
|
|
4981
5022
|
"""POST /api/skills/{name}/toggle - 启用/禁用技能
|
|
@@ -4984,25 +5025,29 @@ window.addEventListener('beforeunload', function() {{
|
|
|
4984
5025
|
"""
|
|
4985
5026
|
name = request.match_info["name"]
|
|
4986
5027
|
try:
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
self.core.skill_registry
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5028
|
+
try:
|
|
5029
|
+
data = await request.json()
|
|
5030
|
+
except Exception:
|
|
5031
|
+
data = {}
|
|
5032
|
+
enabled = data.get("enabled", True)
|
|
5033
|
+
|
|
5034
|
+
# [v1.23.0] 内置平台工具和 CLI 命令不可禁用
|
|
5035
|
+
from aiskills.registry import BUILTIN_TOOLS, CLI_COMMANDS
|
|
5036
|
+
builtin_names = {bt["name"] for bt in BUILTIN_TOOLS}
|
|
5037
|
+
cli_names = {c["name"] for c in CLI_COMMANDS}
|
|
5038
|
+
if name in builtin_names:
|
|
5039
|
+
return web.json_response({"error": f"内置平台工具 '{name}' 不可禁用"}, status=400)
|
|
5040
|
+
if name in cli_names:
|
|
5041
|
+
return web.json_response({"error": f"CLI 命令 '{name}' 不可禁用(由 command 工具统一管理)"}, status=400)
|
|
5042
|
+
|
|
5043
|
+
if self.core.skill_registry:
|
|
5044
|
+
self.core.skill_registry.toggle(name, enabled)
|
|
5045
|
+
# 持久化到配置文件
|
|
5046
|
+
self._save_disabled_skills()
|
|
5047
|
+
return web.json_response({"ok": True, "name": name, "enabled": enabled})
|
|
5048
|
+
except Exception as e:
|
|
5049
|
+
logger.error(f"toggle_skill 失败 ({name}): {e}", exc_info=True)
|
|
5050
|
+
return web.json_response({"error": f"切换技能 '{name}' 状态失败: {e}"}, status=500)
|
|
5006
5051
|
|
|
5007
5052
|
def _save_disabled_skills(self):
|
|
5008
5053
|
"""将禁用技能列表持久化到配置文件"""
|
|
@@ -5025,6 +5070,202 @@ window.addEventListener('beforeunload', function() {{
|
|
|
5025
5070
|
if isinstance(disabled, list):
|
|
5026
5071
|
self.core.skill_registry.disabled_skills = set(disabled)
|
|
5027
5072
|
|
|
5073
|
+
# --- Site Management (v1.31.1) ---
|
|
5074
|
+
|
|
5075
|
+
def _get_site_registry(self):
|
|
5076
|
+
"""获取 SiteRegistry 单例(懒加载)"""
|
|
5077
|
+
if not hasattr(self, '_site_registry'):
|
|
5078
|
+
from core.site_registry import get_site_registry
|
|
5079
|
+
self._site_registry = get_site_registry()
|
|
5080
|
+
return self._site_registry
|
|
5081
|
+
|
|
5082
|
+
def _get_browser_profile_manager(self):
|
|
5083
|
+
"""获取 BrowserProfileManager 单例(懒加载)"""
|
|
5084
|
+
if not hasattr(self, '_browser_profile_mgr'):
|
|
5085
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5086
|
+
self._browser_profile_mgr = get_browser_profile_manager()
|
|
5087
|
+
return self._browser_profile_mgr
|
|
5088
|
+
|
|
5089
|
+
async def handle_list_sites(self, request):
|
|
5090
|
+
"""GET /api/sites - 列出所有网站(支持 ?category=xxx 筛选)"""
|
|
5091
|
+
reg = self._get_site_registry()
|
|
5092
|
+
category = request.query.get("category", "")
|
|
5093
|
+
sites = reg.list_sites(category=category)
|
|
5094
|
+
summary = reg.to_summary()
|
|
5095
|
+
return web.json_response({
|
|
5096
|
+
"sites": sites,
|
|
5097
|
+
"summary": {
|
|
5098
|
+
"total": summary["total"],
|
|
5099
|
+
"builtin_count": summary["builtin_count"],
|
|
5100
|
+
"custom_count": summary["custom_count"],
|
|
5101
|
+
},
|
|
5102
|
+
})
|
|
5103
|
+
|
|
5104
|
+
async def handle_site_categories(self, request):
|
|
5105
|
+
"""GET /api/sites/categories - 获取分类统计"""
|
|
5106
|
+
reg = self._get_site_registry()
|
|
5107
|
+
cats = reg.get_categories()
|
|
5108
|
+
return web.json_response(cats)
|
|
5109
|
+
|
|
5110
|
+
async def handle_get_site(self, request):
|
|
5111
|
+
"""GET /api/sites/{name} - 获取单个网站详情"""
|
|
5112
|
+
name = request.match_info["name"]
|
|
5113
|
+
reg = self._get_site_registry()
|
|
5114
|
+
site = reg.get_site(name)
|
|
5115
|
+
if not site:
|
|
5116
|
+
return web.json_response({"error": f"网站 '{name}' 不存在"}, status=404)
|
|
5117
|
+
# 附加 Profile 信息
|
|
5118
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5119
|
+
mgr = get_browser_profile_manager()
|
|
5120
|
+
profile = mgr.get_profile(name)
|
|
5121
|
+
site["profile"] = profile.to_dict()
|
|
5122
|
+
return web.json_response(site)
|
|
5123
|
+
|
|
5124
|
+
async def handle_add_site(self, request):
|
|
5125
|
+
"""POST /api/sites - 添加自定义网站"""
|
|
5126
|
+
try:
|
|
5127
|
+
data = await request.json()
|
|
5128
|
+
except Exception:
|
|
5129
|
+
return web.json_response({"error": "无效的 JSON 请求体"}, status=400)
|
|
5130
|
+
|
|
5131
|
+
name = data.get("name", "").strip().lower().replace(" ", "_")
|
|
5132
|
+
if not name:
|
|
5133
|
+
return web.json_response({"error": "网站名称不能为空"}, status=400)
|
|
5134
|
+
if not all(c.isalnum() or c in "_-" for c in name):
|
|
5135
|
+
return web.json_response({"error": "网站名称只能包含字母、数字、下划线和连字符"}, status=400)
|
|
5136
|
+
|
|
5137
|
+
reg = self._get_site_registry()
|
|
5138
|
+
try:
|
|
5139
|
+
site = reg.add_site(data)
|
|
5140
|
+
return web.json_response({"ok": True, "site": site})
|
|
5141
|
+
except ValueError as e:
|
|
5142
|
+
return web.json_response({"error": str(e)}, status=400)
|
|
5143
|
+
except Exception as e:
|
|
5144
|
+
logger.error(f"添加网站失败: {e}")
|
|
5145
|
+
return web.json_response({"error": f"添加网站失败: {e}"}, status=500)
|
|
5146
|
+
|
|
5147
|
+
async def handle_update_site(self, request):
|
|
5148
|
+
"""PUT /api/sites/{name} - 更新网站配置"""
|
|
5149
|
+
name = request.match_info["name"]
|
|
5150
|
+
try:
|
|
5151
|
+
data = await request.json()
|
|
5152
|
+
except Exception:
|
|
5153
|
+
return web.json_response({"error": "无效的 JSON 请求体"}, status=400)
|
|
5154
|
+
|
|
5155
|
+
reg = self._get_site_registry()
|
|
5156
|
+
# 只允许更新部分字段
|
|
5157
|
+
allowed = {"display_name", "category", "login_url", "detect_url", "home_url",
|
|
5158
|
+
"urls", "tips", "enabled"}
|
|
5159
|
+
updates = {k: v for k, v in data.items() if k in allowed}
|
|
5160
|
+
|
|
5161
|
+
if not updates:
|
|
5162
|
+
return web.json_response({"error": "没有可更新的字段"}, status=400)
|
|
5163
|
+
|
|
5164
|
+
site = reg.update_site(name, updates)
|
|
5165
|
+
if not site:
|
|
5166
|
+
return web.json_response({"error": f"网站 '{name}' 不存在"}, status=404)
|
|
5167
|
+
return web.json_response({"ok": True, "site": site})
|
|
5168
|
+
|
|
5169
|
+
async def handle_remove_site(self, request):
|
|
5170
|
+
"""DELETE /api/sites/{name} - 删除自定义网站"""
|
|
5171
|
+
name = request.match_info["name"]
|
|
5172
|
+
reg = self._get_site_registry()
|
|
5173
|
+
ok = reg.remove_site(name)
|
|
5174
|
+
if not ok:
|
|
5175
|
+
return web.json_response({"error": f"无法删除网站 '{name}'(内置网站不可删除)"}, status=400)
|
|
5176
|
+
# 同时删除 Profile
|
|
5177
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5178
|
+
mgr = get_browser_profile_manager()
|
|
5179
|
+
mgr.delete_profile(name)
|
|
5180
|
+
return web.json_response({"ok": True})
|
|
5181
|
+
|
|
5182
|
+
async def handle_init_site_profile(self, request):
|
|
5183
|
+
"""POST /api/sites/{name}/init-profile - 初始化网站浏览器 Profile"""
|
|
5184
|
+
name = request.match_info["name"]
|
|
5185
|
+
reg = self._get_site_registry()
|
|
5186
|
+
site = reg.get_site(name)
|
|
5187
|
+
if not site:
|
|
5188
|
+
return web.json_response({"error": f"网站 '{name}' 不存在"}, status=404)
|
|
5189
|
+
try:
|
|
5190
|
+
initialized = reg.init_profiles([name])
|
|
5191
|
+
return web.json_response({"ok": True, "initialized": initialized})
|
|
5192
|
+
except Exception as e:
|
|
5193
|
+
logger.error(f"初始化 Profile 失败 ({name}): {e}")
|
|
5194
|
+
return web.json_response({"error": f"初始化失败: {e}"}, status=500)
|
|
5195
|
+
|
|
5196
|
+
async def handle_list_profiles(self, request):
|
|
5197
|
+
"""GET /api/sites/profiles - 列出所有已初始化的浏览器 Profile"""
|
|
5198
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5199
|
+
mgr = get_browser_profile_manager()
|
|
5200
|
+
profiles = mgr.list_profiles()
|
|
5201
|
+
total_size = mgr.get_total_size_mb()
|
|
5202
|
+
return web.json_response({
|
|
5203
|
+
"profiles": profiles,
|
|
5204
|
+
"total_size_mb": round(total_size, 2),
|
|
5205
|
+
})
|
|
5206
|
+
|
|
5207
|
+
async def handle_delete_profile(self, request):
|
|
5208
|
+
"""DELETE /api/sites/profiles/{name} - 删除浏览器 Profile"""
|
|
5209
|
+
name = request.match_info["name"]
|
|
5210
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5211
|
+
mgr = get_browser_profile_manager()
|
|
5212
|
+
ok = mgr.delete_profile(name)
|
|
5213
|
+
if not ok:
|
|
5214
|
+
return web.json_response({"error": f"Profile '{name}' 不存在或删除失败"}, status=404)
|
|
5215
|
+
return web.json_response({"ok": True})
|
|
5216
|
+
|
|
5217
|
+
async def handle_open_site_browser(self, request):
|
|
5218
|
+
"""POST /api/sites/{name}/open-browser - 打开浏览器登录网站"""
|
|
5219
|
+
name = request.match_info["name"]
|
|
5220
|
+
reg = self._get_site_registry()
|
|
5221
|
+
site = reg.get_site(name)
|
|
5222
|
+
if not site:
|
|
5223
|
+
return web.json_response({"error": f"网站 '{name}' 不存在"}, status=404)
|
|
5224
|
+
|
|
5225
|
+
# Ensure profile is initialized
|
|
5226
|
+
from core.browser_profile import get_browser_profile_manager
|
|
5227
|
+
mgr = get_browser_profile_manager()
|
|
5228
|
+
profile = mgr.get_profile(name)
|
|
5229
|
+
if not profile.is_initialized():
|
|
5230
|
+
profile.ensure_dirs()
|
|
5231
|
+
|
|
5232
|
+
login_url = site.get("login_url", "")
|
|
5233
|
+
|
|
5234
|
+
try:
|
|
5235
|
+
from aiskills.browser_stealth import get_stealth_browser
|
|
5236
|
+
browser = get_stealth_browser(profile_name=name, headless=False)
|
|
5237
|
+
result = await browser.start()
|
|
5238
|
+
if not result.success:
|
|
5239
|
+
return web.json_response({"error": f"启动浏览器失败: {result.error}"}, status=500)
|
|
5240
|
+
|
|
5241
|
+
# Navigate to login URL
|
|
5242
|
+
if login_url:
|
|
5243
|
+
page = browser._page
|
|
5244
|
+
if page:
|
|
5245
|
+
page.get(login_url)
|
|
5246
|
+
|
|
5247
|
+
return web.json_response({
|
|
5248
|
+
"ok": True,
|
|
5249
|
+
"site": name,
|
|
5250
|
+
"login_url": login_url,
|
|
5251
|
+
"message": f"浏览器已打开,请在浏览器中完成 {site.get('display_name', name)} 的登录",
|
|
5252
|
+
})
|
|
5253
|
+
except Exception as e:
|
|
5254
|
+
logger.error(f"打开浏览器失败 ({name}): {e}")
|
|
5255
|
+
return web.json_response({"error": f"打开浏览器失败: {e}"}, status=500)
|
|
5256
|
+
|
|
5257
|
+
async def handle_close_site_browser(self, request):
|
|
5258
|
+
"""POST /api/sites/{name}/close-browser - 关闭网站浏览器"""
|
|
5259
|
+
name = request.match_info["name"]
|
|
5260
|
+
|
|
5261
|
+
try:
|
|
5262
|
+
from aiskills.browser_stealth import close_stealth_browser
|
|
5263
|
+
close_stealth_browser(profile_name=name)
|
|
5264
|
+
return web.json_response({"ok": True, "site": name, "message": f"浏览器已关闭 ({name})"})
|
|
5265
|
+
except Exception as e:
|
|
5266
|
+
logger.error(f"关闭浏览器失败 ({name}): {e}")
|
|
5267
|
+
return web.json_response({"error": f"关闭浏览器失败: {e}"}, status=500)
|
|
5268
|
+
|
|
5028
5269
|
# --- Workdir ---
|
|
5029
5270
|
async def handle_get_workdir(self, request):
|
|
5030
5271
|
return web.json_response({"path": str(self.core.config_mgr.data_dir / "workspace")})
|
|
@@ -7059,8 +7300,8 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7059
7300
|
config = self.core.config
|
|
7060
7301
|
# 检查是否已配置有效的 LLM API Key
|
|
7061
7302
|
needs_setup = not config.llm.api_key
|
|
7062
|
-
#
|
|
7063
|
-
self.
|
|
7303
|
+
# 确保系统 Agent 已初始化(使用异步锁防重复)
|
|
7304
|
+
await self._ensure_agents_initialized()
|
|
7064
7305
|
helper_exists = (self._agent_dir("2") / "config.json").exists()
|
|
7065
7306
|
return web.json_response({
|
|
7066
7307
|
"needs_setup": needs_setup,
|
|
@@ -7073,8 +7314,8 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7073
7314
|
|
|
7074
7315
|
async def handle_setup_complete(self, request):
|
|
7075
7316
|
"""POST /api/setup/complete - 标记首次设置完成"""
|
|
7076
|
-
#
|
|
7077
|
-
self.
|
|
7317
|
+
# 确保系统 Agent 已初始化
|
|
7318
|
+
await self._ensure_agents_initialized()
|
|
7078
7319
|
return web.json_response({"ok": True})
|
|
7079
7320
|
|
|
7080
7321
|
# ── 自动更新管理 ──
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ── admin-core.js — Shared infrastructure (loaded FIRST) ── */
|
|
2
2
|
|
|
3
3
|
const API='';
|
|
4
|
-
const pages={dashboard:'📊 仪表盘',agents:'🤖 Agent 管理',platforms:'🌐 聊天平台',organization:'🏢 组织管理',departments:'🏛 部门管理',agentchat:'💬 私聊记录',sessions:'📋 会话管理',memory:'🧠 记忆管理',permissions:'🔑 权限管理',llm:'🧬 大模型设置',system:'⚙️ 系统配置',executor:'🔧 执行引擎',skills:'🛠 技能管理',files:'📁 工作目录',logs:'📜 查看日志',tasks:'📌 任务记录'};
|
|
4
|
+
const pages={dashboard:'📊 仪表盘',agents:'🤖 Agent 管理',platforms:'🌐 聊天平台',organization:'🏢 组织管理',departments:'🏛 部门管理',agentchat:'💬 私聊记录',sessions:'📋 会话管理',memory:'🧠 记忆管理',permissions:'🔑 权限管理',llm:'🧬 大模型设置',system:'⚙️ 系统配置',executor:'🔧 执行引擎',sites:'🌐 网站管理',skills:'🛠 技能管理',files:'📁 工作目录',logs:'📜 查看日志',tasks:'📌 任务记录'};
|
|
5
5
|
let currentPage='dashboard';
|
|
6
6
|
let allAgentsCache=[];
|
|
7
7
|
let allModelsCache=[];
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// ========== Site Management ==========
|
|
2
|
+
// [v1.31.1] 网站注册/浏览器Profile统一管理
|
|
3
|
+
|
|
4
|
+
const SITE_CAT_ICONS = {
|
|
5
|
+
email: '\u{1F4E7}', social: '\u{1F4AC}', cloud: '\u2601\uFE0F',
|
|
6
|
+
ai_chat: '\u{1F916}', dev: '\u{1F6E0}\uFE0F', content: '\u{1F3AC}', custom: '\u2699\uFE0F'
|
|
7
|
+
};
|
|
8
|
+
const SITE_CAT_LABELS = {
|
|
9
|
+
email: '\u90AE\u7BB1\u670D\u52A1', social: '\u793E\u4EA4\u5A92\u4F53', cloud: '\u4E91\u670D\u52A1',
|
|
10
|
+
ai_chat: 'AI \u5BF9\u8BDD', dev: '\u5F00\u53D1\u5DE5\u5177', content: '\u5185\u5BB9\u5E73\u53F0', custom: '\u81EA\u5B9A\u4E49'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let _sitesFilter = '';
|
|
14
|
+
let _sitesProfileView = false;
|
|
15
|
+
|
|
16
|
+
async function renderSites() {
|
|
17
|
+
_sitesProfileView = false;
|
|
18
|
+
$('content').setAttribute('data-page', 'sites');
|
|
19
|
+
const r = await api('/api/sites');
|
|
20
|
+
if (r.error) {
|
|
21
|
+
$('content').innerHTML = '<div class="empty" style="color:var(--danger)">加载失败: ' + escHtml(r.error) + '</div>';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sites = r.sites || [];
|
|
26
|
+
const summary = r.summary || {};
|
|
27
|
+
const profiles = await api('/api/sites/profiles').catch(function() { return { profiles: [], total_size_mb: 0 }; });
|
|
28
|
+
|
|
29
|
+
// Group by category
|
|
30
|
+
const groups = {};
|
|
31
|
+
for (const s of sites) {
|
|
32
|
+
const cat = s.category || 'custom';
|
|
33
|
+
if (!groups[cat]) groups[cat] = [];
|
|
34
|
+
groups[cat].push(s);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let html = '<div class="flex justify-between items-center mb-16 flex-wrap gap-8">' +
|
|
38
|
+
'<div style="color:var(--text2);font-size:13px">' +
|
|
39
|
+
'共 <strong>' + sites.length + '</strong> 个网站' +
|
|
40
|
+
' (内置' + (summary.builtin_count || 0) +
|
|
41
|
+
' · 自定义' + (summary.custom_count || 0) + ')' +
|
|
42
|
+
' · Profile ' + (profiles.profiles || []).length + ' 个' +
|
|
43
|
+
' (' + (profiles.total_size_mb || 0) + ' MB)' +
|
|
44
|
+
'</div>' +
|
|
45
|
+
'<div class="flex gap-8">' +
|
|
46
|
+
'<input id="siteSearch" placeholder="搜索网站..." style="width:200px" oninput="filterSites()">' +
|
|
47
|
+
'<button class="btn btn-primary btn-sm" onclick="showAddSiteModal()">+ 添加网站</button>' +
|
|
48
|
+
'<button class="btn btn-ghost btn-sm" onclick="initAllProfiles()">初始化所有 Profile</button>' +
|
|
49
|
+
'</div></div>';
|
|
50
|
+
|
|
51
|
+
// Render each category group
|
|
52
|
+
for (const [cat, catSites] of Object.entries(groups)) {
|
|
53
|
+
const icon = SITE_CAT_ICONS[cat] || '\u{1F310}';
|
|
54
|
+
const label = SITE_CAT_LABELS[cat] || cat;
|
|
55
|
+
html += '<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">' +
|
|
56
|
+
'<h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">' +
|
|
57
|
+
'<span>' + icon + '</span> ' + label +
|
|
58
|
+
' <span class="badge badge-blue">' + catSites.length + '</span></h3>';
|
|
59
|
+
for (const s of catSites) {
|
|
60
|
+
html += siteRowHtml(s);
|
|
61
|
+
}
|
|
62
|
+
html += '</div>';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!sites.length) html += '<div class="empty">暂无网站,点击"添加网站"开始配置</div>';
|
|
66
|
+
|
|
67
|
+
$('content').innerHTML = html;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function siteRowHtml(s) {
|
|
71
|
+
const isBuiltin = s.is_builtin;
|
|
72
|
+
const isCustom = s.is_custom;
|
|
73
|
+
const hasProfile = s.has_profile;
|
|
74
|
+
const name = escHtml(s.name || '');
|
|
75
|
+
const displayName = escHtml(s.display_name || s.name || '');
|
|
76
|
+
const loginUrl = escHtml(s.login_url || '');
|
|
77
|
+
const typeTag = isBuiltin ? '<span class="badge badge-blue">内置</span>' :
|
|
78
|
+
isCustom ? '<span class="badge badge-green">自定义</span>' : '';
|
|
79
|
+
const profileTag = hasProfile ? '<span class="badge badge-green">已登录</span>' :
|
|
80
|
+
'<span class="badge badge-yellow">未登录</span>';
|
|
81
|
+
|
|
82
|
+
let actions = '';
|
|
83
|
+
actions += '<button class="btn btn-sm btn-ghost" onclick="viewSiteDetail(\'' + name + '\')">详情</button>';
|
|
84
|
+
if (!isBuiltin) {
|
|
85
|
+
actions += '<button class="btn btn-sm btn-ghost" onclick="editSiteModal(\'' + name + '\')">编辑</button>';
|
|
86
|
+
actions += '<button class="btn btn-sm btn-ghost" style="color:var(--danger)" onclick="removeSite(\'' + name + '\')">删除</button>';
|
|
87
|
+
}
|
|
88
|
+
// 登录按钮 - 始终显示(有 login_url 时突出显示)
|
|
89
|
+
if (s.login_url) {
|
|
90
|
+
actions += '<button class="btn btn-sm btn-primary" onclick="openSiteBrowser(\'' + name + '\')">登录</button>';
|
|
91
|
+
} else {
|
|
92
|
+
actions += '<button class="btn btn-sm btn-ghost" onclick="openSiteBrowser(\'' + name + '\')">登录</button>';
|
|
93
|
+
}
|
|
94
|
+
if (hasProfile) {
|
|
95
|
+
actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" onclick="closeSiteBrowser(\'' + name + '\')">关闭浏览器</button>';
|
|
96
|
+
actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" onclick="deleteSiteProfile(\'' + name + '\')">清除登录</button>';
|
|
97
|
+
} else {
|
|
98
|
+
actions += '<button class="btn btn-sm btn-ghost" onclick="initSiteProfile(\'' + name + '\')">Profile</button>';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return '<div class="skill-row" data-name="' + name + '" data-desc="' + escHtml(s.display_name || '') + '">' +
|
|
102
|
+
'<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">' +
|
|
103
|
+
(SITE_CAT_ICONS[s.category] || '\u{1F310}') + '</div>' +
|
|
104
|
+
'<div class="skill-info">' +
|
|
105
|
+
'<div class="skill-name">' + displayName + ' ' + typeTag + ' ' + profileTag + '</div>' +
|
|
106
|
+
'<div class="skill-desc">' + (loginUrl ? '<code style="font-size:11px;color:var(--accent)">' + loginUrl + '</code> ' : '') +
|
|
107
|
+
escHtml(s.home_url || '') + '</div>' +
|
|
108
|
+
'</div>' +
|
|
109
|
+
'<div class="flex gap-8 items-center" style="flex-shrink:0">' + actions + '</div></div>';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function filterSites() {
|
|
113
|
+
const q = ($('siteSearch')?.value || '').toLowerCase();
|
|
114
|
+
document.querySelectorAll('#content .skill-row').forEach(function(el) {
|
|
115
|
+
var n = (el.dataset.name || '').toLowerCase();
|
|
116
|
+
var d = (el.dataset.desc || '').toLowerCase();
|
|
117
|
+
el.style.display = (n.includes(q) || d.includes(q)) ? '' : 'none';
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── View Site Detail ──
|
|
122
|
+
async function viewSiteDetail(name) {
|
|
123
|
+
var s = await api('/api/sites/' + encodeURIComponent(name));
|
|
124
|
+
if (s.error) { showToast(s.error, 'danger'); return; }
|
|
125
|
+
|
|
126
|
+
var tipsHtml = '';
|
|
127
|
+
if (s.tips && s.tips.length) {
|
|
128
|
+
tipsHtml = '<div class="form-group"><label>使用提示</label><ul style="font-size:12px;color:var(--text2);padding-left:20px">';
|
|
129
|
+
for (var t of s.tips) tipsHtml += '<li style="margin-bottom:4px">' + escHtml(t) + '</li>';
|
|
130
|
+
tipsHtml += '</ul></div>';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var urlsHtml = '';
|
|
134
|
+
if (s.urls && Object.keys(s.urls).length) {
|
|
135
|
+
urlsHtml = '<div class="form-group"><label>常用链接</label><div class="table-wrap"><table>' +
|
|
136
|
+
'<tr><th>名称</th><th>URL</th></tr>';
|
|
137
|
+
for (var k in s.urls) {
|
|
138
|
+
urlsHtml += '<tr><td>' + escHtml(k) + '</td><td><code style="font-size:12px;word-break:break-all">' + escHtml(s.urls[k]) + '</code></td></tr>';
|
|
139
|
+
}
|
|
140
|
+
urlsHtml += '</table></div></div>';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var profileInfo = '';
|
|
144
|
+
if (s.profile) {
|
|
145
|
+
var p = s.profile;
|
|
146
|
+
profileInfo = '<div class="form-group"><label>浏览器 Profile</label>' +
|
|
147
|
+
'<div style="font-size:13px;color:var(--text)">' +
|
|
148
|
+
'状态: ' + (p.initialized ? '<span class="badge badge-green">已初始化</span>' : '<span class="badge badge-yellow">未初始化</span>') +
|
|
149
|
+
' · 大小: ' + (p.size_mb || 0) + ' MB' +
|
|
150
|
+
' · Cookie: ' + (p.has_cookies ? '<span class="badge badge-green">有</span>' : '<span class="badge badge-red">无</span>') +
|
|
151
|
+
'</div></div>';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var html = '<h3>' + escHtml(s.display_name || s.name) + '</h3>' +
|
|
155
|
+
'<div class="form-row">' +
|
|
156
|
+
'<div class="form-group"><label>网站名称</label><div><code>' + escHtml(s.name) + '</code></div></div>' +
|
|
157
|
+
'<div class="form-group"><label>分类</label><div>' + escHtml(SITE_CAT_LABELS[s.category] || s.category) + '</div></div>' +
|
|
158
|
+
'</div>' +
|
|
159
|
+
'<div class="form-row">' +
|
|
160
|
+
'<div class="form-group"><label>类型</label><div>' + (s.is_builtin ? '<span class="badge badge-blue">内置</span>' : '<span class="badge badge-green">自定义</span>') + '</div></div>' +
|
|
161
|
+
'</div>' +
|
|
162
|
+
'<div class="form-group"><label>登录页</label><div><a href="' + escHtml(s.login_url || '') + '" target="_blank" style="color:var(--accent)">' + escHtml(s.login_url || '-') + '</a></div></div>' +
|
|
163
|
+
'<div class="form-group"><label>首页</label><div><a href="' + escHtml(s.home_url || '') + '" target="_blank" style="color:var(--accent)">' + escHtml(s.home_url || '-') + '</a></div></div>' +
|
|
164
|
+
'<div class="form-group"><label>登录检测</label><div><code style="font-size:12px">' + escHtml(s.detect_url || '-') + '</code></div></div>' +
|
|
165
|
+
urlsHtml + tipsHtml + profileInfo +
|
|
166
|
+
'<div class="flex gap-8 mt-16">' +
|
|
167
|
+
'<button class="btn btn-ghost" onclick="closeModal()">关闭</button>' +
|
|
168
|
+
(!s.is_builtin ? '<button class="btn btn-primary" onclick="closeModal();editSiteModal(\'' + escHtml(s.name) + '\')">编辑</button>' : '') +
|
|
169
|
+
'<button class="btn btn-ghost" onclick="initSiteProfile(\'' + escHtml(s.name) + '\');closeModal()">初始化 Profile</button>' +
|
|
170
|
+
'</div>';
|
|
171
|
+
|
|
172
|
+
$('modalContainer').innerHTML = '<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:600px">' + html + '</div></div>';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Add Site Modal ──
|
|
176
|
+
function showAddSiteModal() {
|
|
177
|
+
var html = '<h3>添加自定义网站</h3>' +
|
|
178
|
+
'<div class="form-group"><label>网站名称 (英文标识)</label>' +
|
|
179
|
+
'<input id="siteFormName" placeholder="例如: my_website"></div>' +
|
|
180
|
+
'<div class="form-group"><label>显示名称</label>' +
|
|
181
|
+
'<input id="siteFormDisplay" placeholder="例如: 我的网站"></div>' +
|
|
182
|
+
'<div class="form-group"><label>分类</label>' +
|
|
183
|
+
'<select id="siteFormCategory">' +
|
|
184
|
+
'<option value="custom">自定义</option>' +
|
|
185
|
+
'<option value="email">邮箱服务</option>' +
|
|
186
|
+
'<option value="social">社交媒体</option>' +
|
|
187
|
+
'<option value="cloud">云服务</option>' +
|
|
188
|
+
'<option value="ai_chat">AI 对话</option>' +
|
|
189
|
+
'<option value="dev">开发工具</option>' +
|
|
190
|
+
'<option value="content">内容平台</option>' +
|
|
191
|
+
'</select></div>' +
|
|
192
|
+
'<div class="form-group"><label>登录页 URL</label>' +
|
|
193
|
+
'<input id="siteFormLoginUrl" placeholder="https://example.com/login"></div>' +
|
|
194
|
+
'<div class="form-group"><label>首页 URL</label>' +
|
|
195
|
+
'<input id="siteFormHomeUrl" placeholder="https://example.com/dashboard"></div>' +
|
|
196
|
+
'<div class="form-group"><label>登录检测 URL</label>' +
|
|
197
|
+
'<input id="siteFormDetectUrl" placeholder="https://example.com/dashboard"></div>' +
|
|
198
|
+
'<div class="flex gap-8 mt-16">' +
|
|
199
|
+
'<button class="btn btn-primary" onclick="doAddSite()">添加</button>' +
|
|
200
|
+
'<button class="btn btn-ghost" onclick="closeModal()">取消</button>' +
|
|
201
|
+
'</div>';
|
|
202
|
+
|
|
203
|
+
$('modalContainer').innerHTML = '<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:500px">' + html + '</div></div>';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function doAddSite() {
|
|
207
|
+
var name = ($('siteFormName')?.value || '').trim().toLowerCase().replace(/\s+/g, '_');
|
|
208
|
+
if (!name) { showToast('网站名称不能为空', 'danger'); return; }
|
|
209
|
+
if (!/^[a-z0-9_-]+$/.test(name)) { showToast('网站名称只能包含小写字母、数字、下划线和连字符', 'danger'); return; }
|
|
210
|
+
|
|
211
|
+
var data = {
|
|
212
|
+
name: name,
|
|
213
|
+
display_name: $('siteFormDisplay')?.value || name,
|
|
214
|
+
category: $('siteFormCategory')?.value || 'custom',
|
|
215
|
+
login_url: $('siteFormLoginUrl')?.value || '',
|
|
216
|
+
home_url: $('siteFormHomeUrl')?.value || '',
|
|
217
|
+
detect_url: $('siteFormDetectUrl')?.value || '',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
var r = await api('/api/sites', { method: 'POST', body: JSON.stringify(data) });
|
|
221
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
222
|
+
showToast('已添加网站: ' + name, 'success');
|
|
223
|
+
closeModal();
|
|
224
|
+
renderSites();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Edit Site Modal ──
|
|
228
|
+
async function editSiteModal(name) {
|
|
229
|
+
var s = await api('/api/sites/' + encodeURIComponent(name));
|
|
230
|
+
if (s.error) { showToast(s.error, 'danger'); return; }
|
|
231
|
+
|
|
232
|
+
var html = '<h3>编辑网站: ' + escHtml(s.display_name || s.name) + '</h3>' +
|
|
233
|
+
'<div class="form-group"><label>网站名称</label>' +
|
|
234
|
+
'<input value="' + escHtml(s.name) + '" disabled style="opacity:0.6"></div>' +
|
|
235
|
+
'<div class="form-group"><label>显示名称</label>' +
|
|
236
|
+
'<input id="editSiteDisplay" value="' + escHtml(s.display_name || '') + '"></div>' +
|
|
237
|
+
'<div class="form-group"><label>分类</label>' +
|
|
238
|
+
'<select id="editSiteCategory">' +
|
|
239
|
+
'<option value="custom"' + (s.category === 'custom' ? ' selected' : '') + '>自定义</option>' +
|
|
240
|
+
'<option value="email"' + (s.category === 'email' ? ' selected' : '') + '>邮箱服务</option>' +
|
|
241
|
+
'<option value="social"' + (s.category === 'social' ? ' selected' : '') + '>社交媒体</option>' +
|
|
242
|
+
'<option value="cloud"' + (s.category === 'cloud' ? ' selected' : '') + '>云服务</option>' +
|
|
243
|
+
'<option value="ai_chat"' + (s.category === 'ai_chat' ? ' selected' : '') + '>AI 对话</option>' +
|
|
244
|
+
'<option value="dev"' + (s.category === 'dev' ? ' selected' : '') + '>开发工具</option>' +
|
|
245
|
+
'<option value="content"' + (s.category === 'content' ? ' selected' : '') + '>内容平台</option>' +
|
|
246
|
+
'</select></div>' +
|
|
247
|
+
'<div class="form-group"><label>登录页 URL</label>' +
|
|
248
|
+
'<input id="editSiteLoginUrl" value="' + escHtml(s.login_url || '') + '"></div>' +
|
|
249
|
+
'<div class="form-group"><label>首页 URL</label>' +
|
|
250
|
+
'<input id="editSiteHomeUrl" value="' + escHtml(s.home_url || '') + '"></div>' +
|
|
251
|
+
'<div class="form-group"><label>登录检测 URL</label>' +
|
|
252
|
+
'<input id="editSiteDetectUrl" value="' + escHtml(s.detect_url || '') + '"></div>' +
|
|
253
|
+
'<div class="flex gap-8 mt-16">' +
|
|
254
|
+
'<button class="btn btn-primary" onclick="doUpdateSite(\'' + escHtml(s.name) + '\')">保存</button>' +
|
|
255
|
+
'<button class="btn btn-ghost" onclick="closeModal()">取消</button>' +
|
|
256
|
+
'</div>';
|
|
257
|
+
|
|
258
|
+
$('modalContainer').innerHTML = '<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:500px">' + html + '</div></div>';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function doUpdateSite(name) {
|
|
262
|
+
var data = {
|
|
263
|
+
display_name: $('editSiteDisplay')?.value || '',
|
|
264
|
+
category: $('editSiteCategory')?.value || 'custom',
|
|
265
|
+
login_url: $('editSiteLoginUrl')?.value || '',
|
|
266
|
+
home_url: $('editSiteHomeUrl')?.value || '',
|
|
267
|
+
detect_url: $('editSiteDetectUrl')?.value || '',
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
var r = await api('/api/sites/' + encodeURIComponent(name), { method: 'PUT', body: JSON.stringify(data) });
|
|
271
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
272
|
+
showToast('已更新网站: ' + name, 'success');
|
|
273
|
+
closeModal();
|
|
274
|
+
renderSites();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Remove Site ──
|
|
278
|
+
function removeSite(name) {
|
|
279
|
+
showConfirm('删除网站', '确定要删除网站 "' + name + '" 吗?将同时删除浏览器 Profile。', function() {
|
|
280
|
+
return api('/api/sites/' + encodeURIComponent(name), { method: 'DELETE' }).then(function(r) {
|
|
281
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
282
|
+
showToast('已删除: ' + name, 'success');
|
|
283
|
+
renderSites();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Profile Management ──
|
|
289
|
+
async function initSiteProfile(name) {
|
|
290
|
+
var r = await api('/api/sites/' + encodeURIComponent(name) + '/init-profile', { method: 'POST' });
|
|
291
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
292
|
+
showToast('Profile 已初始化: ' + name, 'success');
|
|
293
|
+
renderSites();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function initAllProfiles() {
|
|
297
|
+
showToast('正在初始化所有 Profile...', 'info');
|
|
298
|
+
var r = await api('/api/sites');
|
|
299
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
300
|
+
var names = (r.sites || []).map(function(s) { return s.name; });
|
|
301
|
+
var count = 0;
|
|
302
|
+
for (var i = 0; i < names.length; i++) {
|
|
303
|
+
try {
|
|
304
|
+
var pr = await api('/api/sites/' + encodeURIComponent(names[i]) + '/init-profile', { method: 'POST' });
|
|
305
|
+
if (pr.ok) count++;
|
|
306
|
+
} catch (e) { /* skip */ }
|
|
307
|
+
}
|
|
308
|
+
showToast('已初始化 ' + count + ' 个 Profile', 'success');
|
|
309
|
+
renderSites();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function deleteSiteProfile(name) {
|
|
313
|
+
showConfirm('清除登录状态', '确定要删除 "' + name + '" 的浏览器 Profile 吗?这将清除登录状态和 Cookie。', function() {
|
|
314
|
+
return api('/api/sites/profiles/' + encodeURIComponent(name), { method: 'DELETE' }).then(function(r) {
|
|
315
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
316
|
+
showToast('Profile 已删除: ' + name, 'success');
|
|
317
|
+
renderSites();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Browser Login ──
|
|
323
|
+
async function openSiteBrowser(name) {
|
|
324
|
+
showToast('正在启动浏览器...', 'info');
|
|
325
|
+
var r = await api('/api/sites/' + encodeURIComponent(name) + '/open-browser', { method: 'POST' });
|
|
326
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
327
|
+
showToast(r.message || '浏览器已打开', 'success', 5000);
|
|
328
|
+
// Refresh to update profile status
|
|
329
|
+
setTimeout(function() { renderSites(); }, 2000);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function closeSiteBrowser(name) {
|
|
333
|
+
var r = await api('/api/sites/' + encodeURIComponent(name) + '/close-browser', { method: 'POST' });
|
|
334
|
+
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
335
|
+
showToast('浏览器已关闭', 'success');
|
|
336
|
+
setTimeout(function() { renderSites(); }, 1000);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Register renderer
|
|
340
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
341
|
+
window._adminRenderers['sites'] = renderSites;
|
|
@@ -5,6 +5,10 @@ const CLI_CAT_LABELS={perception:'感知',search:'搜索',file:'文件操作',do
|
|
|
5
5
|
|
|
6
6
|
async function renderSkills(){
|
|
7
7
|
const cats=await api('/api/skills?view=categorized');
|
|
8
|
+
if(cats.error){
|
|
9
|
+
$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(cats.error)+'</div>';
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
const bt=cats.builtin_tools||[];
|
|
9
13
|
const cc=cats.cli_commands||[];
|
|
10
14
|
const ps=cats.python_skills||[];
|
package/web/ui/index.html
CHANGED
|
@@ -328,6 +328,7 @@ tr:hover{background:var(--surface2)}
|
|
|
328
328
|
<div class="nav-item" data-tooltip="大模型设置" data-page="llm"><span class="icon">🧬</span><span class="icon-text">大模型设置</span></div>
|
|
329
329
|
<div class="nav-item" data-tooltip="系统配置" data-page="system"><span class="icon">⚙️</span><span class="icon-text">系统配置</span></div>
|
|
330
330
|
<div class="nav-item" data-tooltip="执行引擎" data-page="executor"><span class="icon">🔧</span><span class="icon-text">执行引擎</span></div>
|
|
331
|
+
<div class="nav-item" data-tooltip="网站管理" data-page="sites"><span class="icon">🌐</span><span class="icon-text">网站管理</span></div>
|
|
331
332
|
<div class="nav-item" data-tooltip="技能管理" data-page="skills"><span class="icon">🛠</span><span class="icon-text">技能管理</span></div>
|
|
332
333
|
<div class="nav-item" data-tooltip="工作目录" data-page="files"><span class="icon">📁</span><span class="icon-text">工作目录</span></div>
|
|
333
334
|
<div class="nav-item" data-tooltip="查看日志" data-page="logs"><span class="icon">📜</span><span class="icon-text">查看日志</span></div>
|
|
@@ -358,6 +359,7 @@ tr:hover{background:var(--surface2)}
|
|
|
358
359
|
<script src="admin/admin-permissions.js"></script>
|
|
359
360
|
<script src="admin/admin-llm.js"></script>
|
|
360
361
|
<script src="admin/admin-executor.js"></script>
|
|
362
|
+
<script src="admin/admin-sites.js"></script>
|
|
361
363
|
<script src="admin/admin-skills.js"></script>
|
|
362
364
|
<script src="admin/admin-files.js"></script>
|
|
363
365
|
<script src="admin/admin-logs.js"></script>
|
package/worklog.md
CHANGED
|
@@ -396,3 +396,27 @@ Work Log:
|
|
|
396
396
|
- 查询优化:将LIKE前缀查询改为=精确匹配agent_id
|
|
397
397
|
- 向后兼容:同时支持新旧两种格式的session_id
|
|
398
398
|
|
|
399
|
+
---
|
|
400
|
+
Task ID: 1
|
|
401
|
+
Agent: main
|
|
402
|
+
Task: 修复 myagent-ai v1.31.0 中的多个关键漏洞
|
|
403
|
+
|
|
404
|
+
Work Log:
|
|
405
|
+
- 全面审计 browser_stealth.py、main.py、core/browser_profile.py、core/site_registry.py
|
|
406
|
+
- 发现 8 个漏洞(3 个高危、3 个中危、2 个低危)
|
|
407
|
+
- 修复 browser_stealth.py: time.sleep(1) → await asyncio.sleep(1)
|
|
408
|
+
- 修复 browser_stealth.py: _detect_browser 增加 macOS/Windows 路径
|
|
409
|
+
- 修复 browser_stealth.py: get_stealth_browser 清理崩溃实例并重建
|
|
410
|
+
- 修复 main.py: _running_service 在 __init__ 初始化
|
|
411
|
+
- 修复 main.py: shutdown() 关闭所有 stealth 浏览器
|
|
412
|
+
- 修复 main.py: 会话锁 TOCTOU 竞争条件
|
|
413
|
+
- 修复 main.py: 会话锁内存泄漏(_cleanup_session_locks)
|
|
414
|
+
- 修复 main.py: Unicode Ideographic Space 填充问题
|
|
415
|
+
- 修复 main.py: xdpyinfo 分辨率解析过于宽松
|
|
416
|
+
- 版本升级 v1.31.0 → v1.31.1
|
|
417
|
+
- GitHub 推送成功,npm 发布成功
|
|
418
|
+
|
|
419
|
+
Stage Summary:
|
|
420
|
+
- 修复了 8 个漏洞,涵盖事件循环阻塞、属性未初始化、资源泄漏、竞态条件、跨平台兼容性等
|
|
421
|
+
- 已发布 myagent-ai@1.31.1 到 npm 和 GitHub
|
|
422
|
+
|