- 闭环修复:审批 task_id 显式化(N2)、通知回退路径门闸 finally 释放(N1)、 personalization 原子写(S10,公共 atomic_write_json) - server.tasks 拆解:__init__ 轻量化、Blueprint/路由移至 blueprint.py/web.py, app_legacy 装配点惰性挂载;models.py 循环依赖死导入移除 - 执行链 Web 解耦:emit_event/run_background 安全包装(socketio 未绑定静默), 清除 4 处裸 socketio.emit(含 task_stopped 先写事件流再推送的顺序修复) - RuntimeService 补建对话下沉:chat 任务无 conversation_id 时服务层装配, 客户端无需复制 Web 两步流程 - 事件协议:get_task_events 返回 meta.window_start 缺口检测水位 - 验收:test_runtime_standalone_lifecycle 子进程隔离(config import 固化问题), 独立启动 + 协议全链路 + 审批语义全绿;全量 74 测试失败恰为 4 项存量
86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
|
||
from __future__ import annotations
|
||
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
|
||
|
||
|
||
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 _media_path(item: Any) -> str:
|
||
if isinstance(item, dict):
|
||
return str(item.get("path") or "")
|
||
return str(item or "")
|
||
|
||
def _is_video_item(item: Any) -> bool:
|
||
path = _media_path(item)
|
||
if not path:
|
||
return False
|
||
mime, _ = mimetypes.guess_type(path)
|
||
return bool(mime and mime.startswith("video/"))
|
||
|
||
def _is_image_item(item: Any) -> bool:
|
||
path = _media_path(item)
|
||
if not path:
|
||
return False
|
||
mime, _ = mimetypes.guess_type(path)
|
||
return bool(mime and mime.startswith("image/"))
|
||
|
||
def _normalize_files_payload(raw: Any) -> List[str]:
|
||
"""归一化附加文件列表:仅接受字符串形式的工作区相对路径,去重,最多 9 个。"""
|
||
if not isinstance(raw, list):
|
||
return []
|
||
files: List[str] = []
|
||
for item in raw:
|
||
if not isinstance(item, str):
|
||
continue
|
||
path = item.strip()
|
||
if not path or len(path) > 500:
|
||
continue
|
||
if path in files:
|
||
continue
|
||
files.append(path)
|
||
if len(files) >= 9:
|
||
break
|
||
return files
|
||
|
||
|
||
def _normalize_media_payload(images: List[Any], videos: List[Any]) -> tuple[List[Any], List[Any]]:
|
||
"""纠偏媒体字段:把误传到 images 的视频项自动归入 videos。"""
|
||
fixed_images: List[Any] = []
|
||
fixed_videos: List[Any] = []
|
||
|
||
for item in images or []:
|
||
if _is_video_item(item):
|
||
fixed_videos.append(item)
|
||
else:
|
||
fixed_images.append(item)
|
||
|
||
for item in videos or []:
|
||
if _is_image_item(item):
|
||
fixed_images.append(item)
|
||
else:
|
||
fixed_videos.append(item)
|
||
|
||
return fixed_images, fixed_videos
|