ConversationService.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. const crypto = require('crypto')
  2. const db = require('../../plugin/DataBase/db')
  3. const ACTIVE_STATE = 1
  4. const DELETED_STATE = 2
  5. const ASSISTANT_UUID = 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281'
  6. const DEFAULT_TITLE = '新对话'
  7. const MAX_MESSAGE_IMAGES = 3
  8. function toPositiveInt(value, fallback = null) {
  9. const n = Number(value)
  10. if (!Number.isInteger(n) || n <= 0) return fallback
  11. return n
  12. }
  13. function normalizePage(current = 1, pagesize = 20, maxPageSize = 50) {
  14. const c = toPositiveInt(current, 1)
  15. const p = Math.min(toPositiveInt(pagesize, 20), maxPageSize)
  16. return { current: c, pagesize: p, offset: (c - 1) * p }
  17. }
  18. function normalizeText(value, max = 2000) {
  19. return String(value || '').trim().slice(0, max)
  20. }
  21. function normalizeImages(images) {
  22. if (!Array.isArray(images)) return []
  23. return images
  24. .map((item) => {
  25. if (typeof item === 'string') return item
  26. return item?.url || item?.response?.data?.picturePath || item?.response?.picturePath || ''
  27. })
  28. .map((item) => String(item || '').trim())
  29. .filter(Boolean)
  30. .slice(0, MAX_MESSAGE_IMAGES)
  31. }
  32. function buildPreview(content, images = []) {
  33. const text = normalizeText(content, 160).replace(/\s+/g, ' ')
  34. if (text) return text
  35. return images.length > 0 ? `[图片] x${images.length}` : ''
  36. }
  37. function parseImages(value) {
  38. if (!value) return []
  39. if (Array.isArray(value)) return value
  40. try {
  41. const parsed = JSON.parse(value)
  42. return Array.isArray(parsed) ? parsed : []
  43. } catch (_) {
  44. return []
  45. }
  46. }
  47. function normalizeMessage(row) {
  48. return {
  49. ...row,
  50. images: parseImages(row.images)
  51. }
  52. }
  53. async function generateConversationNo() {
  54. for (let i = 0; i < 8; i++) {
  55. const conversationNo = String(crypto.randomInt(100000000000, 999999999999))
  56. const rows = await db.query('SELECT id FROM ai_chat_conversation WHERE conversation_no = ? LIMIT 1', [conversationNo])
  57. if (!rows || rows.length === 0) return conversationNo
  58. }
  59. return `${String(Date.now()).slice(1)}${crypto.randomInt(100, 999)}`
  60. }
  61. async function assertUserConversation(conversationId, uuid) {
  62. const id = toPositiveInt(conversationId)
  63. if (!id) return null
  64. const rows = await db.query(
  65. 'SELECT id, conversation_no, create_user, title, state FROM ai_chat_conversation WHERE id = ? AND create_user = ? AND state = ? LIMIT 1',
  66. [id, uuid, ACTIVE_STATE]
  67. )
  68. return rows && rows.length === 1 ? rows[0] : null
  69. }
  70. class ConversationService {
  71. async listUserConversations({ uuid, current, pagesize }) {
  72. const page = normalizePage(current, pagesize)
  73. const rows = await db.query(
  74. `SELECT id, conversation_no, title, state, last_message_preview, last_message_time, create_time, update_time
  75. FROM ai_chat_conversation
  76. WHERE create_user = ? AND state = ?
  77. ORDER BY update_time DESC
  78. LIMIT ? OFFSET ?`,
  79. [uuid, ACTIVE_STATE, String(page.pagesize), String(page.offset)]
  80. )
  81. const countRows = await db.query(
  82. 'SELECT COUNT(*) AS total FROM ai_chat_conversation WHERE create_user = ? AND state = ?',
  83. [uuid, ACTIVE_STATE]
  84. )
  85. return {
  86. data: rows || [],
  87. pagination: {
  88. current: page.current,
  89. pagesize: page.pagesize,
  90. total: Number(countRows?.[0]?.total || 0)
  91. }
  92. }
  93. }
  94. async createConversation({ uuid, title }) {
  95. const emptyRows = await db.query(
  96. `SELECT id, conversation_no, title, state, last_message_preview, last_message_time, create_time, update_time
  97. FROM ai_chat_conversation
  98. WHERE create_user = ? AND state = ? AND last_message_time = 0
  99. ORDER BY update_time DESC
  100. LIMIT 1`,
  101. [uuid, ACTIVE_STATE]
  102. )
  103. if (emptyRows && emptyRows.length === 1) {
  104. return { ...emptyRows[0], reused: true }
  105. }
  106. const now = Date.now()
  107. const safeTitle = normalizeText(title, 80) || DEFAULT_TITLE
  108. const conversationNo = await generateConversationNo()
  109. const result = await db.query(
  110. `INSERT INTO ai_chat_conversation
  111. (conversation_no, create_user, title, state, last_message_preview, last_message_time, create_time, update_time)
  112. VALUES (?, ?, ?, ?, '', 0, ?, ?)`,
  113. [conversationNo, uuid, safeTitle, ACTIVE_STATE, now, now]
  114. )
  115. return {
  116. id: result?.insertId,
  117. conversation_no: conversationNo,
  118. title: safeTitle,
  119. state: ACTIVE_STATE,
  120. last_message_preview: '',
  121. last_message_time: 0,
  122. create_time: now,
  123. update_time: now,
  124. reused: false
  125. }
  126. }
  127. async deleteConversation({ uuid, conversationId }) {
  128. const now = Date.now()
  129. const result = await db.query(
  130. 'UPDATE ai_chat_conversation SET state = ?, update_time = ? WHERE id = ? AND create_user = ? AND state = ?',
  131. [DELETED_STATE, now, conversationId, uuid, ACTIVE_STATE]
  132. )
  133. return result?.affectedRows === 1
  134. }
  135. async listMessages({ uuid, conversationId, afterId = 0, current, pagesize }) {
  136. const conv = await assertUserConversation(conversationId, uuid)
  137. if (!conv) return null
  138. const after = Number(afterId || 0)
  139. if (Number.isFinite(after) && after > 0) {
  140. const rows = await db.query(
  141. `SELECT id, conversation_id, role, content, images, status, error_msg, create_time, update_time
  142. FROM ai_chat_message
  143. WHERE conversation_id = ? AND create_user = ? AND id > ?
  144. ORDER BY id ASC
  145. LIMIT 100`,
  146. [conversationId, uuid, after]
  147. )
  148. return { data: (rows || []).map(normalizeMessage), pagination: null }
  149. }
  150. const page = normalizePage(current, pagesize, 100)
  151. const rows = await db.query(
  152. `SELECT id, conversation_id, role, content, images, status, error_msg, create_time, update_time
  153. FROM ai_chat_message
  154. WHERE conversation_id = ? AND create_user = ?
  155. ORDER BY id DESC
  156. LIMIT ? OFFSET ?`,
  157. [conversationId, uuid, String(page.pagesize), String(page.offset)]
  158. )
  159. const countRows = await db.query(
  160. 'SELECT COUNT(*) AS total FROM ai_chat_message WHERE conversation_id = ? AND create_user = ?',
  161. [conversationId, uuid]
  162. )
  163. return {
  164. data: (rows || []).map(normalizeMessage).reverse(),
  165. pagination: {
  166. current: page.current,
  167. pagesize: page.pagesize,
  168. total: Number(countRows?.[0]?.total || 0)
  169. }
  170. }
  171. }
  172. async addUserMessage({ uuid, conversationId, content, images }) {
  173. const text = normalizeText(content, 2000)
  174. const imgs = normalizeImages(images)
  175. if (!text && imgs.length === 0) return { missingContent: true }
  176. const preview = buildPreview(text, imgs)
  177. const conv = conversationId
  178. ? await assertUserConversation(conversationId, uuid)
  179. : await this.createConversation({ uuid, title: preview ? preview.slice(0, 40) : DEFAULT_TITLE })
  180. if (!conv) return null
  181. const now = Date.now()
  182. const result = await db.query(
  183. `INSERT INTO ai_chat_message
  184. (conversation_id, create_user, role, content, images, status, error_msg, create_time, update_time)
  185. VALUES (?, ?, 'user', ?, ?, 'done', '', ?, ?)`,
  186. [conv.id, uuid, text, JSON.stringify(imgs), now, now]
  187. )
  188. const title = (!conv.title || conv.title === DEFAULT_TITLE || Number(conv.last_message_time || 0) === 0) && preview
  189. ? preview.slice(0, 40)
  190. : conv.title
  191. await db.query(
  192. 'UPDATE ai_chat_conversation SET title = ?, last_message_preview = ?, last_message_time = ?, update_time = ? WHERE id = ?',
  193. [title, preview, now, now, conv.id]
  194. )
  195. return {
  196. conversationId: conv.id,
  197. conversationNo: conv.conversation_no,
  198. messageId: result?.insertId,
  199. content: text,
  200. images: imgs,
  201. conversation: {
  202. id: conv.id,
  203. conversation_no: conv.conversation_no,
  204. title,
  205. state: ACTIVE_STATE,
  206. last_message_preview: preview,
  207. last_message_time: now,
  208. create_time: conv.create_time || now,
  209. update_time: now
  210. },
  211. message: {
  212. id: result?.insertId,
  213. conversation_id: conv.id,
  214. role: 'user',
  215. content: text,
  216. images: imgs,
  217. status: 'done',
  218. error_msg: '',
  219. create_time: now,
  220. update_time: now
  221. }
  222. }
  223. }
  224. async addAssistantMessage({ conversationId, conversationNo, content, images = [], status = 'done', errorMsg = '' }) {
  225. const whereSql = conversationNo ? 'conversation_no = ?' : 'id = ?'
  226. const whereValue = conversationNo || conversationId
  227. const rows = await db.query(
  228. `SELECT id, create_user FROM ai_chat_conversation WHERE ${whereSql} AND state = ? LIMIT 1`,
  229. [whereValue, ACTIVE_STATE]
  230. )
  231. if (!rows || rows.length !== 1) return false
  232. const targetConversationId = rows[0].id
  233. const uuid = rows[0].create_user
  234. const text = normalizeText(content, 4000)
  235. const imgs = normalizeImages(images)
  236. if (!text && imgs.length === 0 && status !== 'error') return false
  237. const now = Date.now()
  238. await db.query(
  239. `INSERT INTO ai_chat_message
  240. (conversation_id, create_user, role, content, images, status, error_msg, create_time, update_time)
  241. VALUES (?, ?, 'assistant', ?, ?, ?, ?, ?, ?)`,
  242. [targetConversationId, uuid, text, JSON.stringify(imgs), status, normalizeText(errorMsg, 200), now, now]
  243. )
  244. await db.query(
  245. 'UPDATE ai_chat_conversation SET last_message_preview = ?, last_message_time = ?, update_time = ? WHERE id = ?',
  246. [buildPreview(text, imgs), now, now, targetConversationId]
  247. )
  248. return true
  249. }
  250. async addSystemMessage({ conversationId, content, status = 'done', errorMsg = '' }) {
  251. const rows = await db.query(
  252. 'SELECT id, create_user FROM ai_chat_conversation WHERE id = ? AND state = ? LIMIT 1',
  253. [conversationId, ACTIVE_STATE]
  254. )
  255. if (!rows || rows.length !== 1) return false
  256. const now = Date.now()
  257. await db.query(
  258. `INSERT INTO ai_chat_message
  259. (conversation_id, create_user, role, content, images, status, error_msg, create_time, update_time)
  260. VALUES (?, ?, 'system', ?, JSON_ARRAY(), ?, ?, ?, ?)`,
  261. [conversationId, rows[0].create_user, normalizeText(content, 1000), status, normalizeText(errorMsg, 200), now, now]
  262. )
  263. return true
  264. }
  265. async listAdminConversations({ id, conversation_no, create_user, username, state, queryTime, current, pagesize }) {
  266. const page = normalizePage(current, pagesize)
  267. const where = ['1 = 1']
  268. const params = []
  269. const countParams = []
  270. if (id) {
  271. where.push('c.id = ?')
  272. params.push(id)
  273. countParams.push(id)
  274. }
  275. if (conversation_no) {
  276. where.push('c.conversation_no = ?')
  277. params.push(conversation_no)
  278. countParams.push(conversation_no)
  279. }
  280. if (create_user) {
  281. where.push('c.create_user LIKE ?')
  282. params.push(`%${create_user}%`)
  283. countParams.push(`%${create_user}%`)
  284. }
  285. if (username) {
  286. where.push('u.username LIKE ?')
  287. params.push(`%${username}%`)
  288. countParams.push(`%${username}%`)
  289. }
  290. if (state !== undefined && state !== null && state !== '' && Number(state) !== -1) {
  291. where.push('c.state = ?')
  292. params.push(state)
  293. countParams.push(state)
  294. }
  295. if (Array.isArray(queryTime) && queryTime.length === 2) {
  296. where.push('c.update_time >= ? AND c.update_time < ?')
  297. params.push(queryTime[0], queryTime[1])
  298. countParams.push(queryTime[0], queryTime[1])
  299. }
  300. const whereSql = where.join(' AND ')
  301. const rows = await db.query(
  302. `SELECT c.id, c.conversation_no, c.create_user, c.title, c.state, c.last_message_preview, c.last_message_time,
  303. c.create_time, c.update_time, u.username
  304. FROM ai_chat_conversation c
  305. LEFT JOIN users u ON u.uuid = c.create_user
  306. WHERE ${whereSql}
  307. ORDER BY c.update_time DESC
  308. LIMIT ? OFFSET ?`,
  309. [...params, String(page.pagesize), String(page.offset)]
  310. )
  311. const countRows = await db.query(
  312. `SELECT COUNT(*) AS total
  313. FROM ai_chat_conversation c
  314. LEFT JOIN users u ON u.uuid = c.create_user
  315. WHERE ${whereSql}`,
  316. countParams
  317. )
  318. return {
  319. data: rows || [],
  320. pagination: {
  321. current: page.current,
  322. pagesize: page.pagesize,
  323. total: Number(countRows?.[0]?.total || 0)
  324. }
  325. }
  326. }
  327. async getAdminConversationDetail({ conversationId }) {
  328. const convRows = await db.query(
  329. `SELECT c.id, c.conversation_no, c.create_user, c.title, c.state, c.last_message_preview, c.last_message_time,
  330. c.create_time, c.update_time, u.username
  331. FROM ai_chat_conversation c
  332. LEFT JOIN users u ON u.uuid = c.create_user
  333. WHERE c.id = ?
  334. LIMIT 1`,
  335. [conversationId]
  336. )
  337. if (!convRows || convRows.length !== 1) return null
  338. const msgRows = await db.query(
  339. `SELECT id, conversation_id, role, content, images, status, error_msg, create_time, update_time
  340. FROM ai_chat_message
  341. WHERE conversation_id = ?
  342. ORDER BY id ASC`,
  343. [conversationId]
  344. )
  345. return {
  346. ...convRows[0],
  347. messages: (msgRows || []).map(normalizeMessage),
  348. assistantUuid: ASSISTANT_UUID
  349. }
  350. }
  351. }
  352. module.exports = {
  353. ConversationService: new ConversationService(),
  354. normalizeImages,
  355. normalizeText,
  356. ASSISTANT_UUID
  357. }