功能:流式返回、用户聊天记录区分、聊天记录保存(10分钟内不提问,自动删除聊天记录)、支持上下文提问(记得用户的提问以及chatgpt的回复内容)
核心代码:
<?php
namespace app\controller;
use GatewayWorker\Lib\Gateway;
use support\Redis;
use support\Request;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\ServerSentEvents;
use Webman\Push\Api;
class IndexController{
public $api_url="";//chatgpt接口
public $token="";//请求token
public $data_buffer;//缓存,有可能一条data被切分成两部分了,无法解析json,所以需要把上一半缓存起来
public function index(Request $request){
$uid=$request->get("uid");
$token=$request->get("token");
if (!empty($uid)&&$token==getenv("chat_token")){
if (Redis::hExists($uid,"chat")&&Redis::hExists($uid,"answer")){
$chat_array=json_decode(Redis::hGet($uid,"chat"),true);//历史聊天记录
$last_answer=Redis::hGet($uid,"answer");//最后一次chatgpt答复
array_shift($chat_array);//删除第一次You are a helpful assistant.
}else{
$chat_array="";
$last_answer="";
}
return view('index/index',["chat"=>$chat_array,"last_answer"=>$last_answer]);
}else{
return json("uid为空,访问时请携带uid参数例如:http://xxx.xxx.com/?uid=10000&token=xxx,若无token请联系作者");
}
}
public function question(Request $request){
$q=$request->post("q");
$uid=$request->post("uid");
$messages=[
[
'role' => 'system',
'content' => 'You are a helpful assistant.',
]
];
if (Redis::hExists($uid,"chat")){
$talk_array=Redis::hGet($uid,"chat");
$talk_array=json_decode($talk_array,true);
if (Redis::hExists($uid,"answer")){
$answer=Redis::hGet($uid,"answer");
array_push($talk_array,["role"=>"assistant","content"=>$answer]);//先将上次的chagpt回答插入数组
}
array_push($talk_array,["role"=>"user","content"=>$q]);//将本次用户问题插入数组
Redis::hSet($uid,"chat",json_encode($talk_array));//放入redis
Redis::expire($uid,600);//设置10分钟过期时间,10分钟内不提问自动删除
$messages=$talk_array;
}else{
array_push($messages,["role"=>"user","content"=>$q]);
Redis::hSet($uid,"chat",json_encode($messages));
Redis::expire($uid,600);//设置10分钟过期时间,10分钟内不提问自动删除
}
Redis::hDel($uid,"answer");//删除上一次的chatgpt回答内容
$json = json_encode([
'model' => 'gpt-3.5-turbo-0613',
'messages' => $messages,
'temperature' => 0.6,
'stream' => true
]);
$headers = array(
"Content-Type:application/json",
"Authorization: Bearer ".$this->token,
"Connection:keep-alive",
"Accept-Encoding:gzip, deflate, br",
"Accept:*/*"
);
$this->curl_openai($json,$headers,$uid);
}
public function curl_openai($json, $headers,$uid) {
$api = new Api(
'http://127.0.0.1:3232',
config('plugin.webman.push.app.app_key'),
config('plugin.webman.push.app.app_secret')
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1');
// curl_setopt($ch, CURLOPT_PROXYPORT, '7890');
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($api,$uid) {
// 0、把上次缓冲区内数据拼接上本次的data
$buffer = $this->data_buffer.$data;
//拼接完之后,要把缓冲字符串清空
$this->data_buffer = '';
// 1、把所有的 'data: {' 替换为 '{' ,'data: [' 换成 '['
$buffer = str_replace('data: {', '{', $buffer);
$buffer = str_replace('data: [', '[', $buffer);
// 2、把所有的 '}\n\n{' 替换维 '}[br]{' , '}\n\n[' 替换为 '}[br]['
$buffer = str_replace("}\n\n{", '}[br]{', $buffer);
$buffer = str_replace("}\n\n[", '}[br][', $buffer);
// 3、用 '[br]' 分割成多行数组
$lines = explode('[br]', $buffer);
for ($i = 0; $i < count($lines); $i++) {
if (trim($lines[$i]) != '[DONE]') {
$array = json_decode($lines[$i], true);
try {
if ($array['choices'][0]['finish_reason'] != 'stop') {
//循环逐字存储redis
$char = $array['choices'][0]['delta']['content'];
if (Redis::hExists($uid,"answer")){
$question_char=Redis::hGet($uid,"answer");
Redis::hSet($uid,"answer",$question_char.$char);
}else{
Redis::hSet($uid,"answer",$char);
}
$api->trigger('user-1', 'message', [
'content' => $array['choices'][0]['delta']['content']
]);
}
}catch (\Exception $exception){
}
}
}
return strlen($data);
});
$response = curl_exec($ch);
curl_close($ch);
}
}
前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="/plugin/webman/push/push.js"> </script>
<style>
#nr{
overflow: scroll;
overflow-x: hidden;
width: 1000px;
border: 1px solid black;
height: 800px;
padding-bottom: 20px;
}
.ai{
width: 50%;
margin-top: 20px;
background: aliceblue;
float: left;
padding: 20px;
}
.ai img{
width: 10%;
height: 100%;
}
.q{
width: 50%;
float: right;
margin-top: 20px;
background: aliceblue;
padding: 10px;
}
</style>
</head>
<body>
<div id="nr" >
{if $chat}
{volist name="chat" id="item"}
{if $item.role=='user'}
<div class="q">{$item.content}</div>
{elseif $item.role=='assistant'}
<div class="ai"><img src="/images/logo.png">{$item.content}</div>
{/if}
{/volist}
{/if}
{if $last_answer}
<div class="ai"><img src="/images/logo.png"> {$last_answer}</div>
{/if}
</div>
<input type="text" value="" id="text">
<button id="btn">发送</button>
<script>
// 建立连接
var connection = new Push({
url: 'ws://127.0.0.1:3333', // websocket地址 本地就用127.0.0.1:3333 服务器请用服务器ip+端口
app_key: '07f6e28d9f22f1ef52399979f304b88d',
auth: '/plugin/webman/push/auth' // 订阅鉴权(仅限于私有频道)
});
// 假设用户uid为1
var uid = 1;
// 浏览器监听user-1频道的消息,也就是用户uid为1的用户消息
var user_channel = connection.subscribe('user-' + uid);
// 当user-1频道有message事件的消息时
user_channel.on('message', function(data) {
// data里是消息内容
$(".ai:last").append(data.content)//给最后一个chatgpt回答的聊天框追加内容
});
$("#btn").click(function(){
var text=$("#text").val();
if (text!=""){
$("#text").val("");
$("#nr").append('<div class=\"q\">'+text+'</div>')
$("#nr").append('<div class=\"ai\"> <img src="/images/logo.png" alt=""></div>')//提问完问题后,给chatgpt创建好回答元素
$.ajax({
url: "http://chat.dykyzdh.cn/q",
method:"POST",
data:{"q":text,"uid":10000}
})
}else {
alert("请输入内容")
}
});
</script>
</body>
</html>
使用注意:请安装redis扩展及redis本身,redis配置在项目根目录下.env文件修改