Files
wechat-bot-ai/src/index.js
2026-06-09 14:50:53 +08:00

260 lines
8.6 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.
import { Command } from 'commander'
import inquirer from 'inquirer'
import fs from 'fs'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'
import { env, getWechatRuntimeConfig } from './config/env.js'
import { analyzeWechatMessages } from './analysis/wechatAnalyzer.js'
import { larkListMessages, larkLogin, larkSearchMessages, larkSendText, larkStatus } from './adapters/lark.js'
import { runOpenCli, runWxCli } from './adapters/opencli.js'
import { runPi } from './adapters/pi.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const { version, name } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'))
export const serveList = [
{ name: 'ChatGPT', value: 'ChatGPT' },
{ name: 'doubao', value: 'doubao' },
{ name: 'deepseek', value: 'deepseek' },
{ name: 'Kimi', value: 'Kimi' },
{ name: 'Xunfei', value: 'Xunfei' },
{ name: 'deepseek-free', value: 'deepseek-free' },
{ name: '302AI', value: '302AI' },
{ name: 'dify', value: 'dify' },
{ name: 'ollama', value: 'ollama' },
{ name: 'tongyi', value: 'tongyi' },
{ name: 'claude', value: 'claude' },
{ name: 'pi', value: 'pi' },
]
function getMissingConfig(type) {
switch (type) {
case 'ChatGPT':
return env.OPENAI_API_KEY ? [] : ['OPENAI_API_KEY']
case 'doubao':
return env.DOUBAO_API_KEY ? [] : ['DOUBAO_API_KEY']
case 'deepseek':
return env.DEEPSEEK_API_KEY ? [] : ['DEEPSEEK_API_KEY']
case 'Kimi':
return env.KIMI_API_KEY ? [] : ['KIMI_API_KEY']
case 'Xunfei':
return env.XUNFEI_APP_ID && env.XUNFEI_API_KEY && env.XUNFEI_API_SECRET ? [] : ['XUNFEI_APP_ID', 'XUNFEI_API_KEY', 'XUNFEI_API_SECRET']
case 'deepseek-free':
return env.DEEPSEEK_FREE_URL && env.DEEPSEEK_FREE_TOKEN && env.DEEPSEEK_FREE_MODEL
? []
: ['DEEPSEEK_FREE_URL', 'DEEPSEEK_FREE_TOKEN', 'DEEPSEEK_FREE_MODEL']
case '302AI':
return env._302AI_API_KEY ? [] : ['_302AI_API_KEY']
case 'dify':
return env.DIFY_API_KEY && env.DIFY_URL ? [] : ['DIFY_API_KEY', 'DIFY_URL']
case 'ollama':
return env.OLLAMA_URL && env.OLLAMA_MODEL ? [] : ['OLLAMA_URL', 'OLLAMA_MODEL']
case 'tongyi':
return env.TONGYI_URL && env.TONGYI_MODEL ? [] : ['TONGYI_URL', 'TONGYI_MODEL']
case 'claude':
return env.CLAUDE_API_KEY && env.CLAUDE_MODEL ? [] : ['CLAUDE_API_KEY', 'CLAUDE_MODEL']
case 'pi':
return []
default:
return ['SERVICE_TYPE']
}
}
async function startWechat(type) {
const serviceType = type || env.SERVICE_TYPE
if (!serveList.find((item) => item.value === serviceType)) {
console.log('服务类型错误,目前支持:' + serveList.map((item) => item.value).join(' | '))
return
}
const missing = getMissingConfig(serviceType)
if (missing.length) {
console.log(`请先配置 .env 文件中的 ${missing.join('')}`)
return
}
console.log('service type:', serviceType)
const { startWechatBot } = await import('./platforms/wechat/bot.js')
startWechatBot({ serviceType })
}
async function promptAndStart() {
if (env.SERVICE_TYPE) {
await startWechat(env.SERVICE_TYPE)
return
}
const answer = await inquirer.prompt([
{
type: 'list',
name: 'serviceType',
message: '请先选择服务类型',
choices: serveList,
},
])
await startWechat(answer.serviceType)
}
function printAnalysisResult(result) {
console.log(`分析对象:${result.target}`)
console.log(JSON.stringify(result.stats, null, 2))
if (result.analysis) {
console.log('\n分析结果')
console.log(result.analysis)
}
}
const program = new Command(name)
program.alias('we').description('一个基于 WeChaty 结合 AI 服务实现的微信机器人。').version(version, '-v, --version, -V')
program.option('-s, --serve <type>', '跳过交互,直接设置启动的服务类型').action(async () => {
const { serve } = program.opts()
if (serve) {
await startWechat(serve)
return
}
await promptAndStart()
})
program
.command('start')
.description('启动微信 IM终端展示二维码扫码登录')
.option('-s, --serve <type>', '跳过交互,直接设置启动的服务类型')
.action(async (options) => {
if (options.serve) {
await startWechat(options.serve)
return
}
await promptAndStart()
})
program
.command('agent')
.description('启动外部 IM 通道,并使用指定 agent 处理消息')
.option('--im <channel>', '外部通信渠道wechat', 'wechat')
.option('--agent <agent>', '消息处理 agentpi 或其他 serve 类型', 'pi')
.action(async (options) => {
if (options.im !== 'wechat') {
console.log('当前 agent 命令只支持 --im wechat。飞书可先使用 wb lark login/send/messages/search。')
return
}
await startWechat(options.agent)
})
program
.command('analyze')
.description('分析本地捕获的微信聊天记录')
.option('--room <name>', '按群聊名称分析')
.option('--friend <name>', '按好友昵称或备注分析')
.option('--query <keyword>', '只分析包含关键词的消息')
.option('--start <iso>', '开始时间 ISO 8601')
.option('--end <iso>', '结束时间 ISO 8601')
.option('--limit <number>', '最多读取最近 N 条本地消息', '5000')
.option('-s, --serve <type>', '用于生成深度分析的 AI 服务', env.SERVICE_TYPE || 'ChatGPT')
.option('--stats-only', '只输出统计,不调用 AI 服务')
.action(async (options) => {
const config = getWechatRuntimeConfig()
const result = await analyzeWechatMessages({
...options,
serviceType: options.serve,
dataDir: config.dataDir,
limit: Number(options.limit),
})
printAnalysisResult(result)
})
const lark = program.command('lark').description('飞书 IM 登录、发消息和读取消息')
lark
.command('login')
.description('使用 lark-cli device flow 登录飞书 IM')
.option('--scope <scope>', '指定 scopeim:message:readonly')
.option('--domain <domain>', '按 domain 授权', 'im')
.option('--no-wait', '只生成授权链接/扫码信息,不阻塞等待授权完成')
.option('--device-code <code>', '继续完成上一次 --no-wait 返回的 device_code')
.action(async (options) => {
await larkLogin(options)
})
lark
.command('status')
.description('查看当前飞书授权状态')
.action(async () => {
await larkStatus()
})
lark
.command('send')
.description('发送飞书 IM 文本消息')
.option('--as <identity>', 'user 或 bot', 'user')
.option('--chat-id <chatId>', '群聊 IDoc_xxx')
.option('--user-id <userId>', '用户 open_idou_xxx')
.requiredOption('--text <text>', '文本内容')
.action(async (options) => {
await larkSendText(options)
})
lark
.command('messages')
.description('读取某个飞书群聊或 P2P 会话消息')
.option('--as <identity>', 'user 或 bot', 'user')
.option('--chat-id <chatId>', '群聊 IDoc_xxx')
.option('--user-id <userId>', '用户 open_idou_xxx')
.option('--start <iso>', '开始时间 ISO 8601')
.option('--end <iso>', '结束时间 ISO 8601')
.option('--page-size <number>', '分页大小', '50')
.option('--format <format>', 'json | pretty | table | ndjson | csv', 'pretty')
.action(async (options) => {
await larkListMessages(options)
})
lark
.command('search')
.description('搜索飞书 IM 消息')
.option('--query <keyword>', '搜索关键词')
.option('--chat-id <chatId>', '限制群聊 ID')
.option('--chat-type <type>', 'group 或 p2p')
.option('--start <iso>', '开始时间 ISO 8601')
.option('--end <iso>', '结束时间 ISO 8601')
.option('--page-all', '自动翻页')
.option('--page-limit <number>', '最多翻页数', '20')
.option('--format <format>', 'json | pretty | table | ndjson | csv', 'pretty')
.action(async (options) => {
await larkSearchMessages(options)
})
program
.command('opencli')
.description('透传调用 OpenCLI用于本地微信、朋友圈或其他本机工具')
.allowUnknownOption(true)
.argument('[args...]')
.action(async (args) => {
await runOpenCli(args)
})
program
.command('wx')
.description('通过 OpenCLI wx-cli 访问本地微信聊天、联系人、群成员和朋友圈缓存')
.allowUnknownOption(true)
.argument('[args...]')
.action(async (args) => {
await runWxCli(args)
})
program
.command('pi')
.description('透传调用 Pi coding agent')
.allowUnknownOption(true)
.argument('[args...]')
.action(async (args) => {
await runPi(args)
})
program.parseAsync().catch((error) => {
console.error(error.message)
process.exitCode = 1
})