- 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
125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""简单任务 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
|