新增趣味指令剧本功能并接入拍一拍事件触发

1. 新增趣味指令规则数据层与服务层,支持应用级缓存+Redis+MySQL三级读取与缓存刷新。

2. 新增 fun_command_play 插件,支持文本/图片/语音/视频/卡片/App 多媒体响应,并接入群权限开关。

3. 新增拍一拍事件识别(PAT)并纳入统一触发模型。

4. 新增后台页面与API:规则增删改查、启停、命中测试。

5. 将趣味指令剧本接入 Dashboard 菜单与蓝图注册,并补充数据库迁移脚本。
This commit is contained in:
liuwei
2026-04-23 12:31:52 +08:00
parent b1f435c8ff
commit d61fb8bc8a
10 changed files with 1570 additions and 0 deletions

215
db/fun_command_rule_db.py Normal file
View File

@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
"""趣味指令规则数据库操作层。
这里专门封装“趣味指令剧本”相关的 MySQL 读写逻辑,
避免插件层直接拼 SQL后续扩展字段也更安全。
"""
import json
from typing import Any, Dict, List, Optional
from loguru import logger
from db.base import BaseDBOperator
from db.connection import DBConnectionManager
class FunCommandRuleDBOperator(BaseDBOperator):
"""趣味指令规则数据访问对象。"""
def __init__(self, db_manager: DBConnectionManager):
super().__init__(db_manager)
def init_tables(self) -> bool:
"""初始化趣味指令规则表。
说明:
1. responses_json 使用 JSON 字段存储多条响应动作,便于前端以数组方式编排。
2. scope_type + scope_id 用于做“全局/群聊/私聊”多作用域控制。
3. trigger_type + trigger_text/event_key 支持关键词与事件(如拍一拍)混合触发。
"""
try:
return self.execute_update(
"""
CREATE TABLE IF NOT EXISTS t_fun_command_rule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
rule_name VARCHAR(128) NOT NULL,
scope_type VARCHAR(20) NOT NULL DEFAULT 'global',
scope_id VARCHAR(100) NOT NULL DEFAULT '',
trigger_type VARCHAR(20) NOT NULL DEFAULT 'exact',
trigger_text VARCHAR(500) NOT NULL DEFAULT '',
event_key VARCHAR(64) NOT NULL DEFAULT '',
responses_json JSON NOT NULL,
priority INT NOT NULL DEFAULT 100,
cooldown_seconds INT NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 1,
updated_by VARCHAR(100) NOT NULL DEFAULT 'system',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_scope_enabled_priority (scope_type, scope_id, enabled, priority),
INDEX idx_trigger_type (trigger_type),
INDEX idx_event_key (event_key)
)
"""
)
except Exception as e:
logger.error(f"初始化趣味指令规则表失败: {e}")
return False
@staticmethod
def _normalize_row(row: Dict[str, Any]) -> Dict[str, Any]:
"""统一处理数据库行数据。
这里把 JSON 字段解析为 Python 对象,且对关键字段做兜底,
防止旧数据/脏数据导致插件执行阶段崩溃。
"""
if not row:
return {}
responses_value = row.get("responses_json")
if isinstance(responses_value, str):
try:
row["responses_json"] = json.loads(responses_value)
except Exception:
row["responses_json"] = []
elif responses_value is None:
row["responses_json"] = []
if not isinstance(row.get("responses_json"), list):
row["responses_json"] = []
row["enabled"] = bool(row.get("enabled", 0))
row["priority"] = int(row.get("priority", 100) or 100)
row["cooldown_seconds"] = int(row.get("cooldown_seconds", 0) or 0)
row["scope_type"] = str(row.get("scope_type", "global") or "global")
row["scope_id"] = str(row.get("scope_id", "") or "")
row["trigger_type"] = str(row.get("trigger_type", "exact") or "exact")
row["trigger_text"] = str(row.get("trigger_text", "") or "")
row["event_key"] = str(row.get("event_key", "") or "")
return row
def list_rules(self, scope_type: str = "", scope_id: str = "", enabled: Optional[bool] = None) -> List[Dict[str, Any]]:
"""按条件查询规则列表。"""
where_sql = []
params: List[Any] = []
if scope_type:
where_sql.append("scope_type = %s")
params.append(scope_type)
if scope_id:
where_sql.append("scope_id = %s")
params.append(scope_id)
if enabled is not None:
where_sql.append("enabled = %s")
params.append(1 if enabled else 0)
where_clause = f"WHERE {' AND '.join(where_sql)}" if where_sql else ""
rows = self.execute_query(
f"""
SELECT *
FROM t_fun_command_rule
{where_clause}
ORDER BY priority ASC, id DESC
""",
tuple(params) if params else None,
) or []
return [self._normalize_row(dict(row)) for row in rows]
def get_rule(self, rule_id: int) -> Optional[Dict[str, Any]]:
"""按主键获取单条规则。"""
row = self.execute_query(
"""
SELECT *
FROM t_fun_command_rule
WHERE id = %s
LIMIT 1
""",
(int(rule_id),),
fetch_one=True,
)
if not row:
return None
return self._normalize_row(dict(row))
def create_rule(self, payload: Dict[str, Any]) -> bool:
"""创建规则。"""
return self.execute_update(
"""
INSERT INTO t_fun_command_rule (
rule_name, scope_type, scope_id,
trigger_type, trigger_text, event_key,
responses_json, priority, cooldown_seconds,
enabled, updated_by
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
str(payload.get("rule_name", "") or "").strip(),
str(payload.get("scope_type", "global") or "global").strip(),
str(payload.get("scope_id", "") or "").strip(),
str(payload.get("trigger_type", "exact") or "exact").strip(),
str(payload.get("trigger_text", "") or "").strip(),
str(payload.get("event_key", "") or "").strip(),
json.dumps(payload.get("responses_json") or [], ensure_ascii=False),
int(payload.get("priority", 100) or 100),
int(payload.get("cooldown_seconds", 0) or 0),
1 if bool(payload.get("enabled", True)) else 0,
str(payload.get("updated_by", "system") or "system").strip(),
),
)
def update_rule(self, rule_id: int, payload: Dict[str, Any]) -> bool:
"""更新规则。"""
return self.execute_update(
"""
UPDATE t_fun_command_rule
SET
rule_name = %s,
scope_type = %s,
scope_id = %s,
trigger_type = %s,
trigger_text = %s,
event_key = %s,
responses_json = %s,
priority = %s,
cooldown_seconds = %s,
enabled = %s,
updated_by = %s
WHERE id = %s
""",
(
str(payload.get("rule_name", "") or "").strip(),
str(payload.get("scope_type", "global") or "global").strip(),
str(payload.get("scope_id", "") or "").strip(),
str(payload.get("trigger_type", "exact") or "exact").strip(),
str(payload.get("trigger_text", "") or "").strip(),
str(payload.get("event_key", "") or "").strip(),
json.dumps(payload.get("responses_json") or [], ensure_ascii=False),
int(payload.get("priority", 100) or 100),
int(payload.get("cooldown_seconds", 0) or 0),
1 if bool(payload.get("enabled", True)) else 0,
str(payload.get("updated_by", "system") or "system").strip(),
int(rule_id),
),
)
def delete_rule(self, rule_id: int) -> bool:
"""删除规则。"""
return self.execute_update(
"""
DELETE FROM t_fun_command_rule
WHERE id = %s
""",
(int(rule_id),),
)
def toggle_rule(self, rule_id: int, enabled: bool, updated_by: str = "system") -> bool:
"""快速切换规则启停状态。"""
return self.execute_update(
"""
UPDATE t_fun_command_rule
SET enabled = %s, updated_by = %s
WHERE id = %s
""",
(1 if enabled else 0, str(updated_by or "system"), int(rule_id)),
)

View File

@@ -0,0 +1,21 @@
-- 趣味指令剧本规则表
-- 说明:用于配置“文本/事件触发 -> 多媒体响应”玩法规则。
CREATE TABLE IF NOT EXISTS t_fun_command_rule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
rule_name VARCHAR(128) NOT NULL,
scope_type VARCHAR(20) NOT NULL DEFAULT 'global',
scope_id VARCHAR(100) NOT NULL DEFAULT '',
trigger_type VARCHAR(20) NOT NULL DEFAULT 'exact',
trigger_text VARCHAR(500) NOT NULL DEFAULT '',
event_key VARCHAR(64) NOT NULL DEFAULT '',
responses_json JSON NOT NULL,
priority INT NOT NULL DEFAULT 100,
cooldown_seconds INT NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 1,
updated_by VARCHAR(100) NOT NULL DEFAULT 'system',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_scope_enabled_priority (scope_type, scope_id, enabled, priority),
INDEX idx_trigger_type (trigger_type),
INDEX idx_event_key (event_key)
);