agent-Specialization/server/tasks/media.py
JOJO d9bd599c1c feat(runtime): Gateway 独立启动与公共协议落地(改造第 0-3 步)
- 闭环修复:审批 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 项存量
2026-09-07 23:00:15 +08:00

86 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""简单任务 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