const db = require('../../plugin/DataBase/db') const path = require('path') const Logger = require('../Logger') const mq = require('../../plugin/mq') const { mq: mqName } = require('../../plugin/mq/mqPrefix') const EmailTemplate = require('../../plugin/Email/emailTemplate') const { insertBindAudit, BindAuditAction, BindAuditSource } = require('./BindAudit') class Mcp { constructor() { this.logger = new Logger(path.join(__dirname, '../logs/MCP.log'), 'INFO') this.messageQueue = mqName('runforge_message_queue') this.auto_day = [ { label: '周一', value: 1 }, { label: '周二', value: 2 }, { label: '周三', value: 3 }, { label: '周四', value: 4 }, { label: '周五', value: 5 }, { label: '周六', value: 6 }, { label: '周日', value: 0 } ] this.area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区", "随机分配"] this.auto_time = [ { label: '随机分配', value: -1 }, ...Array.from({ length: 17 }, (_, i) => { const hour = i + 7 return { label: `${hour} ~ ${hour + 1}时`, value: hour } }) ] this.autoTimeLabel = (record) => { if (record.auto_time === -1) { if (record.today_auto_time) return `随机-今日${record.today_auto_time}时` return '随机-待分配' } const match = this.auto_time.find(item => item.value === record.auto_time) return match ? match.label : '-' } this.emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ this.banEmailList = ['icloud.com'] } async bind_account({ sender, bind_code }) { try { if ([sender, bind_code].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收绑定账号请求:${sender},绑定码:${bind_code}`) let selectSql = 'SELECT bot_account FROM lepao_extra WHERE bot_account = ?' let selectRows = await db.query(selectSql, [sender]) if (!selectRows) return '系统出错,请稍后再试' if (selectRows.length !== 0) return '您已绑定其他账号,请先解绑' let umo = `QQ2749761853:FriendMessage:${sender}` let sql = ` SELECT f.student_num, a.name, a.create_user, f.bot_account FROM lepao_extra f LEFT JOIN lepao_account a ON f.student_num = a.student_num WHERE f.bind_code = ? ` const rows = await db.query(sql, [bind_code]) if (!rows || rows.length === 0) return '系统出错,请稍后再试' if (rows[0].bot_account !== null && rows[0].bot_account !== '') { if (rows[0].bot_account !== sender) return '该账号已被他人绑定,请联系客服解绑' return '该账号您已绑定' } let insertSql = ` UPDATE lepao_extra SET bot_account = ?, bot_umo = ? WHERE bind_code = ? ` let insertRows = await db.query(insertSql, [sender, umo, bind_code]) if (!insertRows || insertRows.affectedRows !== 1) return '系统出错,请稍后再试' const auditOk = await insertBindAudit({ studentNum: rows[0].student_num, ownerUuid: rows[0].create_user || null, action: BindAuditAction.BOT_BIND, source: BindAuditSource.MCP_QQ, operatorUuid: null, detail: { sender }, createdAt: Date.now() }) if (!auditOk) { this.logger.warn(`MCP绑定审计写入失败 student_num=${rows[0].student_num}`) } return `绑定成功,姓名:${rows[0].name ?? '未更新,请使用乐跑登录器更新账号信息'},学号:${rows[0].student_num}` } catch (error) { this.logger.error(`MCP绑定账号出错:${error.stack}`) return '系统出错,请稍后再试' } } async get_account_info({ sender }) { try { if ([sender].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收获取账号信息请求:${sender}`) let sql = ` SELECT l.name, l.student_num, l.state, l.area, l.auto_time, l.total_num, l.term_num, l.academy_name, l.sex, l.grade_id, l.email, l.auto_run, l.today_auto_time, l.target_count, l.auto_day, l.notice_type FROM lepao_account l LEFT JOIN lepao_extra f ON l.student_num = f.student_num WHERE f.bot_account = ? ` const rows = await db.query(sql, [sender]) if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定' let data = rows[0] let returnMsg = ` 姓名:${data.name ?? '未更新,请使用乐跑登录器更新账号信息'};学号:${data.student_num};账号状态:${data.state === 1 ? '正常' : '需使用乐跑登录器更新账号信息'};乐跑跑区:${data.area == '' ? "随机分配" : data.area};自动乐跑状态:${data.auto_run === 1 ? '开启' : '关闭'} ` if (data.auto_run === 1) { returnMsg += `自动乐跑时间:${this.autoTimeLabel(data)};` returnMsg += `自动乐跑星期:${data.auto_day.slice().sort((a, b) => { if (a === 0) return 1; if (b === 0) return -1; return a - b; }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')};` } if (data.sex) returnMsg += `性别:${data.sex === 1 ? '男' : '女'};` if (data.email) returnMsg += `邮箱:${data.email};` if (data.grade) returnMsg += `邮箱:${data.grade};` if (data.academy_name) returnMsg += `学院:${data.academy_name};` if (data.grade_id) returnMsg += `年级:${data.grade_id}级;` if (data.target_count) returnMsg += `目标乐跑次数:${data.target_count};` if (data.total_num) returnMsg += `累计乐跑次数:${data.total_num};` if (data.notice_type) returnMsg += `通知方式:${data.notice_type};` return returnMsg } catch (error) { this.logger.error(`MCP查询账号信息出错:${error.stack}`) return '系统出错,请稍后再试' } } async unbind_account({ sender }) { try { if ([sender].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收解绑账号请求:${sender}`) const selectSql = ` SELECT e.student_num, a.create_user FROM lepao_extra e LEFT JOIN lepao_account a ON a.student_num = e.student_num WHERE e.bot_account = ? ` const selectRows = await db.query(selectSql, [sender]) if (!selectRows || selectRows.length === 0) return '您尚未绑定乐跑账号,请先绑定' let insertSql = ` UPDATE lepao_extra SET bot_account = NULL, bot_umo = NULL WHERE bot_account = ? ` let insertRows = await db.query(insertSql, [sender]) if (!insertRows || insertRows.affectedRows !== 1) return '系统出错,请稍后再试' const auditOk = await insertBindAudit({ studentNum: selectRows[0].student_num, ownerUuid: selectRows[0].create_user || null, action: BindAuditAction.BOT_UNBIND, source: BindAuditSource.MCP_QQ, operatorUuid: null, detail: { sender }, createdAt: Date.now() }) if (!auditOk) { this.logger.warn(`MCP解绑审计写入失败 student_num=${selectRows[0].student_num}`) } return `解绑成功` } catch (error) { this.logger.error(`MCP解绑账号出错:${error.stack}`) return '系统出错,请稍后再试' } } async change_email({ sender, email }) { try { if ([sender, email].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收更换邮箱请求:${sender}`) if (!this.emailRegex.test(email)) return '请检查邮箱格式是否正确' const emailDomain = email.split('@')[1].toLowerCase() if (this.banEmailList.includes(emailDomain)) return `暂不支持使用 ${emailDomain} 域名的邮箱,请更换其他邮箱后重试` let insertSql = ` UPDATE lepao_account la JOIN lepao_extra le ON la.student_num = le.student_num SET la.email = ? WHERE le.bot_account = ? ` let insertRows = await db.query(insertSql, [email, sender]) if (!insertRows || insertRows.affectedRows === 0) return '您尚未绑定乐跑账号,请先绑定' return `更换成功` } catch (error) { this.logger.error(`MCP更换邮箱出错:${error.stack}`) return '系统出错,请稍后再试' } } async set_notification({ sender, mode }) { try { if ([sender, mode].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收设置通知请求:${sender},mode:${mode}`) if (mode !== 'email' && mode !== 'bot' && mode !== 'none') return '通知type不合法,仅支持 email, bot, none三种模式' let sql = ` SELECT a.notice_type, a.email FROM lepao_extra f LEFT JOIN lepao_account a ON f.student_num = a.student_num WHERE f.bot_account = ? ` const rows = await db.query(sql, [sender]) if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定' if (!rows[0].email) return '该账号还未设置邮箱,请先使用change_email设置邮箱' if (rows[0].notice_type === mode) return `当前已是${mode}通知方式,无需修改` let insertSql = ` UPDATE lepao_account la JOIN lepao_extra le ON la.student_num = le.student_num SET la.notice_type = ? WHERE le.bot_account = ? ` let insertRows = await db.query(insertSql, [mode, sender]) if (!insertRows || insertRows.affectedRows !== 1) return '系统出错,请稍后再试' return `操作成功` } catch (error) { this.logger.error(`MCP更换通知方式出错:${error.stack}`) return '系统出错,请稍后再试' } } async change_area({ sender, area }) { try { if ([sender, area].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收更换跑区请求:${sender},area:${area}`) if (!this.area.includes(area)) return '系统无该跑区,无法设置' let insertSql = ` UPDATE lepao_account la JOIN lepao_extra le ON la.student_num = le.student_num SET la.area = ? WHERE le.bot_account = ? ` let insertRows = await db.query(insertSql, [area === "随机分配" ? '' : area, sender]) if (!insertRows || insertRows.affectedRows === 0) return '您尚未绑定乐跑账号,请先绑定' return `操作成功` } catch (error) { this.logger.error(`MCP更换跑区出错:${error.stack}`) return '系统出错,请稍后再试' } } async change_auto_time({ sender, auto_time }) { try { if ([sender, auto_time].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收设置自动乐跑时间请求:${sender},auto_time:${auto_time}`) if (!(Number.isInteger(auto_time) && ((auto_time >= 7 && auto_time <= 23) || auto_time === -1))) return '乐跑时间不合法,auto_time应为7~23或-1的整数' let sql = ` SELECT a.auto_run, a.auto_time, a.auto_day FROM lepao_extra f LEFT JOIN lepao_account a ON f.student_num = a.student_num WHERE f.bot_account = ? ` const rows = await db.query(sql, [sender]) if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定' if (rows[0].auto_run !== 1) return '该账号未开启自动乐跑,请前往RunForge系统开启自动乐跑后再设置乐跑时间' if (rows[0].auto_time === auto_time) return `乐跑时间与现有时间一致,无需修改` let insertSql = ` UPDATE lepao_account la JOIN lepao_extra le ON la.student_num = le.student_num SET la.auto_time = ? WHERE le.bot_account = ? ` let insertRows = await db.query(insertSql, [auto_time, sender]) if (!insertRows || insertRows.affectedRows === 0) return '系统出错,请稍后再试' const data = { auto_time, auto_day: rows[0].auto_day } return `操作成功,现在自动乐跑时间为:${data.auto_day.slice().sort((a, b) => { if (a === 0) return 1; if (b === 0) return -1; return a - b; }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')} ${auto_time === -1 ? '随机分配时段' : `${auto_time}~${auto_time + 1}时`}` } catch (error) { this.logger.error(`MCP更换设置自动乐跑时间出错:${error.stack}`) return '系统出错,请稍后再试' } } async change_auto_day({ sender, auto_day }) { try { if ([sender, auto_day].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收设置自动乐跑请求:${sender},auto_day:${auto_day}`) if (!Array.isArray(auto_day) || !auto_day.every(v => Number.isInteger(v) && v >= 0 && v <= 6)) return '乐跑星期不合法,auto_day应为仅包含整数0~6的数组' let sql = ` SELECT a.auto_run, a.auto_time, a.auto_day FROM lepao_extra f LEFT JOIN lepao_account a ON f.student_num = a.student_num WHERE f.bot_account = ? ` const rows = await db.query(sql, [sender]) if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定' if (rows[0].auto_run !== 1) return '该账号未开启自动乐跑,请前往RunForge系统开启自动乐跑后再设置乐跑时间' let insertSql = ` UPDATE lepao_account la JOIN lepao_extra le ON la.student_num = le.student_num SET la.auto_day = ? WHERE le.bot_account = ? ` let insertRows = await db.query(insertSql, [JSON.stringify(auto_day), sender]) if (!insertRows || insertRows.affectedRows === 0) return '系统出错,请稍后再试' const auto_time = rows[0].auto_time const data = { auto_time, auto_day } return `操作成功,现在自动乐跑时间为:${data.auto_day.slice().sort((a, b) => { if (a === 0) return 1; if (b === 0) return -1; return a - b; }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')} ${auto_time === -1 ? '随机分配时段' : `${auto_time}~${auto_time + 1}时`}` } catch (error) { this.logger.error(`MCP更换设置自动乐跑时间出错:${error.stack}`) return '系统出错,请稍后再试' } } async create_work_order({ sender, email, title, content }) { try { if ([sender, email, title, content].some(value => value === '' || value === null || value === undefined)) return '缺少参数' this.logger.info(`MCP接收工单创建请求:${sender},Email:${email}`) if (!this.emailRegex.test(email)) return '请检查邮箱格式是否正确' const emailDomain = email.split('@')[1].toLowerCase() if (this.banEmailList.includes(emailDomain)) return `暂不支持使用 ${emailDomain} 域名的邮箱,请更换其他邮箱后重试` let selectSql = ` SELECT l.create_user FROM lepao_account l LEFT JOIN lepao_extra e ON l.student_num = e.student_num WHERE e.bot_account = ? ` let selectRows = await db.query(selectSql, [sender]) if (!selectRows || selectRows.length === 0) return '您还未绑定乐跑账号哦~请绑定后再试' const time = new Date().getTime() const uuid = selectRows[0].create_user let msg = [] let message = { time, content, files: [], uuid, type: 'user' } msg.push(message) const systemMsg = { time, content: `该问题由用户${sender}通过小妍助理自动生成并提交~我们会尽快处理,请耐心等待`, uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281', type: 'system' } msg.push(systemMsg) let sql = 'INSERT INTO work_order (title, email, msg, create_user, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?)' let r = await db.query(sql, [title, email, msg, uuid, time, time]) if (!r || r.affectedRows !== 1) return '系统出错,请稍后再试' let kefuSql = ` SELECT email FROM users WHERE JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"') ` let kefuRows = await db.query(kefuSql); let emails = [...new Set(kefuRows.map(row => row.email))] for (const email of emails) { if (!email) break EmailTemplate.orderNewReply(email, { id: r.insertId || id, content, files: [] }) } return `提交成功` } catch (error) { this.logger.error(`MCP工单创建出错:${error.stack}`) return '系统出错,请稍后再试' } } } const MCP = new Mcp() module.exports.MCP = MCP