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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.12.2",
3
+ "version": "1.12.4",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -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
- """获取技能(先查内置,再查 OpenClaw,跳过禁用)"""
71
+ """获取技能(跳过禁用)"""
244
72
  if self._is_disabled(name):
245
73
  return None
246
- skill = self._skills.get(name)
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
- builtin = [name for name in self._skills.keys() if not self._is_disabled(name)]
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
- all_skills = {**self._skills, **self._openclaw_skills}
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
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:
@@ -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
  ══════════════════════════════════════════════════════ */
@@ -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 (FULL REWRITE) ==========
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} 个技能 · ${builtin.length} 内置 · ${external.length} OpenClaw · ${disabledCount} 已禁用</div>
1317
- <div class="flex gap-8"><input id="skillSearch" placeholder="搜索技能..." style="width:220px" oninput="filterSkills()">
1318
- <button class="btn btn-ghost" onclick="loadAvailableSkills()">📦 可安装技能</button></div></div>`;
1319
-
1320
- // Builtin
1321
- if(builtin.length){
1322
- html+=`<div class="card" style="padding:0;overflow:hidden"><h3 style="padding:12px 16px;margin:0">内置技能 (${builtin.length})</h3>`;
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">暂无技能</div>';
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">${isExt?'📦':'🔧'}</div>
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 ${isExt?'badge-yellow':'badge-green'}">${isExt?'OpenClaw':'内置'}</span>
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 ${isExt?'badge-yellow':'badge-green'}">${isExt?'OpenClaw':'内置'}</span></div></div>
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');