wechaty-puppet-matrix 0.0.52 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cjs/src/engine-schema.d.ts +1 -0
  2. package/dist/cjs/src/engine-schema.d.ts.map +1 -1
  3. package/dist/cjs/src/matrix/messages/message-appmsg.d.ts +1 -0
  4. package/dist/cjs/src/matrix/messages/message-appmsg.d.ts.map +1 -1
  5. package/dist/cjs/src/matrix/messages/message-audio.js +1 -1
  6. package/dist/cjs/src/matrix/messages/message-chat-history.d.ts +49 -0
  7. package/dist/cjs/src/matrix/messages/message-chat-history.d.ts.map +1 -0
  8. package/dist/cjs/src/matrix/messages/message-chat-history.js +91 -0
  9. package/dist/cjs/src/matrix/schema-mapper/message/message-parser-audio.d.ts +3 -0
  10. package/dist/cjs/src/matrix/schema-mapper/message/message-parser-audio.d.ts.map +1 -0
  11. package/dist/cjs/src/matrix/schema-mapper/message/message-parser-audio.js +44 -0
  12. package/dist/cjs/src/matrix/schema-mapper/message/message-parser-refermsg.d.ts.map +1 -1
  13. package/dist/cjs/src/matrix/schema-mapper/message/message-parser-refermsg.js +22 -1
  14. package/dist/cjs/src/matrix/schema-mapper/message/mod.d.ts.map +1 -1
  15. package/dist/cjs/src/matrix/schema-mapper/message/mod.js +2 -0
  16. package/dist/cjs/src/matrix/service/request.d.ts +7 -1
  17. package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
  18. package/dist/cjs/src/matrix/service/request.js +74 -2
  19. package/dist/cjs/src/puppet-matrix.d.ts +5 -0
  20. package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
  21. package/dist/cjs/src/puppet-matrix.js +124 -4
  22. package/dist/esm/src/engine-schema.d.ts +1 -0
  23. package/dist/esm/src/engine-schema.d.ts.map +1 -1
  24. package/dist/esm/src/matrix/messages/message-appmsg.d.ts +1 -0
  25. package/dist/esm/src/matrix/messages/message-appmsg.d.ts.map +1 -1
  26. package/dist/esm/src/matrix/messages/message-audio.js +1 -1
  27. package/dist/esm/src/matrix/messages/message-chat-history.d.ts +49 -0
  28. package/dist/esm/src/matrix/messages/message-chat-history.d.ts.map +1 -0
  29. package/dist/esm/src/matrix/messages/message-chat-history.js +87 -0
  30. package/dist/esm/src/matrix/schema-mapper/message/message-parser-audio.d.ts +3 -0
  31. package/dist/esm/src/matrix/schema-mapper/message/message-parser-audio.d.ts.map +1 -0
  32. package/dist/esm/src/matrix/schema-mapper/message/message-parser-audio.js +7 -0
  33. package/dist/esm/src/matrix/schema-mapper/message/message-parser-refermsg.d.ts.map +1 -1
  34. package/dist/esm/src/matrix/schema-mapper/message/message-parser-refermsg.js +22 -1
  35. package/dist/esm/src/matrix/schema-mapper/message/mod.d.ts.map +1 -1
  36. package/dist/esm/src/matrix/schema-mapper/message/mod.js +2 -0
  37. package/dist/esm/src/matrix/service/request.d.ts +7 -1
  38. package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
  39. package/dist/esm/src/matrix/service/request.js +74 -2
  40. package/dist/esm/src/puppet-matrix.d.ts +5 -0
  41. package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
  42. package/dist/esm/src/puppet-matrix.js +125 -5
  43. package/package.json +1 -1
  44. package/src/engine-schema.ts +1 -0
  45. package/src/matrix/messages/message-appmsg.ts +1 -0
  46. package/src/matrix/messages/message-audio.ts +1 -1
  47. package/src/matrix/messages/message-chat-history.ts +135 -0
  48. package/src/matrix/schema-mapper/message/message-parser-audio.ts +13 -0
  49. package/src/matrix/schema-mapper/message/message-parser-refermsg.ts +32 -1
  50. package/src/matrix/schema-mapper/message/mod.ts +2 -0
  51. package/src/matrix/service/request.ts +93 -2
  52. package/src/puppet-matrix.ts +142 -5
@@ -1109,6 +1109,17 @@ class Client extends EventEmitter {
1109
1109
  return res.data
1110
1110
  }
1111
1111
 
1112
+ /**
1113
+ * 清空后端配置的代理
1114
+ */
1115
+ public async clearProxy (): Promise<void> {
1116
+ const res = await this.postData({
1117
+ path: '/client/set_proxy',
1118
+ data: { proxy: '' },
1119
+ })
1120
+ log.info(PRE, 'clearProxy() res: %s', JSON.stringify(res))
1121
+ }
1122
+
1112
1123
  /**
1113
1124
  * 恢复实例
1114
1125
  */
@@ -1124,7 +1135,7 @@ class Client extends EventEmitter {
1124
1135
  },
1125
1136
  })
1126
1137
  this.restoreTimes++
1127
- log.info(PRE, 'restore() res: %s', res)
1138
+ log.info(PRE, 'restore() res: %s', JSON.stringify(res))
1128
1139
  }
1129
1140
 
1130
1141
  /**
@@ -1310,9 +1321,15 @@ class Client extends EventEmitter {
1310
1321
  /**
1311
1322
  * 获取当前bot信息
1312
1323
  */
1313
- public async getSelfInfo (): Promise<ContactPayload | boolean> {
1324
+ public async getSelfInfo (proxyCleared = false): Promise<ContactPayload | boolean> {
1314
1325
  try {
1315
1326
  const res = await this.postData({ path: '/user/get_profile', data: {} })
1327
+ // 后端配置的代理不可用时,清空代理(restore 会带上 proxy: '')后重试一次
1328
+ if (!proxyCleared && typeof res?.errmsg === 'string' && res.errmsg.includes('Could not connect to proxy')) {
1329
+ log.warn(PRE, 'proxy unavailable, clear proxy and retry: %s', res.errmsg)
1330
+ await this.clearProxy()
1331
+ return await this.getSelfInfo(true)
1332
+ }
1316
1333
  if (res.errcode === -102) {
1317
1334
  log.info(PRE, 'offline login:', res?.errmsg)
1318
1335
  return false
@@ -2004,6 +2021,80 @@ class Client extends EventEmitter {
2004
2021
  return ''
2005
2022
  }
2006
2023
 
2024
+ /**
2025
+ * 语音转文字: 1.生成 voice_id -> 2.检查 -> 3.上传语音 -> 4.获取结果
2026
+ * 文档: /msg/new_trans_voice_id, /msg/check_voice_trans, /msg/upload_voice_trans, /msg/get_voice_trans
2027
+ */
2028
+ public async newVoiceTransId (): Promise<any> {
2029
+ return await this.postData({ path: '/msg/new_trans_voice_id', data: {} })
2030
+ }
2031
+
2032
+ public async checkVoiceTrans (voiceId: string, msgId: string, length: number): Promise<any> {
2033
+ return await this.postData({ path: '/msg/check_voice_trans', data: { voice_id: voiceId, msg_id: msgId, length } })
2034
+ }
2035
+
2036
+ public async uploadVoiceTrans (voiceId: string, msgId: string, length: number): Promise<any> {
2037
+ return await this.postData({ path: '/msg/upload_voice_trans', data: { voice_id: voiceId, msg_id: msgId, length } })
2038
+ }
2039
+
2040
+ public async getVoiceTrans (voiceId: string): Promise<any> {
2041
+ return await this.postData({ path: '/msg/get_voice_trans', data: { voice_id: voiceId } })
2042
+ }
2043
+
2044
+ /**
2045
+ * 语音转文字,转写完成(endFlag === 1)返回文字,失败或超时返回空字符串
2046
+ */
2047
+ public async voiceToText (msgId: string, length: number, timeoutMs = 20000): Promise<string> {
2048
+ // timeSlice.buffer 很长且无需关注,打印时去掉
2049
+ const brief = (res: any) => JSON.stringify(res, (k, v) => (k === 'buffer' ? undefined : v))
2050
+ try {
2051
+ const idRes = await this.newVoiceTransId()
2052
+ const voiceId: string = idRes?.voice_id
2053
+ if (!voiceId) {
2054
+ log.warn(PRE, 'voiceToText: no voice_id: %s', JSON.stringify(idRes))
2055
+ return ''
2056
+ }
2057
+ // length 必须是语音数据字节数(不是时长)。check 返回 uploadCtx 时需要 upload,
2058
+ // 直到 uploadCtx 为空(后端已拿到全部数据)
2059
+ let checkRes: any
2060
+ for (let round = 0; round < 20; round++) {
2061
+ checkRes = await this.checkVoiceTrans(voiceId, msgId, length)
2062
+ log.verbose(PRE, 'voiceToText check#%d(length=%d): %s', round, length, brief(checkRes))
2063
+ if (checkRes?.baseResponse?.ret !== 0) {
2064
+ log.warn(PRE, 'voiceToText: check failed: %s', brief(checkRes))
2065
+ return ''
2066
+ }
2067
+ const ctx = checkRes?.uploadCtx
2068
+ if (!ctx?.dataLen) {
2069
+ break
2070
+ }
2071
+ const uploadRes = await this.uploadVoiceTrans(voiceId, msgId, length)
2072
+ log.verbose(PRE, 'voiceToText upload#%d: %s', round, brief(uploadRes))
2073
+ if (uploadRes?.baseResponse?.ret !== 0) {
2074
+ log.warn(PRE, 'voiceToText: upload failed: %s', brief(uploadRes))
2075
+ return ''
2076
+ }
2077
+ }
2078
+ const interval = (checkRes?.queryCtx?.interval || 2) * 1000
2079
+ const deadline = Date.now() + timeoutMs
2080
+ while (Date.now() < deadline) {
2081
+ await new Promise(resolve => setTimeout(resolve, interval))
2082
+ const res = await this.getVoiceTrans(voiceId)
2083
+ if (res?.baseResponse?.ret !== 0) {
2084
+ log.warn(PRE, 'voiceToText: get result failed: %s', brief(res))
2085
+ return ''
2086
+ }
2087
+ if (res?.transRes?.endFlag === 1) {
2088
+ return res.transRes.result || ''
2089
+ }
2090
+ }
2091
+ log.warn(PRE, 'voiceToText: timeout for msg %s', msgId)
2092
+ } catch (e) {
2093
+ log.warn(PRE, 'voiceToText: error: %s', e)
2094
+ }
2095
+ return ''
2096
+ }
2097
+
2007
2098
  /**
2008
2099
  * 下载视频
2009
2100
  * @param imageInfo
@@ -7,6 +7,7 @@ import {
7
7
  } from 'file-box'
8
8
  import type { ContactPayload, MessagePayload } from './engine-schema.js'
9
9
  import type { FileBoxMetadataMessage } from './matrix/types.js'
10
+ import { WechatMessageType } from './matrix/types.js'
10
11
  import Client from './matrix/service/request.js'
11
12
  import { getUnixTime } from 'date-fns'
12
13
 
@@ -15,7 +16,8 @@ import path from 'path'
15
16
  // 参考
16
17
  import { CacheManager, RoomMemberMap } from './matrix/cache-manager.js'
17
18
  import { isIMContactId, isRoomId } from './matrix/utils/is-type.js'
18
- import { AppMessagePayload, parseAppmsgMessagePayload } from './matrix/messages/message-appmsg.js'
19
+ import { AppMessagePayload, AppMessageType, parseAppmsgMessagePayload } from './matrix/messages/message-appmsg.js'
20
+ import { parseRecordItem, RecordItem, RecordItemType } from './matrix/messages/message-chat-history.js'
19
21
  import { parseMiniProgramMessagePayload } from './matrix/messages/message-miniprogram.js'
20
22
  import { parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
21
23
  import { ImageMessagePayload, parseImageMessagePayload } from './matrix/messages/message-image.js'
@@ -858,6 +860,94 @@ class PuppetMatrix extends PUPPET.Puppet {
858
860
  return locationPayload
859
861
  }
860
862
 
863
+ /**
864
+ * 合并转发消息明细
865
+ */
866
+ override async messageChatHistory (messageId: string): Promise<PUPPET.payloads.ChatHistory[]> {
867
+ const rawPayload = await this.messageRawPayload(messageId)
868
+ const appPayload = await parseAppmsgMessagePayload(rawPayload.msg)
869
+ if (appPayload.type !== AppMessageType.ChatHistory) {
870
+ throw new Error('Can not get chat history from non chat history payload')
871
+ }
872
+ log.verbose(PRE, `messageChatHistory recorditem: ${appPayload.recorditem}`)
873
+ if (!appPayload.recorditem) {
874
+ return []
875
+ }
876
+ const info = await parseRecordItem(appPayload.recorditem)
877
+ return Promise.all(info.items.map(item => this._recordItemToChatHistory(item)))
878
+ }
879
+
880
+ private async _recordItemToChatHistory (item: RecordItem): Promise<PUPPET.payloads.ChatHistory> {
881
+ const base = {
882
+ avatar: item.sourceHeadUrl ? FileBox.fromUrl(item.sourceHeadUrl) : FileBox.fromBuffer(Buffer.alloc(0), 'avatar.png'),
883
+ corpName: '',
884
+ senderName: item.sourceName,
885
+ // sourcetime 形如 "2024-12-30 12:00:00",解析失败则为 0
886
+ time: Math.floor((Date.parse(item.sourceTime.replace(/-/g, '/')) || 0) / 1000),
887
+ }
888
+ const unknown = { ...base, message: undefined as never, title: item.title, type: PUPPET.types.Message.Unknown } as PUPPET.payloads.ChatHistory
889
+ const media = async (kind: 'image' | 'video' | 'file' | 'emoticon'): Promise<FileBoxInterface | undefined> => {
890
+ if (!item.cdnDataUrl) {
891
+ return undefined
892
+ }
893
+ const params = { aes_key: item.cdnDataKey, file_id: item.cdnDataUrl, file_name: item.fileName || item.title, file_size: item.dataSize }
894
+ const url = kind === 'image' || kind === 'emoticon'
895
+ ? await this._client?.downloadImage(params as any)
896
+ : kind === 'video' ? await this._client?.downloadVideo(params as any) : await this._client?.downloadFile(params as any)
897
+ return url ? FileBox.fromUrl(url, item.fileName ? { name: item.fileName } : undefined) : undefined
898
+ }
899
+
900
+ try {
901
+ switch (item.type) {
902
+ case RecordItemType.Text:
903
+ return { ...base, message: item.desc, type: PUPPET.types.Message.Text }
904
+ case RecordItemType.Image: {
905
+ const box = await media('image')
906
+ return box ? { ...base, message: box, type: PUPPET.types.Message.Image } : unknown
907
+ }
908
+ case RecordItemType.Video: {
909
+ const box = await media('video')
910
+ return box ? { ...base, message: box, type: PUPPET.types.Message.Video } : unknown
911
+ }
912
+ case RecordItemType.File: {
913
+ const box = await media('file')
914
+ return box ? { ...base, message: box, type: PUPPET.types.Message.Attachment } : unknown
915
+ }
916
+ case RecordItemType.Emoticon: {
917
+ const box = await media('emoticon')
918
+ return box ? { ...base, message: box, type: PUPPET.types.Message.Emoticon } : unknown
919
+ }
920
+ case RecordItemType.Link:
921
+ if (!item.link) return unknown
922
+ return {
923
+ ...base,
924
+ message: { description: item.link.desc, thumbnailUrl: item.link.thumbUrl, title: item.link.title, url: item.link.url },
925
+ type: PUPPET.types.Message.Url,
926
+ }
927
+ case RecordItemType.Location:
928
+ if (!item.location) return unknown
929
+ return {
930
+ ...base,
931
+ message: { accuracy: 15, address: item.location.label, latitude: item.location.lat, longitude: item.location.lng, name: item.location.poiname },
932
+ type: PUPPET.types.Message.Location,
933
+ }
934
+ case RecordItemType.ChatHistory:
935
+ return {
936
+ ...base,
937
+ message: await Promise.all((item.children || []).map(child => this._recordItemToChatHistory(child))),
938
+ title: item.title,
939
+ type: PUPPET.types.Message.ChatHistory,
940
+ }
941
+ default:
942
+ log.verbose(PRE, `unsupported chat history item type: ${item.type}`)
943
+ return unknown
944
+ }
945
+ } catch (e) {
946
+ log.warn(PRE, `parse chat history item failed, type: ${item.type}, error: ${e}`)
947
+ return unknown
948
+ }
949
+ }
950
+
861
951
  /****************************************************************************
862
952
  * send message
863
953
  ***************************************************************************/
@@ -1206,6 +1296,17 @@ class PuppetMatrix extends PUPPET.Puppet {
1206
1296
  return ret
1207
1297
  }
1208
1298
 
1299
+ /**
1300
+ * 缓存里没有该消息时才写入(用于被引用消息的兜底)
1301
+ */
1302
+ async cacheMessageIfAbsent (payload: MessagePayload): Promise<void> {
1303
+ if (await this._cacheMgr!.hasMessage(payload.id)) {
1304
+ return
1305
+ }
1306
+ log.info(PRE, `cache miss for refer message ${payload.id}, rebuild from refermsg`)
1307
+ await this._cacheMgr!.setMessage(payload.id, payload)
1308
+ }
1309
+
1209
1310
  /**
1210
1311
  * 群数据格式化为Wechaty 支持类型
1211
1312
  * @param rawPayload
@@ -1593,6 +1694,31 @@ class PuppetMatrix extends PUPPET.Puppet {
1593
1694
  }
1594
1695
  }
1595
1696
 
1697
+ /**
1698
+ * 收到语音消息时转文字,结果放到 message.voiceText,由 audio parser 写入消息 text。
1699
+ * 自己发出的语音不处理;失败或超时保持原样
1700
+ */
1701
+ private async _transcribeVoice (message: MessagePayload): Promise<void> {
1702
+ if (message.msgType !== WechatMessageType.Voice || message.talkerId === this.currentUserId) {
1703
+ return
1704
+ }
1705
+ try {
1706
+ const audio = await parseAudioMessagePayload(message)
1707
+ // length 是语音数据字节数,voicelength 是时长(毫秒);接口的 length 参数要的是字节数
1708
+ const length = parseInt(audio.length, 10)
1709
+ if (!length) {
1710
+ return
1711
+ }
1712
+ const text = await this._client?.voiceToText(message.id, length)
1713
+ if (text) {
1714
+ message.voiceText = text
1715
+ await this._cacheMgr!.setMessage(message.id, message)
1716
+ }
1717
+ } catch (e) {
1718
+ log.warn(PRE, `transcribe voice failed for ${message.id}: ${e}`)
1719
+ }
1720
+ }
1721
+
1596
1722
  private async _onPushMessage (message: MessagePayload): Promise<void> {
1597
1723
  const messageId = message.id
1598
1724
  log.silly(PRE, `on push original message: ${JSON.stringify(message)}`)
@@ -1600,6 +1726,7 @@ class PuppetMatrix extends PUPPET.Puppet {
1600
1726
  return
1601
1727
  }
1602
1728
  await this._cacheMgr!.setMessage(message.id, message)
1729
+ await this._transcribeVoice(message)
1603
1730
  const event = await parseEvent(this, message)
1604
1731
  log.info('event-------', JSON.stringify(event))
1605
1732
  switch (event.type) {
@@ -1751,13 +1878,23 @@ class PuppetMatrix extends PUPPET.Puppet {
1751
1878
  * @param messagePayload
1752
1879
  * @private
1753
1880
  */
1881
+ /**
1882
+ * 下载失败(后端没返回 url,常见于媒体已过期)时给出明确错误,而不是 FileBox 的 Invalid URL
1883
+ */
1884
+ private _fileBoxFromUrl (url: string | undefined, messageId: string, name?: string): FileBoxInterface {
1885
+ if (!url) {
1886
+ throw new Error(`download media failed for message ${messageId}: backend returned no url (media may have expired)`)
1887
+ }
1888
+ return name ? FileBox.fromUrl(url, { name }) : FileBox.fromUrl(url)
1889
+ }
1890
+
1754
1891
  private async _getMessageImageFileBox (messageId: string, messagePayload: MessagePayload): Promise<FileBoxInterface> {
1755
1892
  const imageInfo: ImageMessagePayload = await parseImageMessagePayload(messagePayload)
1756
1893
  if (!imageInfo.file_id) {
1757
1894
  log.error(`Can not get file for message: ${messageId}`)
1758
1895
  }
1759
1896
  const url = imageInfo.is_ww ? await this._client?.downloadWWMedia(imageInfo, 1) : await this._client?.downloadImage(imageInfo)
1760
- return FileBox.fromUrl(url)
1897
+ return this._fileBoxFromUrl(url, messageId)
1761
1898
  }
1762
1899
 
1763
1900
  private async _getMessageAudioFileBox (messageId: string, messagePayload: MessagePayload): Promise<FileBoxInterface> {
@@ -1766,7 +1903,7 @@ class PuppetMatrix extends PUPPET.Puppet {
1766
1903
  log.error(`Can not get file for message: ${messageId}`)
1767
1904
  }
1768
1905
  const url = await this._client?.downloadAudio(messageId, audioInfo)
1769
- return FileBox.fromUrl(url)
1906
+ return this._fileBoxFromUrl(url, messageId)
1770
1907
  }
1771
1908
 
1772
1909
  private async _getMessageVideoFileBox (messageId: string, messagePayload: MessagePayload): Promise<FileBoxInterface> {
@@ -1776,7 +1913,7 @@ class PuppetMatrix extends PUPPET.Puppet {
1776
1913
  }
1777
1914
  const url = videoInfo.is_ww ? await this._client?.downloadWWMedia(videoInfo, 2) : await this._client?.downloadVideo(videoInfo)
1778
1915
  log.info(PRE, '_getMessageVideoFileBox(): %s', url)
1779
- return FileBox.fromUrl(url)
1916
+ return this._fileBoxFromUrl(url, messageId)
1780
1917
  }
1781
1918
 
1782
1919
  /**
@@ -1805,7 +1942,7 @@ class PuppetMatrix extends PUPPET.Puppet {
1805
1942
  log.error(`Can not get file url: ${JSON.stringify(params)}`)
1806
1943
  }
1807
1944
 
1808
- return FileBox.fromUrl(url, { name: appPayload.title })
1945
+ return this._fileBoxFromUrl(url, messageId, appPayload.title)
1809
1946
  }
1810
1947
 
1811
1948
  // @ts-ignore