初始提交:识流 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:
figmar
2026-05-30 14:57:45 +08:00
commit 81115dc23d
129 changed files with 56398 additions and 0 deletions
View File
+143
View File
@@ -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` - 旧版机器人日志(如果使用)
---
**祝使用愉快!🎊**
+310
View File
@@ -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);
}
// DeepSeekOpenAI 兼容风格)
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.
+97
View File
@@ -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);
}
+99
View File
@@ -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()]);
}
+44
View File
@@ -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'); // 先用deepseekDify有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;
}
+45
View File
@@ -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`);
+29
View File
@@ -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;
}
+33
View File
@@ -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
+37
View File
@@ -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 方案1UI自动化版(推荐)
echo - 无需OCR,直接读取微信UI
echo - 不受窗口位置影响
echo - 识别准确率100%%
echo 运行: python wechat_ui_bot.py
echo.
echo 方案2OCR识别版(备用)
echo - 使用图像识别
echo - 需要固定窗口位置
echo 运行: python wechat_auto.py
echo.
pause
+6
View File
@@ -0,0 +1,6 @@
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=$1 last;
break;
}
}
+31
View File
@@ -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()
+57
View File
@@ -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";
}
+81
View File
@@ -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";
}
+486
View File
@@ -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()