ConversationService.js 14 KB

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