移除了所有Hermes相关的命名引用,包括: - 从.gitignore中清理相关构建缓存文件 - 将README中的beaver-home路径配置更新 - 完善backend/README.md文档说明Beaver后端主线实现 - 移除Hermes风格的相关注释和兼容性代码 - 清理nanobot环境变量兼容性处理 - 删除技能迁移和服务迁移相关功能代码 - 更新测试用例中相关命名和函数名 BREAKING CHANGE: 移除了Hermes迁移相关API和CLI命令,不再支持nanobot环境变量兼容性
145 lines
3.9 KiB
Python
145 lines
3.9 KiB
Python
"""Skills catalog 的公共辅助函数。
|
||
|
||
这里专门放“解析和校验 skill 文件”的纯函数,避免 `loader.py` 里同时承担:
|
||
|
||
1. 目录扫描
|
||
2. frontmatter 解析
|
||
3. requirements 校验
|
||
4. 文本裁剪/格式化
|
||
|
||
把这些细节拆出来之后,skills catalog 的边界会更清楚,后面无论是 reviews、publisher
|
||
还是 runtime resolver,都可以复用同一套元数据解析规则。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
from typing import Any
|
||
|
||
|
||
def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
|
||
"""解析 Markdown 文件顶部的极简 frontmatter。
|
||
|
||
当前先只支持最常见的:
|
||
|
||
```md
|
||
---
|
||
key: value
|
||
key2: value2
|
||
---
|
||
body...
|
||
```
|
||
|
||
这样足够支撑第一版 skills runtime,不提前把 YAML 解析器引进来。
|
||
"""
|
||
|
||
if not content.startswith("---"):
|
||
return {}, content
|
||
|
||
match = re.match(r"^---\n(.*?)\n---\n?", content, re.DOTALL)
|
||
if match is None:
|
||
return {}, content
|
||
|
||
metadata: dict[str, Any] = {}
|
||
lines = match.group(1).splitlines()
|
||
index = 0
|
||
while index < len(lines):
|
||
line = lines[index]
|
||
if ":" not in line:
|
||
index += 1
|
||
continue
|
||
key, value = line.split(":", 1)
|
||
key = key.strip()
|
||
value = value.strip()
|
||
if not value:
|
||
items: list[str] = []
|
||
lookahead = index + 1
|
||
while lookahead < len(lines):
|
||
candidate = lines[lookahead]
|
||
stripped = candidate.strip()
|
||
if not stripped:
|
||
lookahead += 1
|
||
continue
|
||
if not stripped.startswith("- "):
|
||
break
|
||
items.append(stripped[2:].strip().strip('"\''))
|
||
lookahead += 1
|
||
if items:
|
||
metadata[key] = items
|
||
index = lookahead
|
||
continue
|
||
metadata[key] = value.strip('"\'')
|
||
index += 1
|
||
body = content[match.end():].strip()
|
||
return metadata, body
|
||
|
||
|
||
def strip_frontmatter(content: str) -> str:
|
||
"""去掉 frontmatter,只保留 skill 正文。"""
|
||
|
||
_, body = parse_frontmatter(content)
|
||
return body
|
||
|
||
|
||
def parse_skill_metadata_blob(raw: str) -> dict[str, Any]:
|
||
"""解析 metadata 字段里的 JSON 扩展配置。
|
||
|
||
Supports plain metadata objects and the current `openclaw` namespace.
|
||
|
||
第一版主要关心的字段有:
|
||
- `always`
|
||
- `requires`
|
||
"""
|
||
|
||
try:
|
||
data = json.loads(raw)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return {}
|
||
|
||
if not isinstance(data, dict):
|
||
return {}
|
||
nested = data.get("openclaw", data)
|
||
return nested if isinstance(nested, dict) else {}
|
||
|
||
|
||
def check_requirements(metadata: dict[str, Any]) -> bool:
|
||
"""检查 skill 的最小 requirements 是否满足。"""
|
||
|
||
requires = metadata.get("requires", {})
|
||
if not isinstance(requires, dict):
|
||
return True
|
||
|
||
for binary in requires.get("bins", []):
|
||
if not shutil.which(str(binary)):
|
||
return False
|
||
for env_name in requires.get("env", []):
|
||
if not os.environ.get(str(env_name)):
|
||
return False
|
||
return True
|
||
|
||
|
||
def get_missing_requirements(metadata: dict[str, Any]) -> str:
|
||
"""返回缺失 requirements 的简短描述。"""
|
||
|
||
requires = metadata.get("requires", {})
|
||
if not isinstance(requires, dict):
|
||
return ""
|
||
|
||
missing: list[str] = []
|
||
for binary in requires.get("bins", []):
|
||
if not shutil.which(str(binary)):
|
||
missing.append(f"CLI: {binary}")
|
||
for env_name in requires.get("env", []):
|
||
if not os.environ.get(str(env_name)):
|
||
missing.append(f"ENV: {env_name}")
|
||
return ", ".join(missing)
|
||
|
||
|
||
def escape_xml(value: str) -> str:
|
||
"""给 skills summary 做最小 XML 转义。"""
|
||
|
||
return value.replace("&", "&").replace("<", "<").replace(">", ">")
|