myagent-ai 1.12.2 → 1.12.4
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/agents/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/agents/main_agent.py +0 -7
- package/main.py +0 -2
- package/package.json +1 -1
- package/skills/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/registry.py +6 -239
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +0 -51
- package/web/ui/chat/chat.css +9 -0
- package/web/ui/chat/chat_main.js +40 -1
- package/web/ui/chat/middle_chat.html +3 -0
- package/web/ui/index.html +12 -44
|
Binary file
|
package/agents/main_agent.py
CHANGED
|
@@ -976,15 +976,8 @@ class MainAgent(BaseAgent):
|
|
|
976
976
|
# 数据密集型工具允许更长的输出
|
|
977
977
|
_HEAVY_TOOLS = ("web_search", "web_read", "url_read", "file_list",
|
|
978
978
|
"file_search", "browser_open", "process_list")
|
|
979
|
-
# OpenClaw prompt-only 技能也允许较长输出(SKILL.md 指令)
|
|
980
|
-
_is_openclaw = (
|
|
981
|
-
isinstance(tool_result.get("data"), dict)
|
|
982
|
-
and tool_result.get("data", {}).get("skill_type") == "openclaw"
|
|
983
|
-
)
|
|
984
979
|
if tool_name in _HEAVY_TOOLS:
|
|
985
980
|
_max_output = 6000
|
|
986
|
-
elif _is_openclaw:
|
|
987
|
-
_max_output = 8000
|
|
988
981
|
else:
|
|
989
982
|
_max_output = 3000
|
|
990
983
|
tool_outputs_parts.append(
|
package/main.py
CHANGED
|
@@ -258,8 +258,6 @@ class MyAgentApp:
|
|
|
258
258
|
self.skill_registry = SkillRegistry()
|
|
259
259
|
# 注册内置技能
|
|
260
260
|
self._register_builtin_skills()
|
|
261
|
-
# 加载外部 OpenClaw 技能
|
|
262
|
-
self.skill_registry.load_openclaw_skills()
|
|
263
261
|
skills = self.skill_registry.list_skills()
|
|
264
262
|
self.logger.info(f"技能系统: {len(skills)} 个技能已注册 - {skills}")
|
|
265
263
|
|
package/package.json
CHANGED
|
Binary file
|
package/skills/registry.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
skills/registry.py - 技能注册表
|
|
3
3
|
================================
|
|
4
|
-
|
|
5
|
-
支持动态注册、热加载和 OpenClaw 外部技能桥接。
|
|
4
|
+
管理所有已注册的内置技能,提供查找、调用、列表功能。
|
|
6
5
|
"""
|
|
7
6
|
from __future__ import annotations
|
|
8
7
|
|
|
9
8
|
import importlib
|
|
10
9
|
import inspect
|
|
11
|
-
import json
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from typing import Any, Dict, List, Optional, Set
|
|
14
12
|
|
|
@@ -18,168 +16,6 @@ from skills.base import Skill, SkillResult, SkillParameter
|
|
|
18
16
|
logger = get_logger("myagent.skills")
|
|
19
17
|
|
|
20
18
|
|
|
21
|
-
class OpenClawSkillAdapter(Skill):
|
|
22
|
-
"""
|
|
23
|
-
OpenClaw 外部技能适配器。
|
|
24
|
-
|
|
25
|
-
将 skills/ 目录下的 OpenClaw 格式技能(SKILL.md + scripts/)包装为
|
|
26
|
-
Skill 基类实例,使其可以在 SkillRegistry 中统一管理。
|
|
27
|
-
|
|
28
|
-
OpenClaw 技能结构:
|
|
29
|
-
skills/
|
|
30
|
-
skill-name/
|
|
31
|
-
SKILL.md # 技能描述和指令
|
|
32
|
-
scripts/ # 可执行脚本
|
|
33
|
-
references/ # 参考文档
|
|
34
|
-
skill.json # 技能元数据(可选)
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
def __init__(self, skill_dir: Path):
|
|
38
|
-
super().__init__()
|
|
39
|
-
self._skill_dir = skill_dir
|
|
40
|
-
self._skill_md = ""
|
|
41
|
-
self._scripts: Dict[str, Path] = {}
|
|
42
|
-
self._metadata: Dict[str, Any] = {}
|
|
43
|
-
self._load_skill_info()
|
|
44
|
-
|
|
45
|
-
def _load_skill_info(self):
|
|
46
|
-
"""加载技能信息"""
|
|
47
|
-
self.name = self._skill_dir.name
|
|
48
|
-
|
|
49
|
-
# 读取 SKILL.md
|
|
50
|
-
skill_md_path = self._skill_dir / "SKILL.md"
|
|
51
|
-
if skill_md_path.exists():
|
|
52
|
-
self._skill_md = skill_md_path.read_text(encoding="utf-8", errors="ignore")
|
|
53
|
-
# 优先从 YAML frontmatter 提取 description
|
|
54
|
-
self.description = self._extract_description(self._skill_md)
|
|
55
|
-
|
|
56
|
-
# 读取 skill.json(如果有)
|
|
57
|
-
skill_json_path = self._skill_dir / "skill.json"
|
|
58
|
-
if skill_json_path.exists():
|
|
59
|
-
try:
|
|
60
|
-
self._metadata = json.loads(skill_json_path.read_text(encoding="utf-8"))
|
|
61
|
-
if "name" in self._metadata:
|
|
62
|
-
self.name = self._metadata["name"]
|
|
63
|
-
if "description" in self._metadata:
|
|
64
|
-
self.description = self._metadata["description"]
|
|
65
|
-
except json.JSONDecodeError:
|
|
66
|
-
pass
|
|
67
|
-
|
|
68
|
-
# 发现脚本文件
|
|
69
|
-
scripts_dir = self._skill_dir / "scripts"
|
|
70
|
-
if scripts_dir.exists():
|
|
71
|
-
for script_file in scripts_dir.iterdir():
|
|
72
|
-
if script_file.is_file() and not script_file.name.startswith("_"):
|
|
73
|
-
# 去除扩展名作为参数名
|
|
74
|
-
param_name = script_file.stem
|
|
75
|
-
self._scripts[param_name] = script_file
|
|
76
|
-
|
|
77
|
-
# 构建 OpenClaw 技能的通用参数
|
|
78
|
-
self.parameters = [
|
|
79
|
-
SkillParameter(
|
|
80
|
-
name="task",
|
|
81
|
-
type="string",
|
|
82
|
-
description=f"要执行的任务指令({self.description})",
|
|
83
|
-
required=True,
|
|
84
|
-
),
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
def _extract_description(self, md_content: str) -> str:
|
|
88
|
-
"""从 SKILL.md 内容中提取描述(支持 YAML frontmatter)"""
|
|
89
|
-
lines = md_content.strip().split("\n")
|
|
90
|
-
|
|
91
|
-
# 如果以 --- 开头,解析 YAML frontmatter
|
|
92
|
-
if lines and lines[0].strip() == "---":
|
|
93
|
-
for i, line in enumerate(lines[1:]):
|
|
94
|
-
if line.strip() == "---":
|
|
95
|
-
break
|
|
96
|
-
stripped = line.strip()
|
|
97
|
-
if stripped.startswith("description:"):
|
|
98
|
-
desc_prefix = stripped[len("description:"):].strip()
|
|
99
|
-
# 去掉引号
|
|
100
|
-
if (desc_prefix.startswith('"') and desc_prefix.endswith('"')) or \
|
|
101
|
-
(desc_prefix.startswith("'") and desc_prefix.endswith("'")):
|
|
102
|
-
return desc_prefix[1:-1][:200]
|
|
103
|
-
# YAML 多行格式: > 或 |
|
|
104
|
-
if desc_prefix in (">", ">", "|"):
|
|
105
|
-
# 收集后续缩进行作为描述
|
|
106
|
-
desc_lines = []
|
|
107
|
-
for next_line in lines[i + 2:]: # 跳过 description: > 行和当前行
|
|
108
|
-
if next_line.strip() == "---":
|
|
109
|
-
break
|
|
110
|
-
if next_line.strip() and not next_line.startswith(" "):
|
|
111
|
-
break
|
|
112
|
-
if next_line.strip():
|
|
113
|
-
desc_lines.append(next_line.strip())
|
|
114
|
-
return " ".join(desc_lines)[:200]
|
|
115
|
-
# 单行描述
|
|
116
|
-
return desc_prefix[:200]
|
|
117
|
-
# frontmatter 中没有 description,取第一个 # 标题行
|
|
118
|
-
for line in lines:
|
|
119
|
-
stripped = line.strip()
|
|
120
|
-
if stripped.startswith("# "):
|
|
121
|
-
return stripped[2:].strip()[:200]
|
|
122
|
-
return self.name
|
|
123
|
-
|
|
124
|
-
# 没有 frontmatter,取第一行
|
|
125
|
-
first_line = lines[0] if lines else ""
|
|
126
|
-
return first_line.lstrip("# ").strip()[:200]
|
|
127
|
-
|
|
128
|
-
async def execute(self, **kwargs) -> SkillResult:
|
|
129
|
-
"""
|
|
130
|
-
执行 OpenClaw 技能。
|
|
131
|
-
|
|
132
|
-
将 SKILL.md 的内容作为系统提示,
|
|
133
|
-
结合用户任务参数,返回使用指引。
|
|
134
|
-
"""
|
|
135
|
-
task = kwargs.get("task", "")
|
|
136
|
-
|
|
137
|
-
# 构建使用指引(实际执行由 Agent 的 LLM 解读 SKILL.md 指令完成)
|
|
138
|
-
references_info = ""
|
|
139
|
-
ref_dir = self._skill_dir / "references"
|
|
140
|
-
if ref_dir.exists():
|
|
141
|
-
ref_files = list(ref_dir.glob("*.md"))
|
|
142
|
-
if ref_files:
|
|
143
|
-
references_info = f"\n\n## 参考文档\n可用参考文件: {', '.join(f.name for f in ref_files)}"
|
|
144
|
-
|
|
145
|
-
scripts_info = ""
|
|
146
|
-
if self._scripts:
|
|
147
|
-
scripts_info = f"\n\n## 可用脚本\n"
|
|
148
|
-
for name, path in self._scripts.items():
|
|
149
|
-
scripts_info += f"- `{name}` ({path.suffix}): {path}\n"
|
|
150
|
-
|
|
151
|
-
skill_md_text = self._skill_md[:8000]
|
|
152
|
-
|
|
153
|
-
result_text = (
|
|
154
|
-
f"## 技能: {self.name}\n\n"
|
|
155
|
-
f"### 描述\n{self.description}\n\n"
|
|
156
|
-
f"### 使用说明\n{skill_md_text}"
|
|
157
|
-
f"{references_info}{scripts_info}\n\n"
|
|
158
|
-
f"### 当前任务\n{task}\n\n"
|
|
159
|
-
f"请根据上述技能说明完成当前任务。"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
return SkillResult(
|
|
163
|
-
success=True,
|
|
164
|
-
data={
|
|
165
|
-
"skill_type": "openclaw",
|
|
166
|
-
"skill_name": self.name,
|
|
167
|
-
"skill_dir": str(self._skill_dir),
|
|
168
|
-
"instruction": result_text,
|
|
169
|
-
"has_scripts": bool(self._scripts),
|
|
170
|
-
"scripts": {name: str(path) for name, path in self._scripts.items()},
|
|
171
|
-
},
|
|
172
|
-
output=result_text,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
def to_openclaw_format(self) -> dict:
|
|
176
|
-
"""导出为 OpenClaw 格式"""
|
|
177
|
-
result = super().to_openclaw_format()
|
|
178
|
-
result["source"] = "openclaw_external"
|
|
179
|
-
result["skill_dir"] = str(self._skill_dir)
|
|
180
|
-
return result
|
|
181
|
-
|
|
182
|
-
|
|
183
19
|
class SkillRegistry:
|
|
184
20
|
"""
|
|
185
21
|
技能注册表。
|
|
@@ -197,14 +33,10 @@ class SkillRegistry:
|
|
|
197
33
|
|
|
198
34
|
# 按名称执行
|
|
199
35
|
result = await registry.execute("file_read", path="/tmp/test.txt")
|
|
200
|
-
|
|
201
|
-
# 加载外部 OpenClaw 技能
|
|
202
|
-
registry.load_openclaw_skills("/path/to/skills/")
|
|
203
36
|
"""
|
|
204
37
|
|
|
205
38
|
def __init__(self):
|
|
206
39
|
self._skills: Dict[str, Skill] = {}
|
|
207
|
-
self._openclaw_skills: Dict[str, OpenClawSkillAdapter] = {}
|
|
208
40
|
self.disabled_skills: Set[str] = set()
|
|
209
41
|
|
|
210
42
|
def toggle(self, name: str, enabled: bool):
|
|
@@ -233,27 +65,17 @@ class SkillRegistry:
|
|
|
233
65
|
del self._skills[name]
|
|
234
66
|
logger.debug(f"技能已注销: {name}")
|
|
235
67
|
return True
|
|
236
|
-
if name in self._openclaw_skills:
|
|
237
|
-
del self._openclaw_skills[name]
|
|
238
|
-
logger.debug(f"OpenClaw 技能已注销: {name}")
|
|
239
|
-
return True
|
|
240
68
|
return False
|
|
241
69
|
|
|
242
70
|
def get(self, name: str) -> Optional[Skill]:
|
|
243
|
-
"""
|
|
71
|
+
"""获取技能(跳过禁用)"""
|
|
244
72
|
if self._is_disabled(name):
|
|
245
73
|
return None
|
|
246
|
-
|
|
247
|
-
if skill:
|
|
248
|
-
return skill
|
|
249
|
-
return self._openclaw_skills.get(name)
|
|
74
|
+
return self._skills.get(name)
|
|
250
75
|
|
|
251
76
|
def list_skills(self) -> List[str]:
|
|
252
77
|
"""列出所有技能名称(跳过禁用)"""
|
|
253
|
-
|
|
254
|
-
external = [f"[OpenClaw] {name}" for name in self._openclaw_skills.keys()
|
|
255
|
-
if name not in self._skills and not self._is_disabled(name)]
|
|
256
|
-
return builtin + external
|
|
78
|
+
return [name for name in self._skills.keys() if not self._is_disabled(name)]
|
|
257
79
|
|
|
258
80
|
def list_skills_info(self) -> List[Dict]:
|
|
259
81
|
"""列出所有技能的详细信息(含禁用状态)"""
|
|
@@ -262,17 +84,11 @@ class SkillRegistry:
|
|
|
262
84
|
info = skill.to_openclaw_format()
|
|
263
85
|
info["disabled"] = self._is_disabled(skill.name)
|
|
264
86
|
results.append(info)
|
|
265
|
-
for skill in self._openclaw_skills.values():
|
|
266
|
-
if skill.name not in self._skills:
|
|
267
|
-
info = skill.to_openclaw_format()
|
|
268
|
-
info["disabled"] = self._is_disabled(skill.name)
|
|
269
|
-
results.append(info)
|
|
270
87
|
return results
|
|
271
88
|
|
|
272
89
|
def get_all_schemas(self) -> List[Dict]:
|
|
273
90
|
"""获取所有技能的 JSON Schema (用于 LLM function calling),跳过禁用"""
|
|
274
|
-
|
|
275
|
-
return [skill.get_schema() for name, skill in all_skills.items()
|
|
91
|
+
return [skill.get_schema() for name, skill in self._skills.items()
|
|
276
92
|
if not self._is_disabled(name)]
|
|
277
93
|
|
|
278
94
|
async def execute(self, name: str, **kwargs) -> SkillResult:
|
|
@@ -311,7 +127,7 @@ class SkillRegistry:
|
|
|
311
127
|
|
|
312
128
|
def auto_discover(self, package: str = "skills"):
|
|
313
129
|
"""
|
|
314
|
-
自动发现并注册 skills/
|
|
130
|
+
自动发现并注册 skills/ 目录下的所有内置技能。
|
|
315
131
|
|
|
316
132
|
Args:
|
|
317
133
|
package: 技能包路径
|
|
@@ -343,53 +159,6 @@ class SkillRegistry:
|
|
|
343
159
|
except Exception as e:
|
|
344
160
|
logger.warning(f"模块导入失败 ({module_name}): {e}")
|
|
345
161
|
|
|
346
|
-
def load_openclaw_skills(self, skills_root: str = ""):
|
|
347
|
-
"""
|
|
348
|
-
加载外部 OpenClaw 格式技能。
|
|
349
|
-
|
|
350
|
-
扫描指定目录下所有包含 SKILL.md 的子目录,
|
|
351
|
-
为每个技能创建 OpenClawSkillAdapter 并注册。
|
|
352
|
-
|
|
353
|
-
Args:
|
|
354
|
-
skills_root: 外部技能根目录(默认为项目根目录下的 skills/)
|
|
355
|
-
"""
|
|
356
|
-
if not skills_root:
|
|
357
|
-
# 默认加载 registry.py 所在目录(即 skills/ 目录)下的 OpenClaw 技能
|
|
358
|
-
skills_root = str(Path(__file__).parent)
|
|
359
|
-
|
|
360
|
-
skills_path = Path(skills_root)
|
|
361
|
-
if not skills_path.exists():
|
|
362
|
-
logger.debug(f"外部技能目录不存在: {skills_path}")
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
loaded_count = 0
|
|
366
|
-
for skill_dir in skills_path.iterdir():
|
|
367
|
-
if not skill_dir.is_dir():
|
|
368
|
-
continue
|
|
369
|
-
if skill_dir.name.startswith("_") or skill_dir.name.startswith("."):
|
|
370
|
-
continue
|
|
371
|
-
|
|
372
|
-
# 检查是否包含 SKILL.md
|
|
373
|
-
if not (skill_dir / "SKILL.md").exists():
|
|
374
|
-
continue
|
|
375
|
-
|
|
376
|
-
skill_name = skill_dir.name
|
|
377
|
-
|
|
378
|
-
# 跳过已注册的同名内置技能
|
|
379
|
-
if skill_name in self._skills:
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
try:
|
|
383
|
-
adapter = OpenClawSkillAdapter(skill_dir)
|
|
384
|
-
self._openclaw_skills[skill_name] = adapter
|
|
385
|
-
loaded_count += 1
|
|
386
|
-
logger.info(f"已加载 OpenClaw 技能: {skill_name} - {adapter.description[:80]}")
|
|
387
|
-
except Exception as e:
|
|
388
|
-
logger.warning(f"加载 OpenClaw 技能失败 ({skill_name}): {e}")
|
|
389
|
-
|
|
390
|
-
if loaded_count > 0:
|
|
391
|
-
logger.info(f"共加载 {loaded_count} 个 OpenClaw 外部技能")
|
|
392
|
-
|
|
393
162
|
|
|
394
163
|
# ==============================================================================
|
|
395
164
|
# 全局注册表
|
|
@@ -404,6 +173,4 @@ def get_skill_registry() -> SkillRegistry:
|
|
|
404
173
|
if _global_registry is None:
|
|
405
174
|
_global_registry = SkillRegistry()
|
|
406
175
|
_global_registry.auto_discover()
|
|
407
|
-
# 自动加载外部 OpenClaw 技能
|
|
408
|
-
_global_registry.load_openclaw_skills()
|
|
409
176
|
return _global_registry
|
|
Binary file
|
package/web/api_server.py
CHANGED
|
@@ -294,8 +294,6 @@ class ApiServer:
|
|
|
294
294
|
r.add_get("/api/skills", self.handle_list_skills)
|
|
295
295
|
r.add_get("/api/skills/{name}", self.handle_get_skill)
|
|
296
296
|
r.add_post("/api/skills/{name}/toggle", self.handle_toggle_skill)
|
|
297
|
-
r.add_post("/api/skills/{name}/uninstall", self.handle_uninstall_skill)
|
|
298
|
-
r.add_get("/api/skills/available", self.handle_available_skills)
|
|
299
297
|
r.add_get("/api/executor", self.handle_get_executor)
|
|
300
298
|
r.add_put("/api/executor", self.handle_update_executor)
|
|
301
299
|
r.add_get("/api/workdir", self.handle_get_workdir)
|
|
@@ -2759,55 +2757,6 @@ class ApiServer:
|
|
|
2759
2757
|
self._save_disabled_skills()
|
|
2760
2758
|
return web.json_response({"ok": True, "name": name, "enabled": enabled})
|
|
2761
2759
|
|
|
2762
|
-
async def handle_uninstall_skill(self, request):
|
|
2763
|
-
"""POST /api/skills/{name}/uninstall - 卸载 OpenClaw 技能"""
|
|
2764
|
-
name = request.match_info["name"]
|
|
2765
|
-
if self.core.skill_registry:
|
|
2766
|
-
# 检查是否为 OpenClaw 外部技能
|
|
2767
|
-
skill = self.core.skill_registry.get(name)
|
|
2768
|
-
if not skill:
|
|
2769
|
-
return web.json_response({"error": f"技能 '{name}' 不存在"}, status=404)
|
|
2770
|
-
info = skill.to_openclaw_format()
|
|
2771
|
-
if info.get("source") != "openclaw_external":
|
|
2772
|
-
return web.json_response({"error": f"技能 '{name}' 是内置技能,不可卸载"}, status=400)
|
|
2773
|
-
# 获取技能目录
|
|
2774
|
-
skill_dir = info.get("skill_dir", "")
|
|
2775
|
-
if skill_dir and os.path.isdir(skill_dir):
|
|
2776
|
-
shutil.rmtree(skill_dir)
|
|
2777
|
-
self.core.skill_registry.unregister(name)
|
|
2778
|
-
logger.info(f"已卸载 OpenClaw 技能: {name}")
|
|
2779
|
-
return web.json_response({"ok": True, "name": name})
|
|
2780
|
-
else:
|
|
2781
|
-
return web.json_response({"error": f"技能目录不存在: {skill_dir}"}, status=404)
|
|
2782
|
-
return web.json_response({"error": "技能注册表未初始化"}, status=500)
|
|
2783
|
-
|
|
2784
|
-
async def handle_available_skills(self, request):
|
|
2785
|
-
"""GET /api/skills/available - 列出可安装的 OpenClaw 技能"""
|
|
2786
|
-
skills_root = Path(__file__).resolve().parent.parent.parent / "skills"
|
|
2787
|
-
available = []
|
|
2788
|
-
if not skills_root.exists():
|
|
2789
|
-
return web.json_response(available)
|
|
2790
|
-
# 收集当前已注册技能名
|
|
2791
|
-
registered_names = set()
|
|
2792
|
-
if self.core.skill_registry:
|
|
2793
|
-
for s in self.core.skill_registry.list_skills_info():
|
|
2794
|
-
registered_names.add(s.get("name", ""))
|
|
2795
|
-
for d in sorted(skills_root.iterdir()):
|
|
2796
|
-
if not d.is_dir() or d.name.startswith("_") or d.name.startswith("."):
|
|
2797
|
-
continue
|
|
2798
|
-
if not (d / "SKILL.md").exists():
|
|
2799
|
-
continue
|
|
2800
|
-
if d.name in registered_names:
|
|
2801
|
-
continue
|
|
2802
|
-
# 读取基本信息
|
|
2803
|
-
desc = ""
|
|
2804
|
-
md = d / "SKILL.md"
|
|
2805
|
-
if md.exists():
|
|
2806
|
-
first_line = md.read_text(encoding="utf-8", errors="ignore").strip().split("\n")[0]
|
|
2807
|
-
desc = first_line.lstrip("# ").strip()
|
|
2808
|
-
available.append({"name": d.name, "description": desc, "source": "openclaw_external", "installed": False})
|
|
2809
|
-
return web.json_response(available)
|
|
2810
|
-
|
|
2811
2760
|
def _save_disabled_skills(self):
|
|
2812
2761
|
"""将禁用技能列表持久化到配置文件"""
|
|
2813
2762
|
if not self.core.skill_registry:
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -2152,6 +2152,15 @@ input,textarea,select{font:inherit}
|
|
|
2152
2152
|
white-space: pre-wrap;
|
|
2153
2153
|
}
|
|
2154
2154
|
|
|
2155
|
+
/* ══════════════════════════════════════════════════════
|
|
2156
|
+
── Popout Mode (独立窗口) ──
|
|
2157
|
+
══════════════════════════════════════════════════════ */
|
|
2158
|
+
body.popout-mode .main{margin-left:0 !important;border-left:none !important}
|
|
2159
|
+
body.popout-mode .agent-panel{display:none !important}
|
|
2160
|
+
body.popout-mode .main-header{padding-left:12px}
|
|
2161
|
+
body.popout-mode #popoutBtn{display:none !important}
|
|
2162
|
+
body.popout-mode #debugToggleBtn{display:none !important}
|
|
2163
|
+
|
|
2155
2164
|
/* ══════════════════════════════════════════════════════
|
|
2156
2165
|
── Mobile Responsive (≤768px) ──
|
|
2157
2166
|
══════════════════════════════════════════════════════ */
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -96,6 +96,9 @@ document.addEventListener('click', function(e) {
|
|
|
96
96
|
function checkChatMobile() {
|
|
97
97
|
const btn = document.getElementById('mobileAgentsBtn');
|
|
98
98
|
if (btn) btn.style.display = isMobile() ? 'grid' : 'none';
|
|
99
|
+
// Hide popout button on mobile
|
|
100
|
+
const popoutBtn = document.getElementById('popoutBtn');
|
|
101
|
+
if (popoutBtn) popoutBtn.style.display = isMobile() ? 'none' : 'grid';
|
|
99
102
|
}
|
|
100
103
|
window.addEventListener('resize', checkChatMobile);
|
|
101
104
|
// Run after DOM ready
|
|
@@ -105,6 +108,25 @@ if (document.readyState === 'loading') {
|
|
|
105
108
|
checkChatMobile();
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
// ── Popout Chat Window (Desktop only) ──
|
|
112
|
+
function popoutChat() {
|
|
113
|
+
if (isMobile()) return;
|
|
114
|
+
// Build URL with current agent + session + mode, plus popout flag
|
|
115
|
+
const baseUrl = new URL('.', window.location.href).href + 'chat/chat_container.html';
|
|
116
|
+
const params = new URLSearchParams();
|
|
117
|
+
if (state.activeAgent) params.set('agent', state.activeAgent);
|
|
118
|
+
if (state.activeSessionId && state.activeSessionId !== '__new__') params.set('session', state.activeSessionId);
|
|
119
|
+
if (state.chatMode) params.set('mode', state.chatMode);
|
|
120
|
+
params.set('popout', '1');
|
|
121
|
+
const popoutUrl = baseUrl + '?' + params.toString();
|
|
122
|
+
// Open as a standalone window
|
|
123
|
+
const w = Math.min(900, screen.width - 100);
|
|
124
|
+
const h = Math.min(720, screen.height - 100);
|
|
125
|
+
const left = Math.round((screen.width - w) / 2);
|
|
126
|
+
const top = Math.round((screen.height - h) / 2);
|
|
127
|
+
window.open(popoutUrl, 'myagent_popout_' + (state.activeAgent || 'default'), 'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes');
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
// Override toggleAgentPanel for mobile (deferred since function is defined later)
|
|
109
131
|
var _origToggleAgentPanel = null;
|
|
110
132
|
(function() {
|
|
@@ -273,11 +295,12 @@ function initChat() {
|
|
|
273
295
|
// Restore persisted UI state
|
|
274
296
|
StatePersistence.restoreUIState();
|
|
275
297
|
|
|
276
|
-
// URL 参数处理: ?agent=xxx&mode=exec&session=xxx
|
|
298
|
+
// URL 参数处理: ?agent=xxx&mode=exec&session=xxx&popout=1
|
|
277
299
|
const urlParams = new URLSearchParams(window.location.search);
|
|
278
300
|
const urlAgent = urlParams.get('agent');
|
|
279
301
|
const urlMode = urlParams.get('mode');
|
|
280
302
|
const urlSession = urlParams.get('session');
|
|
303
|
+
const isPopout = urlParams.get('popout') === '1';
|
|
281
304
|
if (urlMode === 'chat' || urlMode === 'exec') {
|
|
282
305
|
state.chatMode = urlMode;
|
|
283
306
|
}
|
|
@@ -285,6 +308,22 @@ function initChat() {
|
|
|
285
308
|
state.activeAgent = urlAgent;
|
|
286
309
|
}
|
|
287
310
|
|
|
311
|
+
// Popout mode: hide sidebar, collapse agent panel, update title
|
|
312
|
+
if (isPopout) {
|
|
313
|
+
document.body.classList.add('popout-mode');
|
|
314
|
+
const sidebar = document.getElementById('sidebar');
|
|
315
|
+
const sidebarToggle = document.getElementById('sidebarToggle');
|
|
316
|
+
if (sidebar) sidebar.style.display = 'none';
|
|
317
|
+
if (sidebarToggle) sidebarToggle.style.display = 'none';
|
|
318
|
+
const agentPanel = document.getElementById('agentPanel');
|
|
319
|
+
if (agentPanel) agentPanel.classList.add('collapsed');
|
|
320
|
+
const agentToggle = document.getElementById('agentToggle');
|
|
321
|
+
if (agentToggle) agentToggle.style.display = 'none';
|
|
322
|
+
// Update page title with agent name
|
|
323
|
+
const agentObj = findAgentByPath(urlAgent);
|
|
324
|
+
document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
|
|
325
|
+
}
|
|
326
|
+
|
|
288
327
|
// Apply restored state to DOM
|
|
289
328
|
const panel = document.getElementById('agentPanel');
|
|
290
329
|
if (panel) panel.classList.toggle('collapsed', !state.agentPanelOpen);
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
<button class="header-btn" id="themeToggle" title="切换主题">
|
|
32
32
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
33
33
|
</button>
|
|
34
|
+
<button class="header-btn" id="popoutBtn" onclick="popoutChat()" title="弹出独立窗口">
|
|
35
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/></svg>
|
|
36
|
+
</button>
|
|
34
37
|
</div>
|
|
35
38
|
</div>
|
|
36
39
|
|
package/web/ui/index.html
CHANGED
|
@@ -1305,51 +1305,39 @@ async function renderExecutor(){
|
|
|
1305
1305
|
async function switchMode(mode){const r=await api('/api/executor',{method:'PUT',body:JSON.stringify({execution_mode:mode})});if(!r.ok){showToast('切换失败: '+(r.error||''),'danger');renderExecutor();}else renderExecutor();}
|
|
1306
1306
|
async function saveExecutor(){await api('/api/executor',{method:'PUT',body:JSON.stringify({sandbox_image:$('sbImage').value,sandbox_memory:$('sbMemory').value,sandbox_network:$('sbNetwork').value==='true',timeout:parseInt($('exTimeout').value),auto_fix:$('exAutoFix').value==='true'})});showToast('已保存','success');renderExecutor();}
|
|
1307
1307
|
|
|
1308
|
-
// ========== Skills
|
|
1308
|
+
// ========== Skills ==========
|
|
1309
1309
|
async function renderSkills(){
|
|
1310
1310
|
const sk=await api('/api/skills');
|
|
1311
|
-
const builtin=sk.filter(s=>s.source!=='openclaw_external');
|
|
1312
|
-
const external=sk.filter(s=>s.source==='openclaw_external');
|
|
1313
1311
|
const disabledCount=sk.filter(s=>s.disabled).length;
|
|
1314
1312
|
|
|
1315
1313
|
let html=`<div class="flex justify-between items-center mb-16 flex-wrap gap-8">
|
|
1316
|
-
<div style="color:var(--text2);font-size:13px">共 ${sk.length}
|
|
1317
|
-
<div class="flex gap-8"><input id="skillSearch" placeholder="
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
for(const s of builtin){html+=skillRowHtml(s);}
|
|
1324
|
-
html+='</div>';
|
|
1325
|
-
}
|
|
1326
|
-
// External
|
|
1327
|
-
if(external.length){
|
|
1328
|
-
html+=`<div class="card" style="padding:0;overflow:hidden;margin-top:16px"><h3 style="padding:12px 16px;margin:0">OpenClaw 技能 (${external.length})</h3>`;
|
|
1329
|
-
for(const s of external){html+=skillRowHtml(s);}
|
|
1314
|
+
<div style="color:var(--text2);font-size:13px">共 ${sk.length} 个内置工具 · ${disabledCount} 已禁用</div>
|
|
1315
|
+
<div class="flex gap-8"><input id="skillSearch" placeholder="搜索工具..." style="width:220px" oninput="filterSkills()">
|
|
1316
|
+
</div></div>`;
|
|
1317
|
+
|
|
1318
|
+
if(sk.length){
|
|
1319
|
+
html+=`<div class="card" style="padding:0;overflow:hidden"><h3 style="padding:12px 16px;margin:0">内置工具 (${sk.length})</h3>`;
|
|
1320
|
+
for(const s of sk){html+=skillRowHtml(s);}
|
|
1330
1321
|
html+='</div>';
|
|
1331
1322
|
}
|
|
1332
|
-
if(!sk.length)html+='<div class="empty"
|
|
1323
|
+
if(!sk.length)html+='<div class="empty">暂无工具</div>';
|
|
1333
1324
|
$('content').innerHTML=html;
|
|
1334
1325
|
}
|
|
1335
1326
|
|
|
1336
1327
|
function skillRowHtml(s){
|
|
1337
|
-
const src=s.source||'builtin';
|
|
1338
|
-
const isExt=src==='openclaw_external';
|
|
1339
1328
|
const isDisabled=s.disabled;
|
|
1340
1329
|
const paramCount=(s.parameters||[]).length;
|
|
1341
1330
|
return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
|
|
1342
|
-
<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px"
|
|
1331
|
+
<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">🔧</div>
|
|
1343
1332
|
<div class="skill-info">
|
|
1344
1333
|
<div class="skill-name">${escHtml(s.name)} ${isDisabled?'<span class="badge badge-red">已禁用</span>':''} ${s.dangerous?'<span class="badge badge-red">⚠ 危险</span>':''}</div>
|
|
1345
1334
|
<div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
|
|
1346
1335
|
</div>
|
|
1347
1336
|
<div class="flex gap-8 items-center" style="flex-shrink:0">
|
|
1348
|
-
<span class="badge
|
|
1337
|
+
<span class="badge badge-green">内置</span>
|
|
1349
1338
|
${s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:''}
|
|
1350
1339
|
<span class="tag">${paramCount} 参数</span>
|
|
1351
1340
|
<label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
|
|
1352
|
-
${isExt?`<button class="btn btn-sm btn-danger" onclick="uninstallSkill('${escHtml(s.name)}')" title="卸载">✕</button>`:''}
|
|
1353
1341
|
<button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
|
|
1354
1342
|
</div>
|
|
1355
1343
|
</div>`;
|
|
@@ -1369,23 +1357,14 @@ async function toggleSkill(name,enabled){
|
|
|
1369
1357
|
showToast(enabled?'已启用':'已禁用','success');renderSkills();
|
|
1370
1358
|
}
|
|
1371
1359
|
|
|
1372
|
-
async function uninstallSkill(name){
|
|
1373
|
-
showConfirm('卸载技能','确认卸载 "'+escHtml(name)+'" 吗?此操作不可恢复。',async()=>{
|
|
1374
|
-
const r=await api(`/api/skills/${encodeURIComponent(name)}/uninstall`,{method:'POST'});
|
|
1375
|
-
if(r.error){showToast(r.error,'danger');closeModal();return}
|
|
1376
|
-
closeModal();showToast('已卸载','success');renderSkills();
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
1360
|
async function viewSkillDetail(name){
|
|
1381
1361
|
const s=await api(`/api/skills/${encodeURIComponent(name)}`);
|
|
1382
1362
|
if(s.error){showToast(s.error,'danger');return}
|
|
1383
|
-
const src=s.source||'builtin';const isExt=src==='openclaw_external';
|
|
1384
1363
|
let html=`<h3>${escHtml(s.name)}</h3>
|
|
1385
1364
|
<div class="form-group"><label>描述</label><div style="font-size:13px;color:var(--text)">${escHtml(s.description||'无')}</div></div>
|
|
1386
1365
|
<div class="form-row">
|
|
1387
1366
|
<div class="form-group"><label>类别</label><div><span class="badge badge-blue">${escHtml(s.category||'general')}</span></div></div>
|
|
1388
|
-
<div class="form-group"><label>来源</label><div><span class="badge
|
|
1367
|
+
<div class="form-group"><label>来源</label><div><span class="badge badge-green">内置</span></div></div>
|
|
1389
1368
|
</div>
|
|
1390
1369
|
<div class="form-group"><label>危险操作</label><div>${s.dangerous?'<span class="badge badge-red">是</span>':'<span class="badge badge-green">否</span>'}</div></div>
|
|
1391
1370
|
<div class="form-group"><label>参数 (${(s.parameters||[]).length})</label>
|
|
@@ -1395,17 +1374,6 @@ async function viewSkillDetail(name){
|
|
|
1395
1374
|
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">${html}<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div></div></div>`;
|
|
1396
1375
|
}
|
|
1397
1376
|
|
|
1398
|
-
async function loadAvailableSkills(){
|
|
1399
|
-
const sk=await api('/api/skills/available');
|
|
1400
|
-
if(!sk||!sk.length){showToast('没有可安装的技能','info');return}
|
|
1401
|
-
let html='<h3>📦 可安装的 OpenClaw 技能</h3><div style="margin-top:12px">';
|
|
1402
|
-
for(const s of sk){html+=`<div class="flex justify-between items-center" style="padding:8px 0;border-bottom:1px solid var(--border)">
|
|
1403
|
-
<div><strong>${escHtml(s.name)}</strong> <span style="font-size:12px;color:var(--text2);margin-left:8px">${escHtml((s.description||'').slice(0,80))}</span></div>
|
|
1404
|
-
<span class="badge badge-yellow">可安装</span></div>`;}
|
|
1405
|
-
html+='</div><div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>';
|
|
1406
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">${html}</div></div>`;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
1377
|
// ========== Files ==========
|
|
1410
1378
|
async function renderFiles(){
|
|
1411
1379
|
const wd=await api('/api/workdir');const files=await api('/api/workdir/files');
|