ConversationService.js 14 KB

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