新增趣味指令剧本功能并接入拍一拍事件触发
1. 新增趣味指令规则数据层与服务层,支持应用级缓存+Redis+MySQL三级读取与缓存刷新。 2. 新增 fun_command_play 插件,支持文本/图片/语音/视频/卡片/App 多媒体响应,并接入群权限开关。 3. 新增拍一拍事件识别(PAT)并纳入统一触发模型。 4. 新增后台页面与API:规则增删改查、启停、命中测试。 5. 将趣味指令剧本接入 Dashboard 菜单与蓝图注册,并补充数据库迁移脚本。
This commit is contained in:
194
admin/dashboard/blueprints/fun_command_rules.py
Normal file
194
admin/dashboard/blueprints/fun_command_rules.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""趣味指令规则后台蓝图。"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, render_template, request
|
||||
|
||||
from .auth import login_required
|
||||
|
||||
|
||||
fun_command_rules_bp = Blueprint("fun_command_rules", __name__, url_prefix="/fun_command_rules")
|
||||
|
||||
|
||||
def _normalize_datetime_text(value):
|
||||
"""统一时间字段展示格式。"""
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime("%Y-%m-%d %H:%M:%S")
|
||||
text = str(value)
|
||||
if "T" in text:
|
||||
return text.replace("T", " ")[:19]
|
||||
return text
|
||||
|
||||
|
||||
def _build_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构造并清洗规则载荷。"""
|
||||
return {
|
||||
"rule_name": str(raw.get("rule_name", "") or "").strip(),
|
||||
"scope_type": str(raw.get("scope_type", "global") or "global").strip().lower(),
|
||||
"scope_id": str(raw.get("scope_id", "") or "").strip(),
|
||||
"trigger_type": str(raw.get("trigger_type", "exact") or "exact").strip().lower(),
|
||||
"trigger_text": str(raw.get("trigger_text", "") or "").strip(),
|
||||
"event_key": str(raw.get("event_key", "") or "").strip().upper(),
|
||||
"responses_json": raw.get("responses_json", []),
|
||||
"priority": int(raw.get("priority", 100) or 100),
|
||||
"cooldown_seconds": int(raw.get("cooldown_seconds", 0) or 0),
|
||||
"enabled": bool(raw.get("enabled", True)),
|
||||
"updated_by": str(raw.get("updated_by", "dashboard") or "dashboard").strip() or "dashboard",
|
||||
}
|
||||
|
||||
|
||||
def _validate_payload(payload: Dict[str, Any], service) -> str:
|
||||
"""校验规则数据,返回空字符串表示通过。"""
|
||||
if not payload["rule_name"]:
|
||||
return "rule_name 不能为空"
|
||||
|
||||
if payload["scope_type"] not in {"global", "group", "private"}:
|
||||
return "scope_type 仅支持 global/group/private"
|
||||
|
||||
# group/private 必须提供 scope_id,防止误配为全量匹配。
|
||||
if payload["scope_type"] in {"group", "private"} and not payload["scope_id"]:
|
||||
return "group/private 作用域必须填写 scope_id"
|
||||
|
||||
if payload["trigger_type"] not in {"exact", "prefix", "contains", "regex", "event"}:
|
||||
return "trigger_type 仅支持 exact/prefix/contains/regex/event"
|
||||
|
||||
if payload["trigger_type"] == "event":
|
||||
if not payload["event_key"]:
|
||||
return "event 触发时 event_key 不能为空"
|
||||
else:
|
||||
if not payload["trigger_text"]:
|
||||
return "文本触发时 trigger_text 不能为空"
|
||||
|
||||
ok, msg, normalized = service.validate_responses(payload.get("responses_json"))
|
||||
if not ok:
|
||||
return msg
|
||||
payload["responses_json"] = normalized
|
||||
return ""
|
||||
|
||||
|
||||
@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/list", methods=["GET"])
|
||||
@login_required
|
||||
def api_list_rules():
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
scope_type = str(request.args.get("scope_type", "") or "").strip().lower()
|
||||
scope_id = str(request.args.get("scope_id", "") or "").strip()
|
||||
|
||||
enabled_raw = str(request.args.get("enabled", "") or "").strip().lower()
|
||||
enabled = None
|
||||
if enabled_raw in {"0", "1", "true", "false"}:
|
||||
enabled = enabled_raw in {"1", "true"}
|
||||
|
||||
rows = service.list_rules(scope_type=scope_type, scope_id=scope_id, enabled=enabled)
|
||||
for row in rows:
|
||||
row["created_at"] = _normalize_datetime_text(row.get("created_at"))
|
||||
row["updated_at"] = _normalize_datetime_text(row.get("updated_at"))
|
||||
|
||||
return jsonify({"success": True, "data": rows})
|
||||
|
||||
|
||||
@fun_command_rules_bp.route("/api/create", methods=["POST"])
|
||||
@login_required
|
||||
def api_create_rule():
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
raw = request.get_json(silent=True) or {}
|
||||
payload = _build_payload(raw)
|
||||
error_text = _validate_payload(payload, service)
|
||||
if error_text:
|
||||
return jsonify({"success": False, "message": error_text}), 400
|
||||
|
||||
ok = service.create_rule(payload)
|
||||
if not ok:
|
||||
return jsonify({"success": False, "message": "创建失败"}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "创建成功"})
|
||||
|
||||
|
||||
@fun_command_rules_bp.route("/api/update/<int:rule_id>", methods=["POST"])
|
||||
@login_required
|
||||
def api_update_rule(rule_id: int):
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
raw = request.get_json(silent=True) or {}
|
||||
payload = _build_payload(raw)
|
||||
error_text = _validate_payload(payload, service)
|
||||
if error_text:
|
||||
return jsonify({"success": False, "message": error_text}), 400
|
||||
|
||||
ok = service.update_rule(rule_id=rule_id, payload=payload)
|
||||
if not ok:
|
||||
return jsonify({"success": False, "message": "更新失败"}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "更新成功"})
|
||||
|
||||
|
||||
@fun_command_rules_bp.route("/api/delete/<int:rule_id>", methods=["POST"])
|
||||
@login_required
|
||||
def api_delete_rule(rule_id: int):
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
ok = service.delete_rule(rule_id=rule_id)
|
||||
if not ok:
|
||||
return jsonify({"success": False, "message": "删除失败"}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "删除成功"})
|
||||
|
||||
|
||||
@fun_command_rules_bp.route("/api/toggle/<int:rule_id>", methods=["POST"])
|
||||
@login_required
|
||||
def api_toggle_rule(rule_id: int):
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
raw = request.get_json(silent=True) or {}
|
||||
enabled = bool(raw.get("enabled", True))
|
||||
updated_by = str(raw.get("updated_by", "dashboard") or "dashboard").strip() or "dashboard"
|
||||
|
||||
ok = service.toggle_rule(rule_id=rule_id, enabled=enabled, updated_by=updated_by)
|
||||
if not ok:
|
||||
return jsonify({"success": False, "message": "切换失败"}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "状态已更新"})
|
||||
|
||||
|
||||
@fun_command_rules_bp.route("/api/test_match", methods=["POST"])
|
||||
@login_required
|
||||
def api_test_match():
|
||||
"""提供后台测试入口,便于快速验证规则命中结果。"""
|
||||
server = current_app.dashboard_server
|
||||
service = server.fun_command_rule_service
|
||||
|
||||
raw = request.get_json(silent=True) or {}
|
||||
scope_type = str(raw.get("scope_type", "group") or "group").strip().lower()
|
||||
scope_id = str(raw.get("scope_id", "") or "").strip()
|
||||
content = str(raw.get("content", "") or "").strip()
|
||||
event_key = str(raw.get("event_key", "") or "").strip().upper()
|
||||
|
||||
session_key = scope_id or "test-session"
|
||||
matched = service.match_rule(
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
content=content,
|
||||
event_key=event_key,
|
||||
session_key=session_key,
|
||||
)
|
||||
|
||||
if not matched:
|
||||
return jsonify({"success": True, "matched": False, "data": None})
|
||||
|
||||
return jsonify({"success": True, "matched": True, "data": matched})
|
||||
Reference in New Issue
Block a user