diff --git a/admin/dashboard/blueprints/fun_command_rules.py b/admin/dashboard/blueprints/fun_command_rules.py index 7fa80eb..9cfa0be 100644 --- a/admin/dashboard/blueprints/fun_command_rules.py +++ b/admin/dashboard/blueprints/fun_command_rules.py @@ -1,16 +1,27 @@ # -*- coding: utf-8 -*- """趣味指令规则后台蓝图。""" +import os +import uuid from datetime import datetime from typing import Any, Dict from flask import Blueprint, current_app, jsonify, render_template, request +from werkzeug.utils import secure_filename from .auth import login_required fun_command_rules_bp = Blueprint("fun_command_rules", __name__, url_prefix="/fun_command_rules") +# 媒体上传白名单: +# 这里仅允许趣味指令管理功能需要的媒体类型,避免上传任意可执行文件带来风险。 +ALLOWED_EXTENSIONS = { + "image": {"png", "jpg", "jpeg", "gif", "webp"}, + "voice": {"mp3", "wav", "amr"}, + "video": {"mp4", "mov", "m4v"}, +} + def _normalize_datetime_text(value): """统一时间字段展示格式。""" @@ -70,12 +81,63 @@ def _validate_payload(payload: Dict[str, Any], service) -> str: return "" +def _allowed_file(filename: str, media_type: str) -> bool: + """判断上传文件扩展名是否允许。""" + if "." not in filename: + return False + ext = filename.rsplit(".", 1)[1].lower() + return ext in ALLOWED_EXTENSIONS.get(media_type, set()) + + @fun_command_rules_bp.route("/") @login_required def page_fun_command_rules(): return render_template("fun_command_rules.html") +@fun_command_rules_bp.route("/api/upload", methods=["POST"]) +@login_required +def api_upload_media(): + """上传媒体文件并返回服务端绝对路径。 + + 设计说明: + 1. 使用独立目录 static/uploads/fun_command_rules,与其它业务上传隔离。 + 2. 返回绝对路径,直接可用于插件发送媒体消息。 + """ + if "file" not in request.files: + return jsonify({"success": False, "message": "未检测到上传文件"}), 400 + + media_type = str(request.form.get("media_type", "") or "").strip().lower() + if media_type not in ALLOWED_EXTENSIONS: + return jsonify({"success": False, "message": "media_type 非法,仅支持 image/voice/video"}), 400 + + upload_file = request.files.get("file") + if not upload_file or not upload_file.filename: + return jsonify({"success": False, "message": "文件名为空"}), 400 + + filename = secure_filename(upload_file.filename) + if not _allowed_file(filename, media_type): + return jsonify({"success": False, "message": f"不支持的文件类型: {filename}"}), 400 + + # 按媒体类型分目录,便于后续清理和排查。 + upload_root = os.path.join(current_app.root_path, "static", "uploads", "fun_command_rules", media_type) + os.makedirs(upload_root, exist_ok=True) + + unique_filename = f"{uuid.uuid4().hex}_{filename}" + abs_path = os.path.abspath(os.path.join(upload_root, unique_filename)) + upload_file.save(abs_path) + + return jsonify({ + "success": True, + "message": "上传成功", + "data": { + "path": abs_path, + "filename": unique_filename, + "media_type": media_type, + } + }) + + @fun_command_rules_bp.route("/api/list", methods=["GET"]) @login_required def api_list_rules(): diff --git a/admin/dashboard/templates/fun_command_rules.html b/admin/dashboard/templates/fun_command_rules.html index c958a4e..ae72521 100644 --- a/admin/dashboard/templates/fun_command_rules.html +++ b/admin/dashboard/templates/fun_command_rules.html @@ -200,6 +200,14 @@