七夕將至,又到了各位程序猿給女友,老婆送禮物的節日。今年老婆規定了,不能花費太多錢,還禁止買淘寶直男禮物。真的太難了😿,想破頭皮也不知道送啥好,頭發卻已經掉了一縷又一縷,什么代碼綻放煙花,照片牆,哄老婆的機器人都做過了。這次怎么辦呢,又不讓花錢,又要有想法,看來只能祭起我的大殺器,碼代碼過七夕了。看到老婆之前喜歡玩抖音,P照片。還經常會用到人臉卡通化,人臉年齡變化,人臉性別變化的特效。那我就想,何不做一個微信機器人,你發照片我幫你自動生成特效,不用任何APP就能實現,還能讓老婆拉閨蜜建個微信群一塊玩。
想好了就開干,之前寫過一個《三步教你用Node做一個微信哄女友(基友)神器》,所以這次再寫一個機器人也不算太難,只是要提前找好相應的圖片生成接口才行,經過一番資料查找,發現騰訊雲有個人臉變換的功能,經過測試后,發現就是我想要的功能,而且效果還不錯,關鍵是每個月有 1000 次的免費額度,這就很香了。三種轉換模式就是 3000 次,白嫖不香么 😏,白嫖騰訊這就更香了,哈哈
功能介紹
本次實現的主要功能是發送照片,根據選擇生成對應的特效。微信機器人的主要實現用的還是Wechaty,協議是基於免費版web協議的,所以不用擔心沒有Wechaty的付費token,如果說你的微信沒法登陸網頁版微信,沒關系wechaty-puppet-wechat
協議是基於 UOS 桌面版的,新賬號也可以用。
已實現功能:
私聊和群內都可以實現照片特效實現
- 多輪交互式對話實現
- 人臉照片動漫化
- 人臉年齡變化
- 人臉性別轉換
效果展示
提前准備騰訊雲賬號
開通照片轉換功能
登錄騰訊雲賬號,沒有就直接 QQ 登錄,直接點擊管理控制台開通即可,不用付費,也不用選資源包,開通后自動有每個月 1000 次的免費額度,如果自己和朋友玩完全足夠了。如果你是想活躍社群或者土豪,就隨便充值了
獲取騰訊的 secretid 和 secretkey
訪問此頁面https://console.cloud.tencent.com/cam/capi獲取你的secretid
和secretkey
,配置插件的時候需要用的到
使用步驟
1、初始化項目
node環境需要自己配置一下,node>=14,。新建一個文件夾face-carton
,在文件夾內部執行npm init
,一路回車即可
2、安裝頭像轉化插件和 Wechaty
這里說明一下,頭像轉化插件wechaty-face-carton
就是我這次做的主要功能,已經開源在github,由於已經發布到npm,所以這里你只需要安裝就可以使用了,對於不關心代碼的童鞋,直接安裝使用就行了。如果想知道代碼怎么實現的,可以到github倉庫查看一下源碼。對於源碼的實現,文后我也會放一部分核心代碼進行說明。
配置 npm
源為淘寶源(重要,因為需要安裝 chromium
,不配置的話下載會失敗或者速度很慢,因為這個玩意 140M 左右)
npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist
npm config set puppeteer_download_host https://npm.taobao.org/mirrors
npm install wechaty wechaty-face-carton wechaty-puppet-wechat --save
如果安裝出現問題,建議刪除node_modules
后多試幾次,對於其他環境問題可以參考:
常見問題處理和 wechaty官網
3、主要代碼(不超過20行)
目錄下新建文件index.js
const { Wechaty } = require('wechaty')
const WechatyFaceCartonPlugin = require('wechaty-face-carton')
const name = 'wechat-carton'
const bot = new Wechaty({ name, puppet: 'wechaty-puppet-wechat' })
bot
.use(
WechatyFaceCartonPlugin({
maxuser: 20, // 支持最多多少人進行對話,建議不要設置太多,否則占用內存會增加
secretId: '騰訊secretId', // 騰訊secretId
secretKey: '騰訊secretKey', // 騰訊secretKey
allowUser: ['Leo_chen'], // 允許哪些好友使用人像漫畫化功能,為空[]代表所有人開啟
allowRoom: ['測試1'], // 允許哪些群使用人像漫畫化功能,為空[]代表不開啟任何一個群
quickModel: true, // 快速體驗模式 默認關閉 開啟后可直接生成二維碼掃描體驗,如果自己代碼有登錄邏輯可以不配置此項
tipsword: '卡通', // 私聊發送消息,觸發照片卡通化提示 如果直接發送圖片,默認進入圖片卡通化功能,不填則當用戶初次發送文字消息時不做任何處理
})
)
.start()
.catch((e) => console.error(e))
參數說明
參數名 | 必填 | 默認值 | 說明 |
---|---|---|---|
maxuser | 否 | 20 | 支持最多多少人進行對話,建議不要設置太多,否則占用內存會增加 |
secretId: | 是 | '' | 騰訊 secretId |
secretKey | 是 | '' | 騰訊 secretKey |
allowUser | 否 | [] | 允許哪些好友使用人像漫畫化功能,為空[]代表所有人開啟 |
allowRoom | 否 | [] | 允許哪些群使用人像漫畫化功能,為空[]代表不開啟任何一個群 |
quickModel | 否 | false | 快速體驗模式 默認關閉 開啟后可直接生成二維碼掃描體驗,如果自己代碼有登錄邏輯可以不配置此項,如果是單獨使用此插件,建議開啟 |
tipsword | 否 | '卡通' | 私聊發送消息,觸發照片卡通化提示。如果直接發送圖片,默認進入圖片卡通化功能,不填則當用戶初次發送文字消息時不做任何處理,建議填寫觸發關鍵詞 |
4、運行項目
node index.js
掃碼登錄后,給小助手發送圖片,即可轉化圖片,對於不能轉化的圖片,小助手會給出原因
docker運行
1、新建Dockerfile
如果遇到過多的環境問題讓你非常苦惱,你也可以在以上第三步完成后,根目錄新建一個Dockerfile
文件,里面填入內容,對!就一行就行!
FROM wechaty/onbuild
2、build鏡像
完成后就可以直接build鏡像
docker build -t wechaty-carton .
3、運行鏡像
build完成后就可以直接run后掃碼了
docker run wechaty-carton
插件核心代碼解析
插件源碼地址:https://github.com/leochen-g/wechaty-face-carton,如果能幫你哄女朋友開心,麻煩給個star,小心心❤送給你 😏
代碼結構
插件主入口為index.js
,service/tencent.js
為調用騰訊雲服務的主要方法,service/multiReply.js
是多輪對話實現的核心,util/index.js
為一些公共的處理方法,包括群發消息,私聊消息的公共方法抽取。
消息監聽
消息監聽很簡單,Wechaty暴露出message
事件,只要根據消息類型進行過濾即可,對於本插件而言,圖片消息是觸發轉化的關鍵
const { contactSay, roomSay, delay } = require('./util/index')
const { BotManage } = require('./service/multiReply')
const Qrterminal = require('qrcode-terminal')
let config = {}
let BotRes = ''
/**
* 根據消息類型過濾私聊消息事件
* @param {*} that bot實例
* @param {*} msg 消息主體
*/
async function dispatchFriendFilterByMsgType(that, msg) {
try {
const type = msg.type()
const contact = msg.talker() // 發消息人
const name = await contact.name()
const isOfficial = contact.type() === that.Contact.Type.Official
const id = await contact.id
switch (type) {
// 文字消息處理
case that.Message.Type.Text:
content = msg.text()
if (!isOfficial) {
console.log(`發消息人${name}:${content}`)
if (content.trim()) {
const multiReply = await BotRes.run(id, { type: 1, content })
let replys = multiReply.replys
let replyIndex = multiReply.replys_index
await delay(1000)
await contactSay(contact, replys[replyIndex])
}
}
break
// 圖片消息處理
case that.Message.Type.Image:
console.log(`發消息人${name}:發了一張圖片`)
// 判斷是否配置了指定人開啟轉換
if (!config.allowUser.length || config.allowUser.includes(name)) {
const file = await msg.toFileBox()
const base = await file.toDataURL()
const multiReply = await BotRes.run(id, { type: 3, url: base })
let replys = multiReply.replys
let replyIndex = multiReply.replys_index
await delay(1000)
await contactSay(contact, replys[replyIndex])
} else {
console.log(`沒有開啟 ${name} 的人臉漫畫化功能, 或者檢查是否已經配置此人微信昵稱`)
}
break
default:
break
}
} catch (error) {
console.log('監聽消息錯誤', error)
}
}
/**
* 根據消息類型過濾群消息事件
* @param {*} that bot實例
* @param {*} room room對象
* @param {*} msg 消息主體
*/
async function dispatchRoomFilterByMsgType(that, room, msg) {
const contact = msg.talker() // 發消息人
const contactName = contact.name()
const roomName = await room.topic()
const type = msg.type()
const userName = await contact.name()
const userSelfName = that.userSelf().name()
const id = await contact.id
switch (type) {
// 文字消息處理
case that.Message.Type.Text:
content = msg.text()
console.log(`群名: ${roomName} 發消息人: ${contactName} 內容: ${content}`)
// 判斷是否配置了指定群開啟轉換
if (config.allowRoom.includes(roomName)) {
const mentionSelf = content.includes(`@${userSelfName}`)
if (mentionSelf) {
content = content.replace(/@[^,,::\s@]+/g, '').trim()
if (content) {
const multiReply = await BotRes.run(id, { type: 1, content })
let replys = multiReply.replys
let replyIndex = multiReply.replys_index
await delay(1000)
await roomSay(room, contact, replys[replyIndex])
}
}
}
break
// 圖片消息處理
case that.Message.Type.Image:
console.log(`群名: ${roomName} 發消息人: ${contactName} 發了一張圖片`)
// 判斷是否配置了指定群開啟轉換
if (config.allowRoom.includes(roomName)) {
console.log(`匹配到群:${roomName}的人臉漫畫化功能已開啟,正在生成中...`)
const file = await msg.toFileBox()
const base = await file.toDataURL()
const multiReply = await BotRes.run(id, { type: 3, url: base })
let replys = multiReply.replys
let replyIndex = multiReply.replys_index
await delay(1000)
await roomSay(room, contact, replys[replyIndex])
} else {
console.log('沒有開通此群人臉漫畫化功能')
}
break
default:
break
}
}
/**
* 消息事件監聽
* @param {*} msg
* @returns
*/
async function onMessage(msg) {
try {
if (!BotRes) {
BotRes = new BotManage(config.maxuser, this, config)
}
const room = msg.room() // 是否為群消息
const msgSelf = msg.self() // 是否自己發給自己的消息
if (msgSelf) return
// 根據不同消息類型進行消息的派發處理
if (room) {
dispatchRoomFilterByMsgType(this, room, msg)
} else {
dispatchFriendFilterByMsgType(this, msg)
}
} catch (e) {
console.log('reply error', e)
}
}
.....
多輪對話核心代碼
對於多輪對話的實現,我是參考大佬@kevinfu1717的python版Wechaty的代碼,把他python代碼中的多輪對話的核心代碼轉換成了js版,具體實現邏輯呢,我就引用他的解釋,一些對應js中的方法名我進行了修改。如果有對python實現有興趣的可以訪問https://github.com/kevinfu1717/multimediaChatbot
service/multiReply.js
文件
- multiReply中的MultiReply使用類似“簡易工廠模式”。(熟悉工廠模式的筒子可以忽略本段)。每一個觸發聊天的用戶都會生成一個user_bot,用戶的輸入就好像工廠里面的原材料,經過BotManage分配到各個工序的工人(各個技能模塊,如:卡通人臉生成、人臉年齡變化、人臉性別變化等)進行處理,最終組裝好的產品給到用戶。不同用戶的輸入就像不同的原材料,不斷送進工廠處理,流水的bot鐵打不變的BotManage,而每個user_bot裝載的是整個聊天過程中的所有對話。以上純屬個人胡扯,工廠模式正規解釋具體見:https://juejin.cn/post/6844903653774458888
const { generateCarton } = require('./tencent')
class MultiReply {
constructor() {
this.userName = ''
this.startTime = 0 // 開始時間
this.queryList = [] // 用戶說的話
this.replys = [] // 每次回復,回復用戶的內容(列表)
this.reply_index = 0 // 回復用戶的話回復到第幾部分
this.step = 0 // 當前step
this.stepRecord = [] // 經歷過的step
this.lastReply = {} // 最后回復的內容
this.imageData = '' // 用戶發送的圖片
this.model = 1 // 默認選擇漫畫模式
this.age = 60 // 用戶選擇的年齡
this.gender = 0 // 用戶性別轉換的模式
}
paramsInit() {
this.startTime = 0 // 開始時間
this.queryList = [] // 用戶說的話
this.replys = [] // 每次回復,回復用戶的內容(列表)
this.reply_index = 0 // 回復用戶的話回復到第幾部分
this.step = 0 // 當前step
this.stepRecord = [] // 經歷過的step
this.lastReply = {} // 最后回復的內容
this.imageData = '' // 用戶發送的圖片
this.model = 1 // 默認選擇漫畫模式
this.age = 60 // 用戶選擇的年齡
this.gender = 0 // 用戶性別轉換的模式
}
}
class BotManage {
constructor(maxuser, that, config) {
this.Bot = that
this.config = config
this.userBotDict = {} // 存放所有對話的用戶
this.userTimeDict = {}
this.maxuser = maxuser // 最大同時處理的用戶數
this.loopLimit = 4
this.replyList = [
{ type: 1, content: '請選擇你要轉換的模式(發送序號):\n\n[1]、卡通化照片\n\n[2]、變換年齡\n\n[3]、變換性別\n\n' },
{ type: 1, content: '請輸入你想要轉換的年齡:請輸入10~80的任意數字' },
{ type: 1, content: '請輸入你想轉換的性別(發送序號):\n\n[0]、男變女\n\n[1]、女變男\n\n' },
{ type: 1, content: '你輸入的序號有誤,請輸入正確的序號' },
{ type: 1, content: '你輸入的年齡有誤,請輸入10~80的任意數字' },
{ type: 1, content: '你選擇的序號有誤,請輸入你想轉換的性別(發送序號):\n\n[0]、男變女\n\n[1]、女變男\n\n' },
]
}
async creatBot(username, content) {
console.log('bot process create')
this.userBotDict[username] = new MultiReply()
this.userBotDict[username].userName = username
this.userBotDict[username].imageData = content.url
return await this.updateBot(username, content)
}
// 更新對話
async updateBot(username, content) {
console.log(`更新{${username}}對話`)
this.userTimeDict[username] = new Date().getTime()
this.userBotDict[username].queryList.push(content)
return await this.talk(username, content)
}
async talk(username, content) {
// 防止進入死循環
if (this.userBotDict[username].stepRecord.length >= this.loopLimit) {
const arr = this.userBotDict[username].stepRecord.slice(-1 * this.loopLimit)
console.log('ini', arr, this.userBotDict[username].stepRecord)
console.log(
'arr.reduce((x, y) => x * y) ',
arr.reduce((x, y) => x * y)
)
console.log(
'arr.reduce((x, y) => x * y) ',
arr.reduce((x, y) => x * y)
)
const lastIndex = this.userBotDict[username].stepRecord.length - 1
console.log('limit last', this.userBotDict[username].stepRecord.length, this.loopLimit)
console.log('limit', this.userBotDict[username].stepRecord[this.userBotDict[username].stepRecord.length - 1] ** this.loopLimit)
if (arr.reduce((x, y) => x * y) === this.userBotDict[username].stepRecord[this.userBotDict[username].stepRecord.length - 1] ** this.loopLimit) {
this.userBotDict[username].step = 100
}
}
// 對話結束
if (this.userBotDict[username].step == 100) {
this.userBotDict[username].paramsInit()
this.userBotDict[username] = this.addReply(username, { type: 1, content: '你已經輸入太多錯誤指令了,小圖已經不知道怎么回答了,還是重新發送照片吧' })
return this.userBotDict[username]
}
// 圖片處理完畢后
if (this.userBotDict[username].step == 101) {
this.userBotDict[username].paramsInit()
this.userBotDict[username] = this.addReply(username, { type: 1, content: '你的圖片已經生成了,如果還想體驗的話,請重新發送照片' })
return this.userBotDict[username]
}
if (this.userBotDict[username].step == 0) {
console.log('第一輪對話,讓用戶選擇轉換的內容')
this.userBotDict[username].stepRecord.push(0)
if (content.type === 3) {
this.userBotDict[username].step += 1
this.userBotDict[username] = this.addReply(username, this.replyList[0])
return this.userBotDict[username]
} else {
if (this.config.tipsword && content.content.includes(this.config.tipsword)) {
// 如果沒有發圖片,直接發文字,觸發關鍵詞
return {
replys: [{ type: 1, content: '想要體驗人臉卡通化功能,請先發送帶人臉的照片給我' }],
replys_index: 0,
}
} else {
// 如果沒有發圖片,直接發文字,沒有觸發關鍵詞
this.removeBot(username)
return {
replys: [{ type: 1, content: '' }],
replys_index: 0,
}
}
}
} else if (this.userBotDict[username].step == 1) {
console.log('第二輪對話,用戶選擇需要轉換的模式')
this.userBotDict[username].stepRecord.push(1)
if (content.type === 1) {
if (parseInt(content.content) === 1) {
// 用戶選擇了漫畫模式
this.userBotDict[username].step = 101
this.userBotDict[username].model = 1
return await this.generateImage(username)
} else if (parseInt(content.content) === 2) {
// 用戶選擇了變換年齡模式
this.userBotDict[username].step += 1
this.userBotDict[username].model = 2
this.userBotDict[username] = this.addReply(username, this.replyList[1])
return this.userBotDict[username]
} else if (parseInt(content.content) === 3) {
// 用戶選擇了變換性別模式
this.userBotDict[username].step += 1
this.userBotDict[username].model = 3
this.userBotDict[username] = this.addReply(username, this.replyList[2])
return this.userBotDict[username]
} else {
// 輸入模式錯誤提示
this.userBotDict[username].step = 1
this.userBotDict[username] = this.addReply(username, this.replyList[3])
return this.userBotDict[username]
}
}
} else if (this.userBotDict[username].step == 2) {
console.log('第三輪對話,用戶輸入指定模式所需要的配置')
this.userBotDict[username].stepRecord.push(2)
if (content.type === 1) {
if (this.userBotDict[username].model === 2) {
// 用戶選擇了年齡變換模式
if (parseInt(content.content) >= 10 && parseInt(content.content) <= 80) {
this.userBotDict[username].step = 101
this.userBotDict[username].age = content.content
return await this.generateImage(username)
} else {
this.userBotDict[username].step = 2
this.userBotDict[username] = this.addReply(username, this.replyList[4])
return this.userBotDict[username]
}
} else if (this.userBotDict[username].model === 3) {
// 用戶選擇了性別變換模式
if (parseInt(content.content) === 0 || parseInt(content.content) === 1) {
this.userBotDict[username].step = 101
this.userBotDict[username].gender = parseInt(content.content)
return await this.generateImage(username)
} else {
this.userBotDict[username].step = 2
this.userBotDict[username] = this.addReply(username, this.replyList[5])
return this.userBotDict[username]
}
}
}
}
}
addReply(username, replys) {
this.userBotDict[username].replys.push(replys)
this.userBotDict[username].replys_index = this.userBotDict[username].replys.length - 1
return this.userBotDict[username]
}
removeBot(dictKey) {
console.log('bot process remove', dictKey)
delete this.userTimeDict[dictKey]
delete this.userBotDict[dictKey]
}
getBotList() {
return this.userBotDict
}
/**
* 生成圖片
* @param {*} username 用戶名
* @returns
*/
async generateImage(username) {
const image = await generateCarton(this.config, this.userBotDict[username].imageData, { model: this.userBotDict[username].model, gender: this.userBotDict[username].gender, age: this.userBotDict[username].age })
this.userBotDict[username] = this.addReply(username, image)
return this.userBotDict[username]
}
getImage(username, content, step) {
this.userBotDict[username].paramsInit()
this.userBotDict[username].step = step
if (content.type === 3) {
this.userBotDict[username].imageData = content.url
}
let replys = { type: 1, content: '請選擇你要轉換的模式(發送序號):\n\n [1]、卡通化照片\n\n[2]、變換年齡\n\n[3]、變換性別\n\n' }
this.userBotDict[username] = this.addReply(username, replys)
return this.userBotDict[username]
}
// 對話入口
async run(username, content) {
if (content.type === 1) {
if (!Object.keys(this.userTimeDict).includes(username)) {
if (this.config.tipsword && content.content.includes(this.config.tipsword)) {
// 如果沒有發圖片,直接發文字,觸發關鍵詞
return {
replys: [{ type: 1, content: '想要體驗人臉卡通化功能,請先發送帶人臉的照片給我' }],
replys_index: 0,
}
} else {
// 如果沒有發圖片,直接發文字,沒有觸發關鍵詞
return {
replys: [{ type: 1, content: '' }],
replys_index: 0,
}
}
} else {
// 如果對話環境中已存在,則更新對話內容
console.log(`${username}用戶正在對話環境中`)
return this.updateBot(username, content)
}
} else if (content.type === 3) {
if (Object.keys(this.userTimeDict).includes(username)) {
console.log(`${username}用戶正在對話環境中`)
return this.getImage(username, content, 1)
} else {
if (this.userBotDict.length > this.maxuser) {
const minNum = Math.min(...Object.values(this.userTimeDict))
const earlyIndex = arr.indexOf(minNum)
const earlyKey = Object.keys(this.userTimeDict)[earlyIndex]
this.removeBot(earlyKey)
}
return await this.creatBot(username, content)
}
}
}
}
module.exports = {
BotManage,
}
util/index.js
文件
roomSay和contactSay會把multiReply中返回的對話內容,“翻譯”成真正發給用戶的內容。例如:是文本的直接發送,是圖片的包裝一下發送給用戶。
const { FileBox, UrlLink, MiniProgram } = require('wechaty')
/**
* 延時函數
* @param {*} ms 毫秒
*/
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* 群回復
* @param {*} contact
* @param {*} msg
* @param {*} isRoom
* type 1 文字 2 圖片url 3 圖片base64 4 url鏈接 5 小程序 6 名片
*/
async function roomSay(room, contact, msg) {
try {
if (msg.type === 1 && msg.content) {
// 文字
console.log('回復內容', msg.content)
contact ? await room.say(msg.content, contact) : await room.say(msg.content)
} else if (msg.type === 2 && msg.url) {
// url文件
let obj = FileBox.fromUrl(msg.url)
console.log('回復內容', obj)
contact ? await room.say('', contact) : ''
await delay(500)
await room.say(obj)
} else if (msg.type === 3 && msg.url) {
// bse64文件
let obj = FileBox.fromDataURL(msg.url, 'room-avatar.jpg')
contact ? await room.say('', contact) : ''
await delay(500)
await room.say(obj)
} else if (msg.type === 4 && msg.url && msg.title && msg.description) {
console.log('in url')
let url = new UrlLink({
description: msg.description,
thumbnailUrl: msg.thumbUrl,
title: msg.title,
url: msg.url,
})
console.log(url)
await room.say(url)
} else if (msg.type === 5 && msg.appid && msg.title && msg.pagePath && msg.description && msg.thumbUrl && msg.thumbKey) {
let miniProgram = new MiniProgram({
appid: msg.appid,
title: msg.title,
pagePath: msg.pagePath,
description: msg.description,
thumbUrl: msg.thumbUrl,
thumbKey: msg.thumbKey,
})
await room.say(miniProgram)
}
} catch (e) {
console.log('群回復錯誤', e)
}
}
/**
* 私聊發送消息
* @param contact
* @param msg
* @param isRoom
* type 1 文字 2 圖片url 3 圖片base64 4 url鏈接 5 小程序 6 名片
*/
async function contactSay(contact, msg, isRoom = false) {
try {
if (msg.type === 1 && msg.content) {
// 文字
console.log('回復內容', msg.content)
await contact.say(msg.content)
} else if (msg.type === 2 && msg.url) {
// url文件
let obj = FileBox.fromUrl(msg.url)
console.log('回復內容', obj)
if (isRoom) {
await contact.say(`@${contact.name()}`)
await delay(500)
}
await contact.say(obj)
} else if (msg.type === 3 && msg.url) {
// bse64文件
let obj = FileBox.fromDataURL(msg.url, 'user-avatar.jpg')
await contact.say(obj)
} else if (msg.type === 4 && msg.url && msg.title && msg.description && msg.thumbUrl) {
let url = new UrlLink({
description: msg.description,
thumbnailUrl: msg.thumbUrl,
title: msg.title,
url: msg.url,
})
await contact.say(url)
} else if (msg.type === 5 && msg.appid && msg.title && msg.pagePath && msg.description && msg.thumbUrl && msg.thumbKey) {
let miniProgram = new MiniProgram({
appid: msg.appid,
title: msg.title,
pagePath: msg.pagePath,
description: msg.description,
thumbUrl: msg.thumbUrl,
thumbKey: msg.thumbKey,
})
await contact.say(miniProgram)
}
} catch (e) {
console.log('私聊發送消息失敗', msg, e)
}
}
module.exports = {
contactSay,
roomSay,
delay,
}
注意
要注意一下,不要把額度用超了,用超了就只能下個月才能玩了。
問題與交流
如有使用問題可以直接加小助手,回復卡通
,進微信群交流,如果
歷史文章
本文由博客一文多發平台 OpenWrite 發布!