作者 lyh

gx

  1 +<?php
  2 +
  3 +namespace App\Helper;
  4 +
  5 +
  6 +
  7 +/**
  8 + * api结果处理
  9 + * @author:dc
  10 + * @time 2023/12/16 14:56
  11 + * Class Resource
  12 + * @package GlobalSo\Tool\Gpt
  13 + */
  14 +abstract class Resource {
  15 +
  16 + /**
  17 + * 函数
  18 + * @var array
  19 + */
  20 + protected $func = [];
  21 +
  22 + /**
  23 + * 是否是调试模式
  24 + * @var bool
  25 + */
  26 + public static $debugInfo = false;
  27 +
  28 + /**
  29 + * 获取内容body所有内容
  30 + * @return string
  31 + */
  32 + abstract public function getBody(): string;
  33 +
  34 +
  35 + /**
  36 + * 获取数据的状态
  37 + * @return int
  38 + */
  39 + abstract public function getCode(): int;
  40 +
  41 + /**
  42 + * 验证code 默认200
  43 + * @param int $code
  44 + * @return bool
  45 + * @time 2023/12/16 16:45
  46 + */
  47 + public function checkCode(int $code = 200):bool {
  48 + return $this->getCode() === $code;
  49 + }
  50 +
  51 +
  52 + /**
  53 + * 获取内容 数据
  54 + * @return mixed
  55 + * @author:dc
  56 + * @time 2024/1/2 15:56
  57 + */
  58 + abstract public function getData();
  59 +
  60 +
  61 + /**
  62 + * 获取错误消息
  63 + * @return string
  64 + */
  65 + abstract public function getMessage(): string;
  66 +
  67 +
  68 + /**
  69 + * 是否是函数
  70 + * @return bool
  71 + * @author:dc
  72 + * @time 2024/1/10 12:35
  73 + */
  74 + public function isFun(){
  75 +
  76 + if($this->getBodyFunc()){
  77 + return true;
  78 + }
  79 +
  80 + return false;
  81 + }
  82 +
  83 + /**
  84 + * @return array|mixed
  85 + * @author:dc
  86 + * @time 2024/4/9 15:55
  87 + */
  88 + public function getBodyFunc(){
  89 + $json = @json_decode($this->getBody(),1);
  90 + if(is_array($json) && isset($json['tool_calls'])){
  91 + return $json['tool_calls'];
  92 + }
  93 + return [];
  94 + }
  95 +
  96 + /**
  97 + * 这个是提交了多少token
  98 + * @return int
  99 + * @author:dc
  100 + * @time 2023/12/18 13:37
  101 + */
  102 + public function getPromptToken():int{
  103 + // 这个是老版本
  104 + if(isset($this->getUsage()['prompt_tokens'])){
  105 + return (int) ($this->getUsage()['prompt_tokens']??0);
  106 + }
  107 + // 这个是新版本
  108 + $num = 0;
  109 + foreach ($this->getUsage() as $item){
  110 + $num += (int) ($item['prompt_tokens']??0);
  111 + }
  112 + return $num;
  113 + }
  114 +
  115 + /**
  116 + * 这个是吐出了多少token
  117 + * @return int
  118 + * @author:dc
  119 + * @time 2023/12/18 13:37
  120 + */
  121 + public function getCompletionToken():int{
  122 + // 这个是老版本
  123 + if(isset($this->getUsage()['completion_tokens'])){
  124 + return (int) ($this->getUsage()['completion_tokens']??0);
  125 + }
  126 + // 这个是新版本
  127 + $num = 0;
  128 + foreach ($this->getUsage() as $item){
  129 + $num += (int) ($item['completion_tokens']??0);
  130 + }
  131 + return $num;
  132 + }
  133 +
  134 +
  135 + /**
  136 + * 获取函数
  137 + * @return array|mixed
  138 + */
  139 + public function getFunc($call=null,...$params)
  140 + {
  141 + if(!$this->func){
  142 + $this->func = $this->getBodyFunc();
  143 + }
  144 +
  145 + // {"code":200,"func":{"name":"taocan","arguments":{"attribute":"\u7528\u91cf","usetime":"\u4eca\u5929"}}}
  146 + $function = $this->func;
  147 +
  148 + if(!empty($this->func) && is_array($this->func)) {
  149 + if (!isset($this->func['name'])) {
  150 + $this->func = [$function[0]];
  151 + }
  152 + }
  153 +
  154 + $result = $this->getFuncAll($call,...$params);
  155 +
  156 + $this->func = $function;
  157 +
  158 + return array_values($result)[0]??null;
  159 + }
  160 +
  161 +
  162 + /**
  163 + * 获取所有的函数体
  164 + * @param null $call
  165 + * @param mixed ...$params
  166 + * @return array|false|mixed
  167 + * @author:dc
  168 + * @time 2024/3/11 14:32
  169 + */
  170 + public function getFuncAll($call=null,...$params)
  171 + {
  172 + if(!$this->func){
  173 + $this->func = $this->getBodyFunc();
  174 + }
  175 + // {"code":200,"func":{"name":"taocan","arguments":{"attribute":"\u7528\u91cf","usetime":"\u4eca\u5929"}}}
  176 + $function = [];
  177 + if(!empty($this->func) && is_array($this->func)){
  178 + $function = $this->func;
  179 + }
  180 +
  181 + if($call){
  182 + if($function){
  183 + // 是否是老版本
  184 + if(!empty($function['name'])){
  185 + // 整理参数
  186 + return [$function['name']=> $this->callFunc(
  187 + $call,
  188 + $function['name'],
  189 + $params,
  190 + $function['arguments']??[]
  191 + )];
  192 + }else{
  193 + // 循环 函数
  194 + $result = [];
  195 + foreach ($function as $func){
  196 + $result[$func['name']] = $this->callFunc(
  197 + $call,
  198 + $func['name'],
  199 + $params,
  200 + $func['arguments']??[]
  201 + );
  202 + }
  203 + return $result;
  204 + }
  205 + }
  206 + return [];
  207 + }
  208 +
  209 + return $function;
  210 + }
  211 +
  212 +
  213 +
  214 + /**
  215 + * call 回调
  216 + * @param $call
  217 + * @param $funcName
  218 + * @param $params
  219 + * @return false|mixed
  220 + * @author:dc
  221 + * @time 2024/3/11 11:35
  222 + */
  223 + private function callFunc($call,$funcName,$params,$attr) {
  224 + $params[] = $attr;
  225 + // 匿名函数
  226 + if($call instanceof \Closure){
  227 + return $call($funcName, ...$params);
  228 + }
  229 + // 类名称
  230 + elseif ($call){
  231 + // 定义了类
  232 + if(is_object($call) || class_exists($call)){
  233 + // 掉用类
  234 + return call_user_func(
  235 + [$call,$funcName]
  236 + ,...$params
  237 + );
  238 + }else
  239 + return call_user_func(
  240 + [$call,$funcName]
  241 + ,...$params
  242 + );
  243 + }
  244 + }
  245 +
  246 +
  247 +
  248 +
  249 +
  250 +}
  1 +<?php
  2 +
  3 +namespace App\Helper;
  4 +
  5 +
  6 +
  7 +/**
  8 + * 流输出
  9 + * @author:dc
  10 + * @time 2024/1/2 14:46
  11 + * Class Stream
  12 + * @package GlobalSo\Tool\Gpt\Resource
  13 + */
  14 +class Stream extends Resource{
  15 +
  16 + /**
  17 + * body内容
  18 + * @var string
  19 + */
  20 + private $body = '';
  21 +
  22 + /**
  23 + * 流输出的文本
  24 + * @var string
  25 + */
  26 + private $text = '';
  27 +
  28 + /**
  29 + * http 状态
  30 + * @var int
  31 + */
  32 + private $status = 200;
  33 +
  34 +
  35 + /**
  36 + * @var \Psr\Http\Message\StreamInterface
  37 + */
  38 + private $stream;
  39 +
  40 + /**
  41 + * @var array 使用了多少token
  42 + */
  43 + private $usage = [];
  44 +
  45 +
  46 + /**
  47 + * Resource constructor.
  48 + * @param \Psr\Http\Message\StreamInterface|array $response
  49 + */
  50 + public function __construct($response)
  51 + {
  52 + if($response instanceof \Psr\Http\Message\StreamInterface){
  53 + $this->stream = $response;
  54 + }
  55 + // 数组,带上下文
  56 + elseif(is_array($response)){
  57 + $this->stream = false;
  58 + // 回答的文本
  59 + $this->text = end($response);
  60 + // 计算token
  61 + $this->usage = [
  62 + [
  63 + 'model'=>'',
  64 + ]
  65 + ];
  66 +
  67 + }
  68 +
  69 + }
  70 +
  71 + /**
  72 + * 最后一行
  73 + * @var array
  74 + */
  75 + private $endLine = [];
  76 +
  77 +
  78 + /**
  79 + * 获取流输出内容
  80 + * @return null
  81 + * @author:dc
  82 + * @time 2024/1/2 13:57
  83 + */
  84 + public function getStreamContent(\Closure $call)
  85 + {
  86 + // 文本
  87 + if($this->stream===false){
  88 + $this->body = $this->text;
  89 + $call($this->text);
  90 + }
  91 + // 流输出
  92 + else{
  93 + while (!$this->stream->eof()) {
  94 + // 获取一行数据
  95 + $line = $this->getStreamContentLine();
  96 + // 必须要有数据
  97 + if($line){
  98 + // 解析成数组
  99 + $arr = @json_decode($line,true);
  100 + // 必须是一个数组
  101 + if(is_array($arr)){
  102 +
  103 + // 是否是函数
  104 + if(!empty($arr['func'])){
  105 + $this->func = $arr['func'] ? : ($arr['tool_calls']??[]);
  106 + continue;
  107 + }
  108 +
  109 + // 这里是新版本
  110 + // 文本
  111 + if(isset($arr['text'])){
  112 + // 拼接
  113 + $this->text .= $arr['text'];
  114 + // 调用
  115 + $call($arr['text']);
  116 + }
  117 + // 到了最后一行
  118 + if (isset($arr['usage'])){
  119 + $this->usage = $arr['usage'];
  120 + $this->endLine = $arr;
  121 + }
  122 + }else{
  123 + // 拼接
  124 + $this->text .= $line;
  125 + // 这里兼容下老版本
  126 + $call($line);
  127 + }
  128 +
  129 + }
  130 + }
  131 +
  132 + // 兼容老版本 老版本没办法获取 实际使用了多少token
  133 + if(!$this->usage){
  134 + $this->usage = [
  135 + [
  136 + 'model'=>'',
  137 + ]
  138 + ];
  139 + }
  140 +
  141 + }
  142 +
  143 + }
  144 +
  145 + /**
  146 + * 流 读取一行
  147 + * @return string
  148 + * @author:dc
  149 + * @time 2024/1/2 14:16
  150 + */
  151 + private function getStreamContentLine(){
  152 + $text = '';
  153 + while (!$this->stream->eof()){
  154 + // 读取一个字符串
  155 + $t = $this->stream->read(1);
  156 + $this->body .= $t;
  157 + if($t === "\n"){
  158 + break;
  159 + }
  160 + // 结束了
  161 + if(ord($t)==1){
  162 + break;
  163 + }
  164 + $text .= $t;
  165 +
  166 + }
  167 + return $text;
  168 + }
  169 +
  170 +
  171 + /**
  172 + * 流输出的所有内容
  173 + * @return string
  174 + */
  175 + public function getBody(): string
  176 + {
  177 + return $this->body;
  178 + }
  179 +
  180 +
  181 + /**
  182 + * @return int
  183 + */
  184 + public function getCode(): int
  185 + {
  186 + return 200;
  187 + }
  188 +
  189 +
  190 + /**
  191 + * 这个是文本内容,就是回答的内容
  192 + * @return array|string
  193 + * @author:dc
  194 + * @time 2024/1/2 14:57
  195 + */
  196 + public function getData()
  197 + {
  198 + return $this->text;
  199 + }
  200 +
  201 + /**
  202 + * @return string
  203 + */
  204 + public function getMessage(): string
  205 + {
  206 + return '';
  207 + }
  208 +
  209 +
  210 + /**
  211 + * @return array
  212 + * @author:dc
  213 + * @time 2024/1/2 14:57
  214 + */
  215 + public function getUsage(): array
  216 + {
  217 + return $this->usage;
  218 + }
  219 +
  220 +
  221 + /**
  222 + * 是否已经输出过头部了
  223 + * @var bool
  224 + */
  225 + protected static $isHeader = false;
  226 +
  227 + /**
  228 + * 是否是sse输出
  229 + * @var bool
  230 + */
  231 + public static $echoSse = false;
  232 +
  233 +
  234 + /**
  235 + * 设置头部
  236 + * @param false $sse
  237 + * @author:dc
  238 + * @time 2024/5/31 15:02
  239 + */
  240 + public static function setStreamHeader(array $header=[]){
  241 + // 默认配置的 头信息 输出一次即可
  242 + if(!self::$isHeader){
  243 + // 流输出 必须的 头信息
  244 + if(self::$echoSse) header("Content-Type:event-stream;Charset=UTF-8;");//event-stream 开启这个数据必须是规定格式
  245 + header("cache-control:no-cache;"); // 告诉浏览器不要进行数据缓存
  246 + header('X-Accel-Buffering: no'); // 关键是加了这一行。告诉浏览器不进行输出的缓冲
  247 + header('Access-Control-Expose-Headers: Content-Disposition, Content-Length, X-Content-Range, X-Duration');
  248 + header('Content-Type: application/json'); // json数据头
  249 + header('Access-Control-Allow-Origin:*'); // 这个是 跨域
  250 + self::$isHeader = true;
  251 + }
  252 +
  253 + // 输出其他header
  254 + foreach ($header as $head){
  255 + header($head);
  256 + }
  257 +
  258 + }
  259 +
  260 + /**
  261 + * 其他地方调用,在ai返回前后都可以调用这个
  262 + * @param $data
  263 + * @param string $type 数据类型
  264 + * @author:dc
  265 + * @time 2024/5/31 15:05
  266 + */
  267 + public static function echo_flush($data,string $type='text'){
  268 +
  269 + self::setStreamHeader();
  270 +
  271 + echo self::$echoSse ? en_sse_data($data,$type) : $data;
  272 +
  273 + ob_flush();
  274 + flush();
  275 + }
  276 +
  277 +
  278 + /**
  279 + * 输出 信息到前端
  280 + * @author:dc
  281 + * @time 2024/5/31 10:16
  282 + */
  283 + public function echo(){
  284 +
  285 + // 如果用户断开,继续脚本的运行
  286 + ignore_user_abort(1);
  287 + set_time_limit(400);
  288 +// // 先把之前的内容 也发送到浏览器
  289 +// @ob_implicit_flush(); // 开启隐式刷新 使用 echo函数时会立即发送到浏览器 开启后就不需要flush调用了
  290 + // 输出内容
  291 + $this->getStreamContent(function ($text) {
  292 + self::echo_flush($text);
  293 + });
  294 + if(self::$debugInfo){
  295 + self::echo_flush($this->endLine['debug']??[],'debug');
  296 + }
  297 + }
  298 +
  299 +
  300 +
  301 +
  302 +}
@@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
9 9
10 namespace App\Http\Logic\Bside\Gpt; 10 namespace App\Http\Logic\Bside\Gpt;
11 11
  12 +use App\Helper\Stream;
12 use App\Http\Logic\Bside\BaseLogic; 13 use App\Http\Logic\Bside\BaseLogic;
13 use App\Models\Gpt\Chat; 14 use App\Models\Gpt\Chat;
14 use App\Models\Gpt\ChatItem; 15 use App\Models\Gpt\ChatItem;
@@ -61,29 +62,11 @@ class ChatLogic extends BaseLogic @@ -61,29 +62,11 @@ class ChatLogic extends BaseLogic
61 ]; 62 ];
62 } 63 }
63 $data = ['message' => $message]; 64 $data = ['message' => $message];
64 -  
65 - return response()->stream(function () use ($gptService, $data, $chatId) {  
66 - $fullResponse = ''; // **存储完整 AI 回复**  
67 - $stream = $gptService->get_ai_chat($data); // 获取流  
68 - if ($stream) {  
69 - while (!$stream->eof()) {  
70 - $chunk = $stream->read(1024); // **逐步读取数据块**  
71 - echo "data: " . json_encode(['message' => $chunk], JSON_UNESCAPED_UNICODE) . "\n\n";  
72 - ob_flush();  
73 - flush();  
74 - $fullResponse .= $chunk; // **拼接完整 AI 回复**  
75 - }  
76 - } else {  
77 - $fullResponse = '服务器繁忙,请重试';  
78 - echo "data: " . json_encode(['message' => $fullResponse], JSON_UNESCAPED_UNICODE) . "\n\n";  
79 - }  
80 - // **流结束后,保存完整 AI 回复**  
81 - $this->saveChatItem($chatId, $fullResponse, 1);  
82 - }, 200, [  
83 - "Content-Type" => "text/event-stream",  
84 - "Cache-Control" => "no-cache",  
85 - "Connection" => "keep-alive",  
86 - ]); 65 + $stream = $gptService->get_ai_chat($data); // 获取流
  66 + $streamHelper = new Stream($stream);
  67 + $streamHelper->echo();
  68 + $res_message = $streamHelper->getData();
  69 + return $this->saveChatItem($chatId, $res_message,1);
87 } 70 }
88 71
89 /** 72 /**