Stream.php 6.8 KB
<?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');
        }
    }




}