Browse Source

add:小程序客服增加coze智能体

yonge 3 weeks ago
parent
commit
60b3fbaa74

+ 1 - 2
crmeb/.env

@@ -1,2 +1 @@
-APP_DEBUG = false

[APP]
DEFAULT_TIMEZONE = Asia/Shanghai

[DATABASE]
TYPE = mysql
HOSTNAME = 192.168.10.11
HOSTPORT = 3306
USERNAME = root
PASSWORD = Hello@2025
DATABASE = crmeb
PREFIX = eb_
CHARSET = utf8mb4
DEBUG = true

[LANG]
default_lang = zh-cn

[CACHE]
DRIVER = redis
CACHE_PREFIX = cache_6909b0858b5b4:
CACHE_TAG_PREFIX = cache_tag_6909b0858b5b4:

[REDIS]
REDIS_HOSTNAME = 192.168.10.10
PORT = 6379
REDIS_PASSWORD = 123456 
SELECT = 0

[QUEUE]
QUEUE_NAME = 6909b0858b5b4
-
[COZE]
API_TOKEN=sat_q8A7mXCf02v9UJJUT5DL7OLABmRPvovdTJPVjrIrXPg28GJdYv6dkgyUBBNmaFPn
BOT_ID=7670455976805826612
API_BASE=https://api.coze.cn
+APP_DEBUG = false

[APP]
DEFAULT_TIMEZONE = Asia/Shanghai

[DATABASE]
TYPE = mysql
HOSTNAME = 192.168.10.11
HOSTPORT = 3306
USERNAME = root
PASSWORD = Hello@2025
DATABASE = crmeb
PREFIX = eb_
CHARSET = utf8mb4
DEBUG = true

[LANG]
default_lang = zh-cn

[CACHE]
DRIVER = redis
CACHE_PREFIX = cache_6909b0858b5b4:
CACHE_TAG_PREFIX = cache_tag_6909b0858b5b4:

[REDIS]
REDIS_HOSTNAME = 192.168.10.10
PORT = 6379
REDIS_PASSWORD = 123456 
SELECT = 0

[QUEUE]
QUEUE_NAME = 6909b0858b5b4

[COZE]
COZE_ENABLE = true
COZE_API_BASE = https://api.coze.cn
COZE_ACCESS_TOKEN = sat_q8A7mXCf02v9UJJUT5DL7OLABmRPvovdTJPVjrIrXPg28GJdYv6dkgyUBBNmaFPn
COZE_BOT_ID = 7670455976805826612
COZE_TIMEOUT = 15
COZE_FALLBACK_REPLY = 当前客服繁忙,请稍后再试。

+ 116 - 0
crmeb/app/jobs/CozeKefuReplyJob.php

@@ -0,0 +1,116 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace app\jobs;
+
+use app\services\kefu\CozeServices;
+use app\services\kefu\service\StoreServiceLogServices;
+use app\services\kefu\service\StoreServiceRecordServices;
+use app\services\kefu\service\StoreServiceServices;
+use crmeb\basic\BaseJobs;
+use crmeb\services\workerman\ChannelService;
+use crmeb\traits\QueueTrait;
+use crmeb\utils\Queue;
+use think\facade\Env;
+use think\facade\Log;
+
+/**
+ * 在线客服 Coze 智能体回复任务。
+ */
+class CozeKefuReplyJob extends BaseJobs
+{
+    use QueueTrait;
+
+    public static function dispatchAsync(array $payload)
+    {
+        if (sys_config('queue_open', 0) != 1 || Env::get('cache.driver', 'file') !== 'redis') {
+            Log::error('在线客服 Coze 回复未入队:请开启 Redis 缓存和队列,避免阻塞 Workerman 聊天进程');
+            return false;
+        }
+
+        return Queue::instance()->job(__CLASS__)->do('doJob')->data($payload)->errorCount(1)->push();
+    }
+
+    public function doJob(array $payload): bool
+    {
+        if (!CozeServices::isEnabled()) {
+            return true;
+        }
+
+        $uid = (int)($payload['uid'] ?? 0);
+        $toUid = (int)($payload['to_uid'] ?? 0);
+        $content = trim((string)($payload['content'] ?? ''));
+        $formType = (int)($payload['form_type'] ?? 0);
+        $isTourist = (int)($payload['is_tourist'] ?? 0);
+
+        if (!$uid || !$toUid || $content === '') {
+            return true;
+        }
+
+        try {
+            /** @var CozeServices $coze */
+            $coze = app()->make(CozeServices::class);
+            $reply = $coze->chatText(($isTourist ? 'tourist_' : 'uid_') . $uid, $content);
+            $reply = trim(strip_tags(htmlspecialchars_decode($reply)));
+            if ($reply === '') {
+                return true;
+            }
+
+            /** @var StoreServiceLogServices $logServices */
+            $logServices = app()->make(StoreServiceLogServices::class);
+            /** @var StoreServiceRecordServices $recordServices */
+            $recordServices = app()->make(StoreServiceRecordServices::class);
+
+            $autoReply = [
+                'to_uid' => $uid,
+                'msn_type' => 1,
+                'msn' => $reply,
+                'uid' => $toUid,
+                'add_time' => time(),
+                'is_tourist' => $isTourist,
+                'type' => 1,
+            ];
+
+            $autoReply = $logServices->save($autoReply)->toArray();
+            $autoReply['_add_time'] = $autoReply['add_time'];
+            $autoReply['add_time'] = strtotime($autoReply['add_time']);
+
+            /** @var StoreServiceServices $serviceServices */
+            $serviceServices = app()->make(StoreServiceServices::class);
+            $kefuInfo = $serviceServices->get(['uid' => $toUid], ['nickname', 'avatar']);
+            $replyMessagesCount = $logServices->getMessageNum([
+                'uid' => $toUid,
+                'to_uid' => $uid,
+                'type' => 0,
+                'is_tourist' => $isTourist,
+            ]);
+
+            $autoReply['recored'] = $recordServices->saveRecord(
+                $toUid,
+                $uid,
+                $reply,
+                $formType,
+                1,
+                $replyMessagesCount,
+                $isTourist,
+                $kefuInfo['nickname'] ?? '',
+                $kefuInfo['avatar'] ?? ''
+            );
+
+            ChannelService::instance()->setTrigger('crmeb_chat')->send('reply', $autoReply, [$uid]);
+        } catch (\Throwable $e) {
+            Log::error('在线客服 Coze 回复失败:' . $e->getMessage());
+        }
+
+        return true;
+    }
+}

+ 26 - 11
crmeb/app/services/kefu/CozeServices.php

@@ -27,11 +27,23 @@ class CozeServices
 
     protected $botId;
 
+    protected $timeout;
+
+    protected $fallbackReply;
+
     public function __construct()
     {
-        $this->apiBase = rtrim((string)$this->env('coze.api_base', 'COZE_API_BASE', 'https://api.coze.cn'), '/');
-        $this->apiToken = (string)$this->env('coze.api_token', 'COZE_API_TOKEN', '');
-        $this->botId = (string)$this->env('coze.bot_id', 'COZE_BOT_ID', '7670455976805826612');
+        $this->apiBase = rtrim((string)$this->env(['coze.coze_api_base', 'coze.api_base'], 'COZE_API_BASE', 'https://api.coze.cn'), '/');
+        $this->apiToken = (string)$this->env(['coze.coze_access_token', 'coze.access_token', 'coze.api_token'], 'COZE_ACCESS_TOKEN', '');
+        $this->botId = (string)$this->env(['coze.coze_bot_id', 'coze.bot_id'], 'COZE_BOT_ID', '');
+        $this->timeout = max(1, (int)$this->env(['coze.coze_timeout', 'coze.timeout'], 'COZE_TIMEOUT', '15'));
+        $this->fallbackReply = trim((string)$this->env(['coze.coze_fallback_reply', 'coze.fallback_reply'], 'COZE_FALLBACK_REPLY', ''));
+    }
+
+    public static function isEnabled(): bool
+    {
+        $value = Env::get('coze.coze_enable', Env::get('coze.enable', getenv('COZE_ENABLE') ?: false));
+        return filter_var($value, FILTER_VALIDATE_BOOLEAN);
     }
 
     /**
@@ -40,7 +52,7 @@ class CozeServices
     public function chatText(string $userId, string $content): string
     {
         $content = trim($content);
-        if ($content === '' || $this->apiToken === '' || $this->botId === '') {
+        if (!self::isEnabled() || $content === '' || $this->apiToken === '' || $this->botId === '') {
             return '';
         }
 
@@ -62,14 +74,15 @@ class CozeServices
             'Authorization: Bearer ' . $this->apiToken,
             'Content-Type: application/json',
             'Accept: text/event-stream',
-        ], 20);
+        ], $this->timeout);
 
         if ($response === false || $response === '') {
             Log::error('Coze 小程序客服调用失败:' . json_encode(HttpService::getStatus(), JSON_UNESCAPED_UNICODE) . ' ' . HttpService::getCurlError());
-            return '';
+            return $this->fallbackReply;
         }
 
-        return $this->parseStreamAnswer($response);
+        $answer = $this->parseStreamAnswer($response);
+        return $answer !== '' ? $answer : $this->fallbackReply;
     }
 
     protected function parseStreamAnswer(string $response): string
@@ -124,11 +137,13 @@ class CozeServices
         return trim($answer);
     }
 
-    protected function env(string $key, string $serverKey, string $default = ''): string
+    protected function env(array $keys, string $serverKey, string $default = ''): string
     {
-        $value = Env::get($key, '');
-        if ($value !== '') {
-            return (string)$value;
+        foreach ($keys as $key) {
+            $value = Env::get($key, '');
+            if ($value !== '') {
+                return (string)$value;
+            }
         }
 
         $serverValue = getenv($serverKey);

+ 14 - 0
crmeb/crmeb/services/workerman/chat/ChatHandle.php

@@ -11,6 +11,8 @@
 
 namespace crmeb\services\workerman\chat;
 
+use app\jobs\CozeKefuReplyJob;
+use app\services\kefu\CozeServices;
 use app\services\kefu\LoginServices;
 use app\services\kefu\service\StoreServiceLogServices;
 use app\services\kefu\service\StoreServiceRecordServices;
@@ -270,6 +272,7 @@ class ChatHandle
         $unMessagesCount = $logServices->getMessageNum(['uid' => $uid, 'to_uid' => $to_uid, 'type' => 0, 'is_tourist' => $isTourist ? 1 : 0]);
         //记录当前用户和他人聊天记录
         $data['recored'] = $serviceRecored->saveRecord($uid, $to_uid, $msn, $formType ?? 0, $msn_type, $unMessagesCount, $isTourist, $data['nickname'], $data['avatar']);
+
         //是否在线
         if ($online) {
             $response->connection($this->service->user()[$to_uid])->send('reply', $data);
@@ -316,6 +319,17 @@ class ChatHandle
             }
         }
         if (!isset($this->service->kefuUser()[$uid])) {
+            if (CozeServices::isEnabled() && $msn_type == 1 && $msn !== '') {
+                CozeKefuReplyJob::dispatchAsync([
+                    'uid' => $uid,
+                    'to_uid' => $to_uid,
+                    'content' => $msn,
+                    'form_type' => $formType ?? 0,
+                    'is_tourist' => $isTourist ? 1 : 0,
+                ]);
+                return true;
+            }
+
             //判断是否有自动回复
             $wechatKeyServices = app()->make(WechatKeyServices::class);
             $replyId = $wechatKeyServices->value(['keys' => $msn, 'key_type' => 1], 'reply_id');