From f16794fe2f29c3668d8974b570ab469fac8a895e Mon Sep 17 00:00:00 2001 From: le1ezera Date: Wed, 4 Feb 2026 17:00:27 +0800 Subject: [PATCH 1/4] add Gemini Support --- README.md | 17 +++++++++++++ src/Gemini/__test__.js | 10 ++++++++ src/Gemini/index.js | 49 ++++++++++++++++++++++++++++++++++++++ src/index.js | 7 ++++++ src/wechaty/serve.js | 3 +++ src/wechaty/testMessage.js | 10 ++++++++ 6 files changed, 96 insertions(+) create mode 100644 src/Gemini/__test__.js create mode 100644 src/Gemini/index.js diff --git a/README.md b/README.md index a3510f5..9ef8151 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,23 @@ CLAUDE_SYSTEM = '' ``` +- Gemini + + 前往[Gemini API Quickstart官网](https://ai.google.dev/gemini-api/docs/quickstart)创建API并在.env中配置即可,里面有详细教程。 + Gemini配置比较简单,只需要一个API就可以了。 + 收费方式和OpenAI差不多,依旧是需要海外信用卡。 + ```bash + # 执行下面命令,拷贝一份 .env.example 文件为 .env,如果已存在则忽略此步 + cp .env.example .env + + # 编辑.env文件并添加claude相关配置 + GEMINI_API_KEY = '你的API KEY' + GEMINI_MODEL = 'gemini-2.5-flash' + + ``` + 关于GEMINI_MODEL处可用的Model List,参见[Models](https://ai.google.dev/gemini-api/docs/models) + + - 其他 (待实践)理论上使用 openAI 格式的 api,都可以使用,在 env 文件中修改对应的 api_key、model、proxy_url 即可。 diff --git a/src/Gemini/__test__.js b/src/Gemini/__test__.js new file mode 100644 index 0000000..18e5cd1 --- /dev/null +++ b/src/Gemini/__test__.js @@ -0,0 +1,10 @@ +import { getGeminiReply } from './index.js' + +// 测试 Gemini api +async function testMessage() { + let message + message = await getGeminiReply('Hello') + console.log('🌸🌸🌸 / message: ', message) +} + +testMessage() diff --git a/src/Gemini/index.js b/src/Gemini/index.js new file mode 100644 index 0000000..498df14 --- /dev/null +++ b/src/Gemini/index.js @@ -0,0 +1,49 @@ +import { GoogleGenAI } from "@google/genai"; +import dotenv from 'dotenv' +const env = dotenv.config().parsed // 环境参数 +import fs from 'fs' +import path from 'path' + +const __dirname = path.resolve() +// 判断是否有 .env 文件, 没有则报错 +const envPath = path.join(__dirname, '.env') +if (!fs.existsSync(envPath)) { + console.log('❌ 请先根据文档,创建并配置.env文件!') + process.exit(1) +} + +if (!env.GEMINI_API_KEY) { + console.log('❌ 请先根据文档,配置GEMINI_API_KEY!') + process.exit(1) +} + +let config = { + apiKey: env.GEMINI_API_KEY, + // 如果没有配置model,则默认使用gemini-2.5-flash + baseModel: env.GEMINI_MODEL ? env.GEMINI_MODEL : "gemini-2.5-flash" +} + +const gemini = new GoogleGenAI(config) + +export async function getGeminiReply(prompt) { + if (!prompt) { + console.warn('⚠️ Warning: Received empty prompt.'); + return ''; + } + console.log('🚀🚀🚀 / prompt', prompt) + try { + const response = await gemini.models.generateContent({ + model: config.baseModel, + contents: prompt, + }); + if (!response || !response.text) { + console.warn('⚠️ Warning: Empty response from Gemini (possibly blocked by safety settings).'); + return ''; + } + console.log('🚀🚀🚀 / reply', response.text); + return `${response.text}` + } catch (error){ + console.error('❌ Gemini API Error:', error.message); + return ''; + } +} diff --git a/src/index.js b/src/index.js index d388c88..76c0244 100644 --- a/src/index.js +++ b/src/index.js @@ -178,6 +178,12 @@ function handleStart(type) { } console.log('❌ 请先配置.env文件中的 CLAUDE_API_KEY 和 CLAUDE_MODEL') break + case 'Gemini': + if (env.GEMINI_API_KEY) { + return botStart() + } + console.log('❌ 请先配置.env文件中的 GEMINI_API_KEY') + break default: console.log('❌ 服务类型错误, 目前支持: ChatGPT | doubao | deepseek | Kimi | Xunfei | DIFY | OLLAMA | TONGYI') } @@ -196,6 +202,7 @@ export const serveList = [ { name: 'ollama', value: 'ollama' }, { name: 'tongyi', value: 'tongyi' }, { name: 'claude', value: 'claude' }, + { name: 'Gemini', value: 'Gemini' }, ] const questions = [ { diff --git a/src/wechaty/serve.js b/src/wechaty/serve.js index 0cc461f..47b74c5 100644 --- a/src/wechaty/serve.js +++ b/src/wechaty/serve.js @@ -9,6 +9,7 @@ import { getDifyReply } from '../dify/index.js' import { getOllamaReply } from '../ollama/index.js' import { getTongyiReply } from '../tongyi/index.js' import { getClaudeReply } from '../claude/index.js' +import { getGeminiReply } from '../Gemini/index.js' /** * 获取ai服务 @@ -39,6 +40,8 @@ export function getServe(serviceType) { return getTongyiReply case 'claude': return getClaudeReply + case 'Gemini': + return getGeminiReply default: return getGptReply } diff --git a/src/wechaty/testMessage.js b/src/wechaty/testMessage.js index 498faf7..4ffb3a7 100644 --- a/src/wechaty/testMessage.js +++ b/src/wechaty/testMessage.js @@ -7,6 +7,8 @@ import { getDeepSeekFreeReply } from '../deepseek-free/index.js' import { get302AiReply } from '../302ai/index.js' import { getDifyReply } from '../dify/index.js' import { getOllamaReply } from '../ollama/index.js' +import { getGeminiReply } from '../Gemini/index.js' + const env = dotenv.config().parsed // 环境参数 // 控制启动 @@ -69,6 +71,14 @@ async function handleRequest(type) { } console.log('❌ 请先配置.env文件中的 OLLAMA_URL') break + case 'Gemini': + if (env.GEMINI_API_KEY) { + const message = await getGeminiReply('hello') + console.log('🌸🌸🌸 / reply: ', message) + return + } + console.log('❌ 请先配置.env文件中的 OLLAMA_URL') + break default: console.log('🚀服务类型错误') } From a18ee83f9134eaa02ad7bf2ca4f66166a9539112 Mon Sep 17 00:00:00 2001 From: le1ezera Date: Thu, 5 Feb 2026 15:37:47 +0800 Subject: [PATCH 2/4] add Gemini support env and dependency in package.json --- .env.example | 4 ++++ package.json | 1 + 2 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index bc0f958..50c470e 100644 --- a/.env.example +++ b/.env.example @@ -92,6 +92,10 @@ OLLAMA_URL='http://127.0.0.1:11434/api/chat' OLLAMA_MODEL='' OLLAMA_SYSTEM_MESSAGE='You are a personal assistant.' +# Gemini +GEMINI_API_KEY = '' +GEMINI_MODEL = '' + # 白名单配置 #定义机器人的名称,这里是为了防止群聊消息太多,所以只有艾特机器人才会回复, #这里不要把@去掉,在@后面加上你启动机器人账号的微信名称 diff --git a/package.json b/package.json index 4b8e8bf..c664f92 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "author": "荣顶", "license": "ISC", "dependencies": { + "@google/genai": "^1.40.0", "axios": "^1.6.8", "chatgpt": "^2.5.2", "commander": "^12.0.0", From 80f59282b7bdbcfaa4d8f81613d9e042f31a3f83 Mon Sep 17 00:00:00 2001 From: le1ezera Date: Thu, 5 Feb 2026 16:00:24 +0800 Subject: [PATCH 3/4] fix testMessage.js Gemini case error log --- src/wechaty/testMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wechaty/testMessage.js b/src/wechaty/testMessage.js index 4ffb3a7..9125261 100644 --- a/src/wechaty/testMessage.js +++ b/src/wechaty/testMessage.js @@ -77,7 +77,7 @@ async function handleRequest(type) { console.log('🌸🌸🌸 / reply: ', message) return } - console.log('❌ 请先配置.env文件中的 OLLAMA_URL') + console.log('❌ 请先配置.env文件中的 GEMINI_API_KEY') break default: console.log('🚀服务类型错误') From a2e0ff322ef6ec0cd9c9369b53ad979ad6d42740 Mon Sep 17 00:00:00 2001 From: Ethan Lau Date: Sat, 27 Jun 2026 11:36:54 +0800 Subject: [PATCH 4/4] update Gemini --- .env.example | 25 ++- src/Gemini/index.js | 55 +++--- src/index.js | 447 ++++++++++++++++++++++--------------------- src/wechaty/serve.js | 52 +++-- 4 files changed, 299 insertions(+), 280 deletions(-) diff --git a/.env.example b/.env.example index 50c470e..7c35261 100644 --- a/.env.example +++ b/.env.example @@ -93,8 +93,9 @@ OLLAMA_MODEL='' OLLAMA_SYSTEM_MESSAGE='You are a personal assistant.' # Gemini +# 默认使用gemini-2.5-flash" GEMINI_API_KEY = '' -GEMINI_MODEL = '' +GEMINI_MODEL = 'gemini-2.5-flash"' # 白名单配置 #定义机器人的名称,这里是为了防止群聊消息太多,所以只有艾特机器人才会回复, @@ -110,3 +111,25 @@ AUTO_REPLY_PREFIX='' # 默认服务 302AI,ChatGPT、Kimi、Xunfei、deepseek-free, ollama, dify, tongyi 八选一,不填则键盘交互 SERVICE_TYPE='' + +# 本地微信消息捕获与命令 +# 默认只记录扫码登录后收到的消息,用于后续本地统计/分析;设为 false 可关闭记录 +WECHAT_DATA_DIR='.data/wechat' +WECHAT_STORE_MESSAGES='true' +BOT_COMMAND_PREFIX='/' +# 出于安全考虑,微信聊天中远程执行 OpenCLI 默认关闭;仅在你确认需要时开启 +ENABLE_REMOTE_OPENCLI='false' + +# 飞书 IM 通过 lark-cli 接入。首次使用可执行:npm run lark:login +LARK_CLI_BIN='lark-cli' +LARK_DEFAULT_IDENTITY='user' + +# OpenCLI 透传。留空时会使用 npx --yes @jackwener/opencli +OPENCLI_BIN='' +OPENCLI_NPM_PACKAGE='@jackwener/opencli' + +# Pi coding agent 透传。留空时会使用 npx --yes @earendil-works/pi-coding-agent +PI_BIN='' +PI_NPM_PACKAGE='@earendil-works/pi-coding-agent' +# Pi 作为 IM 回复 agent 时使用的参数。默认非交互、单轮回复。 +PI_AGENT_ARGS='--print --no-session' diff --git a/src/Gemini/index.js b/src/Gemini/index.js index 498df14..8014f93 100644 --- a/src/Gemini/index.js +++ b/src/Gemini/index.js @@ -1,4 +1,4 @@ -import { GoogleGenAI } from "@google/genai"; +import { GoogleGenAI } from '@google/genai' import dotenv from 'dotenv' const env = dotenv.config().parsed // 环境参数 import fs from 'fs' @@ -13,37 +13,36 @@ if (!fs.existsSync(envPath)) { } if (!env.GEMINI_API_KEY) { - console.log('❌ 请先根据文档,配置GEMINI_API_KEY!') - process.exit(1) + console.log('❌ 请先根据文档,配置GEMINI_API_KEY!') + process.exit(1) } -let config = { - apiKey: env.GEMINI_API_KEY, - // 如果没有配置model,则默认使用gemini-2.5-flash - baseModel: env.GEMINI_MODEL ? env.GEMINI_MODEL : "gemini-2.5-flash" -} +// 默認使用Gemini-2.5-flash +const targetModel = env.GEMINI_MODEL ? env.GEMINI_MODEL : 'gemini-2.5-flash' -const gemini = new GoogleGenAI(config) +const gemini = new GoogleGenAI({ + apiKey: env.GEMINI_API_KEY, +}) export async function getGeminiReply(prompt) { - if (!prompt) { - console.warn('⚠️ Warning: Received empty prompt.'); - return ''; - } - console.log('🚀🚀🚀 / prompt', prompt) - try { - const response = await gemini.models.generateContent({ - model: config.baseModel, - contents: prompt, - }); - if (!response || !response.text) { - console.warn('⚠️ Warning: Empty response from Gemini (possibly blocked by safety settings).'); - return ''; - } - console.log('🚀🚀🚀 / reply', response.text); - return `${response.text}` - } catch (error){ - console.error('❌ Gemini API Error:', error.message); - return ''; + if (!prompt) { + console.warn('⚠️ Warning: Received empty prompt.') + return '' + } + console.log('🚀🚀🚀 / prompt', prompt) + try { + const response = await gemini.models.generateContent({ + model: targetModel, + contents: prompt, + }) + if (!response || !response.text) { + console.warn('⚠️ Warning: Empty response from Gemini (possibly blocked by safety settings).') + return '' } + console.log('🚀🚀🚀 / reply', response.text) + return `${response.text}` + } catch (error) { + console.error('❌ Gemini API Error:', error.message) + return '' + } } diff --git a/src/index.js b/src/index.js index 76c0244..5290e18 100644 --- a/src/index.js +++ b/src/index.js @@ -1,261 +1,262 @@ import { Command } from 'commander' -import { WechatyBuilder, ScanStatus, log } from 'wechaty' import inquirer from 'inquirer' -import qrTerminal from 'qrcode-terminal' -import dotenv from 'dotenv' - import fs from 'fs' import path, { dirname } from 'path' import { fileURLToPath } from 'url' -import { defaultMessage } from './wechaty/sendMessage.js' +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 env = dotenv.config().parsed // 环境参数 const { version, name } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')) -// 扫码 -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 frienddShipRe = /chatgpt|chat/ - if (friendship.type() === 2) { - if (frienddShipRe.test(friendship.hello())) { - await friendship.accept() - } - } -} - -/** - * 消息发送 - * @param msg - * @param isSharding - * @returns {Promise} - */ -async function onMessage(msg) { - // 默认消息回复 - await defaultMessage(msg, bot, serviceType) - // 消息分片 - // await shardingMessage(msg,bot) -} - -// 初始化机器人 -const CHROME_BIN = process.env.CHROME_BIN ? { endpoint: process.env.CHROME_BIN } : {} -let serviceType = '' -export const bot = WechatyBuilder.build({ - name: 'WechatEveryDay', - puppet: 'wechaty-puppet-wechat4u', // 如果有token,记得更换对应的puppet - // puppet: 'wechaty-puppet-wechat', // 如果 wechaty-puppet-wechat 存在问题,也可以尝试使用上面的 wechaty-puppet-wechat4u ,记得安装 wechaty-puppet-wechat4u - puppetOptions: { - uos: true, - ...CHROME_BIN, - }, -}) - -// 扫码 -bot.on('scan', onScan) -// 登录 -bot.on('login', onLogin) -// 登出 -bot.on('logout', onLogout) -// 收到消息 -bot.on('message', onMessage) -// 添加好友 -bot.on('friendship', onFriendShip) -// 错误 -bot.on('error', (e) => { - console.error('❌ bot error handle: ', e) - // console.log('❌ 程序退出,请重新运行程序') - // bot.stop() - - // // 如果 WechatEveryDay.memory-card.json 文件存在,删除 - // if (fs.existsSync('WechatEveryDay.memory-card.json')) { - // fs.unlinkSync('WechatEveryDay.memory-card.json') - // } - // process.exit() -}) - -// 启动微信机器人 -function botStart() { - bot - .start() - .then(() => console.log('Start to log in wechat...')) - .catch((e) => console.error('❌ botStart error: ', e)) -} - -process.on('uncaughtException', (err) => { - if (err.code === 'ERR_ASSERTION') { - console.error('❌ uncaughtException 捕获到断言错误: ', err.message) - } else { - console.error('❌ uncaughtException 捕获到未处理的异常: ', err) - } - // if (fs.existsSync('WechatEveryDay.memory-card.json')) { - // fs.unlinkSync('WechatEveryDay.memory-card.json') - // } -}) +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' }, + { name: 'Gemini', value: 'Gemini' }, +] -// 控制启动 -function handleStart(type) { - serviceType = type - console.log('🌸🌸🌸 / type: ', type) +function getMissingConfig(type) { switch (type) { case 'ChatGPT': - if (env.OPENAI_API_KEY) return botStart() - console.log('❌ 请先配置.env文件中的 OPENAI_API_KEY') - break + return env.OPENAI_API_KEY ? [] : ['OPENAI_API_KEY'] case 'doubao': - if (env.DOUBAO_API_KEY) return botStart() - console.log('❌ 请先配置.env文件中的 DOUBAO_API_KEY') - break + return env.DOUBAO_API_KEY ? [] : ['DOUBAO_API_KEY'] case 'deepseek': - if (env.DEEPSEEK_API_KEY) return botStart() - console.log('❌ 请先配置.env文件中的 DEEPSEEK_API_KEY') - break + return env.DEEPSEEK_API_KEY ? [] : ['DEEPSEEK_API_KEY'] case 'Kimi': - if (env.KIMI_API_KEY) return botStart() - console.log('❌ 请先配置.env文件中的 KIMI_API_KEY') - break + return env.KIMI_API_KEY ? [] : ['KIMI_API_KEY'] case 'Xunfei': - if (env.XUNFEI_APP_ID && env.XUNFEI_API_KEY && env.XUNFEI_API_SECRET) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 XUNFEI_APP_ID,XUNFEI_API_KEY,XUNFEI_API_SECRET') - break + 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': - if (env.DEEPSEEK_FREE_URL && env.DEEPSEEK_FREE_TOKEN && env.DEEPSEEK_FREE_MODEL) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 DEEPSEEK_FREE_URL,DEEPSEEK_FREE_TOKEN,DEEPSEEK_FREE_MODEL') - break + return env.DEEPSEEK_FREE_URL && env.DEEPSEEK_FREE_TOKEN && env.DEEPSEEK_FREE_MODEL + ? [] + : ['DEEPSEEK_FREE_URL', 'DEEPSEEK_FREE_TOKEN', 'DEEPSEEK_FREE_MODEL'] case '302AI': - if (env._302AI_API_KEY) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 _302AI_API_KEY') - break + return env._302AI_API_KEY ? [] : ['_302AI_API_KEY'] case 'dify': - if (env.DIFY_API_KEY && env.DIFY_URL) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 DIFY_API_KEY') - break + return env.DIFY_API_KEY && env.DIFY_URL ? [] : ['DIFY_API_KEY', 'DIFY_URL'] case 'ollama': - if (env.OLLAMA_URL && env.OLLAMA_MODEL) { - return botStart() - } - break + return env.OLLAMA_URL && env.OLLAMA_MODEL ? [] : ['OLLAMA_URL', 'OLLAMA_MODEL'] case 'tongyi': - if (env.TONGYI_URL && env.TONGYI_MODEL) { - return botStart() - } - break + return env.TONGYI_URL && env.TONGYI_MODEL ? [] : ['TONGYI_URL', 'TONGYI_MODEL'] case 'claude': - if (env.CLAUDE_API_KEY && env.CLAUDE_MODEL) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 CLAUDE_API_KEY 和 CLAUDE_MODEL') - break + return env.CLAUDE_API_KEY && env.CLAUDE_MODEL ? [] : ['CLAUDE_API_KEY', 'CLAUDE_MODEL'] + case 'pi': + return [] case 'Gemini': - if (env.GEMINI_API_KEY) { - return botStart() - } - console.log('❌ 请先配置.env文件中的 GEMINI_API_KEY') - break + return env.GEMINI_API_KEY && env.GEMINI_MODEL ? [] : ['GEMINI_API_KEY', 'GEMINI_MODEL'] default: - console.log('❌ 服务类型错误, 目前支持: ChatGPT | doubao | deepseek | Kimi | Xunfei | DIFY | OLLAMA | TONGYI') + return ['SERVICE_TYPE'] } } -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: 'Gemini', value: 'Gemini' }, -] -const questions = [ - { - type: 'list', - name: 'serviceType', //存储当前问题回答的变量key, - message: '请先选择服务类型', - choices: serveList, - }, -] +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 }) +} -function init() { +async function promptAndStart() { if (env.SERVICE_TYPE) { - // 判断env中SERVICE_TYPE是否配置和并且属于serveList数组中value的值 - if (serveList.find((item) => item.value === env.SERVICE_TYPE)) { - handleStart(env.SERVICE_TYPE) - } else { - console.log('❌ 请正确配置.env文件中的 SERVICE_TYPE,或者删除该项') - } - } else { - inquirer - .prompt(questions) - .then((res) => { - handleStart(res.serviceType) - }) - .catch((error) => { - console.log('❌ inquirer error:', error) - }) + 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 ', '跳过交互,直接设置启动的服务类型').action(async () => { + const { serve } = program.opts() + if (serve) { + await startWechat(serve) + return + } + await promptAndStart() +}) + program - .alias('we') - .description('🤖一个基于 WeChaty 结合AI服务实现的微信机器人。') - .version(version, '-v, --version, -V') + .command('start') + .description('启动微信 IM,终端展示二维码扫码登录') .option('-s, --serve ', '跳过交互,直接设置启动的服务类型') - // .option('-p, --proxy ', 'proxy url', '') - .action(function () { - const { serve } = this.opts() - const args = this.args - if (!serve) return init() - handleStart(serve) + .action(async (options) => { + if (options.serve) { + await startWechat(options.serve) + return + } + await promptAndStart() + }) + +program + .command('agent') + .description('启动外部 IM 通道,并使用指定 agent 处理消息') + .option('--im ', '外部通信渠道:wechat', 'wechat') + .option('--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) }) - .command('start') - .option('-s, --serve ', '跳过交互,直接设置启动的服务类型', '') - .action(() => init()) -// program -// .command('config') -// .option('-d, --depth ', 'Set the depth of the folder to be traversed', '10') -// .action(() => { -// // 打印当前项目的路径,而不是执行该文件时的所在路径 -// console.log('请手动修改下面路径中的 config.json 文件') -// console.log(path.resolve(__dirname, '../.env')) -// }) -program.parse() +program + .command('analyze') + .description('分析本地捕获的微信聊天记录') + .option('--room ', '按群聊名称分析') + .option('--friend ', '按好友昵称或备注分析') + .option('--query ', '只分析包含关键词的消息') + .option('--start ', '开始时间 ISO 8601') + .option('--end ', '结束时间 ISO 8601') + .option('--limit ', '最多读取最近 N 条本地消息', '5000') + .option('-s, --serve ', '用于生成深度分析的 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,例:im:message:readonly') + .option('--domain ', '按 domain 授权', 'im') + .option('--no-wait', '只生成授权链接/扫码信息,不阻塞等待授权完成') + .option('--device-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 ', 'user 或 bot', 'user') + .option('--chat-id ', '群聊 ID,oc_xxx') + .option('--user-id ', '用户 open_id,ou_xxx') + .requiredOption('--text ', '文本内容') + .action(async (options) => { + await larkSendText(options) + }) + +lark + .command('messages') + .description('读取某个飞书群聊或 P2P 会话消息') + .option('--as ', 'user 或 bot', 'user') + .option('--chat-id ', '群聊 ID,oc_xxx') + .option('--user-id ', '用户 open_id,ou_xxx') + .option('--start ', '开始时间 ISO 8601') + .option('--end ', '结束时间 ISO 8601') + .option('--page-size ', '分页大小', '50') + .option('--format ', 'json | pretty | table | ndjson | csv', 'pretty') + .action(async (options) => { + await larkListMessages(options) + }) + +lark + .command('search') + .description('搜索飞书 IM 消息') + .option('--query ', '搜索关键词') + .option('--chat-id ', '限制群聊 ID') + .option('--chat-type ', 'group 或 p2p') + .option('--start ', '开始时间 ISO 8601') + .option('--end ', '结束时间 ISO 8601') + .option('--page-all', '自动翻页') + .option('--page-limit ', '最多翻页数', '20') + .option('--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 +}) diff --git a/src/wechaty/serve.js b/src/wechaty/serve.js index 47b74c5..96222d7 100644 --- a/src/wechaty/serve.js +++ b/src/wechaty/serve.js @@ -1,48 +1,44 @@ -import { getGptReply } from '../openai/index.js' -import { getDoubaoReply } from '../doubao/index.js' -import { getDeepseekReply } from '../deepseek/index.js' -import { getKimiReply } from '../kimi/index.js' -import { getXunfeiReply } from '../xunfei/index.js' -import { getDeepSeekFreeReply } from '../deepseek-free/index.js' -import { get302AiReply } from '../302ai/index.js' -import { getDifyReply } from '../dify/index.js' -import { getOllamaReply } from '../ollama/index.js' -import { getTongyiReply } from '../tongyi/index.js' -import { getClaudeReply } from '../claude/index.js' -import { getGeminiReply } from '../Gemini/index.js' +function lazyServe(loader, exportName) { + return async (...args) => { + const module = await loader() + return module[exportName](...args) + } +} /** - * 获取ai服务 - * @param serviceType 服务类型 'GPT' | 'Kimi' - * @returns {Promise} + * 获取 AI 服务 + * @param serviceType 服务类型 + * @returns {Function} */ export function getServe(serviceType) { switch (serviceType) { case 'ChatGPT': - return getGptReply + return lazyServe(() => import('../openai/index.js'), 'getGptReply') case 'doubao': - return getDoubaoReply + return lazyServe(() => import('../doubao/index.js'), 'getDoubaoReply') case 'deepseek': - return getDeepseekReply + return lazyServe(() => import('../deepseek/index.js'), 'getDeepseekReply') case 'Kimi': - return getKimiReply + return lazyServe(() => import('../kimi/index.js'), 'getKimiReply') case 'Xunfei': - return getXunfeiReply + return lazyServe(() => import('../xunfei/index.js'), 'getXunfeiReply') case 'deepseek-free': - return getDeepSeekFreeReply + return lazyServe(() => import('../deepseek-free/index.js'), 'getDeepSeekFreeReply') case '302AI': - return get302AiReply + return lazyServe(() => import('../302ai/index.js'), 'get302AiReply') case 'dify': - return getDifyReply + return lazyServe(() => import('../dify/index.js'), 'getDifyReply') case 'ollama': - return getOllamaReply + return lazyServe(() => import('../ollama/index.js'), 'getOllamaReply') case 'tongyi': - return getTongyiReply + return lazyServe(() => import('../tongyi/index.js'), 'getTongyiReply') case 'claude': - return getClaudeReply + return lazyServe(() => import('../claude/index.js'), 'getClaudeReply') + case 'pi': + return lazyServe(() => import('../pi/index.js'), 'getPiReply') case 'Gemini': - return getGeminiReply + return lazyServe(() => import('../Gemini/index.js'), 'getGeminiReply') default: - return getGptReply + return lazyServe(() => import('../openai/index.js'), 'getGptReply') } }