From 2bfbddb0ddc555578ea6ecc94b79014e07b46030 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 21 Jun 2026 01:45:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(server/tasks):=20=E4=BF=AE=E5=A4=8D=20skill?= =?UTF-8?q?=20=E5=88=9B=E5=BB=BA=E4=BB=BB=E5=8A=A1=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E5=85=85=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补全从 api.py 拆到 skills.py 时遗漏的 _build_skill_context_messages 导入 - 捕获 skill 文件读取时的 UnicodeDecodeError/OSError,转为 400 错误 - 在 skill 路径解析与读取流程中增加详细 debug_log --- server/tasks/api.py | 20 ++++++++++---- server/tasks/skills.py | 61 +++++++++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/server/tasks/api.py b/server/tasks/api.py index 85ee129..fc2d24c 100644 --- a/server/tasks/api.py +++ b/server/tasks/api.py @@ -6,7 +6,7 @@ import json import time import threading import uuid -import re +import traceback from collections import deque from pathlib import Path from typing import Dict, Any, Optional, List @@ -23,14 +23,11 @@ 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 from server.tasks import task_manager +from server.tasks.skills import _build_skill_context_messages from server.tasks.helpers import _task_public_payload from server.tasks.media import _normalize_media_payload -SKILL_FRONTMATTER_RE = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n?", re.S) -SKILL_FIELD_RE = re.compile(r"^(?Pname|description)\s*:\s*(?P.*)$") - - @tasks_bp.route("/api/tasks", methods=["GET"]) @api_login_required @@ -87,14 +84,27 @@ def create_task_api(): raw_skill_refs = payload.get("skill_refs") if raw_skill_refs: try: + debug_log( + f"[SkillsAPI] create_task skill_refs type={type(raw_skill_refs).__name__} " + f"count={len(raw_skill_refs) if isinstance(raw_skill_refs, list) else 'N/A'} " + f"refs={raw_skill_refs!r}" + ) _terminal, workspace = get_user_resources(username, workspace_id) if not workspace: + debug_log(f"[SkillsAPI] create_task workspace unavailable user={username} ws={workspace_id}") return jsonify({"success": False, "error": "工作区不可用"}), 400 + debug_log( + f"[SkillsAPI] create_task workspace OK project_path={getattr(workspace, 'project_path', None)!r} " + f"data_dir={getattr(workspace, 'data_dir', None)!r}" + ) skill_context_messages = _build_skill_context_messages(workspace, raw_skill_refs) + debug_log(f"[SkillsAPI] create_task built {len(skill_context_messages)} skill context messages") except ValueError as exc: + debug_log(f"[SkillsAPI] create_task ValueError: {exc}") return jsonify({"success": False, "error": str(exc)}), 400 except Exception as exc: debug_log(f"[SkillsAPI] 读取技能上下文失败: {exc}") + debug_log(f"[SkillsAPI] 读取技能上下文失败 traceback:\n{traceback.format_exc()}") return jsonify({"success": False, "error": "读取 skill 失败"}), 500 try: debug_log( diff --git a/server/tasks/skills.py b/server/tasks/skills.py index 121cec7..718a542 100644 --- a/server/tasks/skills.py +++ b/server/tasks/skills.py @@ -1,27 +1,17 @@ -"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。""" +"""Workspace skill 读取与 /api/skills 接口。""" 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 typing import Dict, Any, List -from flask import Blueprint, request, jsonify -from flask import current_app, session +from flask import request, jsonify +from flask import 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 +from server.context import get_user_resources +from server.utils_common import debug_log +from config import WORKSPACE_SKILLS_DIRNAME SKILL_FRONTMATTER_RE = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n?", re.S) @@ -48,18 +38,30 @@ 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() + try: + skills_dir = _workspace_skills_dir(workspace) + except Exception as exc: + debug_log(f"[SkillsAPI] resolve skill cannot compute skills_dir: {exc}") + raise ValueError(f"无法定位工作区 skills 目录: {exc}") from exc + debug_log(f"[SkillsAPI] resolve skill skills_dir={skills_dir!r} raw_path={raw_path!r}") + try: + target = Path(str(raw_path or "")).expanduser() + if not target.is_absolute(): + target = Path(workspace.project_path).expanduser().resolve() / target + target = target.resolve() + except Exception as exc: + debug_log(f"[SkillsAPI] resolve skill cannot resolve path: {exc}") + raise ValueError(f"skill 路径解析失败: {exc}") from exc + debug_log(f"[SkillsAPI] resolve skill resolved_target={target!r} name={target.name!r}") if target.name != "SKILL.md": - raise ValueError("skill 路径必须指向 SKILL.md") + raise ValueError(f"skill 路径必须指向 SKILL.md: {target.name}") try: target.relative_to(skills_dir) except ValueError as exc: + debug_log(f"[SkillsAPI] resolve skill path outside skills_dir: target={target!r} skills_dir={skills_dir!r}") raise ValueError("skill 路径必须位于当前工作区 .agents/skills/ 内") from exc if not target.is_file(): + debug_log(f"[SkillsAPI] resolve skill file not found: {target!r}") raise ValueError("skill 文件不存在") return target @@ -83,21 +85,30 @@ def _list_workspace_skills(workspace) -> List[Dict[str, str]]: def _build_skill_context_messages(workspace, raw_refs: Any) -> List[Dict[str, str]]: if not isinstance(raw_refs, list): + debug_log(f"[SkillsAPI] build_skill_context raw_refs is not list: {type(raw_refs).__name__}") return [] messages: List[Dict[str, str]] = [] seen_paths = set() - for item in raw_refs[:10]: + for idx, item in enumerate(raw_refs[:10]): if not isinstance(item, dict): + debug_log(f"[SkillsAPI] build_skill_context item[{idx}] not dict: {item!r}") continue raw_path = str(item.get("path") or "").strip() + debug_log(f"[SkillsAPI] build_skill_context item[{idx}] raw_path={raw_path!r} name={item.get('name')!r}") 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: + debug_log(f"[SkillsAPI] build_skill_context duplicate path skipped: {path_key}") continue seen_paths.add(path_key) - content = skill_file.read_text(encoding="utf-8") + try: + content = skill_file.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"skill 文件编码错误,无法读取: {skill_file.name}") from exc + except OSError as exc: + raise ValueError(f"读取 skill 文件失败: {exc}") from exc metadata = _parse_skill_metadata(content, skill_file.parent.name) messages.append({ "name": metadata.get("name") or skill_file.parent.name,