Files
beaver_project/app-instance/backend/beaver/skills/catalog/utils.py
steven_li 5ba5c7e4c1 feat(app-instance): 集成Beaver后端并更新配置管理
集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。

主要变更包括:
- 在Dockerfile和环境配置中添加Beaver相关路径和配置变量
- 更新工作目录结构从.nanobot到.beaver
- 实现Beaver引擎加载器,支持配置文件加载和工具组装
- 添加内置工具如ListDirectoryTool、ReadFileTool、SearchFilesTool
- 更新消息处理流程,支持通道适配器和网关模式
- 重构技能系统,支持显式工具提示和嵌入式检索
- 改进错误处理和生命周期管理

此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
2026-04-27 17:37:40 +08:00

147 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 扩展配置。
为了兼容旧 nanobot 习惯,这里同时支持:
- `nanobot`
- `openclaw`
第一版主要关心的字段有:
- `always`
- `requires`
"""
try:
data = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return {}
if not isinstance(data, dict):
return {}
nested = data.get("nanobot", 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")