agent-Specialization/server/api_v1.py

239 lines
8.7 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 v1Bearer Token 版轻量接口(后台任务 + 轮询 + 文件)。"""
from __future__ import annotations
import os
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Dict, Any
from flask import Blueprint, request, jsonify, send_file, session
from .api_auth import api_token_required
from .tasks import task_manager
from .context import get_user_resources, ensure_conversation_loaded, get_upload_guard
from .files import sanitize_filename_preserve_unicode
from .utils_common import debug_log
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
def _within_uploads(workspace, rel_path: str) -> Path:
base = Path(workspace.uploads_dir).resolve()
rel = rel_path or ""
# 兼容传入包含 user_upload/ 前缀的路径
if rel.startswith("user_upload/"):
rel = rel.split("user_upload/", 1)[1]
rel = rel.lstrip("/").strip()
target = (base / rel).resolve()
if not str(target).startswith(str(base)):
raise ValueError("非法路径")
return target
@api_v1_bp.route("/conversations", methods=["POST"])
@api_token_required
def create_conversation_api():
username = session.get("username")
terminal, workspace = get_user_resources(username)
if not terminal:
return jsonify({"success": False, "error": "系统未初始化"}), 503
result = terminal.create_new_conversation()
if not result.get("success"):
return jsonify({"success": False, "error": result.get("error") or "创建对话失败"}), 500
return jsonify({"success": True, "conversation_id": result.get("conversation_id")})
@api_v1_bp.route("/messages", methods=["POST"])
@api_token_required
def send_message_api():
username = session.get("username")
payload = request.get_json() or {}
message = (payload.get("message") or "").strip()
images = payload.get("images") or []
conversation_id = payload.get("conversation_id")
model_key = payload.get("model_key")
thinking_mode = payload.get("thinking_mode")
run_mode = payload.get("run_mode")
max_iterations = payload.get("max_iterations")
if not message and not images:
return jsonify({"success": False, "error": "消息不能为空"}), 400
terminal, workspace = get_user_resources(username)
if not terminal or not workspace:
return jsonify({"success": False, "error": "系统未初始化"}), 503
try:
conversation_id, _ = ensure_conversation_loaded(terminal, conversation_id)
except Exception as exc:
return jsonify({"success": False, "error": f"对话加载失败: {exc}"}), 400
try:
rec = task_manager.create_chat_task(
username=username,
message=message,
images=images,
conversation_id=conversation_id,
model_key=model_key,
thinking_mode=thinking_mode,
run_mode=run_mode,
max_iterations=max_iterations,
)
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
except RuntimeError as exc:
# 并发等业务冲突仍用 409
return jsonify({"success": False, "error": str(exc)}), 409
return jsonify({
"success": True,
"task_id": rec.task_id,
"conversation_id": rec.conversation_id,
"status": rec.status,
"created_at": rec.created_at,
}), 202
@api_v1_bp.route("/tasks/<task_id>", methods=["GET"])
@api_token_required
def get_task_events(task_id: str):
username = session.get("username")
rec = task_manager.get_task(username, task_id)
if not rec:
return jsonify({"success": False, "error": "任务不存在"}), 404
try:
offset = int(request.args.get("from", 0))
except Exception:
offset = 0
events = [e for e in rec.events if e["idx"] >= offset]
next_offset = events[-1]["idx"] + 1 if events else offset
return jsonify({
"success": True,
"data": {
"task_id": rec.task_id,
"status": rec.status,
"created_at": rec.created_at,
"updated_at": rec.updated_at,
"error": rec.error,
"events": events,
"next_offset": next_offset,
}
})
@api_v1_bp.route("/tasks/<task_id>/cancel", methods=["POST"])
@api_token_required
def cancel_task_api_v1(task_id: str):
username = session.get("username")
ok = task_manager.cancel_task(username, task_id)
if not ok:
return jsonify({"success": False, "error": "任务不存在"}), 404
return jsonify({"success": True})
@api_v1_bp.route("/files/upload", methods=["POST"])
@api_token_required
def upload_file_api():
username = session.get("username")
terminal, workspace = get_user_resources(username)
if not terminal or not workspace:
return jsonify({"success": False, "error": "系统未初始化"}), 503
if 'file' not in request.files:
return jsonify({"success": False, "error": "未找到文件"}), 400
file_obj = request.files['file']
raw_name = request.form.get('filename') or file_obj.filename
filename = sanitize_filename_preserve_unicode(raw_name)
if not filename:
return jsonify({"success": False, "error": "非法文件名"}), 400
subdir = request.form.get("dir") or ""
try:
target_dir = _within_uploads(workspace, subdir)
target_dir.mkdir(parents=True, exist_ok=True)
target_path = (target_dir / filename).resolve()
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 400
guard = get_upload_guard(workspace)
rel_path = str(target_path.relative_to(workspace.uploads_dir))
try:
result = guard.process_upload(
file_obj,
target_path,
username=username,
source="api_v1",
original_name=raw_name,
relative_path=rel_path,
)
except Exception as exc:
return jsonify({"success": False, "error": f"保存文件失败: {exc}"}), 500
metadata = result.get("metadata", {})
return jsonify({
"success": True,
"path": rel_path,
"filename": target_path.name,
"size": metadata.get("size"),
"sha256": metadata.get("sha256"),
})
@api_v1_bp.route("/files", methods=["GET"])
@api_token_required
def list_files_api():
username = session.get("username")
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "系统未初始化"}), 503
rel = request.args.get("path") or ""
try:
target = _within_uploads(workspace, rel)
if not target.exists():
return jsonify({"success": False, "error": "路径不存在"}), 404
if not target.is_dir():
return jsonify({"success": False, "error": "路径不是文件夹"}), 400
items = []
for entry in sorted(target.iterdir(), key=lambda p: p.name):
stat = entry.stat()
rel_entry = entry.relative_to(workspace.uploads_dir)
items.append({
"name": entry.name,
"is_dir": entry.is_dir(),
"size": stat.st_size,
"modified_at": stat.st_mtime,
"path": str(rel_entry),
})
return jsonify({"success": True, "items": items, "base": str(target.relative_to(workspace.uploads_dir))})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 400
@api_v1_bp.route("/files/download", methods=["GET"])
@api_token_required
def download_file_api():
username = session.get("username")
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "系统未初始化"}), 503
rel = request.args.get("path")
if not rel:
return jsonify({"success": False, "error": "缺少 path"}), 400
try:
target = _within_uploads(workspace, rel)
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 400
if not target.exists():
return jsonify({"success": False, "error": "文件不存在"}), 404
if target.is_dir():
memory_file = BytesIO()
with zipfile.ZipFile(memory_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
for root, _, files in os.walk(target):
for file in files:
full_path = Path(root) / file
arcname = full_path.relative_to(workspace.project_path)
zf.write(full_path, arcname=str(arcname))
memory_file.seek(0)
download_name = f"{target.name}.zip"
return send_file(memory_file, as_attachment=True, download_name=download_name, mimetype='application/zip')
return send_file(target, as_attachment=True, download_name=target.name)
__all__ = ["api_v1_bp"]