Browse Source

增强mcp功能

cq.tianyong 1 day ago
parent
commit
2492f2b13c
2 changed files with 403 additions and 4 deletions
  1. 52 1
      apis/MCP/aiAssistantMcp/McpRPC.js
  2. 351 3
      lib/AIChat/AiAssistantMcp.js

+ 52 - 1
apis/MCP/aiAssistantMcp/McpRPC.js

@@ -143,7 +143,7 @@ const tools = [
     },
     },
     {
     {
         name: 'list_power_bill_records',
         name: 'list_power_bill_records',
-        description: '查询某个寝室/房间的电费账单或余额变动记录。必须提供 sender、area、building、room 精确定位当前用户拥有的电费任务;返回匹配任务和分页 records。所有时间字段均以北京时间 YYYY-MM-DD HH:mm:ss 返回,不暴露内部任务 id 或记录 id。',
+        description: '查询某个寝室/房间的电费账单或余额变动记录。必须提供 sender、area、building、room 精确定位当前用户拥有的电费任务,建议先调用 list_power_bills 获取准确的寝室信息;返回匹配任务和分页 records。不暴露内部任务 id 或记录 id。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
@@ -156,6 +156,57 @@ const tools = [
             required: ['sender', 'area', 'building', 'room']
             required: ['sender', 'area', 'building', 'room']
         }
         }
     },
     },
+    {
+        name: 'send_lepao_count',
+        description: '向其他用户赠送乐跑次数,系统审核通过后到账。需提供接收方用户名和赠送次数,次数范围为 1-9999。',
+        inputSchema: {
+            type: 'object',
+            properties: {
+                sender,
+                username: { type: 'string', description: '接收方用户名,需精确匹配。' },
+                count: { type: 'integer', minimum: 1, maximum: 9999, description: '赠送的乐跑次数,范围 1-9999。' }
+            },
+            required: ['sender', 'username', 'count']
+        }
+    },
+    {
+        name: 'list_send_count_requests',
+        description: '查询当前用户的乐跑次数赠送记录。可按赠送方向和审核状态筛选,返回分页列表。',
+        inputSchema: {
+            type: 'object',
+            properties: {
+                sender,
+                direction: { type: 'string', enum: ['sent', 'received'], description: '赠送方向筛选:sent 我发出的,received 我收到的。不传表示全部。' },
+                status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: '审核状态筛选:pending 待审核,approved 已通过,rejected 已拒绝。不传表示全部。' },
+                ...pageProps
+            },
+            required: ['sender']
+        }
+    },
+    {
+        name: 'list_work_orders',
+        description: '查询当前用户提交的客服工单列表。返回分页列表,包含工单编号、标题、状态、邮箱、创建时间和更新时间。',
+        inputSchema: {
+            type: 'object',
+            properties: {
+                sender,
+                ...pageProps
+            },
+            required: ['sender']
+        }
+    },
+    {
+        name: 'get_work_order_detail',
+        description: '查询当前用户某个客服工单详情。必须提供 work_order_id,只能查询当前 sender 自己创建的工单;返回消息列表和消息发送者 userInfo。',
+        inputSchema: {
+            type: 'object',
+            properties: {
+                sender,
+                work_order_id: { type: 'integer', minimum: 1, description: '工单编号,即 list_work_orders 返回的 work_order_id。' }
+            },
+            required: ['sender', 'work_order_id']
+        }
+    },
     {
     {
         name: 'query_qxs_book_list',
         name: 'query_qxs_book_list',
         description: '使用用户在当前会话中提供的趣选书账号密码查询教材/书单。仅在用户明确要求查询教材、课程用书信息,并提供 username/password 时使用。查询到的结果会分页返回。',
         description: '使用用户在当前会话中提供的趣选书账号密码查询教材/书单。仅在用户明确要求查询教材、课程用书信息,并提供 username/password 时使用。查询到的结果会分页返回。',

+ 351 - 3
lib/AIChat/AiAssistantMcp.js

@@ -1,9 +1,10 @@
 const db = require('../../plugin/DataBase/db')
 const db = require('../../plugin/DataBase/db')
 const { axiosWithQgOutbound } = require('../Lepao/qgOutboundAxios')
 const { axiosWithQgOutbound } = require('../Lepao/qgOutboundAxios')
 const Logger = require('../Logger')
 const Logger = require('../Logger')
-const path = require('path')
-const QxsBookList = require('../../apis/QuXuanShu/GetBookList').GetBookList
-const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const path = require('path')
+const QxsBookList = require('../../apis/QuXuanShu/GetBookList').GetBookList
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const { insertLedgerRecord } = require('../Lepao/CountLedger')
 
 
 const ASSISTANT_UUID = 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281'
 const ASSISTANT_UUID = 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281'
 
 
@@ -168,6 +169,33 @@ function mapPowerRecord(row) {
         change_time: formatBeijingTime(row.change_time)
         change_time: formatBeijingTime(row.change_time)
     }
     }
 }
 }
+
+function mapSendCountRequest(row) {
+    const { id, sender_uuid, receiver_user_id, ...item } = row
+    return {
+        request_id: id,
+        ...item,
+        created_at: formatBeijingTime(item.created_at),
+        reviewed_at: formatBeijingTime(item.reviewed_at)
+    }
+}
+
+function mapWorkOrder(row) {
+    const { id, ...item } = row
+    return {
+        work_order_id: id,
+        ...item,
+        create_time: formatBeijingTime(item.create_time),
+        update_time: formatBeijingTime(item.update_time)
+    }
+}
+
+function mapWorkOrderMessage(msg) {
+    return {
+        ...msg,
+        time: formatBeijingTime(msg.time)
+    }
+}
 
 
 class AiAssistantMcp {
 class AiAssistantMcp {
     constructor() {
     constructor() {
@@ -636,6 +664,326 @@ class AiAssistantMcp {
         })
         })
     }
     }
 
 
+    async send_lepao_count(args) {
+        let { sender, username, count } = args
+        username = typeof username === 'string' ? username.trim() : username
+        count = Number(count)
+
+        if (required([sender, username]) || Number.isNaN(count)) return '缺少 sender、username 或 count'
+        if (!Number.isInteger(count) || count < 1 || count > 9999) return '超出赠送的次数范围,请重新选择赠送次数'
+
+        const conn = await db.connect()
+        try {
+            await conn.beginTransaction()
+
+            const [senderRows] = await conn.execute(
+                'SELECT id, username, lepao_count, COALESCE(send_count_auto_approve, 0) AS send_count_auto_approve FROM users WHERE uuid = ?',
+                [sender]
+            )
+            if (!senderRows || senderRows.length !== 1) {
+                await conn.rollback()
+                return '获取用户信息失败'
+            }
+
+            const [targetRows] = await conn.execute(
+                'SELECT id, uuid FROM users WHERE username = ?',
+                [username]
+            )
+            if (!targetRows || targetRows.length !== 1) {
+                await conn.rollback()
+                return '未找到接收用户,请检查用户名是否正确'
+            }
+            if (targetRows[0].uuid === sender) {
+                await conn.rollback()
+                return '不能给自己赠送次数'
+            }
+
+            const [decResult] = await conn.execute(
+                'UPDATE users SET lepao_count = lepao_count - ? WHERE uuid = ? AND lepao_count >= ?',
+                [count, sender, count]
+            )
+            if (!decResult || decResult.affectedRows !== 1) {
+                await conn.rollback()
+                return '剩余乐跑次数不足,请购买后再赠送'
+            }
+
+            const senderUsername = senderRows[0].username
+            const senderLepaoBefore = Number(senderRows[0].lepao_count || 0)
+            const autoApprove = Number(senderRows[0].send_count_auto_approve) === 1
+
+            if (autoApprove) {
+                const [recvRows] = await conn.execute(
+                    'SELECT uuid, lepao_count FROM users WHERE id = ? FOR UPDATE',
+                    [targetRows[0].id]
+                )
+                if (!recvRows || recvRows.length !== 1) {
+                    await conn.rollback()
+                    return '未找到接收用户,请检查用户名是否正确'
+                }
+
+                const receiverUuid = recvRows[0].uuid
+                const beforeRecv = Number(recvRows[0].lepao_count || 0)
+                const [incResult] = await conn.execute(
+                    'UPDATE users SET lepao_count = lepao_count + ? WHERE id = ?',
+                    [count, targetRows[0].id]
+                )
+                if (!incResult || incResult.affectedRows !== 1) {
+                    await conn.rollback()
+                    return '接收方入账失败,请稍后再试'
+                }
+
+                const [insertResult] = await conn.execute(
+                    `INSERT INTO lepao_send_count_request
+                    (sender_uuid, receiver_user_id, count, status, created_at, reviewed_at, reviewer_uuid)
+                    VALUES (?, ?, ?, 'approved', NOW(), NOW(), NULL)`,
+                    [sender, targetRows[0].id, count]
+                )
+                if (!insertResult || insertResult.affectedRows !== 1) {
+                    await conn.rollback()
+                    return '记录赠送失败,请稍后再试'
+                }
+
+                const requestId = insertResult.insertId
+                await insertLedgerRecord({
+                    executor: conn,
+                    userUuid: sender,
+                    delta: -count,
+                    balanceBefore: senderLepaoBefore,
+                    balanceAfter: senderLepaoBefore - count,
+                    bizType: 'gift_send_lock',
+                    bizId: `send_request:${requestId}`,
+                    remark: `向${username}赠送${count}次`
+                })
+                await insertLedgerRecord({
+                    executor: conn,
+                    userUuid: receiverUuid,
+                    delta: count,
+                    balanceBefore: beforeRecv,
+                    balanceAfter: beforeRecv + count,
+                    bizType: 'gift_receive',
+                    bizId: `send_request:${requestId}`,
+                    operatorUuid: null,
+                    remark: `${senderUsername}赠送${count}次`
+                })
+
+                await conn.commit()
+                const reviewTime = Date.now()
+                Promise.resolve().then(async () => {
+                    try {
+                        const infoRows = await db.query('SELECT email FROM users WHERE id = ?', [targetRows[0].id])
+                        if (infoRows && infoRows.length === 1 && infoRows[0].email) {
+                            await EmailTemplate.sendCountRequestApproved(infoRows[0].email, {
+                                requestId,
+                                senderUsername,
+                                count,
+                                reviewTime
+                            })
+                        }
+                    } catch (mailErr) {
+                        this.logger.error(`[AiAssistantMcp][SendCount][auto][${requestId}] notify failed: ${mailErr.message || mailErr}`)
+                    }
+                })
+
+                return asJson({ success: true, status: 'approved', request_id: requestId, message: '赠送成功,对方已到账' })
+            }
+
+            const [insertResult] = await conn.execute(
+                `INSERT INTO lepao_send_count_request
+                (sender_uuid, receiver_user_id, count, status, created_at)
+                VALUES (?, ?, ?, 'pending', NOW())`,
+                [sender, targetRows[0].id, count]
+            )
+            if (!insertResult || insertResult.affectedRows !== 1) {
+                await conn.rollback()
+                return '提交赠送审核失败,请稍后再试'
+            }
+
+            const requestId = insertResult.insertId
+            await insertLedgerRecord({
+                executor: conn,
+                userUuid: sender,
+                delta: -count,
+                balanceBefore: senderLepaoBefore,
+                balanceAfter: senderLepaoBefore - count,
+                bizType: 'gift_send_lock',
+                bizId: `send_request:${requestId}`,
+                remark: `向${username}赠送${count}次`
+            })
+
+            await conn.commit()
+            const createTime = Date.now()
+            Promise.resolve().then(async () => {
+                try {
+                    const adminRows = await db.query(`
+                        SELECT email
+                        FROM users
+                        WHERE email IS NOT NULL
+                          AND email <> ''
+                          AND (JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"'))
+                    `)
+                    const emails = [...new Set((adminRows || []).map(row => row.email).filter(Boolean))]
+                    for (const email of emails) {
+                        await EmailTemplate.sendCountRequestNotifyAdmins(email, {
+                            requestId,
+                            senderUsername,
+                            receiverUsername: username,
+                            count,
+                            createTime
+                        })
+                    }
+                } catch (mailErr) {
+                    this.logger.error(`[AiAssistantMcp][SendCount][pending][${requestId}] notify failed: ${mailErr.message || mailErr}`)
+                }
+            })
+
+            return asJson({ success: true, status: 'pending', request_id: requestId, message: '已提交审核,审核通过后接收方将到账' })
+        } catch (err) {
+            try { await conn.rollback() } catch (_) { }
+            this.logger.error(`MCP赠送乐跑次数失败:${err.stack || err}`)
+            return '赠送次数失败,请稍后再试'
+        } finally {
+            if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {
+                conn.release()
+            }
+        }
+    }
+
+    async list_send_count_requests(args) {
+        const { sender, direction, status, current, pagesize } = args
+        if (required([sender])) return '缺少 sender'
+
+        if (direction && !['sent', 'received'].includes(direction)) return 'direction 参数错误'
+        if (status && !['pending', 'approved', 'rejected'].includes(status)) return 'status 参数错误'
+
+        const page = pageArgs({ current, pagesize })
+        const userRows = await db.query('SELECT id FROM users WHERE uuid = ?', [sender])
+        if (!userRows || userRows.length !== 1) return '获取用户信息失败'
+
+        const userId = userRows[0].id
+        let sql = `
+            SELECT
+                r.id,
+                r.sender_uuid,
+                su.username AS sender_username,
+                su.avatar AS sender_avatar,
+                r.receiver_user_id,
+                ru.username AS receiver_username,
+                ru.avatar AS receiver_avatar,
+                CASE WHEN r.sender_uuid = ? THEN 'sent' ELSE 'received' END AS direction,
+                CASE WHEN r.sender_uuid = ? THEN ru.username ELSE su.username END AS counterparty_username,
+                CASE WHEN r.sender_uuid = ? THEN ru.avatar ELSE su.avatar END AS counterparty_avatar,
+                r.count,
+                r.status,
+                r.created_at,
+                r.reviewed_at,
+                r.reject_reason
+            FROM lepao_send_count_request r
+            LEFT JOIN users su ON su.uuid = r.sender_uuid
+            LEFT JOIN users ru ON ru.id = r.receiver_user_id
+            WHERE (r.sender_uuid = ? OR r.receiver_user_id = ?)
+        `
+        let countSql = `
+            SELECT COUNT(*) AS total
+            FROM lepao_send_count_request r
+            WHERE (r.sender_uuid = ? OR r.receiver_user_id = ?)
+        `
+        const params = [sender, sender, sender, sender, userId]
+        const countParams = [sender, userId]
+
+        if (direction === 'sent') {
+            sql += ' AND r.sender_uuid = ?'
+            countSql += ' AND r.sender_uuid = ?'
+            params.push(sender)
+            countParams.push(sender)
+        }
+        if (direction === 'received') {
+            sql += ' AND r.receiver_user_id = ?'
+            countSql += ' AND r.receiver_user_id = ?'
+            params.push(userId)
+            countParams.push(userId)
+        }
+        if (status) {
+            sql += ' AND r.status = ?'
+            countSql += ' AND r.status = ?'
+            params.push(status)
+            countParams.push(status)
+        }
+
+        sql += ' ORDER BY r.id DESC LIMIT ? OFFSET ?'
+        params.push(String(page.pagesize), String(page.offset))
+
+        const rows = await db.query(sql, params)
+        const totalRows = await db.query(countSql, countParams)
+        if (!rows || !totalRows) return '获取赠送记录失败'
+
+        return asJson({
+            list: rows.map(mapSendCountRequest),
+            pagination: pagination(page, totalRows)
+        })
+    }
+
+    async list_work_orders(args) {
+        const { sender, current, pagesize } = args
+        if (required([sender])) return '缺少 sender'
+        const page = pageArgs({ current, pagesize })
+
+        const rows = await db.query(
+            `SELECT id, title, state, email, create_time, update_time
+             FROM work_order
+             WHERE create_user = ?
+             ORDER BY update_time DESC
+             LIMIT ? OFFSET ?`,
+            [sender, String(page.pagesize), String(page.offset)]
+        )
+        const totalRows = await db.query(
+            `SELECT COUNT(*) AS total
+             FROM work_order
+             WHERE create_user = ?`,
+            [sender]
+        )
+        if (!rows || !totalRows) return '查询工单列表失败'
+
+        return asJson({
+            list: rows.map(mapWorkOrder),
+            pagination: pagination(page, totalRows)
+        })
+    }
+
+    async get_work_order_detail(args) {
+        const { sender, work_order_id } = args
+        if (required([sender, work_order_id])) return '缺少 sender 或 work_order_id'
+
+        const rows = await db.query(
+            `SELECT id, title, msg, state, email, create_time, update_time
+             FROM work_order
+             WHERE create_user = ? AND id = ?`,
+            [sender, work_order_id]
+        )
+        if (!rows || rows.length !== 1) return '工单异常,请提交新工单'
+
+        const order = mapWorkOrder(rows[0])
+        const msgList = Array.isArray(rows[0].msg) ? rows[0].msg : parseJson(rows[0].msg, [])
+        const uuidList = [...new Set(msgList.map(msg => msg?.uuid).filter(Boolean))]
+        let userInfo = {}
+        if (uuidList.length > 0) {
+            const placeholders = uuidList.map(() => '?').join(', ')
+            const userRows = await db.query(`SELECT uuid, username, avatar FROM users WHERE uuid IN (${placeholders})`, uuidList)
+            if (!userRows || userRows.length === 0) return '用户信息获取失败'
+            userRows.forEach(user => {
+                userInfo[user.uuid] = {
+                    username: user.username,
+                    avatar: user.avatar
+                }
+            })
+        }
+
+        return asJson({
+            ...order,
+            msg: msgList.map(mapWorkOrderMessage),
+            userInfo
+        })
+    }
+
     async query_qxs_book_list(args) {
     async query_qxs_book_list(args) {
         const { sender, username, password, current, pagesize } = args
         const { sender, username, password, current, pagesize } = args
         if (required([sender, username, password])) return '缺少 sender、username 或 password'
         if (required([sender, username, password])) return '缺少 sender、username 或 password'