Browse Source

优化mcp功能

cq.tianyong 2 hours ago
parent
commit
8e057e95d4
2 changed files with 193 additions and 105 deletions
  1. 74 60
      apis/MCP/aiAssistantMcp/McpRPC.js
  2. 119 45
      lib/AIChat/AiAssistantMcp.js

+ 74 - 60
apis/MCP/aiAssistantMcp/McpRPC.js

@@ -3,27 +3,27 @@ const MCP = require('../../../lib/AIChat/AiAssistantMcp').AI_ASSISTANT_MCP
 
 
 const sender = {
 const sender = {
     type: 'string',
     type: 'string',
-    description: 'RunForge user uuid. Required. Only data owned by this user may be read or changed.'
+    description: '当前会话中的 RunForge 用户 uuid。涉及用户私有数据的工具必须传入该字段,并且只能读取或修改该用户拥有的数据。'
 }
 }
 
 
 const pageProps = {
 const pageProps = {
-    current: { type: 'integer', minimum: 1, description: 'Page number, default 1' },
-    pagesize: { type: 'integer', minimum: 1, maximum: 20, description: 'Page size, default 10, max 20' }
+    current: { type: 'integer', minimum: 1, description: '页码。未传时默认为 1。' },
+    pagesize: { type: 'integer', minimum: 1, maximum: 20, description: '每页数量。未传时默认为 10,最大 20。' }
 }
 }
 
 
 const tools = [
 const tools = [
     {
     {
         name: 'list_lepao_accounts',
         name: 'list_lepao_accounts',
-        description: 'Query the user owned Lepao account list with optional filters. Never returns internal auto-increment ids.',
+        description: '查询当前 RunForge 用户绑定的乐跑账号列表。用户询问账号状态、跑区、自动乐跑设置、目标次数、乐跑时间、通知方式或绑定邮箱时,优先使用此工具。支持模糊筛选和分页返回,响应包含 list 和 pagination。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                student_num: { type: 'string' },
-                name: { type: 'string' },
-                area: { type: 'string' },
-                state: { type: 'integer', description: '-1 all, 0 needs login, 1 normal, 2 abnormal' },
-                auto_run: { type: 'integer', enum: [0, 1] },
+                student_num: { type: 'string', description: '学号模糊筛选。' },
+                name: { type: 'string', description: '姓名模糊筛选。' },
+                area: { type: 'string', description: '跑区筛选。用户指定跑区时使用精确跑区名称;用户说随机跑区时使用空值。' },
+                state: { type: 'integer', description: '-1 或不传表示全部;0 表示需重新登录,1 表示正常,2 表示异常。' },
+                auto_run: { type: 'integer', enum: [0, 1], description: '按自动乐跑开关筛选:1 开启,0 关闭。' },
                 ...pageProps
                 ...pageProps
             },
             },
             required: ['sender']
             required: ['sender']
@@ -31,16 +31,16 @@ const tools = [
     },
     },
     {
     {
         name: 'list_lepao_records',
         name: 'list_lepao_records',
-        description: 'Query Lepao run records owned by the user with account, name, result and time filters.',
+        description: '查询当前用户拥有的乐跑记录。用户询问乐跑历史、成功或失败结果、乐跑模式、乐跑时间,或某个绑定账号的记录时使用。支持账号、姓名、结果文本模糊筛选,支持毫秒时间戳范围筛选和分页返回。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                lepao_account: { type: 'string' },
-                name: { type: 'string' },
-                result: { type: 'string' },
-                start_time: { type: 'integer', description: 'Start timestamp in ms' },
-                end_time: { type: 'integer', description: 'End timestamp in ms' },
+                lepao_account: { type: 'string', description: '乐跑学号/账号模糊筛选。' },
+                name: { type: 'string', description: '姓名模糊筛选。' },
+                result: { type: 'string', description: '结果文本模糊筛选,适合查询成功、失败原因等。' },
+                start_time: { type: 'integer', description: '开始时间戳,毫秒,包含该时间。' },
+                end_time: { type: 'integer', description: '结束时间戳,毫秒,不包含该时间。' },
                 ...pageProps
                 ...pageProps
             },
             },
             required: ['sender']
             required: ['sender']
@@ -48,15 +48,15 @@ const tools = [
     },
     },
     {
     {
         name: 'list_lepao_count_ledger',
         name: 'list_lepao_count_ledger',
-        description: 'Query Lepao count change ledger for the user. Never returns internal auto-increment ids.',
+        description: '查询当前用户的乐跑次数余额变动流水。用户询问乐跑次数为什么变化、如何扣减/返还/充值,或想看次数交易历史时使用。支持业务类型、备注关键词、毫秒时间戳范围和分页查询,不暴露内部 id。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                biz_type: { type: 'string' },
-                remark: { type: 'string' },
-                start_time: { type: 'integer', description: 'Start timestamp in ms' },
-                end_time: { type: 'integer', description: 'End timestamp in ms' },
+                biz_type: { type: 'string', description: '业务类型精确筛选,例如消费、返还、充值或流水表定义的其它类型。' },
+                remark: { type: 'string', description: '备注关键词模糊筛选。' },
+                start_time: { type: 'integer', description: '开始时间戳,毫秒,包含该时间。' },
+                end_time: { type: 'integer', description: '结束时间戳,毫秒,包含该时间。' },
                 ...pageProps
                 ...pageProps
             },
             },
             required: ['sender']
             required: ['sender']
@@ -64,25 +64,25 @@ const tools = [
     },
     },
     {
     {
         name: 'list_goods',
         name: 'list_goods',
-        description: 'Query active goods list with optional keyword and category filters. Never returns internal auto-increment ids, icon paths, or other unnecessary media fields.',
+        description: '查询当前上架且可购买的商品列表。用户询问价格、套餐、库存、分类、权益或购买选项前使用。支持关键词、分类筛选和分页返回,响应不包含内部 id 和不必要的媒体字段。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
-                keyword: { type: 'string' },
-                category: { type: 'string' },
+                keyword: { type: 'string', description: '商品名称关键词模糊筛选。' },
+                category: { type: 'string', description: '商品分类精确筛选。' },
                 ...pageProps
                 ...pageProps
             }
             }
         }
         }
     },
     },
     {
     {
         name: 'list_my_orders',
         name: 'list_my_orders',
-        description: 'Query the user order list and order status. Uses order_no instead of internal ids.',
+        description: '查询当前用户创建的订单列表。用户询问支付状态、订单历史、退款状态,或需要定位公开订单号时使用。支持订单状态、订单号筛选和分页返回。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                state: { type: 'integer', description: '-1 all, 0 pending payment, 1 paid, 2 cancelled, 3 abnormal, 4 refunded' },
-                order_id: { type: 'string', description: 'Public order number' },
+                state: { type: 'integer', description: '-1 或不传表示全部;0 待支付,1 已支付,2 已取消,3 异常,4 已退款。' },
+                order_id: { type: 'string', description: '公开订单号精确筛选。这里是用户可见的 order_no/orderId,不是数据库内部 id。' },
                 ...pageProps
                 ...pageProps
             },
             },
             required: ['sender']
             required: ['sender']
@@ -90,67 +90,81 @@ const tools = [
     },
     },
     {
     {
         name: 'save_lepao_account',
         name: 'save_lepao_account',
-        description: 'Add or partially update a Lepao account owned by the user. student_num identifies the account; provide only fields that should change. Example: to change run area, provide sender, student_num, area only.',
+        description: '新增或部分更新当前用户拥有的一个乐跑账号。使用 student_num 识别账号。只传用户明确要设置的字段,未传字段会保持不变。已有账号可更新邮箱、跑区、自动乐跑开关/时间/星期、目标次数、通知方式或备注。不确定账号现状时先调用 list_lepao_accounts,不要用此工具读取账号数据。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                student_num: { type: 'string' },
-                email: { type: 'string' },
-                area: { type: 'string', description: 'Supported examples: 兰花湖校区跑区, 主校区北跑区, 主校区南跑区, 重庆工商大学茶园校区, 随机分配.' },
-                auto_time: { type: 'integer', minimum: -1, maximum: 23 },
-                auto_run: { type: 'integer', enum: [0, 1] },
-                target_count: { type: 'number' },
-                auto_day: { type: 'array', items: { type: 'integer', minimum: 0, maximum: 6 } },
-                notice_type: { type: 'string', enum: ['email', 'none', 'bot'] },
-                notes: { type: 'string' }
+                student_num: { type: 'string', description: '乐跑学号/账号。必填,用于识别账号。' },
+                email: { type: 'string', description: '该乐跑账号的通知邮箱。' },
+                area: { type: 'string', description: '要使用的跑区。用户要求随机跑区时使用空值。' },
+                auto_time: { type: 'integer', minimum: -1, maximum: 23, description: '自动乐跑开始小时,范围 7-21;使用 -1 表示随机时间。' },
+                auto_run: { type: 'integer', enum: [0, 1], description: '自动乐跑开关:1 开启,0 关闭。' },
+                target_count: { type: 'number', description: '目标总乐跑次数。达到目标后后端逻辑可能自动关闭自动乐跑。' },
+                auto_day: { type: 'array', items: { type: 'integer', minimum: 0, maximum: 6 }, description: '自动乐跑星期。0 周日,1 周一,依此类推,6 周六。' },
+                notice_type: { type: 'string', enum: ['email', 'none'], description: '乐跑后的通知渠道:email 邮件,none 不通知。' },
+                notes: { type: 'string', description: '该账号的用户可见备注。' }
             },
             },
             required: ['sender', 'student_num']
             required: ['sender', 'student_num']
         }
         }
     },
     },
+    // {
+    //     name: 'save_power_task',
+    //     description: '新增或部分更新当前用户的电费余额监控任务。已有任务可用 task_id 兼容识别,也可用 area + building + room 识别。只传用户要设置的字段,未传字段保持不变。新增任务时必须提供 area、building、room 和 lowest 阈值。',
+    //     inputSchema: {
+    //         type: 'object',
+    //         properties: {
+    //             sender,
+    //             task_id: { type: 'integer', description: '已有任务 id,主要用于兼容。用户提供区域、楼栋、房间时优先使用 area + building + room。' },
+    //             area: { type: 'string', description: '校区或电费区域名称。' },
+    //             building: { type: 'string', description: '电费系统中的楼栋标识或名称。' },
+    //             room: { type: 'string', description: '电费系统中的房间标识或名称。' },
+    //             email: { type: 'string', description: '用于接收低余额提醒的邮箱。' },
+    //             lowest: { type: 'number', description: '低余额提醒阈值,余额低于该值时后端可能发送提醒。' },
+    //             notes: { type: 'string', description: '该电费任务的用户可见备注。' }
+    //         },
+    //         required: ['sender']
+    //     }
+    // },
     {
     {
-        name: 'save_power_task',
-        description: 'Add or partially update a power query task owned by the user. Existing tasks can be identified by task_id or by area + building + room; provide only fields that should change.',
+        name: 'list_power_bills',
+        description: '查询当前用户的电费监控任务列表。可按校区/电费区域、楼栋、房间筛选,返回分页任务列表;只用于查看任务本身和当前余额信息,不返回账单变动记录。查询某寝室电费账单记录请使用 list_power_bill_records。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                task_id: { type: 'integer', description: 'Existing task id for compatibility. Prefer area + building + room when possible.' },
-                area: { type: 'string' },
-                building: { type: 'string' },
-                room: { type: 'string' },
-                email: { type: 'string' },
-                lowest: { type: 'number' },
-                notes: { type: 'string' }
+                area: { type: 'string', description: '校区或电费区域精确筛选。' },
+                building: { type: 'string', description: '楼栋精确筛选。' },
+                room: { type: 'string', description: '房间精确筛选。' },
+                ...pageProps
             },
             },
             required: ['sender']
             required: ['sender']
         }
         }
     },
     },
     {
     {
-        name: 'list_power_bills',
-        description: 'Query power tasks or one task bill/change records owned by the user. Results never expose internal task or record ids.',
+        name: 'list_power_bill_records',
+        description: '查询某个寝室/房间的电费账单或余额变动记录。必须提供 sender、area、building、room 精确定位当前用户拥有的电费任务;返回匹配任务和分页 records。所有时间字段均以北京时间 YYYY-MM-DD HH:mm:ss 返回,不暴露内部任务 id 或记录 id。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                task_id: { type: 'integer', description: 'Compatibility id filter when caller already has it.' },
-                area: { type: 'string' },
-                building: { type: 'string' },
-                room: { type: 'string' },
+                area: { type: 'string', description: '校区或电费区域,必填,需精确匹配。' },
+                building: { type: 'string', description: '楼栋,必填,需精确匹配。' },
+                room: { type: 'string', description: '寝室/房间,必填,需精确匹配。' },
                 ...pageProps
                 ...pageProps
             },
             },
-            required: ['sender']
+            required: ['sender', 'area', 'building', 'room']
         }
         }
     },
     },
     {
     {
         name: 'query_qxs_book_list',
         name: 'query_qxs_book_list',
-        description: 'Query Quxuanshu textbook list with user provided Quxuanshu credentials.',
+        description: '使用用户在当前会话中提供的趣选书账号密码查询教材/书单。仅在用户明确要求查询教材、课程用书信息,并提供 username/password 时使用。查询到的结果会分页返回。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                username: { type: 'string' },
-                password: { type: 'string' },
+                username: { type: 'string', description: '用户提供的趣选书用户名。' },
+                password: { type: 'string', description: '用户为本次查询提供的趣选书密码。' },
                 ...pageProps
                 ...pageProps
             },
             },
             required: ['sender', 'username', 'password']
             required: ['sender', 'username', 'password']
@@ -158,15 +172,15 @@ const tools = [
     },
     },
     {
     {
         name: 'submit_work_order',
         name: 'submit_work_order',
-        description: 'Submit a support ticket for the current user. Returns no internal auto-increment id; the user can view progress in the ticket list.',
+        description: '为当前用户提交客服工单。当其它工具无法解决问题,或问题需要人工处理时使用。标题应简洁,内容应包含必要上下文。用户可在工单列表查看处理进度。',
         inputSchema: {
         inputSchema: {
             type: 'object',
             type: 'object',
             properties: {
             properties: {
                 sender,
                 sender,
-                title: { type: 'string', maxLength: 80 },
-                content: { type: 'string', maxLength: 4000 },
-                email: { type: 'string' },
-                files: { type: 'array', items: { type: 'string' }, maxItems: 6 }
+                title: { type: 'string', maxLength: 80, description: '简短工单标题,最多 80 个字符。' },
+                content: { type: 'string', maxLength: 4000, description: '详细问题描述和相关上下文,最多 4000 个字符。' },
+                email: { type: 'string', description: '该工单的可选联系邮箱。' },
+                files: { type: 'array', items: { type: 'string' }, maxItems: 6, description: '与问题相关的可选上传文件路径或 URL,最多 6 个。' }
             },
             },
             required: ['sender', 'title', 'content']
             required: ['sender', 'title', 'content']
         }
         }

+ 119 - 45
lib/AIChat/AiAssistantMcp.js

@@ -35,6 +35,33 @@ function asJson(data) {
     return JSON.stringify(data)
     return JSON.stringify(data)
 }
 }
 
 
+function pad2(value) {
+    return String(value).padStart(2, '0')
+}
+
+function formatBeijingTime(value) {
+    if (value === '' || value === null || value === undefined) return ''
+
+    if (typeof value === 'string' && !/^\d+$/.test(value.trim())) {
+        const match = value.trim().match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/)
+        if (match && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(value.trim())) {
+            const [, y, m, d, h = '0', min = '0', s = '0'] = match
+            return `${y}-${pad2(m)}-${pad2(d)} ${pad2(h)}:${pad2(min)}:${pad2(s)}`
+        }
+    }
+
+    const numberValue = typeof value === 'number' || /^\d+$/.test(String(value).trim())
+        ? Number(value)
+        : null
+    const millis = numberValue !== null
+        ? (numberValue < 1000000000000 ? numberValue * 1000 : numberValue)
+        : new Date(value).getTime()
+    if (!Number.isFinite(millis)) return String(value)
+
+    const date = new Date(millis + 8 * 60 * 60 * 1000)
+    return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`
+}
+
 function parseJson(value, fallback = []) {
 function parseJson(value, fallback = []) {
     if (!value) return fallback
     if (!value) return fallback
     if (Array.isArray(value)) return value
     if (Array.isArray(value)) return value
@@ -87,6 +114,55 @@ function mapLepaoAccount(row) {
     }
     }
 }
 }
 
 
+function mapLepaoRecord(row) {
+    return {
+        ...row,
+        time: formatBeijingTime(row.time)
+    }
+}
+
+function mapCountLedger(row) {
+    return {
+        ...row,
+        created_at: formatBeijingTime(row.created_at)
+    }
+}
+
+function mapOrder(row) {
+    const orderStateMap = {
+        0: '待支付',
+        1: '已支付',
+        2: '已取消',
+        3: '异常',
+        4: '已退款'
+    }
+    return {
+        order_no: row.orderId,
+        state: stateText(row.state, orderStateMap),
+        create_time: formatBeijingTime(row.create_time),
+        price: row.price,
+        pay_type: row.pay_type,
+        goods_name: row.goods_name
+    }
+}
+
+function mapPowerTask(row) {
+    const { id, ...task } = row
+    return {
+        ...task,
+        koufei_date: formatBeijingTime(task.koufei_date),
+        update_time: formatBeijingTime(task.update_time)
+    }
+}
+
+function mapPowerRecord(row) {
+    return {
+        ...row,
+        time: formatBeijingTime(row.time),
+        change_time: formatBeijingTime(row.change_time)
+    }
+}
+
 class AiAssistantMcp {
 class AiAssistantMcp {
     constructor() {
     constructor() {
         this.logger = new Logger(path.join(__dirname, '../../logs/AiAssistantMcp.log'), 'INFO')
         this.logger = new Logger(path.join(__dirname, '../../logs/AiAssistantMcp.log'), 'INFO')
@@ -184,7 +260,7 @@ class AiAssistantMcp {
             [...params, String(page.pagesize), String(page.offset)]
             [...params, String(page.pagesize), String(page.offset)]
         )
         )
         if (!rows || !totalRows) return '查询失败'
         if (!rows || !totalRows) return '查询失败'
-        return asJson({ list: rows, pagination: pagination(page, totalRows) })
+        return asJson({ list: rows.map(mapLepaoRecord), pagination: pagination(page, totalRows) })
     }
     }
 
 
     async list_lepao_count_ledger(args) {
     async list_lepao_count_ledger(args) {
@@ -225,7 +301,7 @@ class AiAssistantMcp {
             [...params, String(page.pagesize), String(page.offset)]
             [...params, String(page.pagesize), String(page.offset)]
         )
         )
         if (!rows || !totalRows) return '查询失败'
         if (!rows || !totalRows) return '查询失败'
-        return asJson({ list: rows, pagination: pagination(page, totalRows) })
+        return asJson({ list: rows.map(mapCountLedger), pagination: pagination(page, totalRows) })
     }
     }
 
 
     async list_goods(args) {
     async list_goods(args) {
@@ -256,7 +332,10 @@ class AiAssistantMcp {
             [...params, String(page.pagesize), String(page.offset)]
             [...params, String(page.pagesize), String(page.offset)]
         )
         )
         if (!rows || !totalRows) return '查询失败'
         if (!rows || !totalRows) return '查询失败'
-        return asJson({ list: rows.map(r => ({ ...r, features: parseJson(r.features, []) })), pagination: pagination(page, totalRows) })
+        return asJson({
+            list: rows.map(r => ({ ...r, features: parseJson(r.features, []) })),
+            pagination: pagination(page, totalRows)
+        })
     }
     }
 
 
     async list_my_orders(args) {
     async list_my_orders(args) {
@@ -291,14 +370,7 @@ class AiAssistantMcp {
         )
         )
         if (!rows || !totalRows) return '查询失败'
         if (!rows || !totalRows) return '查询失败'
         return asJson({
         return asJson({
-            list: rows.map(r => ({
-                order_no: r.orderId,
-                state: stateText(r.state, { 0: '待支付', 1: '已支付', 2: '已取消', 3: '异常', 4: '已退款' }),
-                create_time: r.create_time,
-                price: r.price,
-                pay_type: r.pay_type,
-                goods_name: r.goods_name
-            })),
+            list: rows.map(mapOrder),
             pagination: pagination(page, totalRows)
             pagination: pagination(page, totalRows)
         })
         })
     }
     }
@@ -454,15 +526,11 @@ class AiAssistantMcp {
     }
     }
 
 
     async list_power_bills(args) {
     async list_power_bills(args) {
-        const { sender, task_id, area, building, room, current, pagesize } = args
-        if (required([sender])) return '缺少 sender'
+        const { sender, area, building, room, current, pagesize } = args
+        if (required([sender])) return 'missing sender'
         const page = pageArgs({ current, pagesize })
         const page = pageArgs({ current, pagesize })
         const taskWhere = ['create_user = ?']
         const taskWhere = ['create_user = ?']
         const taskParams = [sender]
         const taskParams = [sender]
-        if (task_id) {
-            taskWhere.push('id = ?')
-            taskParams.push(task_id)
-        }
         if (area) {
         if (area) {
             taskWhere.push('area = ?')
             taskWhere.push('area = ?')
             taskParams.push(area)
             taskParams.push(area)
@@ -475,38 +543,41 @@ class AiAssistantMcp {
             taskWhere.push('room = ?')
             taskWhere.push('room = ?')
             taskParams.push(room)
             taskParams.push(room)
         }
         }
-        const isRecordQuery = Boolean(task_id || (area && building && room))
-        if (!isRecordQuery) {
-            const totalRows = await db.query(
-                `SELECT COUNT(*) AS total
-                 FROM power_task
-                 WHERE ${taskWhere.join(' AND ')}`,
-                taskParams
-            )
-            const tasks = await db.query(
-                `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
-                 FROM power_task
-                 WHERE ${taskWhere.join(' AND ')}
-                 ORDER BY id DESC
-                 LIMIT ? OFFSET ?`,
-                [...taskParams, String(page.pagesize), String(page.offset)]
-            )
-            if (!tasks || !totalRows) return '查询失败'
-            return asJson({
-                tasks: tasks.map(({ id, ...task }) => task),
-                pagination: pagination(page, totalRows)
-            })
-        }
-
+        const totalRows = await db.query(
+            `SELECT COUNT(*) AS total
+             FROM power_task
+             WHERE ${taskWhere.join(' AND ')}`,
+            taskParams
+        )
         const tasks = await db.query(
         const tasks = await db.query(
             `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
             `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
              FROM power_task
              FROM power_task
              WHERE ${taskWhere.join(' AND ')}
              WHERE ${taskWhere.join(' AND ')}
              ORDER BY id DESC
              ORDER BY id DESC
+             LIMIT ? OFFSET ?`,
+            [...taskParams, String(page.pagesize), String(page.offset)]
+        )
+        if (!tasks || !totalRows) return 'query failed'
+        return asJson({
+            tasks: tasks.map(mapPowerTask),
+            pagination: pagination(page, totalRows)
+        })
+    }
+
+    async list_power_bill_records(args) {
+        const { sender, area, building, room, current, pagesize } = args
+        if (required([sender, area, building, room])) return 'missing sender, area, building or room'
+        const page = pageArgs({ current, pagesize })
+
+        const tasks = await db.query(
+            `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
+             FROM power_task
+             WHERE create_user = ? AND area = ? AND building = ? AND room = ?
+             ORDER BY id DESC
              LIMIT 1`,
              LIMIT 1`,
-            taskParams
+            [sender, area, building, room]
         )
         )
-        if (!tasks) return '查询失败'
+        if (!tasks) return 'query failed'
         if (tasks.length === 0) return asJson({ task: null, records: [], pagination: pagination(page, [{ total: 0 }]) })
         if (tasks.length === 0) return asJson({ task: null, records: [], pagination: pagination(page, [{ total: 0 }]) })
 
 
         const totalRows = await db.query(
         const totalRows = await db.query(
@@ -523,9 +594,12 @@ class AiAssistantMcp {
              LIMIT ? OFFSET ?`,
              LIMIT ? OFFSET ?`,
             [tasks[0].id, String(page.pagesize), String(page.offset)]
             [tasks[0].id, String(page.pagesize), String(page.offset)]
         )
         )
-        const { id, ...task } = tasks[0]
-        if (!records || !totalRows) return '查询失败'
-        return asJson({ task, records, pagination: pagination(page, totalRows) })
+        if (!records || !totalRows) return 'query failed'
+        return asJson({
+            task: mapPowerTask(tasks[0]),
+            records: records.map(mapPowerRecord),
+            pagination: pagination(page, totalRows)
+        })
     }
     }
 
 
     async query_qxs_book_list(args) {
     async query_qxs_book_list(args) {