Browse Source

add:小程序客服中接入coze智能体

yonge 1 month ago
parent
commit
d9b44f9b99

+ 2 - 1
crmeb/.env

@@ -1 +1,2 @@
-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 = file
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
+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_API_TOKEN=sat_q8A7mXCf02v9UJJUT5DL7OLABmRPvovdTJPVjrIrXPg28GJdYv6dkgyUBBNmaFPn
COZE_BOT_ID=7670455976805826612
COZE_API_BASE=https://api.coze.cn

+ 66 - 0
crmeb/app/jobs/MiniProgramKefuReplyJob.php

@@ -0,0 +1,66 @@
+<?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 crmeb\basic\BaseJobs;
+use crmeb\services\app\MiniProgramService;
+use crmeb\services\CacheService;
+use crmeb\traits\QueueTrait;
+use crmeb\utils\Queue;
+use think\facade\Env;
+use think\facade\Log;
+
+/**
+ * 小程序原生客服消息 AI 回复任务
+ */
+class MiniProgramKefuReplyJob extends BaseJobs
+{
+    use QueueTrait;
+
+    public static function dispatchAsync(string $openid, string $content, string $messageId = '')
+    {
+        if (sys_config('queue_open', 0) != 1 || Env::get('cache.driver', 'file') !== 'redis') {
+            Log::error('小程序客服 Coze 回复未入队:请开启 Redis 缓存和队列,避免微信回调超时');
+            return false;
+        }
+
+        return Queue::instance()->job(__CLASS__)->do('doJob')->data($openid, $content, $messageId)->errorCount(1)->push();
+    }
+
+    public function doJob(string $openid, string $content, string $messageId = ''): bool
+    {
+        if ($openid === '' || trim($content) === '') {
+            return true;
+        }
+
+        $lockKey = $messageId ? 'mini_kefu_coze_' . $messageId : 'mini_kefu_coze_' . md5($openid . $content);
+        if (CacheService::has($lockKey)) {
+            return true;
+        }
+        CacheService::set($lockKey, 1, 3600);
+
+        try {
+            /** @var CozeServices $coze */
+            $coze = app()->make(CozeServices::class);
+            $reply = $coze->chatText('mini_' . $openid, $content);
+            if ($reply !== '') {
+                MiniProgramService::staffTo($openid, $reply);
+            }
+        } catch (\Throwable $e) {
+            Log::error('小程序客服 Coze 回复失败:' . $e->getMessage());
+        }
+
+        return true;
+    }
+}

+ 141 - 0
crmeb/app/services/kefu/CozeServices.php

@@ -0,0 +1,141 @@
+<?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\services\kefu;
+
+use crmeb\services\HttpService;
+use think\facade\Env;
+use think\facade\Log;
+
+/**
+ * Coze 智能体调用服务
+ */
+class CozeServices
+{
+    protected $apiBase;
+
+    protected $apiToken;
+
+    protected $botId;
+
+    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');
+    }
+
+    /**
+     * 发送文本消息到 Coze,返回智能体文本回复。
+     */
+    public function chatText(string $userId, string $content): string
+    {
+        $content = trim($content);
+        if ($content === '' || $this->apiToken === '' || $this->botId === '') {
+            return '';
+        }
+
+        $payload = [
+            'bot_id' => $this->botId,
+            'user_id' => $userId,
+            'stream' => true,
+            'auto_save_history' => true,
+            'additional_messages' => [
+                [
+                    'role' => 'user',
+                    'content' => $content,
+                    'content_type' => 'text',
+                ],
+            ],
+        ];
+
+        $response = HttpService::postRequest($this->apiBase . '/v3/chat', json_encode($payload, JSON_UNESCAPED_UNICODE), [
+            'Authorization: Bearer ' . $this->apiToken,
+            'Content-Type: application/json',
+            'Accept: text/event-stream',
+        ], 20);
+
+        if ($response === false || $response === '') {
+            Log::error('Coze 小程序客服调用失败:' . json_encode(HttpService::getStatus(), JSON_UNESCAPED_UNICODE) . ' ' . HttpService::getCurlError());
+            return '';
+        }
+
+        return $this->parseStreamAnswer($response);
+    }
+
+    protected function parseStreamAnswer(string $response): string
+    {
+        $answer = '';
+        $event = '';
+        $lines = preg_split('/\r\n|\r|\n/', $response);
+        foreach ($lines as $line) {
+            $line = trim($line);
+            if ($line === '') {
+                continue;
+            }
+
+            if (stripos($line, 'event:') === 0) {
+                $event = trim(substr($line, 6));
+                continue;
+            }
+
+            if (stripos($line, 'data:') !== 0) {
+                continue;
+            }
+
+            $data = trim(substr($line, 5));
+            if ($data === '' || $data === '[DONE]' || $data === '"[DONE]"') {
+                continue;
+            }
+
+            $json = json_decode($data, true);
+            if (!is_array($json)) {
+                continue;
+            }
+
+            $currentEvent = (string)($json['event'] ?? $event);
+            $message = $json['message'] ?? $json['data'] ?? $json;
+            if (!is_array($message)) {
+                continue;
+            }
+
+            if (isset($message['type']) && $message['type'] !== 'answer') {
+                continue;
+            }
+
+            if (isset($message['content']) && $message['content'] !== '') {
+                if ($currentEvent === '' || stripos($currentEvent, 'delta') !== false) {
+                    $answer .= (string)$message['content'];
+                } elseif (stripos($currentEvent, 'completed') !== false && $answer === '') {
+                    $answer = (string)$message['content'];
+                }
+            }
+        }
+
+        return trim($answer);
+    }
+
+    protected function env(string $key, string $serverKey, string $default = ''): string
+    {
+        $value = Env::get($key, '');
+        if ($value !== '') {
+            return (string)$value;
+        }
+
+        $serverValue = getenv($serverKey);
+        if ($serverValue !== false && $serverValue !== '') {
+            return (string)$serverValue;
+        }
+
+        return $default;
+    }
+}

+ 9 - 0
crmeb/crmeb/services/app/MiniProgramService.php

@@ -11,6 +11,7 @@
 
 namespace crmeb\services\app;
 
+use app\jobs\MiniProgramKefuReplyJob;
 use app\services\order\StoreOrderTakeServices;
 use app\services\pay\PayServices;
 use app\services\system\SystemPemServices;
@@ -899,6 +900,14 @@ class MiniProgramService
     {
         $server->setMessageHandler(function ($message) {
             switch ($message->MsgType) {
+                case 'text':
+                    $content = trim((string)($message->Content ?? ''));
+                    $openid = (string)($message->FromUserName ?? '');
+                    if ($content !== '' && $openid !== '') {
+                        $messageId = (string)($message->MsgId ?? md5($openid . $content . ($message->CreateTime ?? time())));
+                        MiniProgramKefuReplyJob::dispatchAsync($openid, $content, $messageId);
+                    }
+                    return 'success';
                 case 'event':
                     switch (strtolower($message->Event)) {
                         case 'funds_order_pay':  // 小程序支付管理的