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",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
|
|||||||
@ -113,6 +113,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
"web_search",
|
"web_search",
|
||||||
"extract_webpage",
|
"extract_webpage",
|
||||||
"read_file",
|
"read_file",
|
||||||
|
"read_skill",
|
||||||
"view_image",
|
"view_image",
|
||||||
"view_video",
|
"view_video",
|
||||||
"vlm_analyze",
|
"vlm_analyze",
|
||||||
@ -372,7 +373,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
return raw.lstrip("./").lower()
|
return raw.lstrip("./").lower()
|
||||||
|
|
||||||
def _mark_skill_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
|
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
|
return
|
||||||
if not isinstance(result, dict) or not result.get("success"):
|
if not isinstance(result, dict) or not result.get("success"):
|
||||||
return
|
return
|
||||||
@ -405,7 +406,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
return visited
|
return visited
|
||||||
|
|
||||||
def _mark_file_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
|
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
|
return
|
||||||
if not isinstance(result, dict) or not result.get("success"):
|
if not isinstance(result, dict) or not result.get("success"):
|
||||||
return
|
return
|
||||||
@ -536,7 +537,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"调用 {tool_name} 前必须先阅读 {required_path}",
|
"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_skill": skill_id,
|
||||||
"required_path": required_path,
|
"required_path": required_path,
|
||||||
"enforcement": "skill_read_required"
|
"enforcement": "skill_read_required"
|
||||||
@ -608,6 +609,10 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
result = self._handle_read_tool(arguments)
|
result = self._handle_read_tool(arguments)
|
||||||
self._mark_skill_read_from_result(tool_name, arguments, result)
|
self._mark_skill_read_from_result(tool_name, arguments, result)
|
||||||
self._mark_file_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"}:
|
elif tool_name in {"vlm_analyze", "ocr_image"}:
|
||||||
path = arguments.get("path")
|
path = arguments.get("path")
|
||||||
prompt = arguments.get("prompt")
|
prompt = arguments.get("prompt")
|
||||||
|
|||||||
@ -85,6 +85,78 @@ logger = setup_logger(__name__)
|
|||||||
DISABLE_LENGTH_CHECK = True
|
DISABLE_LENGTH_CHECK = True
|
||||||
|
|
||||||
class MainTerminalToolsReadMixin:
|
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
|
@staticmethod
|
||||||
def _clamp_int(value, default, min_value=None, max_value=None):
|
def _clamp_int(value, default, min_value=None, max_value=None):
|
||||||
"""将输入转换为整数并限制范围。"""
|
"""将输入转换为整数并限制范围。"""
|
||||||
|
|||||||
@ -41,6 +41,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
|||||||
"read_focus": ToolCategory(
|
"read_focus": ToolCategory(
|
||||||
label="阅读聚焦",
|
label="阅读聚焦",
|
||||||
tools=[
|
tools=[
|
||||||
|
"read_skill",
|
||||||
"read_file",
|
"read_file",
|
||||||
"vlm_analyze",
|
"vlm_analyze",
|
||||||
"ocr_image",
|
"ocr_image",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
agent skills系统已启用,以下是可用的skills(含简要说明)
|
agent skills系统已启用,以下是可用的skills(含简要说明)
|
||||||
{skills_list}
|
{skills_list}
|
||||||
[skills_empty]暂无可用的skills[/skills_empty]
|
[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);
|
return renderReadFile(result, args);
|
||||||
} else if (name === 'vlm_analyze') {
|
} else if (name === 'vlm_analyze') {
|
||||||
return renderVlmAnalyze(result, args);
|
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);
|
return renderReadFile(result, args);
|
||||||
} else if (name === 'vlm_analyze') {
|
} else if (name === 'vlm_analyze') {
|
||||||
return renderVlmAnalyze(result, args);
|
return renderVlmAnalyze(result, args);
|
||||||
|
|||||||
@ -67,6 +67,10 @@ const buildText = (entry: ActivityEntry) => {
|
|||||||
const path = args.path || args.file_path || '';
|
const path = args.path || args.file_path || '';
|
||||||
return `阅读 ${path}`;
|
return `阅读 ${path}`;
|
||||||
}
|
}
|
||||||
|
if (tool === 'read_skill') {
|
||||||
|
const skillName = args.skill_name || '';
|
||||||
|
return `阅读技能 ${skillName}`;
|
||||||
|
}
|
||||||
if (tool === 'search_workspace') {
|
if (tool === 'search_workspace') {
|
||||||
const query = args.query || args.keyword || '';
|
const query = args.query || args.keyword || '';
|
||||||
return `在工作区搜索 ${query}`;
|
return `在工作区搜索 ${query}`;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ type ToolPayload = Record<string, any> | null | undefined;
|
|||||||
const RUNNING_ANIMATIONS: Record<string, string> = {
|
const RUNNING_ANIMATIONS: Record<string, string> = {
|
||||||
create_file: 'file-animation',
|
create_file: 'file-animation',
|
||||||
read_file: 'read-animation',
|
read_file: 'read-animation',
|
||||||
|
read_skill: 'read-animation',
|
||||||
delete_file: 'file-animation',
|
delete_file: 'file-animation',
|
||||||
rename_file: 'file-animation',
|
rename_file: 'file-animation',
|
||||||
write_file: 'file-animation',
|
write_file: 'file-animation',
|
||||||
@ -43,7 +44,8 @@ const RUNNING_STATUS_TEXTS: Record<string, string> = {
|
|||||||
update_memory: '正在更新记忆...',
|
update_memory: '正在更新记忆...',
|
||||||
terminal_session: '正在管理终端会话...',
|
terminal_session: '正在管理终端会话...',
|
||||||
terminal_input: '调用 terminal_input',
|
terminal_input: '调用 terminal_input',
|
||||||
terminal_snapshot: '正在获取终端快照...'
|
terminal_snapshot: '正在获取终端快照...',
|
||||||
|
read_skill: '正在读取技能...'
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMPLETED_STATUS_TEXTS: Record<string, string> = {
|
const COMPLETED_STATUS_TEXTS: Record<string, string> = {
|
||||||
@ -63,7 +65,8 @@ const COMPLETED_STATUS_TEXTS: Record<string, string> = {
|
|||||||
update_memory: '记忆更新成功',
|
update_memory: '记忆更新成功',
|
||||||
terminal_session: '终端操作完成',
|
terminal_session: '终端操作完成',
|
||||||
terminal_input: '终端输入完成',
|
terminal_input: '终端输入完成',
|
||||||
terminal_snapshot: '终端快照已返回'
|
terminal_snapshot: '终端快照已返回',
|
||||||
|
read_skill: '技能读取完成'
|
||||||
};
|
};
|
||||||
|
|
||||||
const LANGUAGE_CLASS_MAP: Record<string, string> = {
|
const LANGUAGE_CLASS_MAP: Record<string, string> = {
|
||||||
@ -171,7 +174,7 @@ export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean })
|
|||||||
return `准备调用 ${tool.name}...`;
|
return `准备调用 ${tool.name}...`;
|
||||||
}
|
}
|
||||||
if (tool.status === 'running') {
|
if (tool.status === 'running') {
|
||||||
if (tool.name === 'read_file') {
|
if (tool.name === 'read_file' || tool.name === 'read_skill') {
|
||||||
const readType = String(
|
const readType = String(
|
||||||
tool.argumentSnapshot?.type || tool.arguments?.type || 'read'
|
tool.argumentSnapshot?.type || tool.arguments?.type || 'read'
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
@ -193,7 +196,7 @@ export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean })
|
|||||||
return '等待审批...';
|
return '等待审批...';
|
||||||
}
|
}
|
||||||
if (tool.status === 'completed') {
|
if (tool.status === 'completed') {
|
||||||
if (tool.name === 'read_file') {
|
if (tool.name === 'read_file' || tool.name === 'read_skill') {
|
||||||
return describeReadFileResult(tool);
|
return describeReadFileResult(tool);
|
||||||
}
|
}
|
||||||
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
|
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export const TOOL_ICON_MAP = Object.freeze({
|
|||||||
vlm_analyze: 'camera',
|
vlm_analyze: 'camera',
|
||||||
ocr_image: 'camera',
|
ocr_image: 'camera',
|
||||||
read_file: 'book',
|
read_file: 'book',
|
||||||
|
read_skill: 'book',
|
||||||
rename_file: 'pencil',
|
rename_file: 'pencil',
|
||||||
run_command: 'terminal',
|
run_command: 'terminal',
|
||||||
run_python: 'python',
|
run_python: 'python',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user