diff --git a/admin/dashboard/templates/base.html b/admin/dashboard/templates/base.html index c7bdf9d..95a8754 100644 --- a/admin/dashboard/templates/base.html +++ b/admin/dashboard/templates/base.html @@ -1011,7 +1011,7 @@ { label: '插件管理', path: '/plugins_manage' }, { label: '插件定时任务', path: '/plugin_schedules' }, { label: '群级插件配置', path: '/group_plugin_config' }, - { label: '趣味指令剧本', path: '/fun_command_rules' }, + { label: '响应指令管理', path: '/fun_command_rules' }, { label: '接口文档', path: '/api_docs' } ] }, diff --git a/admin/dashboard/templates/fun_command_rules.html b/admin/dashboard/templates/fun_command_rules.html index 50618a5..c958a4e 100644 --- a/admin/dashboard/templates/fun_command_rules.html +++ b/admin/dashboard/templates/fun_command_rules.html @@ -1,14 +1,14 @@ {% extends "base.html" %} -{% block title %}趣味指令剧本 - 机器人管理后台{% endblock %} +{% block title %}响应指令管理 - 机器人管理后台{% endblock %} {% block content %}
-
Fun Script Rules
-

趣味指令剧本

-

把“文本关键词/拍一拍事件”映射成可编排的多媒体回应,持续沉淀你的机器人玩法库。

+
Response Command Center
+

响应指令管理

+

把“文本关键词/拍一拍事件”映射成可编排的多媒体回应,用 UI 维护玩法,不再依赖手写 JSON。

刷新 @@ -113,10 +113,10 @@ - + - + @@ -132,7 +132,7 @@ - + @@ -169,15 +169,14 @@
新增动作 - 格式化JSON
- + - + @@ -205,9 +238,7 @@
支持占位符:{sender} {roomid} {event}。 - 常见示例: - text -> {"text":"你拍了拍我,我就拍回去~"} - link -> {"title":"今日梗图","desc":"点开看","url":"https://example.com","thumb_url":""} + 示例:文本里可写 你拍了拍我:{sender}
@@ -263,77 +294,139 @@ new Vue({ 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":"","app_type":0}' + // 创建结构化动作对象:所有字段都明确成表单项,避免 JSON 字符串编辑。 + createActionRow(type = 'text') { + const base = { + type, + delay_ms: 0, + text: '', + path: '', + format: 'mp3', + cover_path: '', + title: '', + desc: '', + url: '', + thumb_url: '', + xml: '', + app_type: 0 } - return table[type] || '{"text":""}' + + if (type === 'text') { + base.text = '' + } else if (type === 'image') { + base.path = 'D:/learn/abot/static/uploads/demo.jpg' + } else if (type === 'voice') { + base.path = 'D:/learn/abot/static/uploads/demo.mp3' + base.format = 'mp3' + } else if (type === 'video') { + base.path = 'D:/learn/abot/static/uploads/demo.mp4' + } else if (type === 'link') { + base.url = 'https://example.com' + } else if (type === 'app') { + base.xml = '' + base.app_type = 0 + } + return base }, addAction() { - this.form.responses_json.push({ - type: 'text', - delay_ms: 0, - payload_text: this.defaultPayloadTextByType('text') - }) + this.form.responses_json.push(this.createActionRow('text')) + }, + onActionTypeChange(row) { + // 切换类型时重置为该类型默认结构,保留延迟值,减少“字段残留”导致的误配置。 + const delayMs = Number((row && row.delay_ms) || 0) + const next = this.createActionRow(String((row && row.type) || 'text')) + next.delay_ms = delayMs + Object.assign(row, next) }, 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 || ''}`) + for (const row of (this.form.responses_json || [])) { + if (!row || !row.type) continue + const type = String(row.type || '').toLowerCase() + const action = { + type, + delay_ms: Number(row.delay_ms || 0) } - const action = { - type: String(item.type || '').toLowerCase(), - delay_ms: Number(item.delay_ms || 0) + if (type === 'text') { + if (!String(row.text || '').trim()) { + throw new Error('文本动作的内容不能为空') + } + action.text = String(row.text || '').trim() + } else if (type === 'image') { + if (!String(row.path || '').trim()) { + throw new Error('图片动作的路径不能为空') + } + action.path = String(row.path || '').trim() + } else if (type === 'voice') { + if (!String(row.path || '').trim()) { + throw new Error('语音动作的路径不能为空') + } + action.path = String(row.path || '').trim() + action.format = String(row.format || 'mp3').trim().toLowerCase() || 'mp3' + } else if (type === 'video') { + if (!String(row.path || '').trim()) { + throw new Error('视频动作的视频路径不能为空') + } + action.path = String(row.path || '').trim() + if (String(row.cover_path || '').trim()) { + action.cover_path = String(row.cover_path || '').trim() + } + } else if (type === 'link') { + if (!String(row.url || '').trim()) { + throw new Error('卡片动作的 URL 不能为空') + } + action.title = String(row.title || '').trim() + action.desc = String(row.desc || '').trim() + action.url = String(row.url || '').trim() + action.thumb_url = String(row.thumb_url || '').trim() + } else if (type === 'app') { + if (!String(row.xml || '').trim()) { + throw new Error('App 动作的 XML 不能为空') + } + action.xml = String(row.xml || '').trim() + action.app_type = Number(row.app_type || 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) + const type = String((action && action.type) || 'text').toLowerCase() + const row = this.createActionRow(type) + row.delay_ms = Number((action && action.delay_ms) || 0) + + if (type === 'text') { + row.text = String((action && action.text) || '') + } else if (type === 'image') { + row.path = String((action && action.path) || '') + } else if (type === 'voice') { + row.path = String((action && action.path) || '') + row.format = String((action && action.format) || 'mp3').toLowerCase() + } else if (type === 'video') { + row.path = String((action && action.path) || '') + row.cover_path = String((action && action.cover_path) || '') + } else if (type === 'link') { + row.title = String((action && action.title) || '') + row.desc = String((action && action.desc) || '') + row.url = String((action && action.url) || '') + row.thumb_url = String((action && action.thumb_url) || '') + } else if (type === 'app') { + row.xml = String((action && action.xml) || '') + row.app_type = Number((action && action.app_type) || 0) } + return row }) }, async loadGroups() { @@ -498,6 +591,7 @@ new Vue({ .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} +.action-config{display:flex;flex-direction:column;gap:0} .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}