// +---------------------------------------------------------------------- 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; protected $timeout; protected $fallbackReply; public function __construct() { $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); } /** * 发送文本消息到 Coze,返回智能体文本回复。 */ public function chatText(string $userId, string $content): string { $content = trim($content); if (!self::isEnabled() || $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', ], $this->timeout); if ($response === false || $response === '') { Log::error('Coze 小程序客服调用失败:' . json_encode(HttpService::getStatus(), JSON_UNESCAPED_UNICODE) . ' ' . HttpService::getCurlError()); return $this->fallbackReply; } $answer = $this->parseStreamAnswer($response); return $answer !== '' ? $answer : $this->fallbackReply; } 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(array $keys, string $serverKey, string $default = ''): string { foreach ($keys as $key) { $value = Env::get($key, ''); if ($value !== '') { return (string)$value; } } $serverValue = getenv($serverKey); if ($serverValue !== false && $serverValue !== '') { return (string)$serverValue; } return $default; } }