From a5a3eb9974c2158d3cf5c15b456e5a44d72a5ff7 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 5 Apr 2026 19:00:19 +0800 Subject: [PATCH] fix: separate video payload from images in task API --- server/tasks.py | 58 ++++++++++++++++++++++++++++++++++------ utils/api_client.py | 8 +++++- utils/context_manager.py | 10 +++++-- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/server/tasks.py b/server/tasks.py index bc6db9d..8932abc 100644 --- a/server/tasks.py +++ b/server/tasks.py @@ -1,5 +1,6 @@ """简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。""" from __future__ import annotations +import mimetypes import time import threading import uuid @@ -101,6 +102,7 @@ class TaskManager: message: str, images: List[Any], conversation_id: Optional[str], + videos: Optional[List[Any]] = None, model_key: Optional[str] = None, thinking_mode: Optional[bool] = None, run_mode: Optional[str] = None, @@ -137,7 +139,7 @@ class TaskManager: record.session_data = {} with self._lock: self._tasks[task_id] = record - thread = threading.Thread(target=self._run_chat_task, args=(record, images), daemon=True) + thread = threading.Thread(target=self._run_chat_task, args=(record, images, videos or []), daemon=True) record.thread = thread record.status = "running" record.updated_at = time.time() @@ -199,7 +201,7 @@ class TaskManager: }) rec.updated_at = time.time() - def _run_chat_task(self, rec: TaskRecord, images: List[Any]): + def _run_chat_task(self, rec: TaskRecord, images: List[Any], videos: List[Any]): username = rec.username workspace_id = rec.workspace_id terminal = None @@ -269,6 +271,7 @@ class TaskManager: client_sid=rec.task_id, workspace=workspace, username=username, + videos=videos, ) # 结束状态 @@ -347,8 +350,7 @@ def create_task_api(): workspace_id = session.get("workspace_id") or "default" payload = request.get_json() or {} message = (payload.get("message") or "").strip() - images = payload.get("images") or [] - videos = payload.get("videos") or [] + images, videos = _normalize_media_payload(payload.get("images") or [], payload.get("videos") or []) conversation_id = payload.get("conversation_id") if not message and not images and not videos: return jsonify({"success": False, "error": "消息不能为空"}), 400 @@ -357,16 +359,14 @@ def create_task_api(): run_mode = payload.get("run_mode") max_iterations = payload.get("max_iterations") - # 合并 images 和 videos 到 images 参数(后端统一处理) - all_media = images + videos - try: rec = task_manager.create_chat_task( username, workspace_id, message, - all_media, + images, conversation_id, + videos=videos, model_key=model_key, thinking_mode=thinking_mode, run_mode=run_mode, @@ -385,6 +385,48 @@ def create_task_api(): }), 202 +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_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 + + @tasks_bp.route("/api/tasks/", methods=["GET"]) @api_login_required def get_task_api(task_id: str): diff --git a/utils/api_client.py b/utils/api_client.py index a38c936..c2406d0 100644 --- a/utils/api_client.py +++ b/utils/api_client.py @@ -205,6 +205,7 @@ class DeepSeekClient: return text qwen_video_fps = 2 parts: List[Dict[str, Any]] = [] + extra_videos: List[Any] = [] if text: parts.append({"type": "text", "text": text}) base_path = Path(self.project_path or ".") @@ -214,6 +215,11 @@ class DeepSeekClient: if not abs_path.exists() or not abs_path.is_file(): continue mime, _ = mimetypes.guess_type(abs_path.name) + if mime and mime.startswith("video/"): + extra_videos.append(path) + continue + if mime and not mime.startswith("image/"): + continue if not mime: mime = "image/png" data = abs_path.read_bytes() @@ -221,7 +227,7 @@ class DeepSeekClient: parts.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}) except Exception: continue - for item in videos: + for item in [*videos, *extra_videos]: try: if isinstance(item, dict): path = item.get("path") or "" diff --git a/utils/context_manager.py b/utils/context_manager.py index b78e738..4c44f03 100644 --- a/utils/context_manager.py +++ b/utils/context_manager.py @@ -1608,6 +1608,7 @@ class ContextManager: if not images and not videos: return text parts: List[Dict[str, Any]] = [] + extra_videos: List[Any] = [] supports_video_fps = getattr(getattr(self, "main_terminal", None), "model_key", None) == "qwen3-vl-plus" qwen_video_fps = 2 if text: @@ -1617,9 +1618,14 @@ class ContextManager: abs_path = Path(self.project_path) / path if not abs_path.exists() or not abs_path.is_file(): continue + mime, _ = mimetypes.guess_type(abs_path.name) + if mime and mime.startswith("video/"): + extra_videos.append(path) + continue + if mime and not mime.startswith("image/"): + continue data_url = self._compress_image_if_needed(abs_path) if not data_url: - mime, _ = mimetypes.guess_type(abs_path.name) if not mime: mime = "image/png" data = abs_path.read_bytes() @@ -1628,7 +1634,7 @@ class ContextManager: parts.append({"type": "image_url", "image_url": {"url": data_url}}) except Exception: continue - for item in videos: + for item in [*videos, *extra_videos]: try: if isinstance(item, dict): path = item.get("path") or ""