fix: separate video payload from images in task API
This commit is contained in:
parent
a5412dae34
commit
a5a3eb9974
@ -1,5 +1,6 @@
|
|||||||
"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
|
"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import mimetypes
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
@ -101,6 +102,7 @@ class TaskManager:
|
|||||||
message: str,
|
message: str,
|
||||||
images: List[Any],
|
images: List[Any],
|
||||||
conversation_id: Optional[str],
|
conversation_id: Optional[str],
|
||||||
|
videos: Optional[List[Any]] = None,
|
||||||
model_key: Optional[str] = None,
|
model_key: Optional[str] = None,
|
||||||
thinking_mode: Optional[bool] = None,
|
thinking_mode: Optional[bool] = None,
|
||||||
run_mode: Optional[str] = None,
|
run_mode: Optional[str] = None,
|
||||||
@ -137,7 +139,7 @@ class TaskManager:
|
|||||||
record.session_data = {}
|
record.session_data = {}
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._tasks[task_id] = record
|
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.thread = thread
|
||||||
record.status = "running"
|
record.status = "running"
|
||||||
record.updated_at = time.time()
|
record.updated_at = time.time()
|
||||||
@ -199,7 +201,7 @@ class TaskManager:
|
|||||||
})
|
})
|
||||||
rec.updated_at = time.time()
|
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
|
username = rec.username
|
||||||
workspace_id = rec.workspace_id
|
workspace_id = rec.workspace_id
|
||||||
terminal = None
|
terminal = None
|
||||||
@ -269,6 +271,7 @@ class TaskManager:
|
|||||||
client_sid=rec.task_id,
|
client_sid=rec.task_id,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
username=username,
|
username=username,
|
||||||
|
videos=videos,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 结束状态
|
# 结束状态
|
||||||
@ -347,8 +350,7 @@ def create_task_api():
|
|||||||
workspace_id = session.get("workspace_id") or "default"
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
payload = request.get_json() or {}
|
payload = request.get_json() or {}
|
||||||
message = (payload.get("message") or "").strip()
|
message = (payload.get("message") or "").strip()
|
||||||
images = payload.get("images") or []
|
images, videos = _normalize_media_payload(payload.get("images") or [], payload.get("videos") or [])
|
||||||
videos = payload.get("videos") or []
|
|
||||||
conversation_id = payload.get("conversation_id")
|
conversation_id = payload.get("conversation_id")
|
||||||
if not message and not images and not videos:
|
if not message and not images and not videos:
|
||||||
return jsonify({"success": False, "error": "消息不能为空"}), 400
|
return jsonify({"success": False, "error": "消息不能为空"}), 400
|
||||||
@ -357,16 +359,14 @@ def create_task_api():
|
|||||||
run_mode = payload.get("run_mode")
|
run_mode = payload.get("run_mode")
|
||||||
max_iterations = payload.get("max_iterations")
|
max_iterations = payload.get("max_iterations")
|
||||||
|
|
||||||
# 合并 images 和 videos 到 images 参数(后端统一处理)
|
|
||||||
all_media = images + videos
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rec = task_manager.create_chat_task(
|
rec = task_manager.create_chat_task(
|
||||||
username,
|
username,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
message,
|
message,
|
||||||
all_media,
|
images,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
|
videos=videos,
|
||||||
model_key=model_key,
|
model_key=model_key,
|
||||||
thinking_mode=thinking_mode,
|
thinking_mode=thinking_mode,
|
||||||
run_mode=run_mode,
|
run_mode=run_mode,
|
||||||
@ -385,6 +385,48 @@ def create_task_api():
|
|||||||
}), 202
|
}), 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/<task_id>", methods=["GET"])
|
@tasks_bp.route("/api/tasks/<task_id>", methods=["GET"])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_task_api(task_id: str):
|
def get_task_api(task_id: str):
|
||||||
|
|||||||
@ -205,6 +205,7 @@ class DeepSeekClient:
|
|||||||
return text
|
return text
|
||||||
qwen_video_fps = 2
|
qwen_video_fps = 2
|
||||||
parts: List[Dict[str, Any]] = []
|
parts: List[Dict[str, Any]] = []
|
||||||
|
extra_videos: List[Any] = []
|
||||||
if text:
|
if text:
|
||||||
parts.append({"type": "text", "text": text})
|
parts.append({"type": "text", "text": text})
|
||||||
base_path = Path(self.project_path or ".")
|
base_path = Path(self.project_path or ".")
|
||||||
@ -214,6 +215,11 @@ class DeepSeekClient:
|
|||||||
if not abs_path.exists() or not abs_path.is_file():
|
if not abs_path.exists() or not abs_path.is_file():
|
||||||
continue
|
continue
|
||||||
mime, _ = mimetypes.guess_type(abs_path.name)
|
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:
|
if not mime:
|
||||||
mime = "image/png"
|
mime = "image/png"
|
||||||
data = abs_path.read_bytes()
|
data = abs_path.read_bytes()
|
||||||
@ -221,7 +227,7 @@ class DeepSeekClient:
|
|||||||
parts.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})
|
parts.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
for item in videos:
|
for item in [*videos, *extra_videos]:
|
||||||
try:
|
try:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
path = item.get("path") or ""
|
path = item.get("path") or ""
|
||||||
|
|||||||
@ -1608,6 +1608,7 @@ class ContextManager:
|
|||||||
if not images and not videos:
|
if not images and not videos:
|
||||||
return text
|
return text
|
||||||
parts: List[Dict[str, Any]] = []
|
parts: List[Dict[str, Any]] = []
|
||||||
|
extra_videos: List[Any] = []
|
||||||
supports_video_fps = getattr(getattr(self, "main_terminal", None), "model_key", None) == "qwen3-vl-plus"
|
supports_video_fps = getattr(getattr(self, "main_terminal", None), "model_key", None) == "qwen3-vl-plus"
|
||||||
qwen_video_fps = 2
|
qwen_video_fps = 2
|
||||||
if text:
|
if text:
|
||||||
@ -1617,9 +1618,14 @@ class ContextManager:
|
|||||||
abs_path = Path(self.project_path) / path
|
abs_path = Path(self.project_path) / path
|
||||||
if not abs_path.exists() or not abs_path.is_file():
|
if not abs_path.exists() or not abs_path.is_file():
|
||||||
continue
|
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)
|
data_url = self._compress_image_if_needed(abs_path)
|
||||||
if not data_url:
|
if not data_url:
|
||||||
mime, _ = mimetypes.guess_type(abs_path.name)
|
|
||||||
if not mime:
|
if not mime:
|
||||||
mime = "image/png"
|
mime = "image/png"
|
||||||
data = abs_path.read_bytes()
|
data = abs_path.read_bytes()
|
||||||
@ -1628,7 +1634,7 @@ class ContextManager:
|
|||||||
parts.append({"type": "image_url", "image_url": {"url": data_url}})
|
parts.append({"type": "image_url", "image_url": {"url": data_url}})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
for item in videos:
|
for item in [*videos, *extra_videos]:
|
||||||
try:
|
try:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
path = item.get("path") or ""
|
path = item.get("path") or ""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user