作者 lyh

gx

<?php
namespace App\Helper;
/**
* api结果处理
* @author:dc
* @time 2023/12/16 14:56
* Class Resource
* @package GlobalSo\Tool\Gpt
*/
abstract class Resource {
/**
* 函数
* @var array
*/
protected $func = [];
/**
* 是否是调试模式
* @var bool
*/
public static $debugInfo = false;
/**
* 获取内容body所有内容
* @return string
*/
abstract public function getBody(): string;
/**
* 获取数据的状态
* @return int
*/
abstract public function getCode(): int;
/**
* 验证code 默认200
* @param int $code
* @return bool
* @time 2023/12/16 16:45
*/
public function checkCode(int $code = 200):bool {
return $this->getCode() === $code;
}
/**
* 获取内容 数据
* @return mixed
* @author:dc
* @time 2024/1/2 15:56
*/
abstract public function getData();
/**
* 获取错误消息
* @return string
*/
abstract public function getMessage(): string;
/**
* 是否是函数
* @return bool
* @author:dc
* @time 2024/1/10 12:35
*/
public function isFun(){
if($this->getBodyFunc()){
return true;
}
return false;
}
/**
* @return array|mixed
* @author:dc
* @time 2024/4/9 15:55
*/
public function getBodyFunc(){
$json = @json_decode($this->getBody(),1);
if(is_array($json) && isset($json['tool_calls'])){
return $json['tool_calls'];
}
return [];
}
/**
* 这个是提交了多少token
* @return int
* @author:dc
* @time 2023/12/18 13:37
*/
public function getPromptToken():int{
// 这个是老版本
if(isset($this->getUsage()['prompt_tokens'])){
return (int) ($this->getUsage()['prompt_tokens']??0);
}
// 这个是新版本
$num = 0;
foreach ($this->getUsage() as $item){
$num += (int) ($item['prompt_tokens']??0);
}
return $num;
}
/**
* 这个是吐出了多少token
* @return int
* @author:dc
* @time 2023/12/18 13:37
*/
public function getCompletionToken():int{
// 这个是老版本
if(isset($this->getUsage()['completion_tokens'])){
return (int) ($this->getUsage()['completion_tokens']??0);
}
// 这个是新版本
$num = 0;
foreach ($this->getUsage() as $item){
$num += (int) ($item['completion_tokens']??0);
}
return $num;
}
/**
* 获取函数
* @return array|mixed
*/
public function getFunc($call=null,...$params)
{
if(!$this->func){
$this->func = $this->getBodyFunc();
}
// {"code":200,"func":{"name":"taocan","arguments":{"attribute":"\u7528\u91cf","usetime":"\u4eca\u5929"}}}
$function = $this->func;
if(!empty($this->func) && is_array($this->func)) {
if (!isset($this->func['name'])) {
$this->func = [$function[0]];
}
}
$result = $this->getFuncAll($call,...$params);
$this->func = $function;
return array_values($result)[0]??null;
}
/**
* 获取所有的函数体
* @param null $call
* @param mixed ...$params
* @return array|false|mixed
* @author:dc
* @time 2024/3/11 14:32
*/
public function getFuncAll($call=null,...$params)
{
if(!$this->func){
$this->func = $this->getBodyFunc();
}
// {"code":200,"func":{"name":"taocan","arguments":{"attribute":"\u7528\u91cf","usetime":"\u4eca\u5929"}}}
$function = [];
if(!empty($this->func) && is_array($this->func)){
$function = $this->func;
}
if($call){
if($function){
// 是否是老版本
if(!empty($function['name'])){
// 整理参数
return [$function['name']=> $this->callFunc(
$call,
$function['name'],
$params,
$function['arguments']??[]
)];
}else{
// 循环 函数
$result = [];
foreach ($function as $func){
$result[$func['name']] = $this->callFunc(
$call,
$func['name'],
$params,
$func['arguments']??[]
);
}
return $result;
}
}
return [];
}
return $function;
}
/**
* call 回调
* @param $call
* @param $funcName
* @param $params
* @return false|mixed
* @author:dc
* @time 2024/3/11 11:35
*/
private function callFunc($call,$funcName,$params,$attr) {
$params[] = $attr;
// 匿名函数
if($call instanceof \Closure){
return $call($funcName, ...$params);
}
// 类名称
elseif ($call){
// 定义了类
if(is_object($call) || class_exists($call)){
// 掉用类
return call_user_func(
[$call,$funcName]
,...$params
);
}else
return call_user_func(
[$call,$funcName]
,...$params
);
}
}
}
... ...
<?php
namespace App\Helper;
/**
* 流输出
* @author:dc
* @time 2024/1/2 14:46
* Class Stream
* @package GlobalSo\Tool\Gpt\Resource
*/
class Stream extends Resource{
/**
* body内容
* @var string
*/
private $body = '';
/**
* 流输出的文本
* @var string
*/
private $text = '';
/**
* http 状态
* @var int
*/
private $status = 200;
/**
* @var \Psr\Http\Message\StreamInterface
*/
private $stream;
/**
* @var array 使用了多少token
*/
private $usage = [];
/**
* Resource constructor.
* @param \Psr\Http\Message\StreamInterface|array $response
*/
public function __construct($response)
{
if($response instanceof \Psr\Http\Message\StreamInterface){
$this->stream = $response;
}
// 数组,带上下文
elseif(is_array($response)){
$this->stream = false;
// 回答的文本
$this->text = end($response);
// 计算token
$this->usage = [
[
'model'=>'',
]
];
}
}
/**
* 最后一行
* @var array
*/
private $endLine = [];
/**
* 获取流输出内容
* @return null
* @author:dc
* @time 2024/1/2 13:57
*/
public function getStreamContent(\Closure $call)
{
// 文本
if($this->stream===false){
$this->body = $this->text;
$call($this->text);
}
// 流输出
else{
while (!$this->stream->eof()) {
// 获取一行数据
$line = $this->getStreamContentLine();
// 必须要有数据
if($line){
// 解析成数组
$arr = @json_decode($line,true);
// 必须是一个数组
if(is_array($arr)){
// 是否是函数
if(!empty($arr['func'])){
$this->func = $arr['func'] ? : ($arr['tool_calls']??[]);
continue;
}
// 这里是新版本
// 文本
if(isset($arr['text'])){
// 拼接
$this->text .= $arr['text'];
// 调用
$call($arr['text']);
}
// 到了最后一行
if (isset($arr['usage'])){
$this->usage = $arr['usage'];
$this->endLine = $arr;
}
}else{
// 拼接
$this->text .= $line;
// 这里兼容下老版本
$call($line);
}
}
}
// 兼容老版本 老版本没办法获取 实际使用了多少token
if(!$this->usage){
$this->usage = [
[
'model'=>'',
]
];
}
}
}
/**
* 流 读取一行
* @return string
* @author:dc
* @time 2024/1/2 14:16
*/
private function getStreamContentLine(){
$text = '';
while (!$this->stream->eof()){
// 读取一个字符串
$t = $this->stream->read(1);
$this->body .= $t;
if($t === "\n"){
break;
}
// 结束了
if(ord($t)==1){
break;
}
$text .= $t;
}
return $text;
}
/**
* 流输出的所有内容
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* @return int
*/
public function getCode(): int
{
return 200;
}
/**
* 这个是文本内容,就是回答的内容
* @return array|string
* @author:dc
* @time 2024/1/2 14:57
*/
public function getData()
{
return $this->text;
}
/**
* @return string
*/
public function getMessage(): string
{
return '';
}
/**
* @return array
* @author:dc
* @time 2024/1/2 14:57
*/
public function getUsage(): array
{
return $this->usage;
}
/**
* 是否已经输出过头部了
* @var bool
*/
protected static $isHeader = false;
/**
* 是否是sse输出
* @var bool
*/
public static $echoSse = false;
/**
* 设置头部
* @param false $sse
* @author:dc
* @time 2024/5/31 15:02
*/
public static function setStreamHeader(array $header=[]){
// 默认配置的 头信息 输出一次即可
if(!self::$isHeader){
// 流输出 必须的 头信息
if(self::$echoSse) header("Content-Type:event-stream;Charset=UTF-8;");//event-stream 开启这个数据必须是规定格式
header("cache-control:no-cache;"); // 告诉浏览器不要进行数据缓存
header('X-Accel-Buffering: no'); // 关键是加了这一行。告诉浏览器不进行输出的缓冲
header('Access-Control-Expose-Headers: Content-Disposition, Content-Length, X-Content-Range, X-Duration');
header('Content-Type: application/json'); // json数据头
header('Access-Control-Allow-Origin:*'); // 这个是 跨域
self::$isHeader = true;
}
// 输出其他header
foreach ($header as $head){
header($head);
}
}
/**
* 其他地方调用,在ai返回前后都可以调用这个
* @param $data
* @param string $type 数据类型
* @author:dc
* @time 2024/5/31 15:05
*/
public static function echo_flush($data,string $type='text'){
self::setStreamHeader();
echo self::$echoSse ? en_sse_data($data,$type) : $data;
ob_flush();
flush();
}
/**
* 输出 信息到前端
* @author:dc
* @time 2024/5/31 10:16
*/
public function echo(){
// 如果用户断开,继续脚本的运行
ignore_user_abort(1);
set_time_limit(400);
// // 先把之前的内容 也发送到浏览器
// @ob_implicit_flush(); // 开启隐式刷新 使用 echo函数时会立即发送到浏览器 开启后就不需要flush调用了
// 输出内容
$this->getStreamContent(function ($text) {
self::echo_flush($text);
});
if(self::$debugInfo){
self::echo_flush($this->endLine['debug']??[],'debug');
}
}
}
... ...
... ... @@ -9,6 +9,7 @@
namespace App\Http\Logic\Bside\Gpt;
use App\Helper\Stream;
use App\Http\Logic\Bside\BaseLogic;
use App\Models\Gpt\Chat;
use App\Models\Gpt\ChatItem;
... ... @@ -61,29 +62,11 @@ class ChatLogic extends BaseLogic
];
}
$data = ['message' => $message];
return response()->stream(function () use ($gptService, $data, $chatId) {
$fullResponse = ''; // **存储完整 AI 回复**
$stream = $gptService->get_ai_chat($data); // 获取流
if ($stream) {
while (!$stream->eof()) {
$chunk = $stream->read(1024); // **逐步读取数据块**
echo "data: " . json_encode(['message' => $chunk], JSON_UNESCAPED_UNICODE) . "\n\n";
ob_flush();
flush();
$fullResponse .= $chunk; // **拼接完整 AI 回复**
}
} else {
$fullResponse = '服务器繁忙,请重试';
echo "data: " . json_encode(['message' => $fullResponse], JSON_UNESCAPED_UNICODE) . "\n\n";
}
// **流结束后,保存完整 AI 回复**
$this->saveChatItem($chatId, $fullResponse, 1);
}, 200, [
"Content-Type" => "text/event-stream",
"Cache-Control" => "no-cache",
"Connection" => "keep-alive",
]);
$stream = $gptService->get_ai_chat($data); // 获取流
$streamHelper = new Stream($stream);
$streamHelper->echo();
$res_message = $streamHelper->getData();
return $this->saveChatItem($chatId, $res_message,1);
}
/**
... ...