python-archieve-projects/WechatBot/chatflow/src/index.ts

762 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
import 'dotenv/config.js'
// import fs from 'fs'
import {
Contact,
Message,
ScanStatus,
log,
// Room,
types,
Wechaty,
WechatyBuilder,
} from 'wechaty'
import qrcodeTerminal from 'qrcode-terminal'
import { FileBox } from 'file-box'
import fs, { createWriteStream } from 'fs'
import XLSX from 'xlsx'
import csv from 'fast-csv'
import {
VikaBot,
configData,
sendMsg,
sendNotice,
getFormattedRideInfo,
imclient,
wxai,
ChatDevice,
propertyMessage,
eventMessage,
} from './plugins/index.js'
import type { types as configTypes } from './mods/mod.js'
import { baseConfig, config } from './config.js'
import {
waitForMs as wait,
formatSentMessage,
} from './util/tool.js'
import schedule from 'node-schedule'
import { db } from './db/tables.js'
log.info('db:', db)
log.info('config:', JSON.stringify(config))
// log.info('process.env', JSON.stringify(process.env))
enum Prompts {
a = '输入的信息错误或格式不符合要求,请输入如下格式"维格表token+空间名称",中间用加号分开,例如:\nuskxRhxxxxxxxx3UK959A8093+wechatbot',
b = '启动成功,请输入"维格表token+空间名称",中间用加号分开,例如:\nuskxRhxxxxxxxx3UK959A8093+wechatbot'
}
let bot: Wechaty
let puppet = baseConfig['puppetName'] || process.env['WECHATY_PUPPET']
let token = baseConfig['puppetToken'] || process.env['WECHATY_TOKEN']
const vikaConfig = {
spaceName: baseConfig['VIKA_SPACENAME'] || process.env['VIKA_SPACENAME'],
token: baseConfig['VIKA_TOKEN'] || process.env['VIKA_TOKEN'],
}
// log.info(vikaConfig)
let sysConfig: configTypes.SysConfig
let chatdev: any = {}
// let job: any
let jobs: any
let vika: any
let isVikaOk: boolean = false
let socket: any = {}
// log.info(baseConfig)
function updateBaseConfig (config: configTypes.SysConfig) {
puppet = config['puppetName'] || puppet
token = config['puppetToken'] || token
}
function updateConfig (config:any) {
fs.writeFileSync('src/config.json', JSON.stringify(config))
}
async function createVika () {
try {
vika = new VikaBot(vikaConfig)
await vika.init()
// 初始化获取配置信息
const initReady = await vika.checkInit('主程序载入系统配置成功,等待插件初始化...')
if (!initReady) {
return
}
// 获取系统配置信息
sysConfig = await vika.getConfig()
log.info('config:', JSON.stringify(config))
const configReady = checkConfig(config)
updateBaseConfig(config)
// 配置齐全,启动机器人
if (configReady) {
return vika
}
} catch {
return false
}
}
function getBot () {
const ops: any = {
name: 'qa-bot',
puppet,
puppetOptions: {
token,
},
}
log.info(ops)
if (puppet === 'wechaty-puppet-service') {
process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true'
}
if ([ 'wechaty-puppet-wechat4u', 'wechaty-puppet-xp', 'wechaty-puppet-engine' ].includes(puppet)) {
delete ops.puppetOptions.token
}
if (puppet === 'wechaty-puppet-wechat') {
delete ops.puppetOptions.token
ops.puppetOptions.uos = true
}
log.info('bot ops:', JSON.stringify(ops))
const bot = WechatyBuilder.build(ops)
return bot
}
function getNow () {
return new Date().toLocaleString()
}
function checkConfig (config: { [key: string]: any }) {
const missingConfiguration = []
for (const key in config) {
if (!config[key] && ![ 'imOpen', 'DIFF_REPLY_ONOFF' ].includes(key)) {
missingConfiguration.push(key)
}
}
if (missingConfiguration.length > 0) {
// log.error('\n======================================\n\n', `错误提示:\n缺少${missingConfiguration.join()}配置参数,请检查config.js文件\n\n======================================`)
log.info('bot config:', config)
return false
}
return true
}
async function relpy (bot: Wechaty, vika: any, replyText: string, message: Message) {
await message.say(replyText)
vika.addRecord(await formatSentMessage(bot.currentUser, replyText, message.room() ? undefined : message.talker(), message.room()))
}
async function exportContactsAndRoomsToCSV () {
// 获取所有联系人和群聊
const contacts = await bot.Contact.findAll()
const rooms = await bot.Room.findAll()
// 准备CSV数据
const csvData = []
contacts.forEach((contact: Contact) => {
if (contact.friend()) {
csvData.push({ ID: contact.id, Name: Buffer.from(contact.name(), 'utf-8').toString() || '未知', Type: 'Contact' })
}
})
for (const room of rooms) {
csvData.push({ ID: room.id, Name: Buffer.from(await room.topic(), 'utf-8').toString() || '未知', Type: 'Room' })
}
log.info('通讯录原始数据:', csvData)
const fileName = './db/contacts_and_rooms.csv'
const writeStream = createWriteStream(fileName)
const csvStream = csv.format({ headers: true })
csvStream.pipe(writeStream).on('end', () => {
log.info('CSV file written successfully')
})
csvData.forEach((item) => {
csvStream.write(item)
})
csvStream.end()
// 返回FileBox对象
return FileBox.fromFile(fileName)
}
async function exportContactsAndRoomsToXLSX () {
// 获取所有联系人和群聊
const contacts = await bot.Contact.findAll()
const rooms = await bot.Room.findAll()
// 准备联系人和群聊数据
const contactsData = [ [ 'Name', 'ID' ] ]
const roomsData = [ [ 'Name', 'ID' ] ]
contacts.forEach((contact) => {
if (contact.friend()) {
contactsData.push([ contact.name(), contact.id ])
}
})
for (const room of rooms) {
roomsData.push([ await room.topic(), room.id ])
}
// 创建一个新的工作簿
const workbook = XLSX.utils.book_new()
// 将数据添加到工作簿的不同sheet中
const contactsSheet = XLSX.utils.aoa_to_sheet(contactsData)
const roomsSheet = XLSX.utils.aoa_to_sheet(roomsData)
XLSX.utils.book_append_sheet(workbook, contactsSheet, 'Contacts')
XLSX.utils.book_append_sheet(workbook, roomsSheet, 'Rooms')
// 将工作簿写入文件
const fileName = './db/contacts_and_rooms.xlsx'
XLSX.writeFile(workbook, fileName)
// 返回FileBox对象
return FileBox.fromFile(fileName)
}
async function updateJobs (bot: Wechaty, vika: any) {
try {
const tasks = await vika.getTimedTask()
schedule.gracefulShutdown()
jobs = {}
// log.info(tasks)
for (let i = 0; i < tasks.length; i++) {
const task: any = tasks[i]
if (task.active) {
const curTimeF = new Date(task.time)
// const curTimeF = new Date(task.time+8*60*60*1000)
let curRule = '* * * * * *'
let dayOfWeek: any = '*'
let month: any = '*'
let dayOfMonth: any = '*'
let hour: any = curTimeF.getHours()
let minute: any = curTimeF.getMinutes()
const second = 0
const addMonth = []
switch (task.cycle) {
case '每季度':
month = curTimeF.getMonth()
for (let i = 0; i < 4; i++) {
if (month + 3 <= 11) {
addMonth.push(month)
} else {
addMonth.push(month - 9)
}
month = month + 3
}
month = addMonth
break
case '每天':
break
case '每周':
dayOfWeek = curTimeF.getDay()
break
case '每月':
month = curTimeF.getMonth()
break
case '每小时':
hour = '*'
break
case '每30分钟':
hour = '*'
minute = [ 0, 30 ]
break
case '每15分钟':
hour = '*'
minute = [ 0, 15, 30, 45 ]
break
case '每10分钟':
hour = '*'
minute = [ 0, 10, 20, 30, 40, 50 ]
break
case '每5分钟':
hour = '*'
minute = [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ]
break
case '每分钟':
hour = '*'
minute = '*'
break
default:
month = curTimeF.getMonth()
dayOfMonth = curTimeF.getDate()
break
}
curRule = `${second} ${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}`
log.info(curRule)
try {
schedule.scheduleJob(task.id, curRule, async () => {
try {
const curDate = new Date()
log.info('定时任务:', curTimeF, curRule, curDate, JSON.stringify(task))
// await user.say('心跳:' + curDate)
try {
if (task.contacts.length) {
const contact = await bot.Contact.find({ id: task.contacts[0] })
if (contact) {
await contact.say(task.msg)
vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, contact, undefined))
await wait(200)
}
}
} catch (e) {
log.error('发送好友定时任务失败:', e)
}
try {
if (task.rooms.length) {
const room = await bot.Room.find({ id: task.rooms[0] })
if (room) {
await room.say(task.msg)
vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, undefined, room))
await wait(200)
}
}
} catch (e) {
log.error('发送群定时任务失败:', e)
}
} catch (err) {
log.error('定时任务执行失败:', err)
}
})
jobs[task.id] = task
} catch (e) {
log.error('创建定时任务失败:', e)
}
}
}
log.info('通知提醒任务初始化完成,创建任务数量:', Object.keys(jobs).length)
} catch (err: any) {
log.error('更新通知提醒列表任务失败:', err)
}
}
async function onScan (qrcode: string, status: ScanStatus) {
// 上传二维码到维格表,可通过扫码维格表中二维码登录
if (isVikaOk) await vika.onScan(qrcode, status)
// 控制台显示二维码
if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) {
const qrcodeUrl = encodeURIComponent(qrcode)
const qrcodeImageUrl = [
'https://wechaty.js.org/qrcode/',
qrcodeUrl,
].join('')
log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl)
qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console
} else {
log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status)
}
}
async function onLogin (user: Contact) {
log.info('StarterBot', '%s login', user)
log.info('当前登录的账号信息:', JSON.stringify(user))
if (isVikaOk) {
const curDate = new Date().toLocaleString()
await user.say('上线:' + curDate)
// 启动MQTT通道
if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) {
chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id)
if (sysConfig.mqtt_SUB_ONOFF) {
chatdev.init(bot)
}
}
// 更新云端好友和群
await vika.updateRooms(bot)
await vika.updateContacts(bot)
// 如果开启了MQTT推送心跳同步到MQTT,每30s一次
setInterval(() => {
try {
log.info(curDate)
if (chatdev && sysConfig.mqtt_PUB_ONOFF) {
chatdev.pub_property(propertyMessage('lastActive', curDate))
}
} catch (err) {
log.error('发送心跳失败:', err)
}
}, 300000)
// 启动用户定时通知提醒任务
await updateJobs(bot, vika)
log.info('================================================\n\n登录启动成功程序准备就绪\n\n================================================\n')
} else {
log.info('================================================\n\n登录启动成功但没有配置维格表\n\n================================================\n')
await user.say(Prompts.b)
}
}
async function onReady () {
const user: Contact = bot.currentUser
log.info('StarterBot', '%s ready', user)
log.info('当前登录的账号信息:', JSON.stringify(user))
if (isVikaOk) {
const curDate = new Date().toLocaleString()
await user.say('上线:' + curDate)
// 启动MQTT通道
if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) {
chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id)
if (sysConfig.mqtt_SUB_ONOFF) {
chatdev.init(bot)
}
}
// 更新云端好友和群
await vika.updateRooms(bot)
await vika.updateContacts(bot)
// 如果开启了MQTT推送心跳同步到MQTT,每30s一次
setInterval(() => {
try {
log.info(curDate)
if (chatdev && sysConfig.mqtt_PUB_ONOFF) {
chatdev.pub_property(propertyMessage('lastActive', curDate))
}
} catch (err) {
log.error('发送心跳失败:', err)
}
}, 300000)
// 启动用户定时通知提醒任务
await updateJobs(bot, vika)
log.info('================================================\n\n登录启动成功程序准备就绪\n\n================================================\n')
} else {
log.info('================================================\n\n登录启动成功但没有配置维格表\n\n================================================\n')
await user.say(Prompts.b)
}
}
function onLogout (user: Contact) {
log.info('StarterBot', '%s logout', user)
// job.cancel()
}
async function onMessage (message: Message) {
log.info('onMessage', JSON.stringify(message))
const curDate = new Date().toLocaleString()
const talker = message.talker()
// const listener = message.listener()
const text = message.text()
const room = message.room()
const roomId = room?.id
const topic = await room?.topic()
const keyWord = bot.currentUser.name()
const isSelfMsg = message.self()
let isAdminRoom: boolean = false
log.info('keyWord is:', keyWord)
if (isVikaOk) {
await vika.onMessage(message)
// MQTT上报
if (chatdev && sysConfig.mqtt_PUB_ONOFF) {
/*
将消息通过mqtt通道上报到云端
*/
// chatdev.pub_message(message)
chatdev.pub_event(eventMessage('onMessage', { curDate }))
}
if (room || isSelfMsg) {
isAdminRoom = (topic !== undefined && topic === sysConfig.adminRoomTopic) || isSelfMsg
if (isAdminRoom) {
await sendNotice(bot, message)
}
let replyText: string = ''
if (isAdminRoom && (text === '#指令列表' || text === '#帮助')) {
replyText = `操作指令说明:
#更新配置 更新全部配置
#更新提醒 更新定时提醒任务
#更新通讯录 更新维格表通信录
#下载通讯录 下载通讯录xlsx表
#下载通知模板 下载通知模板`
await relpy(bot, vika, replyText, message)
}
if (isAdminRoom && text === '#更新配置') {
log.info('热更新系统配置~')
try {
sysConfig = await vika.getConfig()
// message.say('配置更新成功:' + JSON.stringify(newConfig))
log.info('newConfig', sysConfig)
replyText = '配置更新成功~'
} catch (e) {
replyText = '配置更新成功~'
}
await relpy(bot, vika, getNow() + replyText, message)
}
if (isAdminRoom && text === '#更新提醒') {
log.info('热更新通知任务~')
try {
await updateJobs(bot, vika)
replyText = '提醒任务更新成功~'
} catch (e) {
replyText = '提醒任务更新失败~'
}
await relpy(bot, vika, getNow() + replyText, message)
}
if (isAdminRoom && text === '#更新通讯录') {
log.info('热更新通讯录到维格表~')
try {
await vika.updateContacts(bot)
await vika.updateRooms(bot)
replyText = '通讯录更新成功~'
} catch (e) {
replyText = '通讯录更新失败~'
}
await relpy(bot, vika, getNow() + replyText, message)
}
if (isAdminRoom && text === '#下载csv通讯录') {
log.info('下载通讯录到csv表~')
try {
const fileBox = await exportContactsAndRoomsToCSV()
await message.say(fileBox)
} catch (err) {
log.error('exportContactsAndRoomsToCSV', err)
await message.say('下载失败~')
}
}
if (isAdminRoom && text === '#下载通讯录') {
log.info('下载通讯录到xlsx表~')
try {
const fileBox = await exportContactsAndRoomsToXLSX()
await message.say(fileBox)
} catch (err) {
log.error('exportContactsAndRoomsToXLSX', err)
}
}
if (isAdminRoom && text === '#下载通知模板') {
log.info('下载通知模板~')
try {
const fileBox = FileBox.fromFile('./src/templates/群发通知模板.xlsx')
await message.say(fileBox)
} catch (err) {
log.error('下载模板失败', err)
await message.say('下载失败,请重试~')
}
}
if (isAdminRoom && text === '#初始化') {
log.info('初始化系统~')
try {
await vika.init()
await message.say('初始化系统表完成~')
} catch (err) {
log.error('初始化系统失败', err)
await message.say('初始化系统失败,请重试~')
}
}
}
try {
if (room && roomId && !isSelfMsg) {
// 检测顺风车信息并格式化
// const KEYWORD_LIST = [ '人找车', '车找人' ]
// try {
// // 判断消息中是否包含关键字
// if (KEYWORD_LIST.some(keyword => message.text().includes(keyword))) {
// const replyMsg = await getFormattedRideInfo(message)
// if (replyMsg) {
// const replyText = replyMsg.choices[0].message.content.replace(/\r/g, '')
// log.info('回复内容:', replyText)
// await room.say(replyText)
// }
// }
// } catch (err) {
// }
// 智能问答开启时执行
if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) {
if (sysConfig.roomWhiteListOpen) {
const isInRoomWhiteList = sysConfig.roomWhiteList.includes(roomId)
if (isInRoomWhiteList) {
log.info('当前群在白名单内,请求问答...')
await wxai(sysConfig, bot, talker, room, message)
} else {
log.info('当前群不在白名单内,流程结束')
}
}
if (!sysConfig.roomWhiteListOpen) {
log.info('系统未开启白名单,请求问答...')
await wxai(sysConfig, bot, talker, room, message)
}
}
// IM服务开启时执行
if (sysConfig.imOpen && types.Message.Text === message.type()) {
configData.clientChatEn.clientChatId = talker.id + ' ' + room.id
configData.clientChatEn.clientChatName = talker.name() + '@' + topic
// log.debug(configData)
socket.emit('CLIENT_ON', {
clientChatEn: configData.clientChatEn,
serverChatId: configData.serverChatEn.serverChatId,
})
const data = {
msg: {
avatarUrl: '/static/image/im_server_avatar.png',
content: text,
contentType: 'text',
role: 'client',
},
}
log.info(JSON.stringify(data))
sendMsg(data)
}
}
if ((!room || !room.id) && !isSelfMsg) {
// 智能问答开启时执行
if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) {
if (sysConfig.contactWhiteListOpen) {
const isInContactWhiteList = sysConfig.contactWhiteList.includes(talker.id)
if (isInContactWhiteList) {
log.info('当前好友在白名单内,请求问答...')
await wxai(sysConfig, bot, talker, undefined, message)
} else {
log.info('当前好友不在白名单内,流程结束')
}
}
if (!sysConfig.contactWhiteListOpen) {
log.info('系统未开启好友白名单,对所有好友有效,请求问答...')
await wxai(sysConfig, bot, talker, undefined, message)
}
}
}
} catch (e) {
log.error('发起请求wxai失败', e)
}
} else {
if (message.self() && message.type() === types.Message.Text && text !== Prompts.a && text !== Prompts.b && text.includes('+')) {
try {
const textArr = text.split('+')
log.info(JSON.stringify(textArr))
if (text.length > 23 && text.length < 33 && textArr.length === 2) {
vikaConfig.spaceName = textArr[1]
vikaConfig.token = textArr[0]
await createVika()
isVikaOk = true
config.baseConfig.VIKA_TOKEN = vikaConfig.token
config.baseConfig.VIKA_SPACENAME = vikaConfig.spaceName
await updateConfig(config)
await talker.say('配置成功,初始化中,请稍后...')
log.info('初始化系统~')
try {
await vika.init()
await talker.say('初始化系统表完成~')
} catch (err) {
log.error('初始化系统失败', err)
await talker.say('初始化系统失败,请发送 #初始化 重试~')
}
} else {
await talker.say(Prompts.a)
}
} catch (err) {
log.error('解析失败:', err)
await talker.say(Prompts.a)
}
}
}
}
async function roomJoin (room: { topic: () => any; id: any; say: (arg0: string, arg1: any) => any }, inviteeList: Contact[], inviter: any) {
const nameList = inviteeList.map(c => c.name()).join(',')
log.info(`Room ${await room.topic()} got new member ${nameList}, invited by ${inviter}`)
// 进群欢迎语,仅对开启了进群欢迎语白名单的群有效
if (isVikaOk && sysConfig.welcomeList.includes(room.id) && inviteeList.length) {
await room.say(`欢迎加入${await room.topic()},请阅读群公告~`, inviteeList)
}
}
async function onError (err: any) {
log.error('bot.onError:', JSON.stringify(err))
// try {
// // job.cancel()
// } catch (e) {
// log.error('销毁定时任务失败:', JSON.stringify(e))
// }
}
async function main (vika: any) {
// 检查维格表配置并启动
if (vikaConfig.spaceName && vikaConfig.token) {
try {
await createVika()
isVikaOk = true
} catch (err) {
log.info('初始化vika失败', err)
}
} else {
log.error('\n================================================\n\nvikaConfig配置不全请重新配置config.json文件中的token和spaceName之后重启或者根据提示进行配置\n\n================================================\n')
}
bot = getBot()
bot.on('scan', onScan)
if (puppet === 'wechaty-puppet-xp') {
bot.on('login', onLogin)
}
if (puppet !== 'wechaty-puppet-xp') {
bot.on('ready', onReady)
}
bot.on('logout', onLogout)
bot.on('message', onMessage)
bot.on('room-join', roomJoin)
bot.on('error', onError)
bot.start()
.then(() => log.info('Starter Bot Started.'))
.catch((e: any) => log.error('bot运行异常', JSON.stringify(e)))
if (isVikaOk && sysConfig.imOpen) {
socket = imclient(bot, vika, configData)
}
}
void main(vika)