OneBotV11.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. const path = require('path')
  2. const WebSocket = require('ws')
  3. const config = require('../../config.json')
  4. const Logger = require('../../lib/Logger')
  5. const db = require('../DataBase/db.js')
  6. const EmailTemplate = require('../Email/emailTemplate')
  7. const { ConversationService } = require('../../lib/AIChat/ConversationService')
  8. const logger = new Logger(path.join(__dirname, '../../logs/OneBotV11.log'), 'INFO')
  9. const BOT_KIND_WORKORDER = 'workorder'
  10. const BOT_KIND_AI_CHAT = 'aiChat'
  11. function createState(kind) {
  12. return {
  13. kind,
  14. wsClient: null,
  15. connectingPromise: null,
  16. reconnectTimer: null,
  17. wsApiClient: null,
  18. connectingApiPromise: null,
  19. apiReconnectTimer: null
  20. }
  21. }
  22. const botStates = {
  23. [BOT_KIND_WORKORDER]: createState(BOT_KIND_WORKORDER),
  24. [BOT_KIND_AI_CHAT]: createState(BOT_KIND_AI_CHAT)
  25. }
  26. function stringifySafe(data) {
  27. try {
  28. return JSON.stringify(data)
  29. } catch (_) {
  30. return '[unserializable]'
  31. }
  32. }
  33. function getRawConfig(kind) {
  34. if (kind === BOT_KIND_AI_CHAT) return config.aiChatOnebotv11 || {}
  35. return config.onebotv11 || {}
  36. }
  37. function getOneBotConfig(kind = BOT_KIND_WORKORDER) {
  38. const onebot = getRawConfig(kind)
  39. const isAiChat = kind === BOT_KIND_AI_CHAT
  40. const defaults = isAiChat
  41. ? {
  42. reverseWsPort: 15701,
  43. senderNickname: 'Xiaoyan Assistant Web User',
  44. botName: 'Xiaoyan Assistant',
  45. botUuid: 'onebot-v11-xiaoyan-chat'
  46. }
  47. : {
  48. reverseWsPort: 15700,
  49. senderNickname: 'Ticket System',
  50. botName: 'Xiaoyan Assistant',
  51. botUuid: 'onebot-v11-xiaoyan-assistant'
  52. }
  53. return {
  54. enabled: onebot.enabled === true,
  55. transport: onebot.transport || 'reverse_ws',
  56. reverseWsUrl: onebot.reverseWsUrl || '',
  57. reverseWsHost: onebot.reverseWsHost || '127.0.0.1',
  58. reverseWsPort: Number(onebot.reverseWsPort || defaults.reverseWsPort),
  59. reverseWsPath: onebot.reverseWsPath || '',
  60. reverseWsToken: onebot.reverseWsToken || '',
  61. callbackToken: onebot.callbackToken || '',
  62. eventClientRole: onebot.eventClientRole || 'Universal',
  63. apiClientRole: onebot.apiClientRole || 'Api',
  64. enableApiWs: onebot.enableApiWs !== false,
  65. ticketSenderNickname: onebot.ticketSenderNickname || defaults.senderNickname,
  66. chatSenderNickname: onebot.chatSenderNickname || onebot.ticketSenderNickname || defaults.senderNickname,
  67. selfId: onebot.selfId || '',
  68. botName: onebot.botName || defaults.botName,
  69. botUuid: onebot.botUuid || defaults.botUuid
  70. }
  71. }
  72. function getWorkOrderOneBotConfig() {
  73. return getOneBotConfig(BOT_KIND_WORKORDER)
  74. }
  75. function getAiChatOneBotConfig() {
  76. return getOneBotConfig(BOT_KIND_AI_CHAT)
  77. }
  78. function getReverseWsUrl(onebot) {
  79. const defaultPath = '/ws'
  80. const explicitPath = onebot.reverseWsPath
  81. ? (onebot.reverseWsPath.startsWith('/') ? onebot.reverseWsPath : `/${onebot.reverseWsPath}`)
  82. : null
  83. if (onebot.reverseWsUrl) {
  84. try {
  85. const u = new URL(onebot.reverseWsUrl)
  86. if (explicitPath) {
  87. u.pathname = explicitPath
  88. return u.href
  89. }
  90. if (u.pathname && u.pathname !== '/') return u.href
  91. u.pathname = defaultPath
  92. return u.href
  93. } catch (_) {
  94. return onebot.reverseWsUrl
  95. }
  96. }
  97. const pathPart = explicitPath || defaultPath
  98. return `ws://${onebot.reverseWsHost}:${onebot.reverseWsPort}${pathPart}`
  99. }
  100. function getState(kind) {
  101. return botStates[kind] || botStates[BOT_KIND_WORKORDER]
  102. }
  103. function getBotLabel(kind) {
  104. return kind === BOT_KIND_AI_CHAT ? 'AIChat OneBot v11' : 'WorkOrder OneBot v11'
  105. }
  106. function scheduleReconnect(kind) {
  107. const state = getState(kind)
  108. if (state.reconnectTimer) return
  109. const onebot = getOneBotConfig(kind)
  110. logger.info(`${getBotLabel(kind)} ws reconnect scheduled in 3s`)
  111. state.reconnectTimer = setTimeout(() => {
  112. state.reconnectTimer = null
  113. connectReverseWs(kind).catch(err => {
  114. logger.error(`${getBotLabel(kind)} reconnect failed: ${err.message}`)
  115. })
  116. }, 3000)
  117. if (!onebot.enabled && state.reconnectTimer) {
  118. clearTimeout(state.reconnectTimer)
  119. state.reconnectTimer = null
  120. }
  121. }
  122. function scheduleApiReconnect(kind) {
  123. const state = getState(kind)
  124. if (state.apiReconnectTimer) return
  125. const onebot = getOneBotConfig(kind)
  126. logger.info(`${getBotLabel(kind)} API ws reconnect scheduled in 3s`)
  127. state.apiReconnectTimer = setTimeout(() => {
  128. state.apiReconnectTimer = null
  129. connectApiReverseWs(kind).catch(err => {
  130. logger.error(`${getBotLabel(kind)} API ws reconnect failed: ${err.message}`)
  131. })
  132. }, 3000)
  133. if (!onebot.enabled && state.apiReconnectTimer) {
  134. clearTimeout(state.apiReconnectTimer)
  135. state.apiReconnectTimer = null
  136. }
  137. }
  138. async function connectReverseWs(kind = BOT_KIND_WORKORDER) {
  139. const state = getState(kind)
  140. if (state.wsClient && state.wsClient.readyState === WebSocket.OPEN) return state.wsClient
  141. if (state.connectingPromise) return state.connectingPromise
  142. const onebot = getOneBotConfig(kind)
  143. if (!onebot.enabled) throw new Error(`${getBotLabel(kind)} is disabled`)
  144. if (onebot.transport !== 'reverse_ws') throw new Error(`${getBotLabel(kind)} only supports reverse_ws`)
  145. const wsUrl = getReverseWsUrl(onebot)
  146. logger.info(`${getBotLabel(kind)} connecting reverse ws: ${wsUrl}`)
  147. if (!onebot.selfId) throw new Error(`${getBotLabel(kind)} missing selfId`)
  148. const selfIdStr = String(onebot.selfId).trim()
  149. if (!selfIdStr) throw new Error(`${getBotLabel(kind)} selfId is empty`)
  150. const headers = {
  151. 'User-Agent': 'runforge/1.0 (OneBot-v11-reverse-ws-client)',
  152. 'X-Client-Role': onebot.eventClientRole,
  153. 'X-Self-ID': selfIdStr
  154. }
  155. if (onebot.reverseWsToken) headers.Authorization = `Bearer ${onebot.reverseWsToken}`
  156. state.connectingPromise = new Promise((resolve, reject) => {
  157. const client = new WebSocket(wsUrl, { headers })
  158. let opened = false
  159. const connTimeout = setTimeout(() => {
  160. if (!opened) {
  161. try { client.terminate() } catch (_) { }
  162. reject(new Error(`${getBotLabel(kind)} connect timeout: ${wsUrl}`))
  163. }
  164. }, 5000)
  165. client.on('unexpected-response', (_req, res) => {
  166. const statusCode = res && res.statusCode
  167. const statusMessage = res && res.statusMessage
  168. const rh = res && typeof res.getHeaders === 'function' ? res.getHeaders() : {}
  169. const rawPreview = res && Array.isArray(res.rawHeaders) ? res.rawHeaders.slice(0, 24) : []
  170. logger.error(`${getBotLabel(kind)} ws unexpected-response: ${statusCode} ${statusMessage}`)
  171. logger.error(`${getBotLabel(kind)} ws response headers: ${stringifySafe({ rh, rawPreview })}`)
  172. })
  173. client.on('open', () => {
  174. clearTimeout(connTimeout)
  175. opened = true
  176. state.wsClient = client
  177. logger.info(`${getBotLabel(kind)} reverse ws connected: ${wsUrl}, selfId=${onebot.selfId}`)
  178. client.send(JSON.stringify(buildLifecycleConnectEvent(onebot)), (err) => {
  179. if (err) {
  180. logger.error(`${getBotLabel(kind)} lifecycle event failed: ${err.message}`)
  181. return
  182. }
  183. logger.info(`${getBotLabel(kind)} lifecycle connect event sent`)
  184. })
  185. resolve(client)
  186. })
  187. client.on('error', (err) => {
  188. clearTimeout(connTimeout)
  189. if (!opened) reject(err)
  190. logger.error(`${getBotLabel(kind)} ws error: ${err.message}`)
  191. })
  192. client.on('close', () => {
  193. state.wsClient = null
  194. state.connectingPromise = null
  195. logger.error(`${getBotLabel(kind)} ws closed`)
  196. if (getOneBotConfig(kind).enabled) scheduleReconnect(kind)
  197. })
  198. client.on('message', (raw) => {
  199. const text = typeof raw === 'string' ? raw : raw.toString('utf8')
  200. logger.info(`${getBotLabel(kind)} received: ${text.slice(0, 1000)}`)
  201. handleWsIncomingFrame(text, client, kind).catch((err) => {
  202. logger.error(`${getBotLabel(kind)} incoming frame failed: ${err.message}`)
  203. })
  204. })
  205. })
  206. try {
  207. return await state.connectingPromise
  208. } finally {
  209. state.connectingPromise = null
  210. }
  211. }
  212. async function connectApiReverseWs(kind = BOT_KIND_WORKORDER) {
  213. const state = getState(kind)
  214. if (state.wsApiClient && state.wsApiClient.readyState === WebSocket.OPEN) return state.wsApiClient
  215. if (state.connectingApiPromise) return state.connectingApiPromise
  216. const onebot = getOneBotConfig(kind)
  217. if (!onebot.enabled) throw new Error(`${getBotLabel(kind)} is disabled`)
  218. if (onebot.transport !== 'reverse_ws') throw new Error(`${getBotLabel(kind)} only supports reverse_ws`)
  219. const wsUrl = getReverseWsUrl(onebot)
  220. if (!onebot.selfId) throw new Error(`${getBotLabel(kind)} API ws missing selfId`)
  221. const selfIdStr = String(onebot.selfId).trim()
  222. if (!selfIdStr) throw new Error(`${getBotLabel(kind)} API ws selfId is empty`)
  223. const headers = {
  224. 'User-Agent': 'runforge/1.0 (OneBot-v11-reverse-ws-api-client)',
  225. 'X-Client-Role': onebot.apiClientRole,
  226. 'X-Self-ID': selfIdStr
  227. }
  228. if (onebot.reverseWsToken) headers.Authorization = `Bearer ${onebot.reverseWsToken}`
  229. state.connectingApiPromise = new Promise((resolve, reject) => {
  230. const client = new WebSocket(wsUrl, { headers })
  231. let opened = false
  232. const connTimeout = setTimeout(() => {
  233. if (!opened) {
  234. try { client.terminate() } catch (_) { }
  235. reject(new Error(`${getBotLabel(kind)} API ws connect timeout: ${wsUrl}`))
  236. }
  237. }, 5000)
  238. client.on('open', () => {
  239. clearTimeout(connTimeout)
  240. opened = true
  241. state.wsApiClient = client
  242. logger.info(`${getBotLabel(kind)} API ws connected: ${wsUrl}`)
  243. resolve(client)
  244. })
  245. client.on('error', (err) => {
  246. clearTimeout(connTimeout)
  247. if (!opened) reject(err)
  248. logger.error(`${getBotLabel(kind)} API ws error: ${err.message}`)
  249. })
  250. client.on('close', () => {
  251. state.wsApiClient = null
  252. state.connectingApiPromise = null
  253. logger.error(`${getBotLabel(kind)} API ws closed`)
  254. if (getOneBotConfig(kind).enabled) scheduleApiReconnect(kind)
  255. })
  256. client.on('message', (raw) => {
  257. const text = typeof raw === 'string' ? raw : raw.toString('utf8')
  258. logger.info(`${getBotLabel(kind)} API ws received: ${text.slice(0, 1000)}`)
  259. handleWsIncomingFrame(text, client, kind).catch((err) => {
  260. logger.error(`${getBotLabel(kind)} API frame failed: ${err.message}`)
  261. })
  262. })
  263. })
  264. try {
  265. return await state.connectingApiPromise
  266. } finally {
  267. state.connectingApiPromise = null
  268. }
  269. }
  270. function buildSyntheticPrivateMessageEvent(text, onebot, orderId) {
  271. const now = Math.floor(Date.now() / 1000)
  272. const selfId = String(onebot.selfId).trim()
  273. const senderIdStr = String(orderId || '').trim()
  274. if (!senderIdStr || !/^\d+$/.test(senderIdStr) || senderIdStr === selfId) {
  275. throw new Error('Invalid orderId for OneBot user_id')
  276. }
  277. const messageArr = [{ type: 'text', data: { text } }]
  278. const messageId = Math.floor(Math.random() * 2000000000) + 1
  279. return {
  280. time: now,
  281. self_id: selfId,
  282. post_type: 'message',
  283. message_type: 'private',
  284. sub_type: 'friend',
  285. message_id: messageId,
  286. user_id: senderIdStr,
  287. message: messageArr,
  288. raw_message: text,
  289. font: 0,
  290. sender: {
  291. user_id: senderIdStr,
  292. nickname: onebot.ticketSenderNickname || 'Ticket System',
  293. sex: 'unknown',
  294. age: 0
  295. }
  296. }
  297. }
  298. function buildSyntheticAiChatMessageEvent(payload, onebot) {
  299. const now = Math.floor(Date.now() / 1000)
  300. const selfId = String(onebot.selfId).trim()
  301. const senderIdStr = String(payload.conversationNo || payload.conversationId || '').trim()
  302. if (!senderIdStr || !/^\d+$/.test(senderIdStr) || senderIdStr === selfId) {
  303. throw new Error('Invalid conversationId for OneBot user_id')
  304. }
  305. const lines = [
  306. `[AIChat#${senderIdStr}]`,
  307. `user_uuid: ${payload.senderUuid || '-'}`,
  308. 'content:',
  309. payload.content || ''
  310. ]
  311. const messageArr = [{ type: 'text', data: { text: lines.join('\n') } }]
  312. for (const url of payload.images || []) {
  313. messageArr.push({ type: 'image', data: { file: url } })
  314. }
  315. const rawImages = (payload.images || []).length > 0
  316. ? `\nimages:\n${(payload.images || []).join('\n')}`
  317. : ''
  318. const rawMessage = lines.join('\n') + rawImages
  319. const messageId = Math.floor(Math.random() * 2000000000) + 1
  320. return {
  321. time: now,
  322. self_id: selfId,
  323. post_type: 'message',
  324. message_type: 'private',
  325. sub_type: 'friend',
  326. message_id: messageId,
  327. user_id: senderIdStr,
  328. message: messageArr,
  329. raw_message: rawMessage,
  330. font: 0,
  331. sender: {
  332. user_id: senderIdStr,
  333. nickname: onebot.chatSenderNickname || 'Xiaoyan Assistant Web User',
  334. sex: 'unknown',
  335. age: 0
  336. }
  337. }
  338. }
  339. function buildLifecycleConnectEvent(onebot) {
  340. return {
  341. time: Math.floor(Date.now() / 1000),
  342. self_id: String(onebot.selfId).trim(),
  343. post_type: 'meta_event',
  344. meta_event_type: 'lifecycle',
  345. sub_type: 'connect'
  346. }
  347. }
  348. async function sendImplementationEvent(eventObj, kind = BOT_KIND_WORKORDER) {
  349. const client = await connectReverseWs(kind)
  350. const line = stringifySafe(eventObj)
  351. logger.info(`${getBotLabel(kind)} reporting event post_type=${eventObj.post_type} message_type=${eventObj.message_type} len=${line.length}`)
  352. return new Promise((resolve, reject) => {
  353. client.send(line, (err) => {
  354. if (err) {
  355. logger.error(`${getBotLabel(kind)} event send failed: ${err.message}`)
  356. return reject(err)
  357. }
  358. logger.info(`${getBotLabel(kind)} event written to WebSocket`)
  359. resolve(true)
  360. })
  361. })
  362. }
  363. function decodeMessageText(message) {
  364. if (!message) return ''
  365. if (typeof message === 'string') return message
  366. if (Array.isArray(message)) {
  367. return message.map(seg => {
  368. if (typeof seg === 'string') return seg
  369. if (seg && seg.type === 'text' && seg.data && typeof seg.data.text === 'string') return seg.data.text
  370. return ''
  371. }).join('')
  372. }
  373. return String(message)
  374. }
  375. async function persistAiReplyAsServerReply(orderId, plainText) {
  376. const parsedOrderId = Number(orderId)
  377. if (!Number.isInteger(parsedOrderId) || parsedOrderId <= 0) return false
  378. const rows = await db.query('SELECT msg, state, email FROM work_order WHERE id = ?', [parsedOrderId])
  379. if (!rows || rows.length !== 1 || rows[0].state === 2) return false
  380. const now = Date.now()
  381. const msg = rows[0].msg || []
  382. msg.push({
  383. time: now,
  384. content: plainText,
  385. files: [],
  386. uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
  387. type: 'ai'
  388. })
  389. await db.query('UPDATE work_order SET msg = ?, update_time = ?, state = 3 WHERE id = ?', [msg, now, parsedOrderId])
  390. if (rows[0].email) {
  391. await EmailTemplate.orderNewReply(rows[0].email, { id: parsedOrderId, content: plainText, files: [] })
  392. }
  393. return true
  394. }
  395. async function persistAiChatReply(conversationId, plainText) {
  396. const parsedConversationNo = String(conversationId || '').trim()
  397. if (!/^\d+$/.test(parsedConversationNo)) return false
  398. return await ConversationService.addAssistantMessage({
  399. conversationNo: parsedConversationNo,
  400. content: plainText
  401. })
  402. }
  403. function ackOneBotApiRequest(j, client, kind) {
  404. if (j.action === undefined || j.echo === undefined || typeof j.echo !== 'object' || j.echo === null || j.echo.seq === undefined) {
  405. return false
  406. }
  407. const resp = {
  408. status: 'ok',
  409. retcode: 0,
  410. data: null,
  411. echo: j.echo
  412. }
  413. try {
  414. client.send(JSON.stringify(resp))
  415. logger.info(`${getBotLabel(kind)} API request acked: action=${j.action} seq=${JSON.stringify(j.echo.seq)}`)
  416. } catch (err) {
  417. logger.error(`${getBotLabel(kind)} API ack failed: ${err.message}`)
  418. }
  419. return true
  420. }
  421. async function handleWsIncomingFrame(text, client, kind = BOT_KIND_WORKORDER) {
  422. let j
  423. try {
  424. j = JSON.parse(text)
  425. } catch (_) {
  426. return
  427. }
  428. if (j == null || typeof j !== 'object') return
  429. if (j.action === 'send_private_msg' && j.params) {
  430. const plainText = decodeMessageText(j.params.message)
  431. logger.info(`${getBotLabel(kind)} decoded private message: ${plainText}`)
  432. if (kind === BOT_KIND_AI_CHAT) {
  433. await persistAiChatReply(j.params.user_id, plainText)
  434. } else {
  435. await persistAiReplyAsServerReply(j.params.user_id, plainText)
  436. }
  437. }
  438. if (ackOneBotApiRequest(j, client, kind)) return
  439. if ('post_type' in j) return
  440. if (j.echo !== undefined && j.status !== undefined) {
  441. if (j.retcode !== 0 && j.retcode !== undefined) {
  442. const wording = j.wording || j.message || stringifySafe(j.data)
  443. logger.error(`${getBotLabel(kind)} API failed echo=${j.echo} retcode=${j.retcode} ${wording}`)
  444. } else {
  445. logger.info(`${getBotLabel(kind)} API success echo=${j.echo}`)
  446. }
  447. }
  448. }
  449. function buildOrderText(payload) {
  450. const {
  451. orderId,
  452. title,
  453. role,
  454. content,
  455. files = [],
  456. senderUuid
  457. } = payload
  458. const fileText = files.length > 0 ? `\nattachments:\n${files.join('\n')}` : ''
  459. return [
  460. `[Ticket#${orderId}] ${title ? title : 'Ticket message updated'}`,
  461. `sender: ${role === '用户' ? 'user' : 'system'}`,
  462. `user_uuid: ${senderUuid || '-'}`,
  463. 'content:',
  464. content
  465. ].join('\n') + fileText
  466. }
  467. async function sendOrderMessage(payload) {
  468. const onebot = getWorkOrderOneBotConfig()
  469. if (!onebot.enabled) return false
  470. if (onebot.transport !== 'reverse_ws') {
  471. logger.error('WorkOrder OneBot v11 transport error: only reverse_ws is supported')
  472. return false
  473. }
  474. const text = buildOrderText(payload)
  475. const eventObj = buildSyntheticPrivateMessageEvent(text, onebot, payload.orderId)
  476. await sendImplementationEvent(eventObj, BOT_KIND_WORKORDER)
  477. logger.info(`WorkOrder OneBot event reported: orderId=${payload.orderId}, userId=${eventObj.user_id}, messageId=${eventObj.message_id}`)
  478. return true
  479. }
  480. async function sendAiChatMessage(payload) {
  481. const onebot = getAiChatOneBotConfig()
  482. if (!onebot.enabled) throw new Error('AIChat OneBot v11 is disabled')
  483. if (onebot.transport !== 'reverse_ws') {
  484. logger.error('AIChat OneBot v11 transport error: only reverse_ws is supported')
  485. throw new Error('AIChat OneBot v11 transport error')
  486. }
  487. const eventObj = buildSyntheticAiChatMessageEvent(payload, onebot)
  488. await sendImplementationEvent(eventObj, BOT_KIND_AI_CHAT)
  489. logger.info(`AIChat OneBot event reported: conversationId=${payload.conversationId}, conversationNo=${payload.conversationNo}, userId=${eventObj.user_id}, messageId=${eventObj.message_id}`)
  490. return true
  491. }
  492. async function initOneBotKind(kind) {
  493. const onebot = getOneBotConfig(kind)
  494. if (!onebot.enabled) {
  495. logger.info(`${getBotLabel(kind)} disabled, skip ws init`)
  496. return false
  497. }
  498. if (onebot.transport !== 'reverse_ws') {
  499. logger.error(`${getBotLabel(kind)} init failed: transport is not reverse_ws`)
  500. return false
  501. }
  502. await connectReverseWs(kind)
  503. if (onebot.enableApiWs) {
  504. await connectApiReverseWs(kind)
  505. } else {
  506. logger.info(`${getBotLabel(kind)} API ws disabled by config`)
  507. }
  508. return true
  509. }
  510. async function initOneBotWs() {
  511. const results = await Promise.allSettled([
  512. initOneBotKind(BOT_KIND_WORKORDER),
  513. initOneBotKind(BOT_KIND_AI_CHAT)
  514. ])
  515. let anyReady = false
  516. for (const [idx, result] of results.entries()) {
  517. const kind = idx === 0 ? BOT_KIND_WORKORDER : BOT_KIND_AI_CHAT
  518. if (result.status === 'fulfilled') {
  519. anyReady = anyReady || result.value === true
  520. continue
  521. }
  522. logger.error(`${getBotLabel(kind)} init failed: ${result.reason && result.reason.message ? result.reason.message : result.reason}`)
  523. }
  524. return anyReady
  525. }
  526. module.exports = {
  527. sendOrderMessage,
  528. sendAiChatMessage,
  529. getOneBotConfig,
  530. getWorkOrderOneBotConfig,
  531. getAiChatOneBotConfig,
  532. initOneBotWs
  533. }