AiAssistantMcp.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. const db = require('../../plugin/DataBase/db')
  2. const { axiosWithQgOutbound } = require('../Lepao/qgOutboundAxios')
  3. const Logger = require('../Logger')
  4. const path = require('path')
  5. const QxsBookList = require('../../apis/QuXuanShu/GetBookList').GetBookList
  6. const EmailTemplate = require('../../plugin/Email/emailTemplate')
  7. const ASSISTANT_UUID = 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281'
  8. function required(values) {
  9. return values.some(v => v === '' || v === null || v === undefined)
  10. }
  11. function isProvided(value) {
  12. return value !== '' && value !== null && value !== undefined
  13. }
  14. function pageArgs({ current = 1, pagesize = 10 } = {}, max = 20) {
  15. const c = Math.max(1, Number.parseInt(current, 10) || 1)
  16. const p = Math.min(Math.max(1, Number.parseInt(pagesize, 10) || 10), max)
  17. return { current: c, pagesize: p, offset: (c - 1) * p }
  18. }
  19. function pagination(page, totalRows) {
  20. const total = Number(totalRows?.[0]?.total || 0)
  21. return {
  22. current: page.current,
  23. pagesize: page.pagesize,
  24. total,
  25. total_pages: Math.ceil(total / page.pagesize)
  26. }
  27. }
  28. function asJson(data) {
  29. return JSON.stringify(data)
  30. }
  31. function pad2(value) {
  32. return String(value).padStart(2, '0')
  33. }
  34. function formatBeijingTime(value) {
  35. if (value === '' || value === null || value === undefined) return ''
  36. if (typeof value === 'string' && !/^\d+$/.test(value.trim())) {
  37. const match = value.trim().match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/)
  38. if (match && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(value.trim())) {
  39. const [, y, m, d, h = '0', min = '0', s = '0'] = match
  40. return `${y}-${pad2(m)}-${pad2(d)} ${pad2(h)}:${pad2(min)}:${pad2(s)}`
  41. }
  42. }
  43. const numberValue = typeof value === 'number' || /^\d+$/.test(String(value).trim())
  44. ? Number(value)
  45. : null
  46. const millis = numberValue !== null
  47. ? (numberValue < 1000000000000 ? numberValue * 1000 : numberValue)
  48. : new Date(value).getTime()
  49. if (!Number.isFinite(millis)) return String(value)
  50. const date = new Date(millis + 8 * 60 * 60 * 1000)
  51. return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`
  52. }
  53. function parseJson(value, fallback = []) {
  54. if (!value) return fallback
  55. if (Array.isArray(value)) return value
  56. try {
  57. const parsed = JSON.parse(value)
  58. return parsed == null ? fallback : parsed
  59. } catch (_) {
  60. return fallback
  61. }
  62. }
  63. function stateText(value, map) {
  64. return map[value] || String(value)
  65. }
  66. function normalizeText(value, max = 2000) {
  67. return String(value || '').trim().slice(0, max)
  68. }
  69. function normalizeRunArea(area) {
  70. if (!isProvided(area)) return ''
  71. const value = String(area).trim()
  72. if (value === '随机分配') return ''
  73. const allowed = new Set([
  74. '兰花湖校区跑区',
  75. '主校区北跑区',
  76. '主校区南跑区',
  77. '重庆工商大学茶园校区',
  78. ''
  79. ])
  80. return allowed.has(value) ? value : null
  81. }
  82. function mapLepaoAccount(row) {
  83. return {
  84. student_num: row.student_num,
  85. name: row.name || '',
  86. state: stateText(row.state, { 0: '需登录', 1: '正常', 2: '异常' }),
  87. area: row.area || '随机分配',
  88. auto_run: row.auto_run === 1,
  89. auto_time: row.auto_time,
  90. today_auto_time: row.today_auto_time,
  91. target_count: row.target_count,
  92. total_num: row.total_num,
  93. term_num: row.term_num,
  94. auto_day: parseJson(row.auto_day, []),
  95. notice_type: row.notice_type,
  96. email: row.email || '',
  97. notes: row.notes || ''
  98. }
  99. }
  100. function mapLepaoRecord(row) {
  101. return {
  102. ...row,
  103. time: formatBeijingTime(row.time)
  104. }
  105. }
  106. function mapCountLedger(row) {
  107. return {
  108. ...row,
  109. created_at: formatBeijingTime(row.created_at)
  110. }
  111. }
  112. function mapOrder(row) {
  113. const orderStateMap = {
  114. 0: '待支付',
  115. 1: '已支付',
  116. 2: '已取消',
  117. 3: '异常',
  118. 4: '已退款'
  119. }
  120. return {
  121. order_no: row.orderId,
  122. state: stateText(row.state, orderStateMap),
  123. create_time: formatBeijingTime(row.create_time),
  124. price: row.price,
  125. pay_type: row.pay_type,
  126. goods_name: row.goods_name
  127. }
  128. }
  129. function mapPowerTask(row) {
  130. const { id, ...task } = row
  131. return {
  132. ...task,
  133. koufei_date: formatBeijingTime(task.koufei_date),
  134. update_time: formatBeijingTime(task.update_time)
  135. }
  136. }
  137. function mapPowerRecord(row) {
  138. return {
  139. ...row,
  140. time: formatBeijingTime(row.time),
  141. change_time: formatBeijingTime(row.change_time)
  142. }
  143. }
  144. class AiAssistantMcp {
  145. constructor() {
  146. this.logger = new Logger(path.join(__dirname, '../../logs/AiAssistantMcp.log'), 'INFO')
  147. this.emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
  148. this.banEmailList = ['icloud.com']
  149. }
  150. async list_lepao_accounts(args) {
  151. const { sender, student_num, name, area, state, auto_run, current, pagesize } = args
  152. if (required([sender])) return '缺少 sender'
  153. const page = pageArgs({ current, pagesize })
  154. const where = ['create_user = ?']
  155. const params = [sender]
  156. if (student_num) {
  157. where.push('student_num LIKE ?')
  158. params.push(`%${student_num}%`)
  159. }
  160. if (name) {
  161. where.push('name LIKE ?')
  162. params.push(`%${name}%`)
  163. }
  164. if (area !== undefined && area !== null && area !== '') {
  165. const normalizedArea = normalizeRunArea(area)
  166. if (normalizedArea === null) return '跑区不合法'
  167. where.push('area = ?')
  168. params.push(normalizedArea)
  169. }
  170. if (state !== undefined && state !== null && state !== '' && Number(state) !== -1) {
  171. where.push('state = ?')
  172. params.push(state)
  173. }
  174. if (auto_run !== undefined && auto_run !== null && auto_run !== '') {
  175. where.push('auto_run = ?')
  176. params.push(Number(auto_run))
  177. }
  178. const totalRows = await db.query(
  179. `SELECT COUNT(*) AS total
  180. FROM lepao_account
  181. WHERE ${where.join(' AND ')}`,
  182. params
  183. )
  184. const rows = await db.query(
  185. `SELECT student_num, name, state, area, auto_run, auto_time, today_auto_time,
  186. target_count, total_num, term_num, auto_day, notice_type, email, notes
  187. FROM lepao_account
  188. WHERE ${where.join(' AND ')}
  189. ORDER BY update_time DESC
  190. LIMIT ? OFFSET ?`,
  191. [...params, String(page.pagesize), String(page.offset)]
  192. )
  193. if (!rows || !totalRows) return '查询失败'
  194. return asJson({ list: rows.map(mapLepaoAccount), pagination: pagination(page, totalRows) })
  195. }
  196. async list_lepao_records(args) {
  197. const { sender, lepao_account, name, start_time, end_time, result, current, pagesize } = args
  198. if (required([sender])) return '缺少 sender'
  199. const page = pageArgs({ current, pagesize })
  200. const where = ['(r.uuid = ? OR a.create_user = ?)']
  201. const params = [sender, sender]
  202. if (lepao_account) {
  203. where.push('r.lepao_account LIKE ?')
  204. params.push(`%${lepao_account}%`)
  205. }
  206. if (name) {
  207. where.push('a.name LIKE ?')
  208. params.push(`%${name}%`)
  209. }
  210. if (result) {
  211. where.push('r.result LIKE ?')
  212. params.push(`%${result}%`)
  213. }
  214. if (start_time) {
  215. where.push('r.time >= ?')
  216. params.push(start_time)
  217. }
  218. if (end_time) {
  219. where.push('r.time < ?')
  220. params.push(end_time)
  221. }
  222. const totalRows = await db.query(
  223. `SELECT COUNT(*) AS total
  224. FROM lepao_record r
  225. LEFT JOIN lepao_account a ON r.lepao_account = a.student_num
  226. WHERE ${where.join(' AND ')}`,
  227. params
  228. )
  229. const rows = await db.query(
  230. `SELECT r.public_id, r.time, r.result, r.run_mode, r.lepao_account, a.name
  231. FROM lepao_record r
  232. LEFT JOIN lepao_account a ON r.lepao_account = a.student_num
  233. WHERE ${where.join(' AND ')}
  234. ORDER BY r.id DESC
  235. LIMIT ? OFFSET ?`,
  236. [...params, String(page.pagesize), String(page.offset)]
  237. )
  238. if (!rows || !totalRows) return '查询失败'
  239. return asJson({ list: rows.map(mapLepaoRecord), pagination: pagination(page, totalRows) })
  240. }
  241. async list_lepao_count_ledger(args) {
  242. const { sender, biz_type, remark, start_time, end_time, current, pagesize } = args
  243. if (required([sender])) return '缺少 sender'
  244. const page = pageArgs({ current, pagesize })
  245. const where = ['user_uuid = ?']
  246. const params = [sender]
  247. if (biz_type) {
  248. where.push('biz_type = ?')
  249. params.push(biz_type)
  250. }
  251. if (remark) {
  252. where.push('remark LIKE ?')
  253. params.push(`%${remark}%`)
  254. }
  255. if (start_time) {
  256. where.push('created_at >= FROM_UNIXTIME(? / 1000)')
  257. params.push(start_time)
  258. }
  259. if (end_time) {
  260. where.push('created_at <= FROM_UNIXTIME(? / 1000)')
  261. params.push(end_time)
  262. }
  263. const totalRows = await db.query(
  264. `SELECT COUNT(*) AS total
  265. FROM lepao_count_ledger
  266. WHERE ${where.join(' AND ')}`,
  267. params
  268. )
  269. const rows = await db.query(
  270. `SELECT delta, balance_before, balance_after, biz_type, remark,
  271. UNIX_TIMESTAMP(created_at) * 1000 AS created_at
  272. FROM lepao_count_ledger
  273. WHERE ${where.join(' AND ')}
  274. ORDER BY id DESC
  275. LIMIT ? OFFSET ?`,
  276. [...params, String(page.pagesize), String(page.offset)]
  277. )
  278. if (!rows || !totalRows) return '查询失败'
  279. return asJson({ list: rows.map(mapCountLedger), pagination: pagination(page, totalRows) })
  280. }
  281. async list_goods(args) {
  282. const { keyword, category, current, pagesize } = args
  283. const page = pageArgs({ current, pagesize })
  284. const where = ['state = 1']
  285. const params = []
  286. if (keyword) {
  287. where.push('name LIKE ?')
  288. params.push(`%${keyword}%`)
  289. }
  290. if (category) {
  291. where.push('category = ?')
  292. params.push(category)
  293. }
  294. const totalRows = await db.query(
  295. `SELECT COUNT(*) AS total
  296. FROM goods
  297. WHERE ${where.join(' AND ')}`,
  298. params
  299. )
  300. const rows = await db.query(
  301. `SELECT name, price, num, \`limit\`, description, category, features
  302. FROM goods
  303. WHERE ${where.join(' AND ')}
  304. ORDER BY id DESC
  305. LIMIT ? OFFSET ?`,
  306. [...params, String(page.pagesize), String(page.offset)]
  307. )
  308. if (!rows || !totalRows) return '查询失败'
  309. return asJson({
  310. list: rows.map(r => ({ ...r, features: parseJson(r.features, []) })),
  311. pagination: pagination(page, totalRows)
  312. })
  313. }
  314. async list_my_orders(args) {
  315. const { sender, state, order_id, current, pagesize } = args
  316. if (required([sender])) return '缺少 sender'
  317. const page = pageArgs({ current, pagesize })
  318. const where = ['o.create_user = ?']
  319. const params = [sender]
  320. if (state !== undefined && state !== null && state !== '' && Number(state) !== -1) {
  321. where.push('o.state = ?')
  322. params.push(state)
  323. }
  324. if (order_id) {
  325. where.push('o.orderId = ?')
  326. params.push(order_id)
  327. }
  328. const totalRows = await db.query(
  329. `SELECT COUNT(*) AS total
  330. FROM orders o
  331. JOIN goods g ON o.goods_id = g.id
  332. WHERE ${where.join(' AND ')}`,
  333. params
  334. )
  335. const rows = await db.query(
  336. `SELECT o.orderId, o.state, o.create_time, o.price, o.pay_type, g.name AS goods_name
  337. FROM orders o
  338. JOIN goods g ON o.goods_id = g.id
  339. WHERE ${where.join(' AND ')}
  340. ORDER BY o.id DESC
  341. LIMIT ? OFFSET ?`,
  342. [...params, String(page.pagesize), String(page.offset)]
  343. )
  344. if (!rows || !totalRows) return '查询失败'
  345. return asJson({
  346. list: rows.map(mapOrder),
  347. pagination: pagination(page, totalRows)
  348. })
  349. }
  350. async save_lepao_account(args) {
  351. const { sender, student_num } = args
  352. if (required([sender, student_num])) return '缺少 sender 或 student_num'
  353. const existRows = await db.query(
  354. `SELECT id, create_user, student_num, email, area, auto_time, auto_run, target_count, auto_day, notice_type, notes
  355. FROM lepao_account
  356. WHERE student_num = ?
  357. LIMIT 1`,
  358. [student_num]
  359. )
  360. if (!existRows) return '查询账号失败'
  361. if (existRows.length > 0 && existRows[0].create_user && existRows[0].create_user !== sender) {
  362. return '该乐跑账号已被其他用户绑定'
  363. }
  364. const fields = []
  365. const params = []
  366. const mutable = [
  367. ['email', 'email'],
  368. ['auto_time', 'auto_time'],
  369. ['auto_run', 'auto_run'],
  370. ['target_count', 'target_count'],
  371. ['notice_type', 'notice_type'],
  372. ['notes', 'notes']
  373. ]
  374. if (isProvided(args.area)) {
  375. const area = normalizeRunArea(args.area)
  376. if (area === null) return '跑区不合法'
  377. fields.push('area = ?')
  378. params.push(area)
  379. }
  380. if (isProvided(args.email) && !this.emailRegex.test(args.email)) return '邮箱格式不正确'
  381. if (isProvided(args.notice_type) && !['email', 'none', 'bot'].includes(args.notice_type)) return '通知方式不合法'
  382. if (isProvided(args.auto_time)) {
  383. const autoTime = Number(args.auto_time)
  384. if (!Number.isInteger(autoTime) || autoTime < -1 || autoTime > 23) return '自动乐跑时间不合法'
  385. args.auto_time = autoTime
  386. }
  387. if (isProvided(args.auto_run) && ![0, 1].includes(Number(args.auto_run))) return 'auto_run 只能为 0 或 1'
  388. if (isProvided(args.auto_day)) {
  389. if (!Array.isArray(args.auto_day) || !args.auto_day.every(v => Number.isInteger(v) && v >= 0 && v <= 6)) return '自动乐跑星期不合法'
  390. fields.push('auto_day = ?')
  391. params.push(JSON.stringify(args.auto_day))
  392. }
  393. for (const [argName, column] of mutable) {
  394. if (isProvided(args[argName])) {
  395. fields.push(`${column} = ?`)
  396. params.push(args[argName])
  397. }
  398. }
  399. const now = Date.now()
  400. let result
  401. if (existRows.length > 0) {
  402. if (fields.length === 0) return '没有需要修改的字段'
  403. fields.push('update_time = ?', 'create_user = ?')
  404. params.push(now, sender)
  405. result = await db.query(
  406. `UPDATE lepao_account SET ${fields.join(', ')} WHERE student_num = ? AND (create_user = ? OR create_user IS NULL)`,
  407. [...params, student_num, sender]
  408. )
  409. } else {
  410. const area = isProvided(args.area) ? normalizeRunArea(args.area) : ''
  411. if (area === null) return '跑区不合法'
  412. const autoDay = isProvided(args.auto_day) ? args.auto_day : [0, 1, 2, 3, 4, 5, 6]
  413. if (!Array.isArray(autoDay) || !autoDay.every(v => Number.isInteger(v) && v >= 0 && v <= 6)) return '自动乐跑星期不合法'
  414. const email = args.email || ''
  415. if (email && !this.emailRegex.test(email)) return '邮箱格式不正确'
  416. result = await db.query(
  417. `INSERT INTO lepao_account
  418. (student_num, email, area, auto_time, auto_run, target_count, create_user, create_time, update_time, notes, auto_day, notice_type)
  419. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  420. [
  421. student_num,
  422. email,
  423. area,
  424. isProvided(args.auto_time) ? Number(args.auto_time) : -1,
  425. isProvided(args.auto_run) ? Number(args.auto_run) : 1,
  426. isProvided(args.target_count) ? args.target_count : 30,
  427. sender,
  428. now,
  429. now,
  430. args.notes || '',
  431. JSON.stringify(autoDay),
  432. args.notice_type || 'email'
  433. ]
  434. )
  435. const bindCode = Math.random().toString(36).slice(2, 8).toUpperCase()
  436. await db.query('INSERT IGNORE INTO lepao_extra (student_num, bind_code) VALUES (?, ?)', [student_num, bindCode])
  437. }
  438. if (!result || result.affectedRows < 1) return '保存失败或无权修改'
  439. return asJson({ success: true, student_num })
  440. }
  441. async save_power_task(args) {
  442. const { sender, task_id, area, building, room } = args
  443. if (required([sender])) return '缺少 sender'
  444. const now = Date.now()
  445. let existing = []
  446. if (task_id) {
  447. existing = await db.query('SELECT id, area, building, room FROM power_task WHERE id = ? AND create_user = ? LIMIT 1', [task_id, sender])
  448. } else if ([area, building, room].every(isProvided)) {
  449. existing = await db.query(
  450. 'SELECT id, area, building, room FROM power_task WHERE create_user = ? AND area = ? AND building = ? AND room = ? LIMIT 1',
  451. [sender, area, building, room]
  452. )
  453. }
  454. if (!existing) return '查询电费任务失败'
  455. const fields = []
  456. const params = []
  457. for (const key of ['area', 'building', 'room', 'email', 'lowest', 'notes']) {
  458. if (isProvided(args[key])) {
  459. fields.push(`${key} = ?`)
  460. params.push(args[key])
  461. }
  462. }
  463. let result
  464. if (existing.length > 0) {
  465. if (fields.length === 0) return '没有需要修改的字段'
  466. fields.push('is_notice = 0', 'update_time = ?')
  467. params.push(now)
  468. result = await db.query(
  469. `UPDATE power_task SET ${fields.join(', ')} WHERE id = ? AND create_user = ?`,
  470. [...params, existing[0].id, sender]
  471. )
  472. } else {
  473. if (required([area, building, room, args.lowest])) return '新增电费任务需提供 area、building、room、lowest'
  474. let balance = ''
  475. let koufeiDate = ''
  476. try {
  477. const endpoint = `https://hqpay.ctbu.edu.cn/weixin/ashx/frmuser.ashx?test=lastlist&pid=${room}&dyid=${building}`
  478. const apiRes = await axiosWithQgOutbound({ method: 'get', url: endpoint, timeout: 15000, logger: this.logger, scene: 'AiAssistantPowerAccount' })
  479. balance = apiRes?.data?.[0]?.[1] ?? ''
  480. koufeiDate = apiRes?.data?.[0]?.[2] ?? ''
  481. } catch (_) { }
  482. result = await db.query(
  483. 'INSERT INTO power_task (create_user, create_time, update_time, area, building, room, email, lowest, notes, balance, koufei_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
  484. [sender, now, now, area, building, room, args.email || '', args.lowest, args.notes || '', balance, koufeiDate]
  485. )
  486. }
  487. if (!result || result.affectedRows < 1) return '保存失败或无权修改'
  488. return asJson({ success: true, area, building, room })
  489. }
  490. async list_power_bills(args) {
  491. const { sender, area, building, room, current, pagesize } = args
  492. if (required([sender])) return 'missing sender'
  493. const page = pageArgs({ current, pagesize })
  494. const taskWhere = ['create_user = ?']
  495. const taskParams = [sender]
  496. if (area) {
  497. taskWhere.push('area = ?')
  498. taskParams.push(area)
  499. }
  500. if (building) {
  501. taskWhere.push('building = ?')
  502. taskParams.push(building)
  503. }
  504. if (room) {
  505. taskWhere.push('room = ?')
  506. taskParams.push(room)
  507. }
  508. const totalRows = await db.query(
  509. `SELECT COUNT(*) AS total
  510. FROM power_task
  511. WHERE ${taskWhere.join(' AND ')}`,
  512. taskParams
  513. )
  514. const tasks = await db.query(
  515. `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
  516. FROM power_task
  517. WHERE ${taskWhere.join(' AND ')}
  518. ORDER BY id DESC
  519. LIMIT ? OFFSET ?`,
  520. [...taskParams, String(page.pagesize), String(page.offset)]
  521. )
  522. if (!tasks || !totalRows) return 'query failed'
  523. return asJson({
  524. tasks: tasks.map(mapPowerTask),
  525. pagination: pagination(page, totalRows)
  526. })
  527. }
  528. async list_power_bill_records(args) {
  529. const { sender, area, building, room, current, pagesize } = args
  530. if (required([sender, area, building, room])) return 'missing sender, area, building or room'
  531. const page = pageArgs({ current, pagesize })
  532. const tasks = await db.query(
  533. `SELECT id, area, building, room, balance, lowest, koufei_date, update_time, notes
  534. FROM power_task
  535. WHERE create_user = ? AND area = ? AND building = ? AND room = ?
  536. ORDER BY id DESC
  537. LIMIT 1`,
  538. [sender, area, building, room]
  539. )
  540. if (!tasks) return 'query failed'
  541. if (tasks.length === 0) return asJson({ task: null, records: [], pagination: pagination(page, [{ total: 0 }]) })
  542. const totalRows = await db.query(
  543. `SELECT COUNT(*) AS total
  544. FROM power_change_record
  545. WHERE task_id = ?`,
  546. [tasks[0].id]
  547. )
  548. const records = await db.query(
  549. `SELECT time, balance, old_balance, change_time
  550. FROM power_change_record
  551. WHERE task_id = ?
  552. ORDER BY id DESC
  553. LIMIT ? OFFSET ?`,
  554. [tasks[0].id, String(page.pagesize), String(page.offset)]
  555. )
  556. if (!records || !totalRows) return 'query failed'
  557. return asJson({
  558. task: mapPowerTask(tasks[0]),
  559. records: records.map(mapPowerRecord),
  560. pagination: pagination(page, totalRows)
  561. })
  562. }
  563. async query_qxs_book_list(args) {
  564. const { sender, username, password, current, pagesize } = args
  565. if (required([sender, username, password])) return '缺少 sender、username 或 password'
  566. try {
  567. const page = pageArgs({ current, pagesize })
  568. const qxs = new QxsBookList()
  569. const { accessToken } = await qxs.qxsLogin(username, password)
  570. const { userInfo } = await qxs.qsxUserInfo(accessToken)
  571. const { bookList } = await qxs.qsxGetList(accessToken, userInfo.termCode)
  572. const list = bookList || []
  573. return asJson({
  574. user: {
  575. realname: userInfo.userName,
  576. termName: userInfo.termName
  577. },
  578. list: list.slice(page.offset, page.offset + page.pagesize).map(book => ({
  579. title: book.title || book.bookName || '',
  580. isbn: book.isbn || '',
  581. author: book.author || '',
  582. publisher: book.publisher || '',
  583. className: book.className || ''
  584. })),
  585. pagination: pagination(page, [{ total: list.length }])
  586. })
  587. } catch (err) {
  588. return err.message || '查询趣选书书单失败'
  589. }
  590. }
  591. async submit_work_order(args) {
  592. const { sender, title, content, email = '', files = [] } = args
  593. if (required([sender, title, content])) return '缺少 sender、title 或 content'
  594. if (email && !this.emailRegex.test(email)) return '邮箱格式不正确'
  595. if (email && this.banEmailList.includes(email.split('@')[1].toLowerCase())) return '暂不支持该邮箱域名'
  596. const safeTitle = normalizeText(title, 80)
  597. const safeContent = normalizeText(content, 4000)
  598. const safeFiles = Array.isArray(files) ? files.map(item => String(item || '').trim()).filter(Boolean).slice(0, 6) : []
  599. if (!safeTitle || !safeContent) return '标题或内容不能为空'
  600. const time = Date.now()
  601. const msg = [
  602. {
  603. time,
  604. content: safeContent,
  605. files: safeFiles,
  606. uuid: sender,
  607. type: 'user'
  608. },
  609. {
  610. time,
  611. content: '您的问题已收到,我们将尽快处理,请耐心等待。',
  612. uuid: ASSISTANT_UUID,
  613. type: 'system'
  614. }
  615. ]
  616. const result = await db.query(
  617. 'INSERT INTO work_order (title, email, msg, create_user, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?)',
  618. [safeTitle, email, msg, sender, time, time]
  619. )
  620. if (!result || result.affectedRows !== 1) return '提交工单失败'
  621. const kefuRows = await db.query(`
  622. SELECT email FROM users
  623. WHERE JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"')
  624. `)
  625. const emails = [...new Set((kefuRows || []).map(row => row.email).filter(Boolean))]
  626. for (const targetEmail of emails) {
  627. await EmailTemplate.orderNewReply(targetEmail, { id: result.insertId, content: safeContent, files: safeFiles })
  628. }
  629. return asJson({ success: true, message: '工单提交成功,可在工单列表查看处理进度' })
  630. }
  631. }
  632. module.exports.AI_ASSISTANT_MCP = new AiAssistantMcp()