from __future__ import annotations from server.chat import chat_bp import json, time from datetime import datetime from typing import Dict, Any, Optional from pathlib import Path from io import BytesIO import zipfile import os from flask import Blueprint, jsonify, request, session, send_file from werkzeug.utils import secure_filename from werkzeug.exceptions import RequestEntityTooLarge import secrets from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS from modules.personalization_manager import ( load_personalization_config, resolve_context_compression_settings, save_personalization_config, THINKING_INTERVAL_MIN, THINKING_INTERVAL_MAX, RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN, RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX, ) from modules.skills_manager import ( get_skills_catalog, infer_private_skills_dir, merge_enabled_skills, sync_workspace_skills, ) from modules.upload_security import UploadSecurityError from modules.host_sandbox_policy import load_policy, save_policy from modules.user_manager import UserWorkspace from core.web_terminal import WebTerminal from config.model_profiles import get_model_context_window from server.auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker from server.security import rate_limited, prune_socket_tokens from server.utils_common import debug_log from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot from server.files import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".agents/user_upload" @chat_bp.route('/api/upload', methods=['POST']) @api_login_required @with_terminal @rate_limited("legacy_upload", 20, 300, scope="user") def upload_file(terminal: WebTerminal, workspace: UserWorkspace, username: str): """处理前端文件上传请求""" policy = resolve_admin_policy(get_current_user_record()) if policy.get("ui_blocks", {}).get("block_upload"): return jsonify({ "success": False, "error": "文件上传已被管理员禁用", "message": "被管理员禁用上传" }), 403 if 'file' not in request.files: return jsonify({ "success": False, "error": "未找到文件", "message": "请求中缺少文件字段" }), 400 uploaded_file = request.files['file'] original_name = (request.form.get('filename') or '').strip() if not uploaded_file or not uploaded_file.filename or uploaded_file.filename.strip() == '': return jsonify({ "success": False, "error": "文件名为空", "message": "请选择要上传的文件" }), 400 raw_name = original_name or uploaded_file.filename filename = sanitize_filename_preserve_unicode(raw_name) if not filename: filename = secure_filename(raw_name) if not filename: return jsonify({ "success": False, "error": "非法文件名", "message": "文件名包含不支持的字符" }), 400 file_manager = getattr(terminal, 'file_manager', None) if file_manager is None: return jsonify({ "success": False, "error": "文件管理器未初始化" }), 500 target_folder_relative = UPLOAD_FOLDER_NAME valid_folder, folder_error, folder_path = file_manager._validate_path(target_folder_relative) if not valid_folder: return jsonify({ "success": False, "error": folder_error }), 400 try: folder_path.mkdir(parents=True, exist_ok=True) except Exception as exc: return jsonify({ "success": False, "error": f"创建上传目录失败: {exc}" }), 500 target_relative = str(Path(target_folder_relative) / filename) valid_file, file_error, target_full_path = file_manager._validate_path(target_relative) if not valid_file: return jsonify({ "success": False, "error": file_error }), 400 final_path = target_full_path if final_path.exists(): stem = final_path.stem suffix = final_path.suffix counter = 1 while final_path.exists(): candidate_name = f"{stem}_{counter}{suffix}" target_relative = str(Path(target_folder_relative) / candidate_name) valid_file, file_error, candidate_path = file_manager._validate_path(target_relative) if not valid_file: return jsonify({ "success": False, "error": file_error }), 400 final_path = candidate_path counter += 1 try: relative_path = str(final_path.relative_to(workspace.project_path)) except Exception as exc: return jsonify({ "success": False, "error": f"路径解析失败: {exc}" }), 400 guard = get_upload_guard(workspace) try: result = guard.process_upload( uploaded_file, final_path, username=username, source="legacy_upload", original_name=raw_name, relative_path=relative_path, ) except UploadSecurityError as exc: return build_upload_error_response(exc) except Exception as exc: return jsonify({ "success": False, "error": f"保存文件失败: {exc}" }), 500 metadata = result.get("metadata", {}) print(f"{OUTPUT_FORMATS['file']} 上传文件: {relative_path}") return jsonify({ "success": True, "path": relative_path, "filename": final_path.name, "folder": target_folder_relative, "scan": metadata.get("scan"), "sha256": metadata.get("sha256"), "size": metadata.get("size"), }) @chat_bp.errorhandler(RequestEntityTooLarge) def handle_file_too_large(error): """全局捕获上传超大小""" size_mb = MAX_UPLOAD_SIZE / (1024 * 1024) return jsonify({ "success": False, "error": "文件过大", "message": f"单个文件大小不可超过 {size_mb:.1f} MB" }), 413 @chat_bp.route('/api/download/file') @api_login_required @with_terminal def download_file_api(terminal: WebTerminal, workspace: UserWorkspace, username: str): """下载单个文件""" path = (request.args.get('path') or '').strip() if not path: return jsonify({"success": False, "error": "缺少路径参数"}), 400 valid, error, full_path = terminal.file_manager._validate_path(path) if not valid or full_path is None: return jsonify({"success": False, "error": error or "路径校验失败"}), 400 if not full_path.exists() or not full_path.is_file(): return jsonify({"success": False, "error": "文件不存在"}), 404 return send_file( full_path, as_attachment=True, download_name=full_path.name ) @chat_bp.route('/api/download/folder') @api_login_required @with_terminal def download_folder_api(terminal: WebTerminal, workspace: UserWorkspace, username: str): """打包并下载文件夹""" path = (request.args.get('path') or '').strip() if not path: return jsonify({"success": False, "error": "缺少路径参数"}), 400 valid, error, full_path = terminal.file_manager._validate_path(path) if not valid or full_path is None: return jsonify({"success": False, "error": error or "路径校验失败"}), 400 if not full_path.exists() or not full_path.is_dir(): return jsonify({"success": False, "error": "文件夹不存在"}), 404 buffer = BytesIO() folder_name = Path(path).name or full_path.name or "archive" with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_buffer: # 确保目录本身被包含 zip_buffer.write(full_path, arcname=folder_name + '/') for item in full_path.rglob('*'): relative_name = Path(folder_name) / item.relative_to(full_path) if item.is_dir(): zip_buffer.write(item, arcname=str(relative_name) + '/') else: zip_buffer.write(item, arcname=str(relative_name)) buffer.seek(0) return send_file( buffer, mimetype='application/zip', as_attachment=True, download_name=f"{folder_name}.zip" )