feat: add read_skill tool and frontend read-skill rendering
This commit is contained in:
parent
9891f68aca
commit
8991ed5084
@ -262,6 +262,23 @@ class MainTerminalToolsDefinitionMixin:
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_skill",
|
||||
"description": "按 skill 名称读取 skills/<name>/SKILL.md 内容;内部等价于 read_file 的 read 模式,并返回解析后的 path。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self._inject_intent({
|
||||
"skill_name": {
|
||||
"type": "string",
|
||||
"description": "skill 名称(支持 skill id 或技能名称)"
|
||||
}
|
||||
}),
|
||||
"required": ["skill_name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@ -113,6 +113,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
"web_search",
|
||||
"extract_webpage",
|
||||
"read_file",
|
||||
"read_skill",
|
||||
"view_image",
|
||||
"view_video",
|
||||
"vlm_analyze",
|
||||
@ -372,7 +373,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
return raw.lstrip("./").lower()
|
||||
|
||||
def _mark_skill_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
|
||||
if tool_name != "read_file":
|
||||
if tool_name not in {"read_file", "read_skill"}:
|
||||
return
|
||||
if not isinstance(result, dict) or not result.get("success"):
|
||||
return
|
||||
@ -405,7 +406,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
return visited
|
||||
|
||||
def _mark_file_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
|
||||
if tool_name != "read_file":
|
||||
if tool_name not in {"read_file", "read_skill"}:
|
||||
return
|
||||
if not isinstance(result, dict) or not result.get("success"):
|
||||
return
|
||||
@ -536,7 +537,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"调用 {tool_name} 前必须先阅读 {required_path}",
|
||||
"message": f"请先使用 read_file 阅读 {required_path},随后再调用该工具。",
|
||||
"message": f"请先使用 read_skill(或 read_file)阅读 {required_path},随后再调用该工具。",
|
||||
"required_skill": skill_id,
|
||||
"required_path": required_path,
|
||||
"enforcement": "skill_read_required"
|
||||
@ -608,6 +609,10 @@ class MainTerminalToolsExecutionMixin:
|
||||
result = self._handle_read_tool(arguments)
|
||||
self._mark_skill_read_from_result(tool_name, arguments, result)
|
||||
self._mark_file_read_from_result(tool_name, arguments, result)
|
||||
elif tool_name == "read_skill":
|
||||
result = self._handle_read_skill_tool(arguments)
|
||||
self._mark_skill_read_from_result(tool_name, arguments, result)
|
||||
self._mark_file_read_from_result(tool_name, arguments, result)
|
||||
elif tool_name in {"vlm_analyze", "ocr_image"}:
|
||||
path = arguments.get("path")
|
||||
prompt = arguments.get("prompt")
|
||||
|
||||
@ -85,6 +85,78 @@ logger = setup_logger(__name__)
|
||||
DISABLE_LENGTH_CHECK = True
|
||||
|
||||
class MainTerminalToolsReadMixin:
|
||||
@staticmethod
|
||||
def _normalize_skill_name(skill_name: Any) -> str:
|
||||
raw = str(skill_name or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
normalized = raw.replace("\\", "/").strip("/")
|
||||
if normalized.lower().startswith("skills/"):
|
||||
normalized = normalized.split("/", 1)[1] if "/" in normalized else normalized
|
||||
if normalized.lower().endswith("/skill.md"):
|
||||
normalized = normalized[: -len("/SKILL.md")]
|
||||
return normalized.strip()
|
||||
|
||||
def _resolve_skill_id(self, skill_name: Any) -> Dict[str, Any]:
|
||||
normalized_input = self._normalize_skill_name(skill_name)
|
||||
if not normalized_input:
|
||||
return {"success": False, "error": "skill_name 不能为空"}
|
||||
|
||||
try:
|
||||
personalization = load_personalization_config(self.data_dir)
|
||||
except Exception:
|
||||
personalization = {}
|
||||
catalog = get_skills_catalog()
|
||||
enabled_skills = merge_enabled_skills(
|
||||
personalization.get("enabled_skills") if isinstance(personalization, dict) else None,
|
||||
catalog,
|
||||
personalization.get("skills_catalog_snapshot") if isinstance(personalization, dict) else None,
|
||||
)
|
||||
enabled_set = set(enabled_skills or [])
|
||||
filtered_catalog = [item for item in catalog if item.get("id") in enabled_set] if enabled_set else list(catalog)
|
||||
|
||||
normalized_lower = normalized_input.lower()
|
||||
|
||||
# 1) 优先按 skill id 精确匹配(忽略大小写)
|
||||
id_map = {str(item.get("id", "")).lower(): item.get("id") for item in filtered_catalog if item.get("id")}
|
||||
if normalized_lower in id_map:
|
||||
return {"success": True, "skill_id": id_map[normalized_lower]}
|
||||
|
||||
# 2) 再按 label 匹配(忽略大小写)
|
||||
label_matches: List[str] = []
|
||||
for item in filtered_catalog:
|
||||
label = str(item.get("label") or "").strip().lower()
|
||||
if label and label == normalized_lower and item.get("id"):
|
||||
label_matches.append(item["id"])
|
||||
if len(label_matches) == 1:
|
||||
return {"success": True, "skill_id": label_matches[0]}
|
||||
if len(label_matches) > 1:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"skill_name 匹配到多个技能: {', '.join(sorted(label_matches))},请改用 skill id"
|
||||
}
|
||||
|
||||
return {"success": False, "error": f"未找到技能: {normalized_input}"}
|
||||
|
||||
def _handle_read_skill_tool(self, arguments: Dict) -> Dict:
|
||||
skill_name = arguments.get("skill_name")
|
||||
resolved = self._resolve_skill_id(skill_name)
|
||||
if not resolved.get("success"):
|
||||
return resolved
|
||||
|
||||
skill_id = resolved["skill_id"]
|
||||
read_args = {
|
||||
"path": f"skills/{skill_id}/SKILL.md",
|
||||
"type": "read",
|
||||
}
|
||||
result = self._handle_read_tool(read_args)
|
||||
if not result.get("success"):
|
||||
return result
|
||||
|
||||
result["skill_id"] = skill_id
|
||||
result["skill_name"] = skill_name
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _clamp_int(value, default, min_value=None, max_value=None):
|
||||
"""将输入转换为整数并限制范围。"""
|
||||
|
||||
@ -41,6 +41,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
||||
"read_focus": ToolCategory(
|
||||
label="阅读聚焦",
|
||||
tools=[
|
||||
"read_skill",
|
||||
"read_file",
|
||||
"vlm_analyze",
|
||||
"ocr_image",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
agent skills系统已启用,以下是可用的skills(含简要说明)
|
||||
{skills_list}
|
||||
[skills_empty]暂无可用的skills[/skills_empty]
|
||||
使用技能时,先用 run_command 查看对应 skills/<name>/ 目录内容,再用 read_file 阅读 SKILL.md 或相关文件获取具体指导
|
||||
使用技能时,优先用 read_skill 通过技能名读取 SKILL.md;需要阅读 skill 目录中的其他文件时再使用 read_file
|
||||
|
||||
@ -130,7 +130,7 @@ function renderEnhancedToolResult(): string {
|
||||
}
|
||||
|
||||
// 阅读聚焦类
|
||||
else if (name === 'read_file') {
|
||||
else if (name === 'read_file' || name === 'read_skill') {
|
||||
return renderReadFile(result, args);
|
||||
} else if (name === 'vlm_analyze') {
|
||||
return renderVlmAnalyze(result, args);
|
||||
|
||||
@ -66,7 +66,7 @@ export function renderEnhancedToolResult(
|
||||
}
|
||||
|
||||
// 阅读聚焦类
|
||||
else if (name === 'read_file') {
|
||||
else if (name === 'read_file' || name === 'read_skill') {
|
||||
return renderReadFile(result, args);
|
||||
} else if (name === 'vlm_analyze') {
|
||||
return renderVlmAnalyze(result, args);
|
||||
|
||||
@ -67,6 +67,10 @@ const buildText = (entry: ActivityEntry) => {
|
||||
const path = args.path || args.file_path || '';
|
||||
return `阅读 ${path}`;
|
||||
}
|
||||
if (tool === 'read_skill') {
|
||||
const skillName = args.skill_name || '';
|
||||
return `阅读技能 ${skillName}`;
|
||||
}
|
||||
if (tool === 'search_workspace') {
|
||||
const query = args.query || args.keyword || '';
|
||||
return `在工作区搜索 ${query}`;
|
||||
|
||||
@ -5,6 +5,7 @@ type ToolPayload = Record<string, any> | null | undefined;
|
||||
const RUNNING_ANIMATIONS: Record<string, string> = {
|
||||
create_file: 'file-animation',
|
||||
read_file: 'read-animation',
|
||||
read_skill: 'read-animation',
|
||||
delete_file: 'file-animation',
|
||||
rename_file: 'file-animation',
|
||||
write_file: 'file-animation',
|
||||
@ -43,7 +44,8 @@ const RUNNING_STATUS_TEXTS: Record<string, string> = {
|
||||
update_memory: '正在更新记忆...',
|
||||
terminal_session: '正在管理终端会话...',
|
||||
terminal_input: '调用 terminal_input',
|
||||
terminal_snapshot: '正在获取终端快照...'
|
||||
terminal_snapshot: '正在获取终端快照...',
|
||||
read_skill: '正在读取技能...'
|
||||
};
|
||||
|
||||
const COMPLETED_STATUS_TEXTS: Record<string, string> = {
|
||||
@ -63,7 +65,8 @@ const COMPLETED_STATUS_TEXTS: Record<string, string> = {
|
||||
update_memory: '记忆更新成功',
|
||||
terminal_session: '终端操作完成',
|
||||
terminal_input: '终端输入完成',
|
||||
terminal_snapshot: '终端快照已返回'
|
||||
terminal_snapshot: '终端快照已返回',
|
||||
read_skill: '技能读取完成'
|
||||
};
|
||||
|
||||
const LANGUAGE_CLASS_MAP: Record<string, string> = {
|
||||
@ -171,7 +174,7 @@ export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean })
|
||||
return `准备调用 ${tool.name}...`;
|
||||
}
|
||||
if (tool.status === 'running') {
|
||||
if (tool.name === 'read_file') {
|
||||
if (tool.name === 'read_file' || tool.name === 'read_skill') {
|
||||
const readType = String(
|
||||
tool.argumentSnapshot?.type || tool.arguments?.type || 'read'
|
||||
).toLowerCase();
|
||||
@ -193,7 +196,7 @@ export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean })
|
||||
return '等待审批...';
|
||||
}
|
||||
if (tool.status === 'completed') {
|
||||
if (tool.name === 'read_file') {
|
||||
if (tool.name === 'read_file' || tool.name === 'read_skill') {
|
||||
return describeReadFileResult(tool);
|
||||
}
|
||||
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
|
||||
|
||||
@ -54,6 +54,7 @@ export const TOOL_ICON_MAP = Object.freeze({
|
||||
vlm_analyze: 'camera',
|
||||
ocr_image: 'camera',
|
||||
read_file: 'book',
|
||||
read_skill: 'book',
|
||||
rename_file: 'pencil',
|
||||
run_command: 'terminal',
|
||||
run_python: 'python',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user