260 lines
8.6 KiB
JavaScript
260 lines
8.6 KiB
JavaScript
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>', '消息处理 agent:pi 或其他 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>', '指定 scope,例:im: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>', '群聊 ID,oc_xxx')
|
||
.option('--user-id <userId>', '用户 open_id,ou_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>', '群聊 ID,oc_xxx')
|
||
.option('--user-id <userId>', '用户 open_id,ou_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
|
||
})
|