微信自动回复机器人,基于截图+OCR识别消息,支持关键词规则和 AI(OpenAI/DeepSeek/Dify)自动回复。 技术栈:PySide6 + Flask + Vue3 + RapidOCR + SQLite 注:OCR大模型文件(.onnx / .pdiparams)不纳入版本控制,需单独下载。 🤖 Generated with [Qoder][https://qoder.com]
309 lines
20 KiB
Plaintext
309 lines
20 KiB
Plaintext
<?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("鉁?瀹屽叏鍖归厤鎴愬姛锛佽繑鍥炶鍒橧D {$rule['id']}");
|
|
return $rule;
|
|
} else {
|
|
error_log("鉁?瀹屽叏鍖归厤澶辫触: '{$contentLower}' !== '{$kwLower}'");
|
|
}
|
|
} else { // contain
|
|
if (mb_strpos($contentLower, $kwLower, 0, 'UTF-8') !== false) {
|
|
error_log("鉁?鍖呭惈鍖归厤鎴愬姛锛佽繑鍥炶鍒橧D {$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锛圤penAI 鍏煎椋庢牸锛?
|
|
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' => new stdClass(),
|
|
'query' => $prompt,
|
|
'response_mode' => 'streaming',
|
|
'user' => $userId ?: DIFY_USER,
|
|
];
|
|
|
|
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); // 澧炲姞瓒呮椂鏃堕棿锛屾敮鎸乻treaming
|
|
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 '鎶辨瓑锛孌ify 鏈嶅姟鏆傛椂涓嶅彲鐢紝璇风◢鍚庡啀璇曪綖锛堢綉缁滈敊璇細' . $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 '鎶辨瓑锛孌ify 鏈嶅姟杩斿洖绌哄搷搴旓紝璇锋鏌PI閰嶇疆銆?;
|
|
}
|
|
|
|
// 澶勭悊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 '鎶辨瓑锛孌ify 鏈嶅姟璇锋眰澶辫触锛岃绋嶅悗鍐嶈瘯锝烇紙鐘舵€佺爜 ' . $statusCode . '锛? . $msg . '锛?;
|
|
}
|
|
|
|
// Dify 杩斿洖鏍煎紡锛歿"answer": "鍥炲鍐呭", "conversation_id": "xxx"}
|
|
$content = $data['answer'] ?? '';
|
|
if (!$content) {
|
|
error_log("Dify杩斿洖鍐呭涓虹┖");
|
|
error_log("瀹屾暣鍝嶅簲: " . print_r($data, true));
|
|
return '鎶辨瓑锛孌ify 鏆傛椂娌℃湁鍚堢悊鐨勫洖澶嶃€?;
|
|
}
|
|
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); // 澧炲姞瓒呮椂鏃堕棿锛屾敮鎸乻treaming
|
|
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 '鎶辨瓑锛孉I 鏈嶅姟鏆傛椂涓嶅彲鐢紝璇风◢鍚庡啀璇曪綖锛堢綉缁滈敊璇細' . $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 '鎶辨瓑锛孉I 鏈嶅姟璇锋眰澶辫触锛岃绋嶅悗鍐嶈瘯锝烇紙鐘舵€佺爜 ' . $statusCode . '锛? . $msg . '锛?;
|
|
}
|
|
|
|
$content = $data['choices'][0]['message']['content'] ?? '';
|
|
if (!$content) {
|
|
error_log("AI杩斿洖鍐呭涓虹┖");
|
|
return '鎶辨瓑锛孉I 鏆傛椂娌℃湁鍚堢悊鐨勫洖澶嶃€?;
|
|
}
|
|
error_log("AI鍥炲: {$content}");
|
|
error_log("=== 璋冪敤AI缁撴潫 ===");
|
|
return trim($content);
|
|
}
|