开源了一个不使用任何后端框架纯 PHP 实现流式调用 OpenAI gpt 接口的项目 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
qiayue
V2EX    分享创造

开源了一个不使用任何后端框架纯 PHP 实现流式调用 OpenAI gpt 接口的项目

  •  
  • nbsp; qiayue
    PRO
    2023-03-24 13:52:53 +08:00 4646 次点击
    这是一个创建于 961 天前的主题,其中的信息可能已经有所发展或是发生改变。

    php-openai-gpt-stream-chat-api-webui

    @qiayue 开源的 纯 PHP 实现 GPT 流式调用和前端实时打印 webui

    仓库地址: https://github.com/qiayue/php-openai-gpt-stream-chat-api-webui

    演示站点: https://aiwanjia.cn/

    提前说明下,本文的价值比开源的代码更高,因为代码只是本文的实现而已。

    目录结构

    / ├─ /class │ ├─ Class.ChatGPT.php │ ├─ Class.DFA.php │ ├─ Class.StreamHandler.php ├─ /static │ ├─ css │ │ ├─ chat.css │ │ ├─ monokai-sublime.css │ ├─ js │ │ ├─ chat.js │ │ ├─ highlight.min.js │ │ ├─ marked.min.js ├─ /chat.php ├─ /index.html ├─ /README. md ├─ /sensitive_words.txt 
    目录 /文件 说明
    / 程序根目录
    /class php 类文件目录
    /class/Class.ChatGPT.php ChatGPT 类,用于处理前端请求,并向 OpenAI 接口提交请求
    /class/Class.DFA.php DFA 类,用于敏感词校验和替换
    /class/Class.StreamHandler.php StreamHandler 类,用于实时处理 OpenAI 流式返回的数据
    /static 存放所有前端页面所需的静态文件
    /static/css 存放前端页面所有的 css 文件
    /static/css/chat.css 前端页面聊天样式文件
    /static/css/monokai-sublime.css highlight 代码高亮插件的主题样式文件
    /static/js 存放前端页面所有的 js 文件
    /static/js/chat.js 前端聊天交互 js 代码
    /static/js/highlight.min.js 代码高亮 js 库
    /static/js/marked.min.js markdown 解析 js 库
    /chat.php 前端聊天请求的后端入口文件,在这里引入 php 类文件
    /index.html 前端页面 html 代码
    /README. md 仓库描述文件
    /sensitive_words.txt 敏感词文件,一行一个敏感词,需要你自己收集敏感词,也可以加我微信(同 GitHub id )找我要

    使用方法

    本项目代码,没有使用任何框架,也没有引入任何第三方后端库,前端引入了代码高亮库 highlight 和 markdown 解析库 marked 都已经下载项目内了,所以拿到代码不用任何安装即可直接使用。

    唯二要做的就是把你自己的 api key 填进去。

    获取源码后,修改 chat.php ,填写 OpenAI 的 api key 进去,具体请见:

    $chat = new ChatGPT([ 'api_key' => '此处需要填入 openai 的 api key ', ]); 

    如果开启敏感词检测功能,需要把敏感词一行一个放入 sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt 文件中。

    开了一个微信群,欢迎入群交流:

    释疑微信群

    原理说明

    流式接收 OpenAI 的返回数据

    后端 Class.ChatGPT.php 中用 curl 向 OpenAI 发起请求,使用 curl 的 CURLOPT_WRITEFUNCTION 设置回调函数,同时请求参数里 'stream' => true 告诉 OpenAI 开启流式传输。

    我们通过 curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this->streamHandler, 'callback']); 设置使用 StreamHandler 类的实例化对象 $this->streamHandlercallback 方法来处理 OpenAI 返回的数据。

    OpenAI 会在模型每次输出时返回 data: {"id":"","object":"","created":1679616251,"model":"","choices":[{"delta":{"content":""},"index":0,"finish_reason":null}]} 格式字符串,其中我们需要的回答就在 choices[0]['delta']['content'] 里,当然我们也要做好异常判断,不能直接这样获取数据。

    另外,实际因为网络传输问题,每次 callback 函数收到的数据并不一定只有一条 data: {"key":"value"} 格式的数据,有可能只有半条,也有可能有多条,还有可能有 N 条半。

    所以我们在 StreamHandler 类中增加了 data_buffer 属性来存储无法解析的半条数据。

    这里根据 OpenAI 的返回数据格式,做了一些特殊处理,具体代码如下:

    public function callback($ch, $data) { $this->counter += 1; file_put_contents('./log/data.'.$this->qmd5.'.log', $this->counter.'=='.$data.PHP_EOL.'--------------------'.PHP_EOL, FILE_APPEND); $result = json_decode($data, TRUE); if(is_array($result)){ $this->end('openai 请求错误:'.json_encode($result)); return strlen($data); } /* 此处步骤仅针对 openai 接口而言 每次触发回调函数时,里边会有多条 data 数据,需要分割 如某次收到 $data 如下所示: data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"以下"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"是"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"使用"},"index":0,"finish_reason":null}]} 最后两条一般是这样的: data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}\n\ndata: [DONE] 根据以上 openai 的数据格式,分割步骤如下: */ // 0 、把上次缓冲区内数据拼接上本次的 data $buffer = $this->data_buffer.$data; // 1 、把所有的 'data: {' 替换为 '{' ,'data: [' 换成 '[' $buffer = str_replace('data: {', '{', $buffer); $buffer = str_replace('data: [', '[', $buffer); // 2 、把所有的 '}\n\n{' 替换维 '}[br]{' , '}\n\n[' 替换为 '}[br][' $buffer = str_replace('}'.PHP_EOL.PHP_EOL.'{', '}[br]{', $buffer); $buffer = str_replace('}'.PHP_EOL.PHP_EOL.'[', '}[br][', $buffer); // 3 、用 '[br]' 分割成多行数组 $lines = explode('[br]', $buffer); // 4 、循环处理每一行,对于最后一行需要判断是否是完整的 json $line_c = count($lines); foreach($lines as $li=>$line){ if(trim($line) == '[DONE]'){ //数据传输结束 $this->data_buffer = ''; $this->counter = 0; $this->sensitive_check(); $this->end(); break; } $line_data = json_decode(trim($line), TRUE); if( !is_array($line_data) || !isset($line_data['choices']) || !isset($line_data['choices'][0]) ){ if($li == ($line_c - 1)){ //如果是最后一行 $this->data_buffer = $line; break; } //如果是中间行无法 json 解析,则写入错误日志中 file_put_contents('./log/error.'.$this->qmd5.'.log', json_encode(['i'=>$this->counter, 'line'=>$line, 'li'=>$li], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT).PHP_EOL.PHP_EOL, FILE_APPEND); continue; } if( isset($line_data['choices'][0]['delta']) && isset($line_data['choices'][0]['delta']['content']) ){ $this->sensitive_check($line_data['choices'][0]['delta']['content']); } } return strlen($data); } 

    敏感词检测

    我们使用了 DFA 算法来实现敏感词检测,按照 ChatGPT 的解释,"DFA"是指“确定性有限自动机”( Deterministic Finite Automaton )DfaFilter (确定有限自动机过滤器)通常是指一种用于文本处理和匹配的算法

    Class.DFA.php 类代码是 GPT4 写的,具体实现代码见源码。

    这里介绍一下使用方法,创建一个 DFA 实例需要传入敏感词文件路径:

    $dfa = new DFA([ 'words_file' => './sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt', ]); 

    特别说明:这里特意用乱码字符串文件名是为了防止他人下载敏感词文件,请你部署后也自己改一个别的乱码文件名,不要使用我这里公开了的文件名

    之后就可以用 $dfa->containsSensitiveWords($inputText) 来判断 $inputText 是否包含敏感词,返回值是 TRUEFALSE 的布尔值,也可以用 $outputText = $dfa->replaceWords($inputText) 来进行敏感词替换,所有在 sensitive_words.txt 中指定的敏感词都会被替换为三个*号。

    如果不想开启敏感词检测,把 chat.php 中的以下三句注释掉即可:

    $dfa = new DFA([ 'words_file' => './sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt', ]); $chat->set_dfa($dfa); 

    如果没有开启敏感词检测,那么每次 OpenAI 的返回都会实时返回给前端。

    如果开启了敏感词检测,会查找 OpenAI 返回中的换行符和停顿符号 [',', '。', ';', '?', '!', '……'] 等来进行分句,每一句都使用 $outputText = $dfa->replaceWords($inputText) 来替换敏感词,之后整句返回给前端。

    开启敏感词后,加载敏感词文件需要时间,每次检测时也是逐句检测,而不是逐词检测,也会导致返回变慢。

    所以如果是自用,可以不开启敏感词检测,如果是部署出去给其他人用,为了保护你的域名安全和你的安全,最好开启敏感词检测。

    流式返回给前端

    直接看 chat.php 的注释会更清楚:

    /* 以下几行注释由 GPT4 生成 */ // 这行代码用于关闭输出缓冲。关闭后,脚本的输出将立即发送到浏览器,而不是等待缓冲区填满或脚本执行完毕。 ini_set('output_buffering', 'off'); // 这行代码禁用了 zlib 压缩。通常情况下,启用 zlib 压缩可以减小发送到浏览器的数据量,但对于服务器发送事件来说,实时性更重要,因此需要禁用压缩。 ini_set('zlib.output_compression', false); // 这行代码使用循环来清空所有当前激活的输出缓冲区。ob_end_flush() 函数会刷新并关闭最内层的输出缓冲区,@ 符号用于抑制可能出现的错误或警告。 while (@ob_end_flush()) {} // 这行代码设置 HTTP 响应的 Content-Type 为 text/event-stream ,这是服务器发送事件( SSE )的 MIME 类型。 header('Content-Type: text/event-stream'); // 这行代码设置 HTTP 响应的 Cache-Control 为 no-cache ,告诉浏览器不要缓存此响应。 header('Cache-Control: no-cache'); // 这行代码设置 HTTP 响应的 Connection 为 keep-alive ,保持长连接,以便服务器可以持续发送事件到客户端。 header('Connection: keep-alive'); // 这行代码设置 HTTP 响应的自定义头部 X-Accel-Buffering 为 no ,用于禁用某些代理或 Web 服务器(如 Nginx )的缓冲。 // 这有助于确保服务器发送事件在传输过程中不会受到缓冲影响。 header('X-Accel-Buffering: no'); 

    之后我们每次想给前端返回数据,用以下代码即可:

    echo 'data: '.json_encode(['time'=>date('Y-m-d H:i:s'), 'content'=>'答: ']).PHP_EOL.PHP_EOL; flush(); 

    这里我们定义了我们自己使用的一个数据格式,里边只放了 time 和 content ,不用解释都懂,time 是时间,content 就是我们要返回给前端的内容。

    注意,回答全部传输完毕后,我们需要关闭连接,可以用以下代码:

    echo 'retry: 86400000'.PHP_EOL; // 告诉前端如果发生错误,隔多久之后才轮询一次 echo 'event: close'.PHP_EOL; // 告诉前端,结束了,该说再见了 echo 'data: Connection closed'.PHP_EOL.PHP_EOL; // 告诉前端,连接已关闭 flush(); 

    EventSource

    前端 js 通过 const eventSource = new EventSource(url); 开启一个 EventSource 请求。

    之后服务器按照 data: {"kev1":"value1","kev2":"value2"} 格式向前端发送数据,前端就可以在 EventSource 的 message 回调事件中的 event.data 里获取 {"kev1":"value1","kev2":"value2"} 字符串形式 json 数据,再通过 JSON.parse(event.data) 就可以得到 js 对象。

    具体代码在 getAnswer 函数中,如下所示:

    function getAnswer(inputValue){ inputValue = inputValue.replace('+', '{[$add$]}'); const url = "./chat.php?q="+inputValue; const eventSource = new EventSource(url); eventSource.addEventListener("open", (event) => { console.log("连接已建立", JSON.stringify(event)); }); eventSource.addEventListener("message", (event) => { //console.log("接收数据:", event); try { var result = JSON.parse(event.data); if(result.time && result.content ){ answerWords.push(result.content); contentIdx += 1; } } catch (error) { console.log(error); } }); eventSource.addEventListener("error", (event) => { console.error("发生错误:", JSON.stringify(event)); }); eventSource.addEventListener("close", (event) => { console.log("连接已关闭", JSON.stringify(event.data)); eventSource.close(); cOntentEnd= true; console.log((new Date().getTime()), 'answer end'); }); } 

    说明一下,原生的 EventSource 请求,只能是 GET 请求,所以这里演示时,直接把提问放到 GETURL 参数里了。 如果要想用 POST 请求,一般有两种办法:

    1. 前后端一起改: [先发 POST 后发 GET ] 用 POST 向后端提问,后端根据提问和时间生成一个唯一 key 随着 POST 请求返回给前端,前端拿到后,再发起一个 GET 请求,在参数里携带问题 key ,获取回答,这种方式需要修改后端代码;

    2. 只改前端: [只发一个 POST 请求] 后端代码不用大改,只需要把 chat.php$question = urldecode($_GET['q'] ?? '') 改为 $question = urldecode($_POST['q'] ?? '') 即可,但是前端需要改造,不能用原生 EventSource 请求,需要用 fetch ,设置流式接收,具体可见下方 GPT4 给出的代码示例。

    async function fetchAiResponse(message) { try { const respOnse= await fetch("./chat.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: message }] }), }); if (!response.ok) { throw new Error(response.statusText); } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); while (true) { const { value, done } = await reader.read(); if (value) { const partialRespOnse= decoder.decode(value, { stream: true }); displayMessage("assistant", partialResponse); } if (done) { break; } } } catch (error) { console.error("Error fetching AI response:", error); displayMessage("assistant", "Error: Failed to fetch AI response."); } } 

    上方代码,关键点在于 const partialRespOnse= decoder.decode(value, { stream: true }) 中的 { stream: true }

    打字机效果

    对于后端返回的所有回复内容,我们需要用打字机形式打印出来。

    最初的方案是

    function typingWords(){ if(contentEnd && cOntentIdx==typingIdx){ clearInterval(typingTimer); answerCOntent= ''; answerWords = []; answers = []; qaIdx += 1; typingIdx = 0; cOntentIdx= 0; cOntentEnd= false; lastWord = ''; lastLastWord = ''; input.disabled = false; sendButton.disabled = false; console.log((new Date().getTime()), 'typing end'); return; } if(contentIdx<=typingIdx){ return; } if(typing){ return; } typing = true; if(!answers[qaIdx]){ answers[qaIdx] = document.getElementById('answer-'+qaIdx); } const cOntent= answerWords[typingIdx]; if(content.indexOf('`') != -1){ if(content.indexOf('```') != -1){ codeStart = !codeStart; }else if(content.indexOf('``') != -1 && (lastWord + content).indexOf('```') != -1){ codeStart = !codeStart; }else if(content.indexOf('`') != -1 && (lastLastWord + lastWord + content).indexOf('```') != -1){ codeStart = !codeStart; } } lastLastWord = lastWord; lastWord = content; answerContent += content; answers[qaIdx].innerHTML = marked.parse(answerContent+(codeStart?'\n\n```':'')); typingIdx += 1; typing = false; } 

    其它

    更多其它细节请看代码,如果对代码有疑问的,请加我微信(同 GitHub id )

    License

    BSD 2-Clause

    14 条回复    2023-03-25 08:54:49 +08:00
    qiayue
        1
    qiayue  
    OP
    PRO
       2023-03-24 14:15:26 +08:00
    原文 [打字机效果] 没写完,这里补上:
    最初的方案是每次接收到后端的返回后就立即显示到页面里,后来发现这样速度太快了,眨眼就显示完了,没有打印机效果。 所以后来的方案就改成了用定时器实现定时打印,那么就需要把收到的先放进数组里缓存起来,然后定时每 50 毫秒执行一次,打印一个内容出来。 具体实现代码如下:
    qiayue
        2
    qiayue  
    OP
    PRO
       2023-03-24 14:16:26 +08:00
    原文最后补充

    ### 代码渲染

    如果严格按照输出什么打印什么的话,那么当正在打印一段代码,需要等到代码全部打完,才能被格式化为代码块,才能高亮显示代码。
    那这个体验也太差了。
    有什么办法能够解决这个问题呢?
    答案就在问题里,既然是因为代码块有开始标记没有结束标记,那就我们给他补全结束标记就好了,直到真的结束标记来了,才不需要补全。

    具体的实现就是下面几行代码:

    ```js
    if(content.indexOf('`') != -1){
    if(content.indexOf('```') != -1){
    codeStart = !codeStart;
    }else if(content.indexOf('``') != -1 && (lastWord + content).indexOf('```') != -1){
    codeStart = !codeStart;
    }else if(content.indexOf('`') != -1 && (lastLastWord + lastWord + content).indexOf('```') != -1){
    codeStart = !codeStart;
    }
    }

    lastLastWord = lastWord;
    lastWord = content;

    answerContent += content;
    answers[qaIdx].innerHTML = marked.parse(answerContent+(codeStart?'\n\n```':''));
    ```
    meta2048
        3
    meta2048  
       2023-03-24 15:55:08 +08:00
    这个必须得支持一下
    go522000
        4
    go522000  
       2023-03-24 16:02:52 +08:00
    非常清晰,感谢分享。
    brader
        5
    brader  
       2023-03-24 16:14:57 +08:00
    有几个建议仅供参考:
    一、看了你 demo 站,没有逐字输出效果,初步怀疑是你没有关闭 nginx 缓冲区造成的。
    二、你的 messages 没有复传完整的上下文对话数组,导致 chatgpt 失去了连续对话能力。
    三、你虽然实现了 EventSource 消息的解析,但解析代码和传输数据强耦合在了一起,EventSource 有其标准的数据格式,可参考文献 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events

    下面是我在项目实际应用中使用 Guzzle 包简单实现的一个方法封装 demo ,它不是完整的代码,有部分初始化工作在其他地方,仅供参考:
    ```
    /**
    * 创建聊天(以流的形式直接输出)
    *
    * @param array $messages 内容 [{"role":"user","content":"你好"},{"role":"assistant","content":"你好!我是 AI 助手,请问你有什么需要帮助的吗?"},{"role":"user","content":"怎么称呼你"}]
    */
    public function createChatCompletionStream($messages = [])
    {
    if (empty($messages)) {
    exit();
    }

    try {
    $respOnse= $this->guzzle->request("POST", '/v1/chat/completions', [
    'json' => [
    'model' => 'gpt-3.5-turbo',
    'messages' => $messages,
    'stream' => true,
    ],
    'stream' => true,
    ]);

    $body = $response->getBody();
    $buffer = '';
    while (!$body->eof()) {
    $buffer .= $body->read(128);
    // 这里使用 while 是因为读取 n 个字节有可能同时读出 n 条 EventSource 消息
    while (($pos = strpos($buffer, "\n\n")) !== false) {
    $msg = substr($buffer, 0, $pos); // 一条 event 消息
    $buffer = substr($buffer, $pos + 2); // 去除已被解析的部分

    if (substr($msg, 0, 6) === 'data: ') { // 只解析了 data ,实际的 EventSource 还有 event 、id 、retry
    $obj = json_decode(substr($msg, 6));
    if (isset($obj->choices[0]->delta->content)) {
    echo $obj->choices[0]->delta->content;
    ob_flush();
    flush();
    }
    }
    }
    }
    exit();

    } catch (GuzzleException $e) {
    Log::error($e->getMessage());
    return response('请求失败,请稍后重试', 500);
    }
    }
    ```
    brader
        6
    brader  
       2023-03-24 16:19:32 +08:00
    抱歉,上面的代码格式无法保持,大家自己粘贴了格式化。在 V2EX 回复我不清楚如何使用 md 语法,这每次让我很苦恼
    uplee
        7
    uplee  
       2023-03-24 16:22:11 +08:00
    现在 PHP 不用包管理是缺点吧,我用的这个
    https://github.com/orhanerday/open-ai
    wizzer
        8
    wizzer  
       2023-03-24 16:26:35 +08:00
    不错
    qiayue
        9
    qiayue  
    OP
    PRO
       2023-03-24 16:26:53 +08:00
    @uplee 我也在用这个,但是想自己搞清楚整个传输流程,所以就去研究了一下,写了点 demo ,写完后发现这里细节还挺多的,就想着写篇文章介绍下,既然写了文章当然就要配套的代码,所以才想着搞成一个稍微完整点的项目开源出来的。
    brader
        10
    brader  
       2023-03-24 16:28:25 +08:00   1
    @uplee 起初我也想用这个包,很遗憾,作为一个工具,它对旧项目不那么友好,PHP 7.4+的要求让我望而却步,我仅需要使用到 chatgpt 的几个 API 而已,而且他的 API 非常易接入,就自己实现了
    JoeyWang321
        11
    JoeyWang321  
       2023-03-24 16:28:36 +08:00
    你好,你的这个演示 demo 好像不支持上下文,是我的测试有问题吗
    qiayue
        12
    qiayue  
    OP
    PRO
       2023-03-24 16:29:59 +08:00
    @brader 感谢,demo 站开启了敏感词校验,所以不是逐字返回给前端,而是逐行返回给前端的,所以看起来是一顿一顿的输出。连续对话能力特意没做的,这个项目暂时只是为了让大家了解清楚流式传输的原理。数据格式的确是我的问题,没按照标准来。
    qiayue
        13
    qiayue  
    OP
    PRO
       2023-03-24 16:30:35 +08:00
    @JoeyWang321 特意没做上下文功能。
    qiayue
        14
    qiayue  
    OP
    PRO
       2023-03-25 08:54:49 +08:00
    @brader 关于你说的没有打字机效果,我改了下,之前是逐句检测,逐句输出,现在改成逐句检测,但是如果不包含敏感词则逐字输出,这样大多数情况下,都会有打字机效果了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1111 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 17:26 PVG 01:26 LAX 09:26 JFK 12:26
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86