AiAssistantMcp.js 24 KB

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