first commit
This commit is contained in:
75
src/platforms/wechat/bot.js
Normal file
75
src/platforms/wechat/bot.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { WechatyBuilder, ScanStatus, log } from 'wechaty'
|
||||
import qrTerminal from 'qrcode-terminal'
|
||||
import { defaultMessage } from '../../wechaty/sendMessage.js'
|
||||
import { captureWechatMessage } from './messageStore.js'
|
||||
import { getWechatRuntimeConfig } from '../../config/env.js'
|
||||
|
||||
function onScan(qrcode, status) {
|
||||
if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) {
|
||||
qrTerminal.generate(qrcode, { small: true })
|
||||
const qrcodeImageUrl = ['https://api.qrserver.com/v1/create-qr-code/?data=', encodeURIComponent(qrcode)].join('')
|
||||
console.log('onScan:', qrcodeImageUrl, ScanStatus[status], status)
|
||||
} else {
|
||||
log.info('onScan: %s(%s)', ScanStatus[status], status)
|
||||
}
|
||||
}
|
||||
|
||||
function onLogin(user) {
|
||||
console.log(`${user} has logged in`)
|
||||
const date = new Date()
|
||||
console.log(`Current time:${date}`)
|
||||
console.log('Automatic robot chat mode has been activated')
|
||||
}
|
||||
|
||||
function onLogout(user) {
|
||||
console.log(`${user} has logged out`)
|
||||
}
|
||||
|
||||
async function onFriendShip(friendship) {
|
||||
const friendShipRe = /chatgpt|chat/
|
||||
if (friendship.type() === 2 && friendShipRe.test(friendship.hello())) {
|
||||
await friendship.accept()
|
||||
}
|
||||
}
|
||||
|
||||
export function createWechatBot(options = {}) {
|
||||
const config = getWechatRuntimeConfig()
|
||||
const chromeBin = process.env.CHROME_BIN ? { endpoint: process.env.CHROME_BIN } : {}
|
||||
const serviceType = options.serviceType || ''
|
||||
|
||||
const bot = WechatyBuilder.build({
|
||||
name: 'WechatEveryDay',
|
||||
puppet: 'wechaty-puppet-wechat4u',
|
||||
puppetOptions: {
|
||||
uos: true,
|
||||
...chromeBin,
|
||||
},
|
||||
})
|
||||
|
||||
bot.on('scan', onScan)
|
||||
bot.on('login', onLogin)
|
||||
bot.on('logout', onLogout)
|
||||
bot.on('friendship', onFriendShip)
|
||||
bot.on('message', async (message) => {
|
||||
await captureWechatMessage(message, bot, {
|
||||
dataDir: config.dataDir,
|
||||
storeMessages: config.storeMessages,
|
||||
})
|
||||
await defaultMessage(message, bot, serviceType)
|
||||
})
|
||||
bot.on('error', (error) => {
|
||||
console.error('bot error handle: ', error)
|
||||
})
|
||||
|
||||
return bot
|
||||
}
|
||||
|
||||
export function startWechatBot(options = {}) {
|
||||
const bot = createWechatBot(options)
|
||||
bot
|
||||
.start()
|
||||
.then(() => console.log('Start to log in wechat...'))
|
||||
.catch((error) => console.error('botStart error: ', error))
|
||||
|
||||
return bot
|
||||
}
|
||||
81
src/platforms/wechat/commandRouter.js
Normal file
81
src/platforms/wechat/commandRouter.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { analyzeWechatMessages } from '../../analysis/wechatAnalyzer.js'
|
||||
import { getWechatRuntimeConfig } from '../../config/env.js'
|
||||
import { runOpenCli } from '../../adapters/opencli.js'
|
||||
|
||||
function stripMention(content, botName) {
|
||||
return content.replace(botName, '').trim()
|
||||
}
|
||||
|
||||
function parseTarget(tokens) {
|
||||
const type = tokens[1]
|
||||
const value = tokens.slice(2).join(' ').trim()
|
||||
|
||||
if (['群', '群聊', 'room', 'group'].includes(type)) {
|
||||
return { room: value }
|
||||
}
|
||||
|
||||
if (['好友', 'friend', 'contact'].includes(type)) {
|
||||
return { friend: value }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export async function handleWechatCommand(content, context = {}) {
|
||||
const config = getWechatRuntimeConfig()
|
||||
const normalized = stripMention(content, config.botName)
|
||||
|
||||
if (!normalized.startsWith(config.commandPrefix)) {
|
||||
return { handled: false }
|
||||
}
|
||||
|
||||
const commandLine = normalized.slice(config.commandPrefix.length).trim()
|
||||
const tokens = commandLine.split(/\s+/).filter(Boolean)
|
||||
const command = tokens[0]
|
||||
|
||||
if (['分析', 'analyze', '统计', 'stats'].includes(command)) {
|
||||
const statsOnly = ['统计', 'stats'].includes(command)
|
||||
const target = parseTarget(tokens)
|
||||
const result = await analyzeWechatMessages({
|
||||
...target,
|
||||
serviceType: context.serviceType,
|
||||
dataDir: config.dataDir,
|
||||
statsOnly,
|
||||
})
|
||||
|
||||
if (statsOnly || !result.analysis) {
|
||||
return {
|
||||
handled: true,
|
||||
reply: [
|
||||
`${result.target}`,
|
||||
`消息数:${result.stats.totalMessages}`,
|
||||
`文本消息:${result.stats.textMessages}`,
|
||||
`平均长度:${result.stats.averageTextLength}`,
|
||||
`高频发言:${result.stats.topSpeakers.map((item) => `${item.name}(${item.count})`).join(',') || '无'}`,
|
||||
].join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handled: true,
|
||||
reply: result.analysis,
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'opencli') {
|
||||
if (!config.enableRemoteOpenCli) {
|
||||
return {
|
||||
handled: true,
|
||||
reply: '远程 OpenCLI 执行未开启。需要在 .env 中显式设置 ENABLE_REMOTE_OPENCLI=true。',
|
||||
}
|
||||
}
|
||||
|
||||
await runOpenCli(tokens.slice(1))
|
||||
return {
|
||||
handled: true,
|
||||
reply: 'OpenCLI 命令已执行,结果请看本机控制台。',
|
||||
}
|
||||
}
|
||||
|
||||
return { handled: false }
|
||||
}
|
||||
96
src/platforms/wechat/messageStore.js
Normal file
96
src/platforms/wechat/messageStore.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const MESSAGE_FILE = 'messages.jsonl'
|
||||
|
||||
function ensureDir(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
export function getMessageStorePath(dataDir = '.data/wechat') {
|
||||
return path.resolve(process.cwd(), dataDir, MESSAGE_FILE)
|
||||
}
|
||||
|
||||
export async function captureWechatMessage(message, bot, options = {}) {
|
||||
const dataDir = options.dataDir || '.data/wechat'
|
||||
const storeMessages = options.storeMessages !== false
|
||||
if (!storeMessages) return null
|
||||
|
||||
const talker = message.talker()
|
||||
const receiver = message.to()
|
||||
const room = message.room()
|
||||
const isText = message.type() === bot.Message.Type.Text
|
||||
const roomName = room ? await room.topic() : ''
|
||||
const talkerAlias = talker ? await talker.alias() : ''
|
||||
const talkerName = talker ? await talker.name() : ''
|
||||
const receiverName = receiver ? await receiver.name() : ''
|
||||
|
||||
const record = {
|
||||
id: message.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
type: message.type(),
|
||||
typeName: bot.Message.Type[message.type()] || String(message.type()),
|
||||
isText,
|
||||
isRoom: Boolean(room),
|
||||
roomName,
|
||||
talkerName,
|
||||
talkerAlias,
|
||||
receiverName,
|
||||
text: isText ? message.text() : '',
|
||||
self: Boolean(talker?.self?.()),
|
||||
}
|
||||
|
||||
const storePath = getMessageStorePath(dataDir)
|
||||
ensureDir(path.dirname(storePath))
|
||||
fs.appendFileSync(storePath, `${JSON.stringify(record)}\n`, 'utf8')
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
export function loadWechatMessages(options = {}) {
|
||||
const storePath = getMessageStorePath(options.dataDir)
|
||||
if (!fs.existsSync(storePath)) return []
|
||||
|
||||
const lines = fs
|
||||
.readFileSync(storePath, 'utf8')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const limit = Number(options.limit || 0)
|
||||
const selectedLines = limit > 0 ? lines.slice(-limit) : lines
|
||||
|
||||
return selectedLines
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function filterWechatMessages(records, filters = {}) {
|
||||
const startTime = filters.start ? new Date(filters.start).getTime() : null
|
||||
const endTime = filters.end ? new Date(filters.end).getTime() : null
|
||||
const query = filters.query ? filters.query.toLowerCase() : ''
|
||||
|
||||
return records.filter((record) => {
|
||||
if (filters.room && record.roomName !== filters.room) return false
|
||||
if (filters.friend) {
|
||||
const names = [record.talkerName, record.talkerAlias, record.receiverName].filter(Boolean)
|
||||
if (!names.includes(filters.friend)) return false
|
||||
}
|
||||
if (
|
||||
query &&
|
||||
!String(record.text || '')
|
||||
.toLowerCase()
|
||||
.includes(query)
|
||||
)
|
||||
return false
|
||||
if (startTime && new Date(record.timestamp).getTime() < startTime) return false
|
||||
if (endTime && new Date(record.timestamp).getTime() > endTime) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user