控制器Coze.php代码:
<?php
namespace app\home\controller;
use think\Controller;
use app\home\logic\CozeLogic;
class Coze extends Controller
{
private $cozeLogic;
/**
* 析构函数
*/
function __construct()
{
parent::__construct();
$this->cozeLogic = new CozeLogic;
}
public function kefu()
{
// 获取访问令牌access_token
$tokenData = $this->cozeLogic->get_access_token();
if (!empty($tokenData['code'])) {
$users = session('users'); // 获取登录用户信息
if (empty($users)) { // 未登录,以游客方式显示
$users['users_id'] = 0;
$users['head_pic'] = get_head_pic();
$users['nickname'] = '游客(IP:'.clientIP().')';
} else { // 已登录
$nickname = empty($users['nickname']) ? $users['username'] : $users['nickname'];
$users['nickname'] = $nickname.'(IP:'.clientIP().')';
}
// 用于设置对话框中的显示用户信息,包括对话框中的用户头像、用户名等信息。
$coze_userInfo = [
'url' => $users['head_pic'],
'nickname' => $users['nickname'],
];
if (!empty($users['users_id'])) {
// 用户的 ID,也就是用户在你的网站或应用中的账号 ID。未指定 ID 时,Chat SDK 会根据用户使用的设备随机分配一个用户 ID。
$coze_userInfo['id'] = strval($users['users_id']);
}
// 智能体配置
$coze_options = [
'config' => [
'botId' => "7469685534995152896", // 智能体 ID
],
// 'componentProps' => [
// 'title' => "智能客服",
// ],
'auth' => [
'type' => 'token',
'token' => $tokenData['access_token'],
],
'userInfo' => $coze_userInfo,
'ui' => [
'base' => [
// 'icon' => 'https://p6-flow-product-sign.byteimg.com/tos-cn-i-13w3uml6bg/78f519713ce46901120fb7695f257c9a.png~tplv-13w3uml6bg-resize:128:128.image?rk3s=2e2596fd&x-expires=1742003492&x-signature=JjvhuPmgyWwFzX7bLM92InFvnXQ%3D',
// 'lang' => 'zh-CN',
'zIndex' => 1000,
],
'header' => [
'isShow' => true,
'isNeedClose' => true,
],
'asstBtn' => [
'isNeed' => true, // 悬浮球的显示与隐藏
],
'footer' => [
'isShow' => true,
'expressionText' => 'Powered by © {{name}} {{name1}} 版权所有',
'linkvars' => [
'name' => [
'text' => '2016-2024',
],
'name1' => [
'text' => 'Coze',
'link' => 'https://www.coze.cn',
],
],
],
'chatBot' => [
'title' => "智能客服",
'uploadable' => true,
'width' => 400,
],
],
];
$data = [
'users' => $users,
'coze_options' => $coze_options,
];
$this->success('成功', null, $data);
}
$this->error($tokenData['msg']);
}
public function get_access_token()
{
$tokenData = $this->cozeLogic->get_access_token();
if (!empty($tokenData['code'])) {
$this->success('成功', null, ['access_token'=>$tokenData['access_token']]);
}
$this->error($tokenData['msg']);
}
}
业务层CozeLogic.php代码:
<?php
namespace app\home\logic;
class CozeLogic
{
public function get_access_token()
{
$apiUrl = "https://api.coze.cn/api/permission/oauth2/token";
$postData = [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // 固定值
'duration_seconds' => 86399, // 申请的 AccessToken 有效期,单位为秒,默认 900 秒,即 15 分钟。最大可设置为 86399 秒,即 24 小时。
];
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->get_jwt()
];
$response = $this->httpRequest($apiUrl, 'POST', json_encode($postData, JSON_UNESCAPED_UNICODE), $headers, 300);
$result = json_decode($response, true);
if (!empty($result['access_token'])) {
return [
'code' => 1,
'access_token' => $result['access_token'],
];
}
return [
'code' => 0,
'msg' => empty($result['error_message']) ? "获取访问令牌失败" : "获取访问令牌失败,错误码: {$result['error_code']},错误信息: {$result['error_message']}",
];
}
private function get_jwt()
{
// OAuth 应用的公钥指纹
$public_key_id = 'OAuth应用的公钥指纹';
// OAuth 应用的 ID,必须是字符串类型
$app_id = '应用的ID';
// Header 参数
$header = [
'alg' => 'RS256', // 固定为RS256
'typ' => 'JWT', // 固定为 JWT
'kid' => strval($public_key_id) // OAuth 应用的公钥指纹
];
/**
* 访问令牌的会话标识。目前仅限在会话隔离场景下使用,即将 session_name 指定为用户在业务侧的 UID,以此区分不同业务侧用户的对话历史。
* 若未指定 session_name,不同用户的对话历史可能会掺杂在一起。
*/
$session_name = cookie('coze_session_name');
if (empty($session_name) && session('?users_id')) {
$session_name = 'coze_' . strval(session('users_id')) . session('users.username'); // 根据业务改成等于用户的uid
}
if (empty($session_name)) {
$session_name = session_id();
}
cookie('coze_session_name', $session_name);
// Payload 参数
$payload = [
'iss' => strval($app_id), // OAuth 应用的 ID
'aud' => 'api.coze.cn', //扣子 API 的Endpoint
'iat' => time(), // JWT开始生效的时间,秒级时间戳
'exp' => time() + 86400, // JWT过期时间,秒级时间戳
'jti' => (string) $this->get_rand_str(48, 0, 0), // 随机字符串,防止重放攻击
'session_name' => strval($session_name), // 必须是字符串类型
];
$encodedHeader = $this->base64url_encode(json_encode($header));
$encodedPayload = $this->base64url_encode(json_encode($payload));
$signature = '';
$data = $encodedHeader . '.' . $encodedPayload;
// 私钥,改成自己的私钥
$private_key = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDi5cjkJd0ni5tq\nzochBDgyjyenp4OZSQtMZz/8jl9LS+vdg5cfSFub06s7g6BtRJI/AgFmGlfcz3Jg\ni0FeJ0Rx/6i4omKCV+CrhO4yg440JtYnvGmV8iXdQrjbtJSQmjgn8q5XhOu5iIxf\nc4lYWpwOEKKyltBLO6rRfAyU6UYo6/Dd30u/IXQ7Ycq2wdaX9dgkpnJSHiOCJ4ur\nvv6cl11li8OiBSQXOZ/S+MVclLf3dKUOOD43Lo9t0tZ5AQsTYwzAfSJuOpSi3Dq7\nXasaFYxez2ocaOnvXzCXtxeR2/ps5WLpMcOtVtqDGNbyTbyw74+BdYxzhVz92ZP0\nRdFEDU8fAgMBAAECggEAapOTlrLAXCRj1rn22AfS29DJSCNTmaspPvSapic71LLv\nMefvErcarKmymyxbLSgR572p6YtDLQlWCMowZKjeKgvz9PH1gpMDiZ1Rg3Gu2IlL\ne3OxRsjshy8hhLWgOQHvkbgYsyxQyx+8C9PJtjItbh/bRDa51TTMKkTx5fpixbIX\nUaGFf/cB+ubWV11kCneSO+NvMEP97nO1XgTgXanmcUfs4kQ07T9pthWc3tIhQ5E5\nAAVj05IzqlOjjDQIzqMwARTLfURmqCTs3CCDGwUAlC+QKn7IXKxOPYvxdFaAJ1gU\nyVpYAHa8DOrpeEZAwYbzHZ/icXUn2UiUBe6jkOEJVQKBgQDyF2P4S0N1HZU7NcBX\n6iD/JiGQFr7VRqWV997ypm+UQJgt6kbluQ68RarCuxdog6yfx25tyU+xHRle4s8u\nZb04XnIky8pxFWTvfOu739sbd6jnymwm6oDG/aKad6AVJyBLEnOJgOMUaaUqrOgZ\n0wW6e+z12YilbsOhJzF9P4KACwKBgQDv7u265sEvi9TbqVdjj686VtOpcpgZW993\nzosD3p5W+6Rkmuw8BNdMrBWQmKQgELPx47ozrsPvQ224eHbAvBFpBPMUzYMt5+QR\nrMwUXOjs5SMBSmhZU0r0naI3b5D4RaW5OgQVhIbMAKuIjTvydmWBI+VSL5gBiBJZ\nYoG136e1vQKBgQDeDEQ+hlH+Rr8NCvU77dyMCp54OdSZbG4OisasS6pGEaVDYAHd\nTQrN9xxz9gS3cSbRequ3+RCmRCl0FZawWNLgtAf/5gfuBxCIdU0HCoE+xVdgGxNt\n8mNhoVwWUCi9niCJmwfsFBK0M14yMVvF6/7yGB61NOyXyGuZEYjb93esFwKBgQDq\nfjZDq1D1mhEf2gI5oO0d9EwLWJ8EccEWw3sj/gf8dRY26R6mjgipu6mb10LYv9pO\nN0X9SvndtqXeW4WcWnIdlKPKGgzx6agCCkKuSBuxo0Qx97nCS2B/cCxhR52Et/T+\n8Y+aRBrF+IWtAHOtRT9xFlcCmtdR794XE+o3iW3MdQKBgD/Y8LRuMn3fET7urLJi\neluWAsSuJVS2qc8VPx7a7v288fUoS99t88GzzOQ7dXn+fwD1op9ylyC3jAgZInh6\nrwQa9fcA3JrQihgCFNsWfH5sZfP1uMfnlJ67k+70kUVZzyWeIrw48FEasV80EWbV\nLRqCfMEPknRNgGABLFOJKeOz\n-----END PRIVATE KEY-----";
openssl_sign($data, $signature, $private_key, 'sha256WithRSAEncryption');
$encodeSignature = $this->base64url_encode($signature);
return $encodedHeader . '.' . $encodedPayload . '.' . $encodeSignature;
}
private function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* 获取随机字符串
* @param int $randLength 长度
* @param int $addtime 是否加入当前时间戳
* @param int $includenumber 是否包含数字
* @return string
*/
private function get_rand_str($randLength = 6, $addtime = 1, $includenumber = 0)
{
if (1 == $includenumber) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
} else if (2 == $includenumber) {
$chars = '123456789';
} else if (3 == $includenumber) {
$chars = 'ABCDEFGHJKLMNPQEST123456789';
} else {
$chars = 'abcdefghijklmnopqrstuvwxyz';
}
$len = strlen($chars);
$randStr = '';
for ($i = 0; $i < $randLength; $i++) {
$randStr .= $chars[rand(0, $len - 1)];
}
$tokenvalue = $randStr;
if ($addtime) {
$tokenvalue = $randStr . time();
}
return $tokenvalue;
}
/**
* CURL请求
*
* @param $url 请求url地址
* @param $method 请求方法 get post
* @param null $postfields post数据数组
* @param array $headers 请求header信息
* @param bool|false $debug 调试开启 默认false
* @return mixed
*/
private function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false)
{
$method = strtoupper($method);
$ci = curl_init();
/* Curl settings */
// curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); // 使用哪个版本
curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */
// curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_RETURNTRANSFER, true);
switch ($method) {
case "POST":
curl_setopt($ci, CURLOPT_POST, true);
if (!empty($postfields)) {
if (is_string($postfields) && preg_match('/^([\w\-]+=[\w\-]+(&[\w\-]+=[\w\-]+)*)$/', $postfields)) {
parse_str($postfields, $output);
$postfields = $output;
}
if (is_array($postfields)) {
$tmpdatastr = http_build_query($postfields);
} else {
$tmpdatastr = $postfields;
}
curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr);
}
break;
default:
curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */
break;
}
$ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE;
curl_setopt($ci, CURLOPT_URL, $url);
if ($ssl) {
curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在
// curl_setopt($ci, CURLOPT_SSLVERSION, 4); //因为之前的POODLE 病毒爆发,许多网站禁用了sslv3(nginx默认是禁用的,ssl_protocols 默认值为TLSv1 TLSv1.1 TLSv1.2;),最新使用sslv4
}
//curl_setopt($ci, CURLOPT_HEADER, true); /*启用时会将头文件的信息作为数据流输出*/
if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) {
curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1);
}
curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/
curl_setopt($ci, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ci, CURLINFO_HEADER_OUT, true);
/*curl_setopt($ci, CURLOPT_COOKIE, $Cookiestr); * *COOKIE带过去** */
$response = curl_exec($ci);
$requestinfo = curl_getinfo($ci);
$http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
if ($debug) {
echo "=====post data======\r\n";
var_dump($postfields);
echo "=====info===== \r\n";
print_r($requestinfo);
echo "=====response=====\r\n";
print_r($response);
}
curl_close($ci);
return $response;
//return array($http_code, $response,$requestinfo);
}
}
web前端代码:
<!DOCTYPE html>
<html>
<head>
<title>thinkphp接入扣子(coze)智能体Ai客服</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
</head>
<body>
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.1.0-beta.3/libs/cn/index.js"></script>
<script>
$(document).ready(function(){
coze_kefu();
function coze_kefu()
{
$.ajax({
type : 'get',
url : "{:url('home/Coze/kefu')}",
data : {},
dataType : 'json',
success : function(res){
if (res.code == 1) {
var options = res.data.coze_options;
var users = res.data.users;
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: options.config,
// componentProps: options.componentProps,
auth: {
// Authentication methods, the default type is 'unauth', which means no authentication is required; it is recommended to set it to 'token', indicating authentication through PAT (Personal Access Token) or OAuth
type: options.auth.type,
// When the type is set to 'token', it is necessary to configure a PAT (Personal Access Token) or OAuth access token for authentication.
token: options.auth.token,
// When the access token expires, use a new token and set it as needed.
onRefreshToken: async () => {
$.ajax({
type : 'get',
url : "{:url('home/Coze/get_access_token')}",
data : {},
dataType : 'json',
success : function(ref_res){
if (ref_res.code == 1) {
if (ref_res.data && ref_res.data.access_token) {
return ref_res.data.access_token;
} else {
console.log('Response does not contain access_token');
}
} else {
console.log(ref_res.msg);
}
return '';
},
error: function(e){
console.log(e.responseText);
return '';
}
});
},
},
userInfo: options.userInfo,
ui: {
base: options.ui.base,
header: options.ui.header,
asstBtn: options.ui.asstBtn,
footer: options.ui.footer,
chatBot: {
title: options.ui.chatBot.title,
uploadable: options.ui.chatBot.uploadable,
width: options.ui.chatBot.width, // PC 端窗口的宽度,单位为 px,默认为 460。
el: undefined,
// el: document.getElementById('position_demo'),
onHide: () => { // 当聊天框隐藏的时候,会回调该方法。
console.log('onHide')
// todo...
},
onShow: () => { // 当聊天框显示的时候,会回调该方法。
console.log('onShow')
// todo...
},
onBeforeShow: () => { // 聊天框显示前调用,如果返回了 false,则不会显示。支持异步函数。
// return false;
console.log('onBeforeShow')
// todo...
},
onBeforeHide: () => { // 聊天框隐藏前调用,如果返回了 false,则不会隐藏。支持异步函数。
// return false;
console.log('onBeforeHide')
// todo...
},
},
},
});
} else {
console.log(res.msg);
}
},
error: function(e){
console.log(e.responseText);
}
});
}
});
</script>
</body>
</html>
如果涉及到用户登录,还需要在退出逻辑加上清除代码:
session('coze_session_name', null);
cookie('coze_session_name', null);