Files
beaver_project/app-instance/backend/beaver/skills/catalog/utils.py
steven_li 520a21a027 feat(coordinator): 添加团队节点默认最大工具迭代次数配置
添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数,
并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。

fix(runtime): 修复团队节点运行成功判断逻辑

更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况
视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。

feat(mcp): 添加团队工作流MCP工具类别支持

增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能,
为团队工作流提供本地工具支持。

refactor(engine): 调整AgentLoop最大工具迭代次数设置

将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100,
同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。

perf(mcp): 优化MCP连接管理避免重复连接

添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次,
提高性能并避免不必要的重复连接。

refactor(skills): 移除技能团队模板相关功能

移除与技能团队模板相关的代码,包括解析、存储和处理逻辑,
简化技能记录结构和加载流程。

feat(process): 增强会话过程投影器功能

添加技能激活快照事件处理,改进团队运行完成消息显示,
并增强技能激活事件的时间戳记录功能。

refactor(tasks): 简化任务尝试编排器团队执行逻辑

移除团队执行相关代码,将所有任务统一按单步执行处理,
简化任务编排器的复杂度并提升执行效率。

fix(evidence): 修复节点证据评估中需求验证逻辑

更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证,
只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
2026-06-26 16:36:29 +08:00

180 lines
5.1 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 extract_required_tool_names(body: str) -> list[str]:
"""从 canonical skill 正文的 `## Required Tools` 段落提取工具名。
这是 frontmatter `tools` 的容错补充,不从任意正文里猜工具。只读取明确
命名的 Required Tools section支持常见 bullet/code 格式。
"""
if not body:
return []
match = re.search(
r"(?ims)^##\s+Required\s+Tools\s*$\n(?P<section>.*?)(?=^##\s+|\Z)",
body,
)
if match is None:
return []
names: list[str] = []
for line in match.group("section").splitlines():
stripped = line.strip()
if not stripped or not stripped.startswith(("-", "*")):
continue
candidate = stripped[1:].strip()
code_matches = re.findall(r"`([^`]+)`", candidate)
raw_items = code_matches or re.split(r"[,]", candidate)
for raw_item in raw_items:
name = raw_item.strip().strip("`\"' ")
if not name:
continue
token = name.split()[0].strip("`\"' :-")
if re.fullmatch(r"[A-Za-z0-9_.:-]+", token) and token not in names:
names.append(token)
return names
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")