CozeServices.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\services\kefu;
  13. use crmeb\services\HttpService;
  14. use think\facade\Env;
  15. use think\facade\Log;
  16. /**
  17. * Coze 智能体调用服务
  18. */
  19. class CozeServices
  20. {
  21. protected $apiBase;
  22. protected $apiToken;
  23. protected $botId;
  24. protected $timeout;
  25. protected $fallbackReply;
  26. public function __construct()
  27. {
  28. $this->apiBase = rtrim((string)$this->env(['coze.coze_api_base', 'coze.api_base'], 'COZE_API_BASE', 'https://api.coze.cn'), '/');
  29. $this->apiToken = (string)$this->env(['coze.coze_access_token', 'coze.access_token', 'coze.api_token'], 'COZE_ACCESS_TOKEN', '');
  30. $this->botId = (string)$this->env(['coze.coze_bot_id', 'coze.bot_id'], 'COZE_BOT_ID', '');
  31. $this->timeout = max(1, (int)$this->env(['coze.coze_timeout', 'coze.timeout'], 'COZE_TIMEOUT', '15'));
  32. $this->fallbackReply = trim((string)$this->env(['coze.coze_fallback_reply', 'coze.fallback_reply'], 'COZE_FALLBACK_REPLY', ''));
  33. }
  34. public static function isEnabled(): bool
  35. {
  36. $value = Env::get('coze.coze_enable', Env::get('coze.enable', getenv('COZE_ENABLE') ?: false));
  37. return filter_var($value, FILTER_VALIDATE_BOOLEAN);
  38. }
  39. /**
  40. * 发送文本消息到 Coze,返回智能体文本回复。
  41. */
  42. public function chatText(string $userId, string $content): string
  43. {
  44. $content = trim($content);
  45. if (!self::isEnabled() || $content === '' || $this->apiToken === '' || $this->botId === '') {
  46. return '';
  47. }
  48. $payload = [
  49. 'bot_id' => $this->botId,
  50. 'user_id' => $userId,
  51. 'stream' => true,
  52. 'auto_save_history' => true,
  53. 'additional_messages' => [
  54. [
  55. 'role' => 'user',
  56. 'content' => $content,
  57. 'content_type' => 'text',
  58. ],
  59. ],
  60. ];
  61. $response = HttpService::postRequest($this->apiBase . '/v3/chat', json_encode($payload, JSON_UNESCAPED_UNICODE), [
  62. 'Authorization: Bearer ' . $this->apiToken,
  63. 'Content-Type: application/json',
  64. 'Accept: text/event-stream',
  65. ], $this->timeout);
  66. if ($response === false || $response === '') {
  67. Log::error('Coze 小程序客服调用失败:' . json_encode(HttpService::getStatus(), JSON_UNESCAPED_UNICODE) . ' ' . HttpService::getCurlError());
  68. return $this->fallbackReply;
  69. }
  70. $answer = $this->parseStreamAnswer($response);
  71. return $answer !== '' ? $answer : $this->fallbackReply;
  72. }
  73. protected function parseStreamAnswer(string $response): string
  74. {
  75. $answer = '';
  76. $event = '';
  77. $lines = preg_split('/\r\n|\r|\n/', $response);
  78. foreach ($lines as $line) {
  79. $line = trim($line);
  80. if ($line === '') {
  81. continue;
  82. }
  83. if (stripos($line, 'event:') === 0) {
  84. $event = trim(substr($line, 6));
  85. continue;
  86. }
  87. if (stripos($line, 'data:') !== 0) {
  88. continue;
  89. }
  90. $data = trim(substr($line, 5));
  91. if ($data === '' || $data === '[DONE]' || $data === '"[DONE]"') {
  92. continue;
  93. }
  94. $json = json_decode($data, true);
  95. if (!is_array($json)) {
  96. continue;
  97. }
  98. $currentEvent = (string)($json['event'] ?? $event);
  99. $message = $json['message'] ?? $json['data'] ?? $json;
  100. if (!is_array($message)) {
  101. continue;
  102. }
  103. if (isset($message['type']) && $message['type'] !== 'answer') {
  104. continue;
  105. }
  106. if (isset($message['content']) && $message['content'] !== '') {
  107. if ($currentEvent === '' || stripos($currentEvent, 'delta') !== false) {
  108. $answer .= (string)$message['content'];
  109. } elseif (stripos($currentEvent, 'completed') !== false && $answer === '') {
  110. $answer = (string)$message['content'];
  111. }
  112. }
  113. }
  114. return trim($answer);
  115. }
  116. protected function env(array $keys, string $serverKey, string $default = ''): string
  117. {
  118. foreach ($keys as $key) {
  119. $value = Env::get($key, '');
  120. if ($value !== '') {
  121. return (string)$value;
  122. }
  123. }
  124. $serverValue = getenv($serverKey);
  125. if ($serverValue !== false && $serverValue !== '') {
  126. return (string)$serverValue;
  127. }
  128. return $default;
  129. }
  130. }