初始提交:识流 AI 助手项目
微信自动回复机器人,基于截图+OCR识别消息,支持关键词规则和 AI(OpenAI/DeepSeek/Dify)自动回复。 技术栈:PySide6 + Flask + Vue3 + RapidOCR + SQLite 注:OCR大模型文件(.onnx / .pdiparams)不纳入版本控制,需单独下载。 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# 微信 AI 自动回复机器人 - 安装使用指南
|
||||
|
||||
## 📦 安装依赖
|
||||
|
||||
```bash
|
||||
pip install wcferry requests
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 启动 PHP 后端服务
|
||||
确保 phpstudy 已启动,访问 http://127.0.0.1/shiliu_ai/admin.html 确认后端正常
|
||||
|
||||
### 2. 登录微信
|
||||
在电脑上打开微信并登录(必须是 Windows 微信客户端)
|
||||
|
||||
### 3. 运行机器人
|
||||
```bash
|
||||
python wechat_bot.py
|
||||
```
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 新版本 (wechat_bot.py) - 推荐使用 ✅
|
||||
- ✅ 基于 WeChatFerry 框架
|
||||
- ✅ 无需 OCR,100% 准确识别消息
|
||||
- ✅ 不需要固定窗口位置
|
||||
- ✅ 自动回复私聊消息
|
||||
- ✅ 可选开启群聊回复
|
||||
- ✅ 支持 DeepSeek AI 智能回复
|
||||
- ✅ 支持关键词规则匹配
|
||||
- ✅ 所有消息记录到数据库
|
||||
|
||||
### 旧版本 (wechat_auto.py) - 已保留
|
||||
- 基于 OCR 识别
|
||||
- 需要固定窗口位置
|
||||
- 识别准确率较低
|
||||
- 仅供参考学习
|
||||
|
||||
### 手动测试版 (wechat_manual.py)
|
||||
- 手动输入消息测试 AI 回复
|
||||
- 用于调试和测试
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 修改 wechat_bot.py 中的配置:
|
||||
|
||||
```python
|
||||
# PHP 后端接口地址
|
||||
BACKEND_URL = "http://127.0.0.1/shiliu_ai/api_receive_message.php"
|
||||
|
||||
# 是否自动回复群聊(默认只回复私聊)
|
||||
ENABLE_GROUP_REPLY = False # 改为 True 可开启群聊回复
|
||||
```
|
||||
|
||||
### 修改 config.php 配置 AI:
|
||||
|
||||
```php
|
||||
// 选择 AI 提供商:mock / openai / deepseek
|
||||
define('AI_PROVIDER', 'deepseek');
|
||||
|
||||
// DeepSeek API 配置
|
||||
define('DEEPSEEK_API_KEY', '你的API密钥');
|
||||
define('DEEPSEEK_API_BASE', 'https://api.deepseek.com');
|
||||
define('DEEPSEEK_MODEL', 'deepseek-chat');
|
||||
```
|
||||
|
||||
## 📝 使用流程
|
||||
|
||||
1. **接收消息** → 机器人自动监听微信消息
|
||||
2. **规则匹配** → 先检查是否有关键词规则
|
||||
3. **AI 回复** → 没有规则则调用 DeepSeek 生成回复
|
||||
4. **自动发送** → 将回复发送给用户
|
||||
5. **记录保存** → 所有消息保存到数据库
|
||||
|
||||
## 🎯 管理后台
|
||||
|
||||
访问 http://127.0.0.1/shiliu_ai/admin.html 可以:
|
||||
- 查看消息记录
|
||||
- 管理自动回复规则
|
||||
- 配置系统设置
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### Q: 提示 "WeChatFerry 初始化失败"
|
||||
**A:** 确保:
|
||||
1. 微信已经登录
|
||||
2. 已安装 wcferry: `pip install wcferry`
|
||||
3. 使用的是 Windows 微信客户端
|
||||
|
||||
### Q: 机器人没有回复
|
||||
**A:** 检查:
|
||||
1. PHP 后端是否正常运行
|
||||
2. 查看日志文件 `wechat_bot.log`
|
||||
3. 确认 DeepSeek API Key 是否正确
|
||||
|
||||
### Q: 想要回复群聊消息
|
||||
**A:** 修改 `wechat_bot.py` 中的配置:
|
||||
```python
|
||||
ENABLE_GROUP_REPLY = True
|
||||
```
|
||||
|
||||
### Q: 如何添加关键词规则
|
||||
**A:** 访问管理后台 admin.html,在"自动回复规则"中添加
|
||||
|
||||
## 📂 文件说明
|
||||
|
||||
```
|
||||
shiliu_ai/
|
||||
├── wechat_bot.py # 新版机器人(推荐使用)⭐
|
||||
├── wechat_auto.py # 旧版 OCR 机器人(已保留)
|
||||
├── wechat_manual.py # 手动测试工具
|
||||
├── config.php # 配置文件
|
||||
├── ai_helper.php # AI 调用逻辑
|
||||
├── api_receive_message.php # 消息接收接口
|
||||
├── admin.html # 管理后台
|
||||
├── database.sql # 数据库结构
|
||||
└── wechat_bot.log # 运行日志
|
||||
```
|
||||
|
||||
## 🎉 开始使用
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
pip install wcferry requests
|
||||
|
||||
# 2. 确保微信已登录
|
||||
|
||||
# 3. 启动机器人
|
||||
python wechat_bot.py
|
||||
|
||||
# 4. 发送消息测试
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请查看日志文件:
|
||||
- `wechat_bot.log` - 机器人运行日志
|
||||
- `wechat_auto.log` - 旧版机器人日志(如果使用)
|
||||
|
||||
---
|
||||
|
||||
**祝使用愉快!🎊**
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
/**
|
||||
* 从规则表中查找是否有匹配的关键词回复
|
||||
*/
|
||||
function find_rule_reply(string $content): ?array
|
||||
{
|
||||
$pdo = get_pdo();
|
||||
|
||||
// 先查完全匹配,再查包含匹配,简单 MVP 版本
|
||||
$sql = "SELECT * FROM auto_reply_rules WHERE is_active = 1 ORDER BY id ASC";
|
||||
$stmt = $pdo->query($sql);
|
||||
$rules = $stmt->fetchAll();
|
||||
|
||||
$contentLower = mb_strtolower($content, 'UTF-8');
|
||||
error_log("=== 规则匹配开始 ===");
|
||||
error_log("用户消息原文: '{$content}'");
|
||||
error_log("转小写后: '{$contentLower}'");
|
||||
error_log("规则总数: " . count($rules));
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$keyword = trim((string)$rule['keyword']);
|
||||
if ($keyword === '') {
|
||||
error_log("规则ID {$rule['id']}: 关键词为空,跳过");
|
||||
continue;
|
||||
}
|
||||
$kwLower = mb_strtolower($keyword, 'UTF-8');
|
||||
error_log("规则ID {$rule['id']}: 关键词='{$keyword}', 小写='{$kwLower}', 类型={$rule['match_type']}");
|
||||
|
||||
if ($rule['match_type'] === 'equal') {
|
||||
if ($contentLower === $kwLower) {
|
||||
error_log("✓ 完全匹配成功!返回规则ID {$rule['id']}");
|
||||
return $rule;
|
||||
} else {
|
||||
error_log("✗ 完全匹配失败: '{$contentLower}' !== '{$kwLower}'");
|
||||
}
|
||||
} else { // contain
|
||||
if (mb_strpos($contentLower, $kwLower, 0, 'UTF-8') !== false) {
|
||||
error_log("✓ 包含匹配成功!返回规则ID {$rule['id']}");
|
||||
return $rule;
|
||||
} else {
|
||||
error_log("✗ 包含匹配失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error_log("未找到任何匹配规则");
|
||||
error_log("=== 规则匹配结束 ===");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单系统配置读取 / 写入
|
||||
*/
|
||||
function get_setting(string $key, $default = null)
|
||||
{
|
||||
$pdo = get_pdo();
|
||||
$stmt = $pdo->prepare("SELECT `value` FROM settings WHERE `key` = :k LIMIT 1");
|
||||
$stmt->execute([':k' => $key]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
return $default;
|
||||
}
|
||||
return $row['value'];
|
||||
}
|
||||
|
||||
function set_setting(string $key, string $value): void
|
||||
{
|
||||
$pdo = get_pdo();
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO settings(`key`, `value`, updated_at)
|
||||
VALUES(:k, :v, NOW())
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), updated_at = NOW()
|
||||
");
|
||||
$stmt->execute([':k' => $key, ':v' => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用大模型 API(这里以 OpenAI 为例)
|
||||
* 如你用国内模型,可在此处替换调用逻辑。
|
||||
*/
|
||||
function call_ai(string $prompt, string $userId = ''): string
|
||||
{
|
||||
error_log("=== 调用AI开始 ===");
|
||||
error_log("用户消息: '{$prompt}'");
|
||||
error_log("AI提供商: " . AI_PROVIDER);
|
||||
|
||||
if (AI_PROVIDER === 'mock') {
|
||||
return '【自动回复】你刚才说了:' . mb_substr($prompt, 0, 100, 'UTF-8');
|
||||
}
|
||||
|
||||
// OpenAI 兼容接口
|
||||
if (AI_PROVIDER === 'openai') {
|
||||
$url = rtrim(OPENAI_API_BASE, '/') . '/chat/completions';
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: ' . 'Bearer ' . OPENAI_API_KEY,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'model' => OPENAI_MODEL,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => '你是一个专业的微信私域运营助手,用简洁自然的中文回复用户。',
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt,
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'user' => $userId ?: null,
|
||||
];
|
||||
|
||||
return do_llm_request($url, $headers, $payload);
|
||||
}
|
||||
|
||||
// DeepSeek(OpenAI 兼容风格)
|
||||
if (AI_PROVIDER === 'deepseek') {
|
||||
$url = rtrim(DEEPSEEK_API_BASE, '/') . '/chat/completions';
|
||||
error_log("请求URL: {$url}");
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . DEEPSEEK_API_KEY,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'model' => DEEPSEEK_MODEL,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => '你是一个简洁高效的微信助手。回复要求:1.一句话回答,不超过50字 2.不要啰嗦重复 3.直接回答问题,不要客套话 4.不要使用emoji表情',
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt,
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 100, // 限制回复长度
|
||||
'user' => $userId ?: null,
|
||||
];
|
||||
|
||||
return do_llm_request($url, $headers, $payload);
|
||||
}
|
||||
|
||||
// Dify(对话型应用)
|
||||
if (AI_PROVIDER === 'dify') {
|
||||
$url = rtrim(DIFY_API_BASE, '/') . '/chat-messages';
|
||||
error_log("请求URL: {$url}");
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . DIFY_API_KEY,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'inputs' => (object)[],
|
||||
'query' => $prompt,
|
||||
'response_mode' => 'streaming',
|
||||
'user' => $userId ?: DIFY_USER,
|
||||
'conversation_id' => '',
|
||||
];
|
||||
|
||||
error_log("Dify payload: " . json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
return do_dify_request($url, $headers, $payload);
|
||||
}
|
||||
|
||||
// 其他厂商可在此扩展
|
||||
return 'AI_PROVIDER 未配置正确,请检查 config.php。';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dify 专用请求封装
|
||||
*/
|
||||
function do_dify_request(string $url, array $headers, array $payload): string
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 增加超时时间,支持streaming
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // 小缓冲区,支持流式读取
|
||||
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // 允许进度回调
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
error_log("cURL错误: {$err}");
|
||||
return '抱歉,Dify 服务暂时不可用,请稍后再试~(网络错误:' . $err . ')';
|
||||
}
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
error_log("HTTP状态码: {$statusCode}");
|
||||
error_log("响应内容长度: " . strlen($response));
|
||||
error_log("响应内容: {$response}");
|
||||
|
||||
// 处理空响应
|
||||
if (empty($response)) {
|
||||
error_log("Dify返回空响应");
|
||||
return '抱歉,Dify 服务返回空响应,请检查API配置。';
|
||||
}
|
||||
|
||||
// 处理streaming模式的响应(SSE格式)
|
||||
if (strpos($response, 'data:') !== false || strpos($response, 'event:') !== false) {
|
||||
error_log("检测到streaming模式响应");
|
||||
$lines = explode("\n", $response);
|
||||
$fullAnswer = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (strpos($line, 'data:') === 0) {
|
||||
$jsonStr = trim(substr($line, 5));
|
||||
if (empty($jsonStr) || $jsonStr === '[DONE]') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = json_decode($jsonStr, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// Dify streaming格式:{"event":"message","answer":"内容"}
|
||||
if (isset($data['answer'])) {
|
||||
$fullAnswer .= $data['answer'];
|
||||
}
|
||||
// 或者 {"event":"agent_message","answer":"内容"}
|
||||
if (isset($data['event']) && $data['event'] === 'agent_message' && isset($data['answer'])) {
|
||||
$fullAnswer .= $data['answer'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fullAnswer)) {
|
||||
error_log("Dify回复(streaming): {$fullAnswer}");
|
||||
error_log("=== 调用AI结束 ===");
|
||||
return trim($fullAnswer);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理blocking模式的响应(JSON格式)
|
||||
$data = json_decode($response, true);
|
||||
if ($statusCode >= 400 || !is_array($data)) {
|
||||
$msg = $data['message'] ?? '未知错误';
|
||||
error_log("Dify API错误: {$msg}");
|
||||
return '抱歉,Dify 服务请求失败,请稍后再试~(状态码 ' . $statusCode . ':' . $msg . ')';
|
||||
}
|
||||
|
||||
// Dify 返回格式:{"answer": "回复内容", "conversation_id": "xxx"}
|
||||
$content = $data['answer'] ?? '';
|
||||
if (!$content) {
|
||||
error_log("Dify返回内容为空");
|
||||
error_log("完整响应: " . print_r($data, true));
|
||||
return '抱歉,Dify 暂时没有合理的回复。';
|
||||
}
|
||||
error_log("Dify回复(blocking): {$content}");
|
||||
error_log("=== 调用AI结束 ===");
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用大模型 HTTP 请求封装
|
||||
*/
|
||||
function do_llm_request(string $url, array $headers, array $payload): string
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 增加超时时间,支持streaming
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // 小缓冲区,支持流式读取
|
||||
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // 允许进度回调
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
error_log("cURL错误: {$err}");
|
||||
return '抱歉,AI 服务暂时不可用,请稍后再试~(网络错误:' . $err . ')';
|
||||
}
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
error_log("HTTP状态码: {$statusCode}");
|
||||
error_log("响应内容: {$response}");
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if ($statusCode >= 400 || !is_array($data)) {
|
||||
$msg = $data['error']['message'] ?? '未知错误';
|
||||
error_log("API错误: {$msg}");
|
||||
return '抱歉,AI 服务请求失败,请稍后再试~(状态码 ' . $statusCode . ':' . $msg . ')';
|
||||
}
|
||||
|
||||
$content = $data['choices'][0]['message']['content'] ?? '';
|
||||
if (!$content) {
|
||||
error_log("AI返回内容为空");
|
||||
return '抱歉,AI 暂时没有合理的回复。';
|
||||
}
|
||||
error_log("AI回复: {$content}");
|
||||
error_log("=== 调用AI结束 ===");
|
||||
return trim($content);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
// Python 客户端调用此接口,将微信新消息传进来
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/ai_helper.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_response(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
$data = $_POST; // 兼容表单
|
||||
}
|
||||
|
||||
$content = trim((string)($data['content'] ?? ''));
|
||||
$wxUserId = trim((string)($data['wx_user_id'] ?? ''));
|
||||
$wxNickname = trim((string)($data['wx_nickname'] ?? ''));
|
||||
$isFriendRequest = (int)($data['is_friend_request'] ?? 0);
|
||||
|
||||
if ($content === '' && !$isFriendRequest) {
|
||||
json_response(['error' => 'content is empty'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = get_pdo();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 记录收到的消息
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO messages (wx_user_id, wx_nickname, direction, content, is_friend_request, created_at)
|
||||
VALUES (:uid, :nick, 'in', :content, :fr, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
':uid' => $wxUserId,
|
||||
':nick' => $wxNickname,
|
||||
':content' => $content,
|
||||
':fr' => $isFriendRequest,
|
||||
]);
|
||||
$inMsgId = (int)$pdo->lastInsertId();
|
||||
|
||||
$autoOn = get_setting('auto_reply_enabled', '1') === '1';
|
||||
$replyText = '';
|
||||
$usedRuleId = null;
|
||||
|
||||
if ($autoOn) {
|
||||
// 先规则匹配
|
||||
$rule = find_rule_reply($content);
|
||||
if ($rule) {
|
||||
$replyText = (string)$rule['reply_text'];
|
||||
$usedRuleId = (int)$rule['id'];
|
||||
// 调试信息
|
||||
error_log("匹配到规则ID: {$usedRuleId}, 关键词: {$rule['keyword']}, 回复: {$replyText}");
|
||||
} else {
|
||||
// 没有规则就走 AI
|
||||
error_log("未匹配到规则,调用AI,用户消息: {$content}");
|
||||
$replyText = call_ai($content, $wxUserId);
|
||||
error_log("AI返回: {$replyText}");
|
||||
}
|
||||
}
|
||||
|
||||
$shouldReply = $autoOn && $replyText !== '';
|
||||
$replyMsgId = null;
|
||||
|
||||
if ($shouldReply) {
|
||||
$stmt2 = $pdo->prepare("
|
||||
INSERT INTO messages (wx_user_id, wx_nickname, direction, content, is_ai_reply, rule_id, created_at)
|
||||
VALUES (:uid, :nick, 'out', :content, :is_ai, :rule_id, NOW())
|
||||
");
|
||||
$stmt2->execute([
|
||||
':uid' => $wxUserId,
|
||||
':nick' => $wxNickname,
|
||||
':content' => $replyText,
|
||||
':is_ai' => 1,
|
||||
':rule_id' => $usedRuleId,
|
||||
]);
|
||||
$replyMsgId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
json_response([
|
||||
'success' => true,
|
||||
'should_reply' => $shouldReply,
|
||||
'reply_text' => $replyText,
|
||||
'in_message_id' => $inMsgId,
|
||||
'reply_message_id' => $replyMsgId,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
json_response(['error' => 'server_error', 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
// 简单规则配置接口:被 Web 后台用 Ajax 调用
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/ai_helper.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? 'list';
|
||||
$pdo = get_pdo();
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'list':
|
||||
$stmt = $pdo->query("SELECT * FROM auto_reply_rules ORDER BY id DESC");
|
||||
$rules = $stmt->fetchAll();
|
||||
json_response(['success' => true, 'data' => $rules]);
|
||||
break;
|
||||
|
||||
case 'create':
|
||||
$keyword = trim((string)($_POST['keyword'] ?? ''));
|
||||
$matchType = $_POST['match_type'] ?? 'contain';
|
||||
$replyText = trim((string)($_POST['reply_text'] ?? ''));
|
||||
$isActive = (int)($_POST['is_active'] ?? 1);
|
||||
|
||||
if ($keyword === '' || $replyText === '') {
|
||||
json_response(['success' => false, 'message' => '关键词和回复内容不能为空']);
|
||||
}
|
||||
|
||||
if (!in_array($matchType, ['contain', 'equal'], true)) {
|
||||
$matchType = 'contain';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO auto_reply_rules(keyword, match_type, reply_text, is_active, created_at, updated_at)
|
||||
VALUES(:kw, :mt, :rt, :act, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
':kw' => $keyword,
|
||||
':mt' => $matchType,
|
||||
':rt' => $replyText,
|
||||
':act' => $isActive,
|
||||
]);
|
||||
json_response(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$isActive = (int)($_POST['is_active'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
json_response(['success' => false, 'message' => '参数错误']);
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE auto_reply_rules SET is_active = :act, updated_at = NOW() WHERE id = :id");
|
||||
$stmt->execute([':act' => $isActive, ':id' => $id]);
|
||||
json_response(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
json_response(['success' => false, 'message' => '参数错误']);
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM auto_reply_rules WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
json_response(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'settings_get':
|
||||
$autoOn = get_setting('auto_reply_enabled', '1');
|
||||
json_response(['success' => true, 'auto_reply_enabled' => $autoOn === '1']);
|
||||
break;
|
||||
|
||||
case 'settings_set':
|
||||
$autoOn = ($_POST['auto_reply_enabled'] ?? '1') === '1' ? '1' : '0';
|
||||
set_setting('auto_reply_enabled', $autoOn);
|
||||
json_response(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'messages_recent':
|
||||
$limit = max(1, min(100, (int)($_GET['limit'] ?? 50)));
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT * FROM messages
|
||||
ORDER BY id DESC
|
||||
LIMIT :lim
|
||||
");
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
json_response(['success' => true, 'data' => $rows]);
|
||||
break;
|
||||
|
||||
default:
|
||||
json_response(['success' => false, 'message' => '未知操作']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
json_response(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// 基础配置文件,请根据你的环境修改
|
||||
|
||||
// 数据库配置
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_PORT', '3306');
|
||||
define('DB_NAME', 'shiliu_ai');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', 'root');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// AI 大模型配置(以 OpenAI / DeepSeek / Dify 为例,可自行替换为其他厂商)
|
||||
// 可选值:mock / openai / deepseek / dify
|
||||
define('AI_PROVIDER', 'dify'); // 先用deepseek,Dify有401错误
|
||||
|
||||
// OpenAI 兼容接口配置
|
||||
define('OPENAI_API_KEY', 'YOUR_OPENAI_API_KEY_HERE');
|
||||
define('OPENAI_API_BASE', 'https://api.openai.com/v1');
|
||||
define('OPENAI_MODEL', 'gpt-4.1-mini');
|
||||
|
||||
// DeepSeek 兼容接口配置(请在这里填入你自己的 key)
|
||||
define('DEEPSEEK_API_KEY', 'sk-012531a0108d4fe086fcba34e1c758fe');
|
||||
define('DEEPSEEK_API_BASE', 'https://api.deepseek.com');
|
||||
define('DEEPSEEK_MODEL', 'deepseek-chat');
|
||||
|
||||
// Dify 配置(请填入你的 Dify API Key 和 URL)
|
||||
define('DIFY_API_KEY', 'app-a9dofsiQi4e157uDYTx8Lrja'); // 在Dify后台获取
|
||||
define('DIFY_API_BASE', 'http://47.92.48.126/v1'); // 修改:v1 → api
|
||||
define('DIFY_USER', 'wechat_user'); // 用户标识
|
||||
|
||||
// 系统基础配置
|
||||
define('APP_TIMEZONE', 'Asia/Shanghai');
|
||||
date_default_timezone_set(APP_TIMEZONE);
|
||||
|
||||
// 简单的 JSON 输出工具
|
||||
function json_response($data, int $code = 200)
|
||||
{
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 创建数据库(如果还没建的话)
|
||||
CREATE DATABASE IF NOT EXISTS `shiliu_ai` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `shiliu_ai`;
|
||||
|
||||
-- 消息记录表
|
||||
CREATE TABLE IF NOT EXISTS `messages` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`wx_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信用户唯一标识(可用备注/手机号等人工映射)',
|
||||
`wx_nickname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '微信昵称',
|
||||
`direction` ENUM('in','out') NOT NULL DEFAULT 'in' COMMENT 'in=收到, out=发出',
|
||||
`content` TEXT NOT NULL COMMENT '消息内容',
|
||||
`is_ai_reply` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为 AI 自动回复',
|
||||
`rule_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '命中的规则 ID',
|
||||
`is_friend_request` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为好友申请类通知',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_time` (`wx_user_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 自动回复规则表
|
||||
CREATE TABLE IF NOT EXISTS `auto_reply_rules` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`keyword` VARCHAR(255) NOT NULL COMMENT '关键词',
|
||||
`match_type` ENUM('contain','equal') NOT NULL DEFAULT 'contain' COMMENT '匹配方式:包含 / 完全匹配',
|
||||
`reply_text` TEXT NOT NULL COMMENT '回复内容',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS `settings` (
|
||||
`key` VARCHAR(64) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 默认开启自动回复
|
||||
INSERT INTO `settings`(`key`, `value`, `updated_at`)
|
||||
VALUES ('auto_reply_enabled', '1', NOW())
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
function get_pdo(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_NAME,
|
||||
DB_CHARSET
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
echo ========================================
|
||||
echo 安装OCR微信机器人依赖
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1/5] 安装基础依赖...
|
||||
pip install pillow pyautogui pyperclip opencv-python numpy requests -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo [2/5] 安装PaddleOCR(推荐,准确率最高)...
|
||||
pip install paddlepaddle paddleocr -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo [3/5] 备用:安装EasyOCR...
|
||||
pip install easyocr -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo [4/5] 安装键盘控制库...
|
||||
pip install keyboard -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 安装完成!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 使用方法:
|
||||
echo 1. 打开微信并登录
|
||||
echo 2. 打开要自动回复的聊天窗口
|
||||
echo 3. 运行: python wechat_bot_fixed.py
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
echo ========================================
|
||||
echo 安装微信自动回复依赖
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1/3] 安装UI自动化库(推荐)...
|
||||
pip install uiautomation pyperclip requests -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo [2/3] 安装OCR版本依赖(备用)...
|
||||
pip install opencv-python numpy pillow pyautogui -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo [3/3] 安装PaddleOCR(可选)...
|
||||
pip install paddleocr -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 安装完成!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 使用方法:
|
||||
echo.
|
||||
echo 方案1:UI自动化版(推荐)
|
||||
echo - 无需OCR,直接读取微信UI
|
||||
echo - 不受窗口位置影响
|
||||
echo - 识别准确率100%%
|
||||
echo 运行: python wechat_ui_bot.py
|
||||
echo.
|
||||
echo 方案2:OCR识别版(备用)
|
||||
echo - 使用图像识别
|
||||
echo - 需要固定窗口位置
|
||||
echo 运行: python wechat_auto.py
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,6 @@
|
||||
location / {
|
||||
if (!-e $request_filename) {
|
||||
rewrite ^(.*)$ /index.php?s=$1 last;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
import py_backend
|
||||
from wechat_multi_chat_bot import WechatMultiChatBot
|
||||
|
||||
HOST = os.getenv("APP_HOST", "127.0.0.1")
|
||||
PORT = int(os.getenv("APP_PORT", "5000"))
|
||||
OPEN_BROWSER = os.getenv("OPEN_BROWSER", "1") == "1"
|
||||
|
||||
|
||||
def run_backend():
|
||||
py_backend.start_backend(host=HOST, port=PORT)
|
||||
|
||||
|
||||
def run_bot():
|
||||
bot = WechatMultiChatBot()
|
||||
bot.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
backend_thread = threading.Thread(target=run_backend, daemon=True)
|
||||
backend_thread.start()
|
||||
|
||||
time.sleep(1.5)
|
||||
if OPEN_BROWSER:
|
||||
webbrowser.open(f"http://{HOST}:{PORT}/admin.html")
|
||||
|
||||
run_bot()
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
echo "=== 测试Dify配置 ===\n\n";
|
||||
|
||||
echo "AI_PROVIDER: " . AI_PROVIDER . "\n";
|
||||
echo "DIFY_API_KEY: " . DIFY_API_KEY . "\n";
|
||||
echo "DIFY_API_BASE: " . DIFY_API_BASE . "\n";
|
||||
echo "DIFY_USER: " . DIFY_USER . "\n\n";
|
||||
|
||||
// 测试请求
|
||||
$url = rtrim(DIFY_API_BASE, '/') . '/chat-messages';
|
||||
echo "请求URL: {$url}\n\n";
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . DIFY_API_KEY,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'inputs' => (object)[], // 空对象
|
||||
'query' => '你好',
|
||||
'response_mode' => 'blocking',
|
||||
'user' => DIFY_USER,
|
||||
];
|
||||
|
||||
echo "请求数据:\n";
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
echo "发送请求...\n";
|
||||
$response = curl_exec($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
echo "HTTP状态码: {$statusCode}\n";
|
||||
echo "响应内容:\n";
|
||||
echo $response . "\n\n";
|
||||
|
||||
if ($statusCode == 200) {
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['answer'])) {
|
||||
echo "✓ 成功!AI回复: " . $data['answer'] . "\n";
|
||||
} else {
|
||||
echo "✗ 响应格式错误\n";
|
||||
}
|
||||
} else {
|
||||
echo "✗ 请求失败\n";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
echo "=== 测试Dify配置 ===\n\n";
|
||||
|
||||
echo "AI_PROVIDER: " . AI_PROVIDER . "\n";
|
||||
echo "DIFY_API_KEY: " . DIFY_API_KEY . "\n";
|
||||
echo "DIFY_API_BASE: " . DIFY_API_BASE . "\n";
|
||||
echo "DIFY_USER: " . DIFY_USER . "\n\n";
|
||||
|
||||
// 测试请求
|
||||
$url = rtrim(DIFY_API_BASE, '/') . '/chat-messages';
|
||||
echo "请求URL: {$url}\n\n";
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . DIFY_API_KEY,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'inputs' => (object)[], // 空对象
|
||||
'query' => '你好',
|
||||
'response_mode' => 'blocking',
|
||||
'user' => DIFY_USER,
|
||||
];
|
||||
|
||||
echo "请求数据:\n";
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, true); // 开启详细输出
|
||||
|
||||
echo "发送请求...\n";
|
||||
$response = curl_exec($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
echo "HTTP状态码: {$statusCode}\n";
|
||||
echo "响应内容长度: " . strlen($response) . " 字节\n";
|
||||
|
||||
if ($error) {
|
||||
echo "cURL错误: {$error}\n";
|
||||
}
|
||||
|
||||
echo "\n响应内容:\n";
|
||||
if (empty($response)) {
|
||||
echo "(空响应)\n\n";
|
||||
} else {
|
||||
echo $response . "\n\n";
|
||||
}
|
||||
|
||||
if ($statusCode == 200) {
|
||||
if (empty($response)) {
|
||||
echo "✗ 响应为空\n\n";
|
||||
echo "可能的原因:\n";
|
||||
echo "1. Dify应用未发布或已停用\n";
|
||||
echo "2. API Key不正确\n";
|
||||
echo "3. response_mode='blocking' 不支持(试试改成 'streaming')\n";
|
||||
echo "4. Dify服务器问题\n";
|
||||
} else {
|
||||
$data = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo "✗ JSON解析失败: " . json_last_error_msg() . "\n";
|
||||
} elseif (isset($data['answer'])) {
|
||||
echo "✓ 成功!AI回复: " . $data['answer'] . "\n";
|
||||
} else {
|
||||
echo "✗ 响应格式错误\n";
|
||||
echo "JSON内容: " . print_r($data, true) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "✗ 请求失败(HTTP {$statusCode})\n";
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
微信自动回复机器人 - 优化版
|
||||
整合开源项目优点:
|
||||
1. 快速红点检测(numpy矩阵运算,比逐像素快100倍)
|
||||
2. 智能区域过滤(只检测特定X坐标范围)
|
||||
3. 性能统计(详细的耗时分析)
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import hashlib
|
||||
import logging
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import requests
|
||||
import pyperclip
|
||||
import pyautogui
|
||||
from PIL import ImageGrab
|
||||
import uiautomation as auto
|
||||
|
||||
# ========== 配置 ==========
|
||||
|
||||
BAIDU_API_KEY = "ElIQN30iAqpEGi9zv0VlrtQX"
|
||||
BAIDU_SECRET_KEY = "7wrO2wDTx7FehuelgG0NCBDFOklnqSz0"
|
||||
BACKEND_URL = "http://127.0.0.1/shiliu_ai/api_receive_message.php"
|
||||
LOOP_INTERVAL = 3
|
||||
|
||||
# 红点检测配置(借鉴开源项目的精确检测)
|
||||
RED_DOT_CONFIG = {
|
||||
'target_color_bgr': np.array([81, 81, 255]), # 微信红点BGR颜色
|
||||
'color_tolerance': 10, # 颜色容差
|
||||
'x_range': (60, 200), # 检测区域X坐标范围
|
||||
}
|
||||
|
||||
NO_REPLY_KEYWORDS = [
|
||||
"谢谢", "好的", "嗯", "哦", "ok", "收到",
|
||||
"[图片]", "[语音]", "[视频]", "[文件]"
|
||||
]
|
||||
|
||||
# ========== 日志 ==========
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("wechat_bot_optimized.log", encoding="utf-8"),
|
||||
logging.StreamHandler()
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ========== OCR类 ==========
|
||||
|
||||
class BaiduOCR:
|
||||
"""百度OCR识别"""
|
||||
|
||||
def __init__(self):
|
||||
self.access_token = self._get_access_token()
|
||||
logger.info("✓ 百度OCR初始化成功")
|
||||
|
||||
def _get_access_token(self):
|
||||
url = "https://aip.baidubce.com/oauth/2.0/token"
|
||||
params = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": BAIDU_API_KEY,
|
||||
"client_secret": BAIDU_SECRET_KEY
|
||||
}
|
||||
response = requests.post(url, params=params)
|
||||
return response.json().get("access_token")
|
||||
|
||||
def recognize(self, image_bytes):
|
||||
url = f"https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token={self.access_token}"
|
||||
|
||||
payload = {
|
||||
'image': base64.b64encode(image_bytes).decode('utf-8'),
|
||||
'detect_direction': 'false',
|
||||
'paragraph': 'false',
|
||||
'probability': 'false'
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, data=payload)
|
||||
result = response.json()
|
||||
|
||||
if 'words_result' in result:
|
||||
return [item['words'] for item in result['words_result']]
|
||||
return []
|
||||
|
||||
# ========== 微信机器人类(优化版)==========
|
||||
|
||||
class WechatBotOptimized:
|
||||
"""微信自动回复机器人 - 优化版"""
|
||||
|
||||
def __init__(self):
|
||||
self.ocr = BaiduOCR()
|
||||
self.processed_messages = {}
|
||||
self.running = False
|
||||
self.performance_stats = {
|
||||
'red_dot_detect': [],
|
||||
'ocr_recognize': [],
|
||||
'total_process': []
|
||||
}
|
||||
|
||||
def get_window_rect(self):
|
||||
"""获取微信窗口位置"""
|
||||
try:
|
||||
wechat_window = auto.WindowControl(searchDepth=1, Name="微信")
|
||||
if wechat_window.Exists(0, 0):
|
||||
rect = wechat_window.BoundingRectangle
|
||||
return {
|
||||
'left': rect.left,
|
||||
'top': rect.top,
|
||||
'right': rect.right,
|
||||
'bottom': rect.bottom,
|
||||
'width': rect.right - rect.left,
|
||||
'height': rect.bottom - rect.top
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取窗口失败: {e}")
|
||||
return None
|
||||
|
||||
def get_contact_list_rect(self, window_rect):
|
||||
"""获取联系人列表区域"""
|
||||
left = window_rect['left'] + 10
|
||||
top = window_rect['top'] + 50
|
||||
right = window_rect['left'] + int(window_rect['width'] * 0.25) - 10
|
||||
bottom = window_rect['bottom'] - 50
|
||||
|
||||
return {
|
||||
'left': left,
|
||||
'top': top,
|
||||
'right': right,
|
||||
'bottom': bottom
|
||||
}
|
||||
|
||||
def detect_red_dots_fast(self, window_rect):
|
||||
"""
|
||||
快速红点检测(借鉴开源项目)
|
||||
使用numpy矩阵运算,比逐像素遍历快100倍
|
||||
"""
|
||||
start_time = time.time()
|
||||
contact_rect = self.get_contact_list_rect(window_rect)
|
||||
|
||||
try:
|
||||
# 截图
|
||||
screenshot = ImageGrab.grab(bbox=(
|
||||
contact_rect['left'], contact_rect['top'],
|
||||
contact_rect['right'], contact_rect['bottom']
|
||||
))
|
||||
|
||||
# 转换为numpy数组(BGR格式)
|
||||
img_np = np.array(screenshot)
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# 获取配置
|
||||
target_color = RED_DOT_CONFIG['target_color_bgr']
|
||||
tolerance = RED_DOT_CONFIG['color_tolerance']
|
||||
x_range = RED_DOT_CONFIG['x_range']
|
||||
|
||||
# 生成坐标网格(性能优化关键!)
|
||||
height, width = img_bgr.shape[:2]
|
||||
x_coords, y_coords = np.meshgrid(
|
||||
np.arange(width),
|
||||
np.arange(height)
|
||||
)
|
||||
|
||||
# 颜色匹配(矩阵运算,比循环快100倍)
|
||||
lower_bound = target_color - tolerance
|
||||
upper_bound = target_color + tolerance
|
||||
color_mask = np.all((lower_bound <= img_bgr) & (img_bgr <= upper_bound), axis=-1)
|
||||
|
||||
# 区域过滤(只检测特定X坐标范围)
|
||||
region_mask = (x_coords >= x_range[0]) & (x_coords <= x_range[1])
|
||||
|
||||
# 获取候选坐标
|
||||
matched_points = np.column_stack((
|
||||
x_coords[color_mask & region_mask],
|
||||
y_coords[color_mask & region_mask]
|
||||
))
|
||||
|
||||
if matched_points.size == 0:
|
||||
return []
|
||||
|
||||
# 按Y坐标分组(同一联系人的多个红点合并)
|
||||
red_dots = []
|
||||
used = set()
|
||||
|
||||
for i, point in enumerate(matched_points):
|
||||
if i in used:
|
||||
continue
|
||||
|
||||
# 找到Y坐标相近的点
|
||||
group = [point]
|
||||
for j, other in enumerate(matched_points):
|
||||
if j != i and j not in used:
|
||||
if abs(point[1] - other[1]) < 50:
|
||||
group.append(other)
|
||||
used.add(j)
|
||||
|
||||
# 计算平均位置
|
||||
avg_x = int(np.mean([p[0] for p in group]))
|
||||
avg_y = int(np.mean([p[1] for p in group]))
|
||||
|
||||
red_dots.append({
|
||||
'x': contact_rect['left'] + avg_x,
|
||||
'y': contact_rect['top'] + avg_y
|
||||
})
|
||||
used.add(i)
|
||||
|
||||
# 记录性能
|
||||
elapsed = time.time() - start_time
|
||||
self.performance_stats['red_dot_detect'].append(elapsed)
|
||||
|
||||
return red_dots
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"红点检测失败: {e}")
|
||||
return []
|
||||
|
||||
def click_contact_by_red_dot(self, red_dot, window_rect):
|
||||
"""点击联系人"""
|
||||
contact_rect = self.get_contact_list_rect(window_rect)
|
||||
|
||||
click_x = (contact_rect['left'] + contact_rect['right']) // 2
|
||||
click_y = red_dot['y']
|
||||
|
||||
pyautogui.click(click_x, click_y)
|
||||
time.sleep(2.5)
|
||||
|
||||
logger.info(f"点击联系人位置: ({click_x}, {click_y})")
|
||||
|
||||
def get_latest_message_area(self, window_rect):
|
||||
"""获取最新消息区域"""
|
||||
chat_left = window_rect['left'] + int(window_rect['width'] * 0.30)
|
||||
chat_right = window_rect['right'] - 20
|
||||
chat_top = window_rect['top'] + int(window_rect['height'] * 0.15)
|
||||
chat_bottom = window_rect['bottom'] - int(window_rect['height'] * 0.20)
|
||||
|
||||
return {
|
||||
'left': chat_left,
|
||||
'top': max(chat_bottom - 300, chat_top),
|
||||
'right': chat_right,
|
||||
'bottom': chat_bottom
|
||||
}
|
||||
|
||||
def process_current_chat(self, window_rect, contact_key):
|
||||
"""处理当前聊天"""
|
||||
start_time = time.time()
|
||||
msg_rect = self.get_latest_message_area(window_rect)
|
||||
|
||||
try:
|
||||
screenshot = ImageGrab.grab(bbox=(
|
||||
msg_rect['left'], msg_rect['top'],
|
||||
msg_rect['right'], msg_rect['bottom']
|
||||
))
|
||||
|
||||
# OCR识别
|
||||
ocr_start = time.time()
|
||||
img_byte_arr = BytesIO()
|
||||
screenshot.save(img_byte_arr, format='PNG')
|
||||
img_bytes = img_byte_arr.getvalue()
|
||||
|
||||
lines = self.ocr.recognize(img_bytes)
|
||||
ocr_elapsed = time.time() - ocr_start
|
||||
self.performance_stats['ocr_recognize'].append(ocr_elapsed)
|
||||
|
||||
if not lines:
|
||||
return False
|
||||
|
||||
# 过滤消息(严格过滤)
|
||||
import re
|
||||
valid_lines = []
|
||||
for line in lines:
|
||||
if len(line) < 3:
|
||||
continue
|
||||
if re.match(r'^\d{1,2}:\d{2}$', line):
|
||||
continue
|
||||
if any(char in line for char in ['©', 'ò', 'v0', 'V0']):
|
||||
continue
|
||||
valid_lines.append(line)
|
||||
|
||||
if not valid_lines:
|
||||
return False
|
||||
|
||||
latest = valid_lines[-1]
|
||||
|
||||
print(f" [识别] {latest}")
|
||||
|
||||
# 判断是否需要回复
|
||||
if not self.should_reply(latest):
|
||||
print(f" [跳过] 不需要回复")
|
||||
return False
|
||||
|
||||
if not self.is_new_message(latest, contact_key):
|
||||
print(f" [跳过] 已处理")
|
||||
return False
|
||||
|
||||
print(f" [新消息] {latest}")
|
||||
|
||||
# 获取AI回复
|
||||
reply = self.get_ai_reply(latest)
|
||||
if reply:
|
||||
print(f" [AI回复] {reply}")
|
||||
self.send_message(reply, window_rect)
|
||||
|
||||
# 记录性能
|
||||
total_elapsed = time.time() - start_time
|
||||
self.performance_stats['total_process'].append(total_elapsed)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理聊天失败: {e}")
|
||||
return False
|
||||
|
||||
def should_reply(self, message):
|
||||
"""判断是否需要回复"""
|
||||
if not message or len(message) < 2:
|
||||
return False
|
||||
|
||||
for keyword in NO_REPLY_KEYWORDS:
|
||||
if keyword in message:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_new_message(self, message, contact_key):
|
||||
"""判断是否为新消息"""
|
||||
msg_hash = hashlib.md5(message.encode()).hexdigest()
|
||||
|
||||
if contact_key in self.processed_messages:
|
||||
if msg_hash in self.processed_messages[contact_key]:
|
||||
return False
|
||||
else:
|
||||
self.processed_messages[contact_key] = set()
|
||||
|
||||
self.processed_messages[contact_key].add(msg_hash)
|
||||
return True
|
||||
|
||||
def get_ai_reply(self, message):
|
||||
"""获取AI回复"""
|
||||
try:
|
||||
response = requests.post(
|
||||
BACKEND_URL,
|
||||
json={'message': message},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get('reply', '')
|
||||
except Exception as e:
|
||||
logger.error(f"AI回复失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def send_message(self, text, window_rect):
|
||||
"""发送消息"""
|
||||
try:
|
||||
original_clipboard = pyperclip.paste()
|
||||
|
||||
pyperclip.copy(text)
|
||||
time.sleep(0.1)
|
||||
|
||||
pyautogui.hotkey('ctrl', 'v')
|
||||
time.sleep(0.1)
|
||||
|
||||
pyautogui.press('enter')
|
||||
time.sleep(0.3)
|
||||
|
||||
pyperclip.copy(original_clipboard)
|
||||
|
||||
print(f"✓ 已发送")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败: {e}")
|
||||
|
||||
def print_performance_stats(self):
|
||||
"""打印性能统计"""
|
||||
if not self.performance_stats['red_dot_detect']:
|
||||
return
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("性能统计")
|
||||
print("="*70)
|
||||
|
||||
avg_red_dot = np.mean(self.performance_stats['red_dot_detect']) * 1000
|
||||
avg_ocr = np.mean(self.performance_stats['ocr_recognize']) * 1000 if self.performance_stats['ocr_recognize'] else 0
|
||||
avg_total = np.mean(self.performance_stats['total_process']) * 1000 if self.performance_stats['total_process'] else 0
|
||||
|
||||
print(f"红点检测平均耗时: {avg_red_dot:.1f}ms")
|
||||
print(f"OCR识别平均耗时: {avg_ocr:.1f}ms")
|
||||
print(f"总处理平均耗时: {avg_total:.1f}ms")
|
||||
print("="*70 + "\n")
|
||||
|
||||
def run_forever(self):
|
||||
"""启动监听"""
|
||||
print("=" * 70)
|
||||
print("微信自动回复(优化版)")
|
||||
print("=" * 70)
|
||||
print("\n优化特性:")
|
||||
print(" ✓ 快速红点检测(numpy矩阵运算,比逐像素快100倍)")
|
||||
print(" ✓ 智能区域过滤(只检测特定X坐标范围)")
|
||||
print(" ✓ 性能统计(详细的耗时分析)")
|
||||
print("=" * 70)
|
||||
print("\n监听中... 按 Ctrl+C 停止\n")
|
||||
|
||||
self.running = True
|
||||
round_count = 0
|
||||
|
||||
# 初始化窗口
|
||||
logger.info("正在查找微信窗口...")
|
||||
window_rect = self.get_window_rect()
|
||||
if not window_rect:
|
||||
logger.error("未找到微信窗口")
|
||||
return
|
||||
logger.info("✓ 找到微信窗口")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
round_count += 1
|
||||
print(f"\n{'='*70}")
|
||||
print(f"[第 {round_count} 轮检查] {datetime.now().strftime('%H:%M:%S')}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
# 快速检测红点
|
||||
red_dots = self.detect_red_dots_fast(window_rect)
|
||||
|
||||
if not red_dots:
|
||||
print("未检测到新消息")
|
||||
time.sleep(LOOP_INTERVAL)
|
||||
continue
|
||||
|
||||
print(f"检测到 {len(red_dots)} 个新消息")
|
||||
|
||||
# 处理每个红点
|
||||
for i, red_dot in enumerate(red_dots, 1):
|
||||
print(f"\n[处理第 {i}/{len(red_dots)} 个新消息]")
|
||||
|
||||
self.click_contact_by_red_dot(red_dot, window_rect)
|
||||
|
||||
contact_key = f"{red_dot['x']}_{red_dot['y']}"
|
||||
self.process_current_chat(window_rect, contact_key)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 每10轮打印一次性能统计
|
||||
if round_count % 10 == 0:
|
||||
self.print_performance_stats()
|
||||
|
||||
print(f"\n本轮处理完成,等待 {LOOP_INTERVAL} 秒...\n")
|
||||
time.sleep(LOOP_INTERVAL)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"循环出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
time.sleep(3)
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
bot = WechatBotOptimized()
|
||||
bot.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
bot.stop()
|
||||
print("\n程序已停止")
|
||||
except Exception as e:
|
||||
print(f"\n错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
Reference in New Issue
Block a user