agent-Specialization/server/tasks/skills.py
JOJO f661dc6456 refactor(server,modules,utils,frontend): split oversized modules into packages and fix regressions
- Split server/chat.py, server/status.py, server/tasks.py into sub-packages
- Split utils/context_manager.py, utils/conversation_manager.py into mixin packages
- Split modules/file_manager.py, persistent_terminal.py, terminal_ops.py, mcp_client_manager.py into packages
- Split core/main_terminal_parts/context.py into base + mixins
- Split static/src/app/methods/{ui,message,upload,taskPolling,conversation} into sub-modules
- Fix flattened method exports, dynamic import paths, missing cross-module imports
- Add missing permission/network/execution mode constants and _PERMISSION_MODE_LABEL
- Update AGENTS.md, CLAUDE.md and project memories to reflect new structure
2026-06-20 15:54:48 +08:00

125 lines
4.7 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.

"""简单任务 API将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
from __future__ import annotations
from server.tasks import tasks_bp
import mimetypes
import json
import time
import threading
import uuid
import re
from collections import deque
from pathlib import Path
from typing import Dict, Any, Optional, List
from flask import Blueprint, request, jsonify
from flask import current_app, session
from server.auth_helpers import api_login_required, get_current_username
from server.context import get_user_resources, ensure_conversation_loaded
from server.chat_flow import run_chat_task_sync
from server.state import stop_flags
from server.utils_common import debug_log, log_conn_diag
from utils.host_workspace_debug import write_host_workspace_debug
from config import DATA_DIR, WORKSPACE_SKILLS_DIRNAME
from modules.goal_state_manager import GoalStateManager, REASON_USER_CANCEL
SKILL_FRONTMATTER_RE = re.compile(r"^---\s*\n(?P<body>.*?)\n---\s*\n?", re.S)
SKILL_FIELD_RE = re.compile(r"^(?P<key>name|description)\s*:\s*(?P<value>.*)$")
def _parse_skill_metadata(content: str, fallback_name: str) -> Dict[str, str]:
metadata = {"name": fallback_name, "description": ""}
match = SKILL_FRONTMATTER_RE.match(content or "")
if not match:
return metadata
for raw_line in match.group("body").splitlines():
field = SKILL_FIELD_RE.match(raw_line.strip())
if not field:
continue
key = field.group("key")
value = field.group("value").strip().strip('"').strip("'")
metadata[key] = value
metadata["name"] = metadata.get("name") or fallback_name
return metadata
def _workspace_skills_dir(workspace) -> Path:
return (Path(workspace.project_path).expanduser().resolve() / WORKSPACE_SKILLS_DIRNAME).resolve()
def _resolve_workspace_skill_path(workspace, raw_path: str) -> Path:
skills_dir = _workspace_skills_dir(workspace)
target = Path(str(raw_path or "")).expanduser()
if not target.is_absolute():
target = Path(workspace.project_path).expanduser().resolve() / target
target = target.resolve()
if target.name != "SKILL.md":
raise ValueError("skill 路径必须指向 SKILL.md")
try:
target.relative_to(skills_dir)
except ValueError as exc:
raise ValueError("skill 路径必须位于当前工作区 .agents/skills/ 内") from exc
if not target.is_file():
raise ValueError("skill 文件不存在")
return target
def _list_workspace_skills(workspace) -> List[Dict[str, str]]:
skills_dir = _workspace_skills_dir(workspace)
if not skills_dir.is_dir():
return []
result: List[Dict[str, str]] = []
for skill_file in sorted(skills_dir.glob("*/SKILL.md"), key=lambda p: p.parent.name.lower()):
try:
content = skill_file.read_text(encoding="utf-8")
except Exception:
continue
metadata = _parse_skill_metadata(content, skill_file.parent.name)
result.append({
"name": metadata.get("name") or skill_file.parent.name,
"description": metadata.get("description") or "",
"path": str(skill_file.resolve()),
})
return result
def _build_skill_context_messages(workspace, raw_refs: Any) -> List[Dict[str, str]]:
if not isinstance(raw_refs, list):
return []
messages: List[Dict[str, str]] = []
seen_paths = set()
for item in raw_refs[:10]:
if not isinstance(item, dict):
continue
raw_path = str(item.get("path") or "").strip()
if not raw_path:
continue
skill_file = _resolve_workspace_skill_path(workspace, raw_path)
path_key = str(skill_file)
if path_key in seen_paths:
continue
seen_paths.add(path_key)
content = skill_file.read_text(encoding="utf-8")
metadata = _parse_skill_metadata(content, skill_file.parent.name)
messages.append({
"name": metadata.get("name") or skill_file.parent.name,
"path": path_key,
"content": content,
})
return messages
@tasks_bp.route("/api/skills", methods=["GET"])
@api_login_required
def list_skills_api():
username = get_current_username()
workspace_id = session.get("workspace_id") or "default"
try:
_terminal, workspace = get_user_resources(username, workspace_id)
if not workspace:
return jsonify({"success": False, "error": "工作区不可用"}), 400
return jsonify({
"success": True,
"skills": _list_workspace_skills(workspace),
})
except Exception as exc:
debug_log(f"[SkillsAPI] 列出技能失败: {exc}")
return jsonify({"success": False, "error": str(exc)}), 500