OneBotV11.js 21 KB

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