chcode 0.1.0__py3-none-any.whl
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.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills 发现和加载器(ps: 自 https://github.com/NanmiCoder/skills-agent-proto 二次开发 )
|
|
3
|
+
|
|
4
|
+
演示 Skills 三层加载机制的核心实现:
|
|
5
|
+
- Level 1: scan_skills() - 扫描并加载所有 Skills 元数据到 system prompt
|
|
6
|
+
- Level 2: load_skill(skill_name: str) - 根据skill name加载指定 Skill 的详细指令(只返回 instructions - skill.md文档))
|
|
7
|
+
- Level 3: 由 bash tool 执行脚本(见 tools.py),大模型从指令中自己发现脚本
|
|
8
|
+
|
|
9
|
+
核心设计理念:
|
|
10
|
+
让大模型成为真正的"智能体",自己阅读指令、发现脚本、决定执行。
|
|
11
|
+
代码层面不需要特殊处理脚本发现/执行逻辑。
|
|
12
|
+
|
|
13
|
+
Skills 目录结构:
|
|
14
|
+
my-skill/
|
|
15
|
+
├── SKILL.md # 必需:指令和元数据
|
|
16
|
+
├── scripts/ # 可选:可执行脚本
|
|
17
|
+
├── references/ # 可选:参考文档
|
|
18
|
+
└── assets/ # 可选:模板和资源
|
|
19
|
+
|
|
20
|
+
SKILL.md 格式:
|
|
21
|
+
---
|
|
22
|
+
name: skill-name
|
|
23
|
+
description: 何时使用此 skill 的描述
|
|
24
|
+
---
|
|
25
|
+
# Skill Title
|
|
26
|
+
详细指令内容...
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
import zipfile
|
|
31
|
+
import tarfile
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Optional
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 默认 Skills 搜索路径(项目级优先,用户级兜底)
|
|
39
|
+
DEFAULT_SKILL_PATHS = [
|
|
40
|
+
Path.cwd() / ".chat" / "skills", # 项目级 Skills (.chat/skills/) - 优先
|
|
41
|
+
Path.home() / ".chat" / "skills", # 用户级 Skills (~/.chat/skills/) - 兜底
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SkillMetadata:
|
|
47
|
+
"""
|
|
48
|
+
Skill 元数据(Level 1)
|
|
49
|
+
|
|
50
|
+
启动时从 YAML frontmatter 解析,用于注入 system prompt。
|
|
51
|
+
每个 skill 约 100 tokens。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str # skill 唯一名称
|
|
55
|
+
description: str # 何时使用此 skill 的描述
|
|
56
|
+
skill_path: Path # skill 目录路径
|
|
57
|
+
|
|
58
|
+
def to_prompt_line(self) -> str:
|
|
59
|
+
"""生成 system prompt 中的单行描述"""
|
|
60
|
+
return f"- **{self.name}**: {self.description}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class SkillContent:
|
|
65
|
+
"""
|
|
66
|
+
Skill 完整内容(Level 2)
|
|
67
|
+
|
|
68
|
+
用户请求匹配时加载,包含 SKILL.md 的完整指令。
|
|
69
|
+
约 5k tokens。
|
|
70
|
+
|
|
71
|
+
注意:不收集 scripts 和 additional_docs,让大模型从指令中自己发现。
|
|
72
|
+
这是 Anthropic Skills 的核心设计理念。
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
metadata: SkillMetadata
|
|
76
|
+
instructions: str # SKILL.md body 内容
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SkillLoader:
|
|
80
|
+
"""
|
|
81
|
+
Skills 加载器
|
|
82
|
+
|
|
83
|
+
核心职责:
|
|
84
|
+
1. scan_skills(): 发现文件系统中的 Skills,解析元数据
|
|
85
|
+
2. load_skill(): 按需加载 Skill 详细内容
|
|
86
|
+
3. build_system_prompt(): 生成包含 Skills 列表的 system prompt
|
|
87
|
+
|
|
88
|
+
使用示例:
|
|
89
|
+
loader = SkillLoader()
|
|
90
|
+
|
|
91
|
+
# Level 1: 获取 system prompt
|
|
92
|
+
system_prompt = loader.build_system_prompt()
|
|
93
|
+
|
|
94
|
+
# Level 2: 加载具体 skill
|
|
95
|
+
skill = loader.load_skill("news-extractor")
|
|
96
|
+
print(skill.instructions)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, skill_paths: list[Path] | None = None):
|
|
100
|
+
"""
|
|
101
|
+
初始化加载器
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
skill_paths: 自定义 Skills 搜索路径,默认为:
|
|
105
|
+
- .claude/skills/ (项目级,优先)
|
|
106
|
+
- ~/.claude/skills/ (用户级,兜底)
|
|
107
|
+
"""
|
|
108
|
+
self.skill_paths = skill_paths or DEFAULT_SKILL_PATHS
|
|
109
|
+
self._metadata_cache: dict[str, SkillMetadata] = {}
|
|
110
|
+
self._scan_cache: list[SkillMetadata] | None = None
|
|
111
|
+
self._dir_mtimes: dict[str, float] = {}
|
|
112
|
+
self._file_mtimes: dict[str, float] = {}
|
|
113
|
+
|
|
114
|
+
def _is_cache_valid(self) -> bool:
|
|
115
|
+
for base_path in self.skill_paths:
|
|
116
|
+
key = str(base_path)
|
|
117
|
+
try:
|
|
118
|
+
if base_path.exists():
|
|
119
|
+
dir_mtime = base_path.stat().st_mtime
|
|
120
|
+
if key not in self._dir_mtimes:
|
|
121
|
+
return False
|
|
122
|
+
if dir_mtime != self._dir_mtimes[key]:
|
|
123
|
+
return False
|
|
124
|
+
else:
|
|
125
|
+
if key in self._dir_mtimes:
|
|
126
|
+
return False
|
|
127
|
+
except OSError:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
for fpath, cached_mtime in self._file_mtimes.items():
|
|
131
|
+
try:
|
|
132
|
+
if (
|
|
133
|
+
not Path(fpath).exists()
|
|
134
|
+
or Path(fpath).stat().st_mtime != cached_mtime
|
|
135
|
+
):
|
|
136
|
+
return False
|
|
137
|
+
except OSError:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def _save_mtimes(self) -> None:
|
|
143
|
+
self._dir_mtimes.clear()
|
|
144
|
+
self._file_mtimes.clear()
|
|
145
|
+
for base_path in self.skill_paths:
|
|
146
|
+
try:
|
|
147
|
+
if base_path.exists():
|
|
148
|
+
self._dir_mtimes[str(base_path)] = base_path.stat().st_mtime
|
|
149
|
+
for skill_dir in base_path.iterdir():
|
|
150
|
+
if skill_dir.is_dir():
|
|
151
|
+
skill_md = skill_dir / "SKILL.md"
|
|
152
|
+
if skill_md.exists():
|
|
153
|
+
self._file_mtimes[str(skill_md)] = (
|
|
154
|
+
skill_md.stat().st_mtime
|
|
155
|
+
)
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def scan_skills(self, *, force: bool = False) -> list[SkillMetadata]:
|
|
160
|
+
"""
|
|
161
|
+
Level 1: 扫描所有 Skills 元数据
|
|
162
|
+
|
|
163
|
+
遍历 skill_paths,查找包含 SKILL.md 的目录,
|
|
164
|
+
解析 YAML frontmatter 提取 name 和 description。
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
force: 强制忽略缓存,重新扫描磁盘
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
所有发现的 Skills 元数据列表
|
|
171
|
+
"""
|
|
172
|
+
if not force and self._scan_cache is not None and self._is_cache_valid():
|
|
173
|
+
return self._scan_cache
|
|
174
|
+
|
|
175
|
+
skills = []
|
|
176
|
+
seen_names = set()
|
|
177
|
+
|
|
178
|
+
for base_path in self.skill_paths:
|
|
179
|
+
if not base_path.exists():
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
for skill_dir in base_path.iterdir():
|
|
183
|
+
if not skill_dir.is_dir():
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
skill_md = skill_dir / "SKILL.md"
|
|
187
|
+
if not skill_md.exists():
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
metadata = self._parse_skill_metadata(skill_md)
|
|
191
|
+
if metadata and metadata.name not in seen_names:
|
|
192
|
+
skills.append(metadata)
|
|
193
|
+
seen_names.add(metadata.name)
|
|
194
|
+
self._metadata_cache[metadata.name] = metadata
|
|
195
|
+
|
|
196
|
+
self._scan_cache = skills
|
|
197
|
+
self._save_mtimes()
|
|
198
|
+
return skills
|
|
199
|
+
|
|
200
|
+
# 解析skill元数据
|
|
201
|
+
def _parse_skill_metadata(self, skill_md_path: Path) -> Optional[SkillMetadata]:
|
|
202
|
+
"""
|
|
203
|
+
解析 SKILL.md 的 YAML frontmatter
|
|
204
|
+
|
|
205
|
+
SKILL.md 格式:
|
|
206
|
+
---
|
|
207
|
+
name: skill-name
|
|
208
|
+
description: Brief description when to use it
|
|
209
|
+
---
|
|
210
|
+
# Instructions...
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
skill_md_path: SKILL.md 文件路径
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
解析后的元数据,解析失败返回 None
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
content = skill_md_path.read_text(encoding="utf-8")
|
|
220
|
+
except Exception:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# 使用正则提取 YAML frontmatter
|
|
224
|
+
# 格式: ---\n...yaml...\n---
|
|
225
|
+
frontmatter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
|
|
226
|
+
|
|
227
|
+
if not frontmatter_match:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# 解析 YAML
|
|
232
|
+
frontmatter = yaml.safe_load(
|
|
233
|
+
frontmatter_match.group(1)
|
|
234
|
+
) # group:['匹配的完成的内容','第一个组的内容']
|
|
235
|
+
name = frontmatter.get("name", "")
|
|
236
|
+
description = frontmatter.get("description", "")
|
|
237
|
+
|
|
238
|
+
if not name:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
return SkillMetadata(
|
|
242
|
+
name=name,
|
|
243
|
+
description=description,
|
|
244
|
+
skill_path=skill_md_path.parent,
|
|
245
|
+
)
|
|
246
|
+
except yaml.YAMLError:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
# 从skill字典读取skill完整数据
|
|
250
|
+
def load_skill(self, skill_name: str) -> Optional[SkillContent]:
|
|
251
|
+
"""
|
|
252
|
+
Level 2: 加载 Skill 完整内容
|
|
253
|
+
|
|
254
|
+
读取 SKILL.md 的完整指令,以及其他 .md 文件和脚本列表。
|
|
255
|
+
这是 load_skill tool 的核心实现。
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
skill_name: Skill 名称(如 "news-extractor")
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Skill 完整内容,未找到返回 None
|
|
262
|
+
"""
|
|
263
|
+
# 先检查缓存
|
|
264
|
+
metadata = self._metadata_cache.get(skill_name)
|
|
265
|
+
|
|
266
|
+
# 原始冗余代码
|
|
267
|
+
if not metadata:
|
|
268
|
+
# 尝试重新扫描
|
|
269
|
+
self.scan_skills()
|
|
270
|
+
metadata = self._metadata_cache.get(skill_name)
|
|
271
|
+
|
|
272
|
+
if not metadata:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
# 读取 SKILL.md 完整内容
|
|
276
|
+
skill_md = metadata.skill_path / "SKILL.md"
|
|
277
|
+
try:
|
|
278
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
279
|
+
except Exception:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
# 提取 body(去除 frontmatter)
|
|
283
|
+
body_match = re.match(r"^---\s*\n.*?\n---\s*\n(.*)$", content, re.DOTALL)
|
|
284
|
+
instructions = body_match.group(1).strip() if body_match else content
|
|
285
|
+
|
|
286
|
+
# 只返回 instructions,让大模型从指令中自己发现脚本和文档
|
|
287
|
+
return SkillContent(
|
|
288
|
+
metadata=metadata,
|
|
289
|
+
instructions=instructions,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def build_system_prompt(self, base_prompt: str = "") -> str:
|
|
293
|
+
"""
|
|
294
|
+
构建包含 Skills 列表的 system prompt
|
|
295
|
+
|
|
296
|
+
这是 Level 1 的核心输出:将所有 Skills 的元数据
|
|
297
|
+
注入到 system prompt 中。
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
base_prompt: 基础 system prompt(可选)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
完整的 system prompt
|
|
304
|
+
"""
|
|
305
|
+
skills = self.scan_skills()
|
|
306
|
+
|
|
307
|
+
# 构建 Skills 部分
|
|
308
|
+
if skills:
|
|
309
|
+
skills_section = "## Available Skills\n\n"
|
|
310
|
+
skills_section += "You have access to the following specialized skills:\n\n"
|
|
311
|
+
for skill in skills:
|
|
312
|
+
skills_section += skill.to_prompt_line() + "\n"
|
|
313
|
+
skills_section += "\n"
|
|
314
|
+
skills_section += "### How to Use Skills\n\n"
|
|
315
|
+
skills_section += "1. **Discover**: Review the skills list above\n"
|
|
316
|
+
skills_section += (
|
|
317
|
+
"2. **Load**: When a user request matches a skill's description, "
|
|
318
|
+
)
|
|
319
|
+
skills_section += (
|
|
320
|
+
"use `load_skill(skill_name)` to get detailed instructions\n"
|
|
321
|
+
)
|
|
322
|
+
skills_section += (
|
|
323
|
+
"3. **Execute**: Follow the skill's instructions, which may include "
|
|
324
|
+
)
|
|
325
|
+
skills_section += "running scripts via `bash`\n\n"
|
|
326
|
+
skills_section += "**Important**: Only load a skill when it's relevant to the user's request. "
|
|
327
|
+
skills_section += (
|
|
328
|
+
"Script code never enters the context - only their output does.\n"
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
skills_section = "## Skills\n\nNo skills currently available.\n"
|
|
332
|
+
|
|
333
|
+
# 组合完整 prompt
|
|
334
|
+
if base_prompt:
|
|
335
|
+
return f"{base_prompt}\n\n{skills_section}"
|
|
336
|
+
else:
|
|
337
|
+
return f"You are a helpful coding assistant.\n\n{skills_section}"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def scan_all_skills(project_path: Path | None = None) -> list[dict]:
|
|
341
|
+
"""扫描所有技能(项目级和全局级)
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
project_path: 项目路径,如果提供则同时扫描项目级技能
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
技能信息列表,每个技能包含 name, type, description, path
|
|
348
|
+
"""
|
|
349
|
+
skills = []
|
|
350
|
+
loader = SkillLoader()
|
|
351
|
+
|
|
352
|
+
# 扫描项目级技能
|
|
353
|
+
if project_path:
|
|
354
|
+
project_skills_path = project_path / ".chat" / "skills"
|
|
355
|
+
if project_skills_path.exists():
|
|
356
|
+
project_skills = _scan_skills_in_path(project_skills_path, "项目", loader)
|
|
357
|
+
skills.extend(project_skills)
|
|
358
|
+
|
|
359
|
+
# 扫描全局技能
|
|
360
|
+
global_skills_path = Path.home() / ".chat" / "skills"
|
|
361
|
+
if global_skills_path.exists():
|
|
362
|
+
global_skills = _scan_skills_in_path(global_skills_path, "全局", loader)
|
|
363
|
+
skills.extend(global_skills)
|
|
364
|
+
|
|
365
|
+
return skills
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _scan_skills_in_path(
|
|
369
|
+
skills_path: Path, skill_type: str, loader: SkillLoader
|
|
370
|
+
) -> list[dict]:
|
|
371
|
+
"""扫描指定路径下的技能"""
|
|
372
|
+
skills = []
|
|
373
|
+
|
|
374
|
+
if not skills_path.exists():
|
|
375
|
+
return skills
|
|
376
|
+
|
|
377
|
+
for skill_dir in skills_path.iterdir():
|
|
378
|
+
if not skill_dir.is_dir():
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
# 检查是否存在 SKILL.md
|
|
382
|
+
skill_md = skill_dir / "SKILL.md"
|
|
383
|
+
if not skill_md.exists():
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
# 解析技能元数据
|
|
387
|
+
metadata = loader._parse_skill_metadata(skill_md)
|
|
388
|
+
if metadata:
|
|
389
|
+
skills.append(
|
|
390
|
+
{
|
|
391
|
+
"name": metadata.name,
|
|
392
|
+
"type": skill_type,
|
|
393
|
+
"description": metadata.description,
|
|
394
|
+
"path": str(skill_dir),
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return skills
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _extract_archive(archive_path, target_path):
|
|
402
|
+
"""安全解压压缩包,过滤路径穿越攻击"""
|
|
403
|
+
resolved_target = target_path.resolve()
|
|
404
|
+
if archive_path.endswith(".zip"):
|
|
405
|
+
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
|
406
|
+
for member in zip_ref.namelist():
|
|
407
|
+
if not (target_path / member).resolve().is_relative_to(resolved_target):
|
|
408
|
+
return False
|
|
409
|
+
zip_ref.extractall(target_path)
|
|
410
|
+
elif archive_path.endswith((".tar.gz", ".tgz")):
|
|
411
|
+
with tarfile.open(archive_path, "r:gz") as tar_ref:
|
|
412
|
+
for member in tar_ref.getmembers():
|
|
413
|
+
if (
|
|
414
|
+
not (target_path / member.name)
|
|
415
|
+
.resolve()
|
|
416
|
+
.is_relative_to(resolved_target)
|
|
417
|
+
):
|
|
418
|
+
return False
|
|
419
|
+
tar_ref.extractall(target_path)
|
|
420
|
+
elif archive_path.endswith(".tar.bz2"):
|
|
421
|
+
with tarfile.open(archive_path, "r:bz2") as tar_ref:
|
|
422
|
+
for member in tar_ref.getmembers():
|
|
423
|
+
if (
|
|
424
|
+
not (target_path / member.name)
|
|
425
|
+
.resolve()
|
|
426
|
+
.is_relative_to(resolved_target)
|
|
427
|
+
):
|
|
428
|
+
return False
|
|
429
|
+
tar_ref.extractall(target_path)
|
|
430
|
+
else:
|
|
431
|
+
return False
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def validate_skill_package(archive_path: str) -> dict | None:
|
|
436
|
+
"""验证技能包是否有效
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
archive_path: 压缩包路径
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
技能信息字典或None(如果无效)
|
|
443
|
+
"""
|
|
444
|
+
try:
|
|
445
|
+
import tempfile
|
|
446
|
+
from pathlib import Path
|
|
447
|
+
|
|
448
|
+
loader = SkillLoader()
|
|
449
|
+
|
|
450
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
451
|
+
temp_path = Path(temp_dir)
|
|
452
|
+
|
|
453
|
+
if not _extract_archive(archive_path, temp_path):
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
# 查找包含SKILL.md的目录
|
|
457
|
+
skill_dir = _find_skill_dir(temp_path)
|
|
458
|
+
if not skill_dir:
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
skill_md = skill_dir / "SKILL.md"
|
|
462
|
+
if not skill_md.exists():
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
# 解析元数据
|
|
466
|
+
metadata = loader._parse_skill_metadata(skill_md)
|
|
467
|
+
if not metadata:
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
"name": metadata.name,
|
|
472
|
+
"description": metadata.description,
|
|
473
|
+
"source_path": str(skill_dir),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
print(f"验证技能包失败: {e}")
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _find_skill_dir(search_path: Path) -> Path | None:
|
|
482
|
+
"""在搜索路径中查找包含SKILL.md的目录"""
|
|
483
|
+
# 首先检查根目录
|
|
484
|
+
if (search_path / "SKILL.md").exists():
|
|
485
|
+
return search_path
|
|
486
|
+
|
|
487
|
+
# 检查子目录
|
|
488
|
+
for item in search_path.iterdir():
|
|
489
|
+
if item.is_dir():
|
|
490
|
+
if (item / "SKILL.md").exists():
|
|
491
|
+
return item
|
|
492
|
+
# 递归检查更深层的目录
|
|
493
|
+
result = _find_skill_dir(item)
|
|
494
|
+
if result:
|
|
495
|
+
return result
|
|
496
|
+
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def install_skill(archive_path: str, install_path: Path) -> bool:
|
|
501
|
+
"""安装技能到指定位置
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
archive_path: 压缩包路径
|
|
505
|
+
install_path: 安装目录路径
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
是否安装成功
|
|
509
|
+
"""
|
|
510
|
+
try:
|
|
511
|
+
import tempfile
|
|
512
|
+
import shutil
|
|
513
|
+
from pathlib import Path
|
|
514
|
+
|
|
515
|
+
# 创建安装目录
|
|
516
|
+
install_path.mkdir(parents=True, exist_ok=True)
|
|
517
|
+
|
|
518
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
519
|
+
temp_path = Path(temp_dir)
|
|
520
|
+
|
|
521
|
+
if not _extract_archive(archive_path, temp_path):
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
# 查找技能目录
|
|
525
|
+
skill_source_dir = _find_skill_dir(temp_path)
|
|
526
|
+
if not skill_source_dir:
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
# 获取技能名称
|
|
530
|
+
skill_md = skill_source_dir / "SKILL.md"
|
|
531
|
+
loader = SkillLoader()
|
|
532
|
+
metadata = loader._parse_skill_metadata(skill_md)
|
|
533
|
+
if not metadata:
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
skill_name = metadata.name
|
|
537
|
+
target_dir = install_path / skill_name
|
|
538
|
+
|
|
539
|
+
# 如果已存在,先删除
|
|
540
|
+
if target_dir.exists():
|
|
541
|
+
shutil.rmtree(target_dir)
|
|
542
|
+
|
|
543
|
+
# 复制到目标位置
|
|
544
|
+
shutil.copytree(skill_source_dir, target_dir)
|
|
545
|
+
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(f"安装技能失败: {e}")
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@dataclass
|
|
554
|
+
class SkillAgentContext:
|
|
555
|
+
"""
|
|
556
|
+
Agent 运行时上下文
|
|
557
|
+
|
|
558
|
+
通过 ToolRuntime[SkillAgentContext] 在 tool 中访问
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
skill_loader: SkillLoader
|
|
562
|
+
model_config: dict
|
|
563
|
+
working_directory: Path
|
|
564
|
+
thread_id: str = ""
|
|
565
|
+
extra: dict = field(default_factory=dict)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def get_text_content(content: str | list) -> str:
|
|
2
|
+
"""将消息内容转为纯文本,处理多模态消息的列表格式"""
|
|
3
|
+
if isinstance(content, str):
|
|
4
|
+
return content
|
|
5
|
+
if isinstance(content, list):
|
|
6
|
+
text_parts = []
|
|
7
|
+
for part in content:
|
|
8
|
+
if isinstance(part, dict):
|
|
9
|
+
if part.get("type") == "text":
|
|
10
|
+
text_parts.append(part.get("text", ""))
|
|
11
|
+
elif isinstance(part, str):
|
|
12
|
+
text_parts.append(part)
|
|
13
|
+
return "".join(text_parts).strip()
|
|
14
|
+
return str(content)
|