WordPress 简单实现 chatGPT 文章摘要

2024-03-12 10:44:17 浏览数 (2)

灵感来源于之前在浏览 HEO 博文时候偶然看到文章前有一段 AI 摘要,第三人称以打字形式来简述文章内容还是蛮酷的~ 于是拟了个把这个功能集成到 2BLOG 主题的计划。之前也用过 chatGPT,感觉这个需求应该不是很难,毕竟直接在 chat.openai.com 提问也可以拿到结果。因 eventStream 流式传输比较繁杂的原因(懒),故本文主要方式为简单粗暴的直接请求 chatGPT 返回响应结果。

近期本地测试的差不多了,所以发一篇 chatGPT 具体实现思路顺便也好做个首次AI应用的记录,鉴于请求付费问题,目前仅用于日志记录文章页面(本文例外)。

注:文章仅作个人记录,部分内容尚未开发完善,代码仅供参考,可能无法适用部分情况

准备工作

一切操作的起源,所有数据均由 chatGPT 生成后进行调用,故需注册一枚 OPENAI 账号(注册流程自行检索,我这里用的接?平台是比较熟悉的 sms-activate,充了2刀,当时选的号段是印度尼西亚的,直接过了),注册后进入账号设置中获取 API Keys。

需要注意的是,每个免费账号只有5美刀的有限期内额度,留意:chatGPT 目前已于4月13日对免费账号进行限速处理,参考:

最近速度慢是因为 OpenAI 对于免费账号限速了,在 platform.openai.com 绑定了信用卡的才是之前的正常速度; 限速指的是流式请求时,首个 token 返回需要 20 秒左右,而绑定了信用卡的账号,在 2 秒左右;

反代 API

默认情况下使用 chatGPT 官方文档中提供的 api 调用地址 https://api.openai.com/v1/completions 在大陆是调不通的,所以我们需要另外自行准备一台国外VPS服务器来做反向代理我们自定义的域名(其中宝塔 nginx 环境参考配置如下:(要把伪静态啥的先关掉

代码语言:javascript复制
location ^~ / {
    proxy_pass https://api.openai.com;
    proxy_set_header Host api.openai.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_ssl_server_name on; #注意此行为开启HTTPS后必填
    add_header X-Cache $upstream_cache_status;

    #Set Nginx Cache
    set $static_filebZf74aXt 0;
    if ( $uri ~* ".(gif|png|jpg|css|js|woff|woff2)$" ){
    	set $static_filebZf74aXt 1;
    	expires 12h;
    }
    if ( $static_filebZf74aXt = 0 ){
        add_header Cache-Control no-cache;
    }
}

自此,我们手里应该已经有了 chatGPT 的 API Keys 和自定义反代的的 API 的地址。

实现流程

其实思路很简单,甚至你可以注册账号后直接在 chat.openai.com 提问即可,这里的实现思路仅供参考。首先是运行环境,我目前使用的博客是 wordpress 平台,所以在 php 环境搭建,这里的实现方式和之前实现企业微信推送评论提醒略有相似之处,比如本地缓存等。

在 chatGPT API 文档中提到有多种对话模式,text-davinci-003gpt-3.5-turbo 等(具体花费金额也不同,可在官网查看),像实现文章摘要这种无需交互的功能,使用 text-davinci-003 就可以,最好别使用这个接口,太tm费token了!相比之下用 gpt-3.5-turbo 会好很多倍,体验基本都一样(有时甚至更好)。先在后台预置好 php 接口,然后在前端异步调用 php 文件接口返回数据即可(前端模拟打字效果) 。

后端

首先获取GET、POST接收请求数据为文章 $post->ID(后设置具体请求数据),拿到 id 后组合chatGPT请求数据内容,再通过 curl 发送 chatGPT 反代 API 请求以获取 chatGPT 返回数据(发送请求后随即将请求记录到本地防止并发同一请求),拿到数据后再将实际返回数据覆写到本地记录,最后返回过滤结果到前端操作。

执行 chatGPT 请求后会在同目录生成名为 chat_data.php 文件,该文件为本地缓存,首次请求写入后续将直接从文件读取数据以避免 chatGPT 重复请求造成多次付费。如需更新摘要内容需要手动定位文章id进行删除,暂无集成删除控件计划到主题(已实现,正在集成中..,已集成至 beta-v1.3.7.8),尚未挂载 wp 文章发布更新 hook。

接受参数为 pid,已做错误处理,代码仅供参考,其中部分内容为 wordpress 主题集成所用,不保证兼容及实际可行性。

代码语言:javascript复制
<?php
    parse_str($_SERVER['QUERY_STRING'], $Params);
    // 判断url传参或form表单参数
    $pid = array_key_exists('pid', $Params) ? $Params['pid'] : $_POST['pid'];
    if($pid){
        define('WP_USE_THEMES', false);  // No need for the template engine
        require_once($_SERVER['DOCUMENT_ROOT'].'/wp-load.php');  // Load WordPress Core 
        $pids = get_post($pid);
        $title = $pids->post_title;
        $author =  get_the_author_meta('display_name', get_post_field('post_author', $pid));
        $content = str_replace(array("rn", "r", "n"), " ", wp_strip_all_tags($pids->post_content));
        $post_type = $pids->post_type;
        $post_exist = get_post_status($pid);
        $requirements = '标题:'.$title.',作者:'.$author.',内容:'.$content; 
        define('OPENAI_API_KEY', 'apikey');  //替换 API Keys
        define('OPENAI_PROXY', 'https://api.openai.com');  //替换代理API
        define('OPENAI_MODEL', 'text-davinci-003');  //可选 gpt-3.5-turbo
        
        function curlRequest($question, $maxlen=1024) {
            $gpt_turbo = OPENAI_MODEL==='gpt-3.5-turbo';
            $post_data = array(
                "model" => OPENAI_MODEL,
                'temperature' => 0.8,
                "max_tokens" => $maxlen,  // works for completion_tokens only
                "prompt" => $question.'。分析上述内容,简述文章用意,注意精简字数',
            );
            if($gpt_turbo){
                unset($post_data['prompt']);
                $post_data = array_merge($post_data, array('messages' => [["role" => "system", "content" => '分析文章内容,简述文章用意,注意精简字数'],["role" => "user", "content" => $question]]));
            }
            $curl = curl_init();
            curl_setopt_array($curl, array(
              CURLOPT_URL => $gpt_turbo ? OPENAI_PROXY.'/v1/chat/completions' : OPENAI_PROXY.'/v1/completions', //聊天模型
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_ENCODING => "",
              CURLOPT_MAXREDIRS => 10,
              CURLOPT_TIMEOUT => 0,
              CURLOPT_FOLLOWLOCATION => true,
              CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
              CURLOPT_CUSTOMREQUEST => "POST",
              CURLOPT_POSTFIELDS => json_encode($post_data),
              CURLOPT_HTTPHEADER => array(
                "Content-Type: application/json",
                "Authorization: Bearer " . OPENAI_API_KEY
              ),
            ));
            $res = curl_exec($curl);
            curl_close($curl);
            return $res;
        }
        
        header("Content-type:text/html;charset=utf-8");  // 声明页面header
        $cached_post = array();
        $cached_path = './chat_data.php';
        // 初始化php文件,返回记录
        function chatGPT_init($caches, $new=false){
            global $pid, $requirements, $cached_path, $response, $post_exist, $post_type;
            switch (true) {
                case !$post_exist:
                    $err_msg = 'Unabled to reach request post: not found';
                    break;
                case $post_type!=='post':
                    $err_msg = 'Unsupported request post type found: pid';
                    break;
                default:
                    $err_msg = NULL;
                    break;
            }
            $request_ip = $_SERVER["REMOTE_ADDR"];
            $request_ua = $_SERVER["HTTP_USER_AGENT"];
            // 创建临时记录,防止多请求并发
                $caches['chat_pid_'.$pid] = array('error' => array ('message' => 'standby, another requesting in busy..','type' => 'request_inqueue_busy','created'=>time(),'ip'=>$request_ip,'ua'=>$request_ua));
                $temp = '<?php'.PHP_EOL.'$cached_post = '.var_export($caches,true).';'.PHP_EOL.'?>';
                $newfile = fopen($cached_path,"w");
                fwrite($newfile, $temp);
                fclose($newfile);
            // 生成 chatGPT 请求数据
            $caches['chat_pid_'.$pid] = $post_exist&&$post_type==='post' ? json_decode(curlRequest($requirements, 256), true) : array('error' => array ('message' => $err_msg,'type' => 'invalid_request_param','created'=>time(),'ip'=>$request_ip,'ua'=>$request_ua));
            $arr = '<?php'.PHP_EOL.'$cached_post = '.var_export($caches,true).';'.PHP_EOL.'?>';
            $arrfile = fopen($cached_path,"w");
            fwrite($arrfile, $arr);
            fclose($arrfile);
            // 读取临时/已请求记录
            $response = json_encode($caches['chat_pid_'.$pid]); //$caches['chat_pid_'.$pid]
        }
        
        //overwrite response record
        if(!file_exists($cached_path)){
            chatGPT_init($cached_post);  // 文件不存在,创建文件后新增记录
        }else{
            include $cached_path;  // 读取文件记录
            if(array_key_exists('chat_pid_'.$pid, $cached_post)){
                $response = json_encode($cached_post['chat_pid_'.$pid]); //$cached_post['chat_pid_'.$pid]
            }else{
                chatGPT_init($cached_post);  // 记录不存在,新增记录
            }
        }
        // formart responses text-result
        $response = json_decode($response);
        if(array_key_exists('error', $response)){
            $response = $response->error->message;
        }else{
            $choices = $response->choices[0];
            $response = array_key_exists('message', $choices) ? $choices->message->content : $choices->text;
            $response = preg_replace('/.*n/','', $response);
        }
    }else{
        $response = 'arguments [pid] err';
    }
    print_r($response);
?>

前端

这里直接发送 xhr/ajax/fetch 等请求到后端接口(上述后端文件路径),传入 pid 参数为文章 id 即可。前端样式效果可根据个人喜欢定制,这里主要写个简单打字机的效果,该方法接受三个参数:输出的dom元素、接口返回的字符串以及打字速度ms。

代码语言:javascript复制
function words_typer(el, str, speed=100){
    try{
        if(!str||typeof(str)!='string'||str.replace(/^s |s $/g,"").replace( /^s*/, '')=="") throw new Error("invalid string");
        new Promise(function(resolve,reject){
            setTimeout(() => {
                el.classList.remove('load');
                for(let i=0,textLen=el.innerText.length;i<textLen;i  ){
                    // real-time data stream
                    let elText = el.innerText,
                        elLen = elText.length-1;
                    setTimeout(() => {
                        el.innerText = elText.slice(0, elLen-i); // console.log(i '-' elLen);
                        if(i===elLen) resolve(el);
                    }, i*5);
                }
            }, 700);
        }).then((res)=>{
            setTimeout(() => {
                res.classList.remove('load');
                for(let i=0,strLen=str.length;i<strLen;i  ){
                    setTimeout(() => {
                        res.innerText  = str[i]; // console.log(str[i]);
                        if(i 1===strLen) res.classList.add('done');
                    }, i*speed);
                }
            }, 300);
        }).catch(function(err){
            console.log(err)
        });
    }catch(err){
        console.log(err);
    }
};
// 示例 fetch 请求,修改 dom 为输出元素,pid 为文章 id
fetch('https://xxx.com/gpt.php?pid=xxx').then(res=>{
    words_typer(document.querySelector('dom'), res, 25);
});

扩展内容

出于安全考量还是建议在 api 上再套一层 cdn(如 cloudflare 等) 用作 api 请求缓存设置请求鉴权限制等。

另外还有个已知问题:当文章过于冗长时,发送请求会触发最大 max_tokens 限制,需要额外做分段请求后进行拼接处理,包括请求字段过长时被截断的问题,这里由于请求付费等原因,暂且搁置。

集成特性

将该功能集成到 wordpress 主题控件后支持了以下两个特性:可自定义分类文章是否开启数据调用以及可选的 chatGPT 对话模型。0426更新:新增 api 文件中转鉴权域名请求 api,适用前端 xhr 校验,可配合 nginx 配置域名请求路径。0509更新:集成本地缓存控制到后台管理面板,已做WP鉴权。

4月29更新

现已支持长篇文章摘要,具体实现为当文章字符请求总数所需 token 超过 4096 时将分割文章为上下文两段并分别请求摘要,完成后再合并上下文摘要请求全文综合摘要。控件目前已集成到 2BLOG 主题面板,相关 gpt 更新内容后续推送至 github,已更新至 beta-v1.3.7.5(现已支持当文章过于冗长导致下半段请求失败时,可选仅摘要首次 文章尾段综合摘要)

增强 api 调用安全逻辑,集成 CDN 请求验证等。

参考链接

charGPT API Refrence:https://platform.openai.com/docs/api-reference/introduction

Best practices for prompt engineering with OpenAI API

0 人点赞