fix(server/tasks): 修复 skill 创建任务失败并补充调试日志

- 补全从 api.py 拆到 skills.py 时遗漏的 _build_skill_context_messages 导入
- 捕获 skill 文件读取时的 UnicodeDecodeError/OSError,转为 400 错误
- 在 skill 路径解析与读取流程中增加详细 debug_log
This commit is contained in:
JOJO 2026-06-21 01:45:45 +08:00
parent 8e1a339102
commit 2bfbddb0dd
2 changed files with 51 additions and 30 deletions

View File

@ -6,7 +6,7 @@ import json
import time import time
import threading import threading
import uuid import uuid
import re import traceback
from collections import deque from collections import deque
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional, List 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 config import DATA_DIR, WORKSPACE_SKILLS_DIRNAME
from modules.goal_state_manager import GoalStateManager, REASON_USER_CANCEL from modules.goal_state_manager import GoalStateManager, REASON_USER_CANCEL
from server.tasks import task_manager 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.helpers import _task_public_payload
from server.tasks.media import _normalize_media_payload from server.tasks.media import _normalize_media_payload
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>.*)$")
@tasks_bp.route("/api/tasks", methods=["GET"]) @tasks_bp.route("/api/tasks", methods=["GET"])
@api_login_required @api_login_required
@ -87,14 +84,27 @@ def create_task_api():
raw_skill_refs = payload.get("skill_refs") raw_skill_refs = payload.get("skill_refs")
if raw_skill_refs: if raw_skill_refs:
try: 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) _terminal, workspace = get_user_resources(username, workspace_id)
if not workspace: if not workspace:
debug_log(f"[SkillsAPI] create_task workspace unavailable user={username} ws={workspace_id}")
return jsonify({"success": False, "error": "工作区不可用"}), 400 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) 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: except ValueError as exc:
debug_log(f"[SkillsAPI] create_task ValueError: {exc}")
return jsonify({"success": False, "error": str(exc)}), 400 return jsonify({"success": False, "error": str(exc)}), 400
except Exception as exc: except Exception as exc:
debug_log(f"[SkillsAPI] 读取技能上下文失败: {exc}") debug_log(f"[SkillsAPI] 读取技能上下文失败: {exc}")
debug_log(f"[SkillsAPI] 读取技能上下文失败 traceback:\n{traceback.format_exc()}")
return jsonify({"success": False, "error": "读取 skill 失败"}), 500 return jsonify({"success": False, "error": "读取 skill 失败"}), 500
try: try:
debug_log( debug_log(

View File

@ -1,27 +1,17 @@
"""简单任务 API将聊天任务与 WebSocket 解耦,支持后台运行与轮询""" """Workspace skill 读取与 /api/skills 接口"""
from __future__ import annotations from __future__ import annotations
from server.tasks import tasks_bp from server.tasks import tasks_bp
import mimetypes
import json
import time
import threading
import uuid
import re import re
from collections import deque
from pathlib import Path 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 request, jsonify
from flask import current_app, session from flask import session
from server.auth_helpers import api_login_required, get_current_username from server.auth_helpers import api_login_required, get_current_username
from server.context import get_user_resources, ensure_conversation_loaded from server.context import get_user_resources
from server.chat_flow import run_chat_task_sync from server.utils_common import debug_log
from server.state import stop_flags from config import WORKSPACE_SKILLS_DIRNAME
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_FRONTMATTER_RE = re.compile(r"^---\s*\n(?P<body>.*?)\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() return (Path(workspace.project_path).expanduser().resolve() / WORKSPACE_SKILLS_DIRNAME).resolve()
def _resolve_workspace_skill_path(workspace, raw_path: str) -> Path: def _resolve_workspace_skill_path(workspace, raw_path: str) -> Path:
skills_dir = _workspace_skills_dir(workspace) try:
target = Path(str(raw_path or "")).expanduser() skills_dir = _workspace_skills_dir(workspace)
if not target.is_absolute(): except Exception as exc:
target = Path(workspace.project_path).expanduser().resolve() / target debug_log(f"[SkillsAPI] resolve skill cannot compute skills_dir: {exc}")
target = target.resolve() 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": if target.name != "SKILL.md":
raise ValueError("skill 路径必须指向 SKILL.md") raise ValueError(f"skill 路径必须指向 SKILL.md: {target.name}")
try: try:
target.relative_to(skills_dir) target.relative_to(skills_dir)
except ValueError as exc: 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 raise ValueError("skill 路径必须位于当前工作区 .agents/skills/ 内") from exc
if not target.is_file(): if not target.is_file():
debug_log(f"[SkillsAPI] resolve skill file not found: {target!r}")
raise ValueError("skill 文件不存在") raise ValueError("skill 文件不存在")
return target 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]]: def _build_skill_context_messages(workspace, raw_refs: Any) -> List[Dict[str, str]]:
if not isinstance(raw_refs, list): if not isinstance(raw_refs, list):
debug_log(f"[SkillsAPI] build_skill_context raw_refs is not list: {type(raw_refs).__name__}")
return [] return []
messages: List[Dict[str, str]] = [] messages: List[Dict[str, str]] = []
seen_paths = set() seen_paths = set()
for item in raw_refs[:10]: for idx, item in enumerate(raw_refs[:10]):
if not isinstance(item, dict): if not isinstance(item, dict):
debug_log(f"[SkillsAPI] build_skill_context item[{idx}] not dict: {item!r}")
continue continue
raw_path = str(item.get("path") or "").strip() 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: if not raw_path:
continue continue
skill_file = _resolve_workspace_skill_path(workspace, raw_path) skill_file = _resolve_workspace_skill_path(workspace, raw_path)
path_key = str(skill_file) path_key = str(skill_file)
if path_key in seen_paths: if path_key in seen_paths:
debug_log(f"[SkillsAPI] build_skill_context duplicate path skipped: {path_key}")
continue continue
seen_paths.add(path_key) 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) metadata = _parse_skill_metadata(content, skill_file.parent.name)
messages.append({ messages.append({
"name": metadata.get("name") or skill_file.parent.name, "name": metadata.get("name") or skill_file.parent.name,