first commit

This commit is contained in:
2026-06-09 14:50:53 +08:00
commit 6aebc60bfd
55 changed files with 3126 additions and 0 deletions

View 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
}

View 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 }
}

View 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
})
}