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

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

View 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})

View File

@@ -16,6 +16,8 @@ from db.member_context_db import MemberContextDBOperator
from db.message_storage import MessageStorageDB
from db.stats_db import StatsDBOperator
from db.task_db import TaskDBOperator
from db.fun_command_rule_db import FunCommandRuleDBOperator
from utils.fun_command_rule_service import FunCommandRuleService
from wechat_ipad import WechatAPIClient
# 添加项目根目录到系统路径,确保可以导入项目模块
@@ -56,6 +58,16 @@ class DashboardServer:
self.group_plugin_config_db = robot_instance.group_plugin_config_db
self.llm_catalog_db = robot_instance.llm_catalog_db
self.group_plugin_config_service = robot_instance.group_plugin_config_service
# 趣味指令规则服务:用于“文案/事件触发多媒体玩法回复”后台配置与缓存。
# 这里统一在 Dashboard 启动时初始化,保证管理端可直接读写规则。
self.fun_command_rule_db = FunCommandRuleDBOperator(self.db_manager)
self.fun_command_rule_service = FunCommandRuleService(
db_operator=self.fun_command_rule_db,
redis_client=self.db_manager.get_redis_connection(),
local_ttl_seconds=30,
)
self.fun_command_rule_service.init_tables()
self.fun_command_rule_service.refresh_cache()
# 获取联系人管理器实例
self.contact_manager = robot_instance.contact_manager
self.plugin_manager = robot_instance.plugin_manager
@@ -175,6 +187,7 @@ class DashboardServer:
from admin.dashboard.blueprints.system_jobs import system_jobs_bp
from admin.dashboard.blueprints.plugin_schedules import plugin_schedules_bp
from admin.dashboard.blueprints.group_plugin_config import group_plugin_config_bp
from admin.dashboard.blueprints.fun_command_rules import fun_command_rules_bp
from admin.dashboard.blueprints.trendradar_webhook import trendradar_webhook_bp
# 在app.register_blueprint部分添加
@@ -193,6 +206,7 @@ class DashboardServer:
app.register_blueprint(system_jobs_bp)
app.register_blueprint(plugin_schedules_bp)
app.register_blueprint(group_plugin_config_bp)
app.register_blueprint(fun_command_rules_bp)
app.register_blueprint(trendradar_webhook_bp)
self.LOG.info("所有蓝图已注册")

View File

@@ -1011,6 +1011,7 @@
{ label: '插件管理', path: '/plugins_manage' },
{ label: '插件定时任务', path: '/plugin_schedules' },
{ label: '群级插件配置', path: '/group_plugin_config' },
{ label: '趣味指令剧本', path: '/fun_command_rules' },
{ label: '接口文档', path: '/api_docs' }
]
},

View File

@@ -0,0 +1,504 @@
{% extends "base.html" %}
{% block title %}趣味指令剧本 - 机器人管理后台{% endblock %}
{% block content %}
<div class="page-shell">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Fun Script Rules</div>
<h1>趣味指令剧本</h1>
<p>把“文本关键词/拍一拍事件”映射成可编排的多媒体回应,持续沉淀你的机器人玩法库。</p>
</div>
<div class="page-hero-actions">
<el-button type="success" @click="loadRows">刷新</el-button>
<el-button type="primary" @click="openCreate">新增规则</el-button>
</div>
</div>
<el-card shadow="hover" style="margin-bottom: 14px;">
<el-form inline>
<el-form-item label="作用域">
<el-select v-model="filters.scope_type" clearable placeholder="全部" style="width:160px" @change="loadRows">
<el-option label="全局" value="global"></el-option>
<el-option label="群聊" value="group"></el-option>
<el-option label="私聊" value="private"></el-option>
</el-select>
</el-form-item>
<el-form-item label="启用状态">
<el-select v-model="filters.enabled" clearable placeholder="全部" style="width:150px" @change="loadRows">
<el-option label="启用" value="1"></el-option>
<el-option label="停用" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="作用域ID">
<el-input v-model="filters.scope_id" placeholder="群ID/用户ID" style="width:260px" @keyup.enter.native="loadRows"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" plain @click="loadRows">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="hover">
<el-table :data="rows" style="width:100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="rule_name" label="规则名" min-width="150"></el-table-column>
<el-table-column label="作用域" width="130">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.scope_type === 'global' ? 'info' : 'success'">{% raw %}{{ scopeLabel(scope.row.scope_type) }}{% endraw %}</el-tag>
</template>
</el-table-column>
<el-table-column prop="scope_id" label="作用域ID" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="触发" min-width="210">
<template slot-scope="scope">
<div class="trigger-box">
<div class="trigger-type">{% raw %}{{ scope.row.trigger_type }}{% endraw %}</div>
<div class="trigger-text">{% raw %}{{ scope.row.trigger_type === 'event' ? scope.row.event_key : scope.row.trigger_text }}{% endraw %}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="90"></el-table-column>
<el-table-column prop="cooldown_seconds" label="冷却(s)" width="90"></el-table-column>
<el-table-column label="启用" width="90">
<template slot-scope="scope">
<el-switch :value="!!scope.row.enabled" @change="toggleEnabled(scope.row, $event)"></el-switch>
</template>
</el-table-column>
<el-table-column label="响应数" width="90">
<template slot-scope="scope">
{% raw %}{{ (scope.row.responses_json || []).length }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="180"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="openEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" plain @click="removeRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="hover" style="margin-top:14px;">
<div slot="header"><strong>规则测试</strong></div>
<el-form inline>
<el-form-item label="作用域">
<el-select v-model="tester.scope_type" style="width:120px">
<el-option label="群聊" value="group"></el-option>
<el-option label="私聊" value="private"></el-option>
<el-option label="全局" value="global"></el-option>
</el-select>
</el-form-item>
<el-form-item label="作用域ID">
<el-input v-model="tester.scope_id" placeholder="群ID/用户ID" style="width:220px"></el-input>
</el-form-item>
<el-form-item label="事件">
<el-select v-model="tester.event_key" clearable placeholder="无" style="width:140px">
<el-option label="PAT(拍一拍)" value="PAT"></el-option>
</el-select>
</el-form-item>
<el-form-item label="内容">
<el-input v-model="tester.content" placeholder="测试文案" style="width:260px"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="testMatch">测试命中</el-button>
</el-form-item>
</el-form>
<el-alert
v-if="testResult"
:type="testResult.matched ? 'success' : 'info'"
:title="testResult.matched ? ('命中规则:#' + testResult.data.id + ' ' + testResult.data.rule_name) : '未命中规则'"
:closable="false">
</el-alert>
</el-card>
<el-dialog :title="editing ? '编辑规则' : '新增规则'" :visible.sync="dialogVisible" width="860px">
<el-form label-width="110px">
<el-form-item label="规则名称">
<el-input v-model="form.rule_name" placeholder="例如:开场梗回复"></el-input>
</el-form-item>
<el-form-item label="作用域类型">
<el-select v-model="form.scope_type" style="width:180px">
<el-option label="全局" value="global"></el-option>
<el-option label="群聊" value="group"></el-option>
<el-option label="私聊" value="private"></el-option>
</el-select>
</el-form-item>
<el-form-item label="作用域ID" v-if="form.scope_type !== 'global'">
<el-select v-if="form.scope_type === 'group'" v-model="form.scope_id" filterable placeholder="请选择群" style="width:100%">
<el-option v-for="item in groupOptions" :key="item.wxid" :label="item.name" :value="item.wxid"></el-option>
</el-select>
<el-input v-else v-model="form.scope_id" placeholder="私聊用户 wxid"></el-input>
</el-form-item>
<el-form-item label="触发类型">
<el-select v-model="form.trigger_type" style="width:200px">
<el-option label="精确匹配" value="exact"></el-option>
<el-option label="前缀匹配" value="prefix"></el-option>
<el-option label="包含匹配" value="contains"></el-option>
<el-option label="正则匹配" value="regex"></el-option>
<el-option label="事件触发" value="event"></el-option>
</el-select>
</el-form-item>
<el-form-item label="触发文本" v-if="form.trigger_type !== 'event'">
<el-input v-model="form.trigger_text" placeholder="例如:早安"></el-input>
</el-form-item>
<el-form-item label="事件键" v-else>
<el-select v-model="form.event_key" style="width:200px">
<el-option label="PAT(拍一拍)" value="PAT"></el-option>
</el-select>
</el-form-item>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="优先级">
<el-input-number v-model="form.priority" :min="1" :max="10000"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="冷却秒数">
<el-input-number v-model="form.cooldown_seconds" :min="0" :max="86400"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="启用">
<el-switch v-model="form.enabled"></el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="响应动作">
<div class="action-toolbar">
<el-button size="mini" type="primary" plain @click="addAction">新增动作</el-button>
<el-button size="mini" @click="formatActionsJson">格式化JSON</el-button>
</div>
<el-table :data="form.responses_json" size="mini" border style="width:100%">
<el-table-column label="#" width="60">
<template slot-scope="scope">{% raw %}{{ scope.$index + 1 }}{% endraw %}</template>
</el-table-column>
<el-table-column label="类型" width="140">
<template slot-scope="scope">
<el-select v-model="scope.row.type" style="width:120px">
<el-option label="文本" value="text"></el-option>
<el-option label="图片" value="image"></el-option>
<el-option label="语音" value="voice"></el-option>
<el-option label="视频" value="video"></el-option>
<el-option label="卡片" value="link"></el-option>
<el-option label="App" value="app"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="延迟(ms)" width="120">
<template slot-scope="scope">
<el-input-number v-model="scope.row.delay_ms" :min="0" :max="60000" :step="100"></el-input-number>
</template>
</el-table-column>
<el-table-column label="动作内容(JSON)" min-width="360">
<template slot-scope="scope">
<el-input type="textarea" :rows="3" v-model="scope.row.payload_text" placeholder='示例: {"text":"你好"}'></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template slot-scope="scope">
<el-button size="mini" type="danger" plain @click="removeAction(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="payload-tip">
支持占位符:<code>{sender}</code> <code>{roomid}</code> <code>{event}</code>
常见示例:
<code>text -> {"text":"你拍了拍我,我就拍回去~"}</code>
<code>link -> {"title":"今日梗图","desc":"点开看","url":"https://example.com","thumb_url":""}</code>
</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible=false">取消</el-button>
<el-button type="primary" @click="saveForm">保存</el-button>
</div>
</el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
loading: false,
rows: [],
groupOptions: [],
filters: { scope_type: '', scope_id: '', enabled: '' },
dialogVisible: false,
editing: false,
editingRuleId: 0,
testResult: null,
tester: {
scope_type: 'group',
scope_id: '',
event_key: '',
content: ''
},
form: {
rule_name: '',
scope_type: 'global',
scope_id: '',
trigger_type: 'exact',
trigger_text: '',
event_key: '',
priority: 100,
cooldown_seconds: 0,
enabled: true,
responses_json: []
}
}
},
mounted() {
this.loadGroups()
this.loadRows()
},
methods: {
scopeLabel(scopeType) {
const map = { global: '全局', group: '群聊', private: '私聊' }
return map[scopeType] || scopeType
},
defaultPayloadTextByType(type) {
const table = {
text: '{"text":""}',
image: '{"path":"D:/learn/abot/static/uploads/demo.jpg"}',
voice: '{"path":"D:/learn/abot/static/uploads/demo.mp3","format":"mp3"}',
video: '{"path":"D:/learn/abot/static/uploads/demo.mp4"}',
link: '{"title":"","desc":"","url":"https://example.com","thumb_url":""}',
app: '{"xml":"<appmsg></appmsg>","app_type":0}'
}
return table[type] || '{"text":""}'
},
addAction() {
this.form.responses_json.push({
type: 'text',
delay_ms: 0,
payload_text: this.defaultPayloadTextByType('text')
})
},
removeAction(index) {
this.form.responses_json.splice(index, 1)
},
formatActionsJson() {
this.form.responses_json = (this.form.responses_json || []).map(item => {
let payloadObj = {}
try {
payloadObj = JSON.parse(item.payload_text || '{}')
} catch (e) {
payloadObj = {}
}
return {
...item,
payload_text: JSON.stringify(payloadObj, null, 2)
}
})
this.$message.success('动作 JSON 已格式化')
},
normalizeActionsForSubmit() {
const actions = []
for (const item of (this.form.responses_json || [])) {
if (!item || !item.type) continue
let payloadObj = {}
try {
payloadObj = JSON.parse(item.payload_text || '{}')
} catch (e) {
throw new Error(`动作 payload JSON 格式错误: ${item.payload_text || ''}`)
}
const action = {
type: String(item.type || '').toLowerCase(),
delay_ms: Number(item.delay_ms || 0)
}
Object.assign(action, payloadObj)
actions.push(action)
}
if (!actions.length) {
throw new Error('至少配置一条响应动作')
}
return actions
},
mapActionsForEdit(actions) {
return (actions || []).map(action => {
const item = { ...action }
const type = String(item.type || 'text').toLowerCase()
const delayMs = Number(item.delay_ms || 0)
delete item.type
delete item.delay_ms
return {
type,
delay_ms: delayMs,
payload_text: JSON.stringify(item, null, 2)
}
})
},
async loadGroups() {
const resp = await axios.get('/contacts/api/groups')
const groups = (resp.data && resp.data.data && resp.data.data.groups) || {}
this.groupOptions = Object.entries(groups).map(([wxid, name]) => ({ wxid, name: String(name || wxid) }))
},
async loadRows() {
this.loading = true
try {
const resp = await axios.get('/fun_command_rules/api/list', { params: this.filters })
if (resp.data && resp.data.success) {
this.rows = resp.data.data || []
}
} finally {
this.loading = false
}
},
openCreate() {
this.editing = false
this.editingRuleId = 0
this.form = {
rule_name: '',
scope_type: 'global',
scope_id: '',
trigger_type: 'exact',
trigger_text: '',
event_key: '',
priority: 100,
cooldown_seconds: 0,
enabled: true,
responses_json: []
}
this.addAction()
this.dialogVisible = true
},
openEdit(row) {
this.editing = true
this.editingRuleId = row.id
this.form = {
rule_name: row.rule_name || '',
scope_type: row.scope_type || 'global',
scope_id: row.scope_id || '',
trigger_type: row.trigger_type || 'exact',
trigger_text: row.trigger_text || '',
event_key: row.event_key || '',
priority: Number(row.priority || 100),
cooldown_seconds: Number(row.cooldown_seconds || 0),
enabled: !!row.enabled,
responses_json: this.mapActionsForEdit(row.responses_json || [])
}
if (!this.form.responses_json.length) {
this.addAction()
}
this.dialogVisible = true
},
async saveForm() {
let actions = []
try {
actions = this.normalizeActionsForSubmit()
} catch (e) {
this.$message.error(e.message || '响应动作格式错误')
return
}
const payload = {
...this.form,
responses_json: actions,
updated_by: 'dashboard'
}
if (payload.trigger_type === 'event') {
payload.trigger_text = ''
} else {
payload.event_key = ''
}
const url = this.editing
? `/fun_command_rules/api/update/${this.editingRuleId}`
: '/fun_command_rules/api/create'
try {
const resp = await axios.post(url, payload)
if (resp.data && resp.data.success) {
this.$message.success(resp.data.message || '保存成功')
this.dialogVisible = false
await this.loadRows()
return
}
this.$message.error((resp.data && resp.data.message) || '保存失败')
} catch (error) {
const msg = (error.response && error.response.data && error.response.data.message) || '保存失败'
this.$message.error(msg)
}
},
async removeRow(row) {
try {
await this.$confirm('确认删除该规则吗?', '提示', { type: 'warning' })
} catch (e) {
return
}
try {
const resp = await axios.post(`/fun_command_rules/api/delete/${row.id}`)
if (resp.data && resp.data.success) {
this.$message.success(resp.data.message || '删除成功')
await this.loadRows()
return
}
this.$message.error((resp.data && resp.data.message) || '删除失败')
} catch (error) {
const msg = (error.response && error.response.data && error.response.data.message) || '删除失败'
this.$message.error(msg)
}
},
async toggleEnabled(row, enabled) {
try {
const resp = await axios.post(`/fun_command_rules/api/toggle/${row.id}`, {
enabled: !!enabled,
updated_by: 'dashboard'
})
if (resp.data && resp.data.success) {
this.$message.success('状态已更新')
row.enabled = !!enabled
return
}
this.$message.error((resp.data && resp.data.message) || '状态更新失败')
} catch (error) {
const msg = (error.response && error.response.data && error.response.data.message) || '状态更新失败'
this.$message.error(msg)
}
row.enabled = !enabled
},
async testMatch() {
try {
const resp = await axios.post('/fun_command_rules/api/test_match', this.tester)
if (resp.data && resp.data.success) {
this.testResult = {
matched: !!resp.data.matched,
data: resp.data.data || null
}
return
}
this.$message.error((resp.data && resp.data.message) || '测试失败')
} catch (error) {
const msg = (error.response && error.response.data && error.response.data.message) || '测试失败'
this.$message.error(msg)
}
}
}
})
</script>
{% endblock %}
{% block styles %}
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}
.page-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(12,148,93,.10), rgba(45,212,191,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}
.page-hero-actions{display:flex;align-items:center;gap:12px}
.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#0f766e;font-weight:700;margin-bottom:8px}
.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}
.page-hero-copy p{color:#64748b;font-size:14px}
.trigger-box{display:flex;flex-direction:column;gap:4px}
.trigger-type{font-size:12px;color:#0f766e;font-weight:600}
.trigger-text{font-size:13px;color:#334155;word-break:break-all}
.action-toolbar{display:flex;gap:8px;margin-bottom:8px}
.payload-tip{margin-top:8px;color:#64748b;font-size:12px;line-height:1.7}
.payload-tip code{background:#f1f5f9;border:1px solid #dbe3ee;padding:1px 6px;border-radius:6px;margin-right:6px}
</style>
{% endblock %}