我是跟着B站一个老师写的:B站老师视频
文章末尾附全部代码,如有那个地方不对的或者说是不准确的还请各位大佬指出来哇(っ˘зʕ•̫͡•ʔ
目录
1.首先得创建一个空的文件夹,在vscode中打开
2.安装可以直接运行TS的开发工具
3.新建TS文件
4.在vscode中可以有node一些内置包的提示
5.在创建的TS文件中输入代码
6.在终端运行
6.如果以上都正确的话,就说明我们基本的效果已经出来了
7.接下来就可以对接ChatGPT的接口了
8.新建一个窗口(终端),安装axios
9.接着就是在TS文件中导入axios和api实例
10.创建一个env文件
11.因为nodejs中无法直接使用env的文件,所以这里需要安装一个包
12.接着就导入dotenv
13.接下来就是createServer方法中的了
14.因为请求的速度会很慢,所以需要安装proxy
15.导入socks-proxy-agent
16.接着就是在env和TS中写入代理地址
17.接着就试着一下输出stream格式
18.也可以试着更改message中的内容
19.接下来就开始前端的业务了
19.1创建一个html文件
19.2再来看一下req
19.3请求的是跟地址
19.4请求的是chat
19.5如果请求的是其他地址
19.6注意一点
19.7html中的代码
19.8返回字符串
19.9在页面输出
19.10用文本框的形式输出
20.全部代码
TS代码
env代码
html代码
1.首先得创建一个空的文件夹,在vscode中打开
2.安装可以直接运行TS的开发工具
npm i -g ts-node-dev
3.新建TS文件
4.在vscode中可以有node一些内置包的提示
npm i -D @types/node
5.在创建的TS文件中输入代码
// 从http中导入createServer方法,目的是创建一个web服务 import {createServer} from 'http' createServer((req, res) => { res.end('ok') }).listen(9001)// 这个9001是监听的端口号 console.log('http://localhost:9001');
复制
6.在终端运行
tsnd server.ts => 我这里的server是我起的名字,你们根据自己起的名字写
最后效果如图所示:
因为我代码里面写的端口号是9001,所以我输出的是http://localhost:9001,最后运行结束后,终端里面会出现log出来的接口,Ctrl+单击终端中的链接,浏览器就会出现我代码中的OK
6.如果以上都正确的话,就说明我们基本的效果已经出来了
7.接下来就可以对接ChatGPT的接口了
https://platform.openai.com/docs/api-reference/making-requests
打开上面的链接(ChatGPT的官方文档),找到下图这个地方,这个图片中就是ChatGPT的接口和其他一些东西了
8.新建一个窗口(终端),安装axios
npm i axios
9.接着就是在TS文件中导入axios和api实例
// 导入axios import axios from 'axios' // 创建axios的一个api实例 const api = axios.create({ baseURL:'https://api.openai.com/v1',//这里写的就是ChatGPT的接口 timeout: 5000,// 可以不写 headers: { 'Content-Type': 'application/json', // 这个可以不写,重要的是下面的Authorization Authorization: `Bearer ${}`// 这里是咱们自己的token,看env文件,这里我把key放到了环境变量中 } })
复制
10.创建一个env文件
这里的key就是之前群里发的那个Excel表格里面的东西
OPENAI_API_KEY = 你自己的key复制
11.因为nodejs中无法直接使用env的文件,所以这里需要安装一个包
npm i dotenv
12.接着就导入dotenv
从一下代码中可以看到Authorization后面添加了东西,其实这是引入dotenv/config后,会自动加载dotenv文件,并且会输出到process.env中去,然后后面再加上OPENAI_API_KEY
就可以了。这就设置好了最基本的axios实例
import 'dotenv/config' const api = axios.create({ baseURL:'https://api.openai.com/v1',//这里写的就是ChatGPT的接口 timeout: 5000,// 可以不写 headers: { 'Content-Type': 'application/json', // 这个可以不写,重要的是下面的Authorization Authorization: `Bearer ${process.env.OPENAI_API_KEY}`// 这里是咱们自己的token } })
复制
13.接下来就是createServer方法中的了
createServer(async (req, res) => { // 这里是ChatGPT的接口根地址,返回的数据是JSON格式 const {data} = await api.post("chat/completions", { // 这里是ChatGPT的JSON数据 model: "gpt-3.5-turbo", messages: [{ role: "user", content: "Say this is a test!" }], max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 // temperature: 0.7, }); // res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串 res.end(JSON.stringify(data)); }).listen(9001); // 这个9001是监听的端口号
复制
上面代码中的这个部分是ChatGPT官方文档中的-d这个部分的东西。content: "Say this is a test!" 中引号里面的东西就是咱们平时向小柴提问的问题
{ // 这里是ChatGPT的JSON数据 model: "gpt-3.5-turbo", messages: [{ role: "user", content: "Say this is a test!" }], max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 // temperature: 0.7, }
复制
14.因为请求的速度会很慢,所以需要安装proxy
npm i socks-proxy-agent
15.导入socks-proxy-agent
import {SocksProxyAgent} from 'socks-proxy-agent'复制
16.接着就是在env和TS中写入代理地址
从上述图片中可以看到,红框框中的地址是7890,所以我这里写的是7890,你们需要根据自己的来写
SOCKS_PROXY = socks5://127.0.0.1:7890
复制
httpsAgent: new SocksProxyAgent(process.env.SOCKS_PROXY)
复制
如图所示:这就是运行出来的结果,ChatGPT直接返回的是一个JSON对象,最重要的是choices中的。可以看到message下的content是直接返回的一个内容
17.接着就试着一下输出stream格式
// 导入stream import { Stream } from 'stream'; createServer(async (req, res) => { // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式 const {data} = await api.post<Stream>("chat/completions", { // 这里是ChatGPT的JSON数据 "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Say this is a test!" }], max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 // temperature: 0.7, stream:true, },{ // 设置返回的数据类型为stream responseType:'stream' }); data.pipe(res);// 可以直接流向res响应的对象 // res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串。但是因为ChatGPT的返回是stream格式,所以需要用pipe方法将stream转成字符串,这里就把JSON给注销了 // res.end(JSON.stringify(data)); }).listen(9001); // 这个9001是监听的端口号
复制
如下图所示,这就是运行出来的结果:
18.也可以试着更改message中的内容
"messages": [{ "role": "user", "content": "你可以讲一个爱情故事吗?" }], max_tokens: 100,// 并且,我把这里的最大限度设成100,在运行过程中就可以看到页面在不停输出,直到我设置的最大限度为止
复制
19.接下来就开始前端的业务了
19.1创建一个html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流式响应</title> </head> <body> <p>流式响应是指服务器在发送响应时,可以不等待客户端请求,而是直接发送响应,这样可以减少服务器的响应时间,提高服务器的性能。</p> </body> </html>
复制
19.2再来看一下req
依然是在刚刚的TS文件当中,在createServer中所有代码之前加上下方代码即可
createServer(async (req, res) => { console.log(req.url);// 来看一下req的url,看他请求的是什么 return res.end(req.url); }).listen(9001); // 这个9001是监听的端口号
复制
如图所示,这就是浏览器页面中输出的东西
下方图片是vscode终端中输出的东西
所以我们需要实现一个界面,上方url中的地址是/的时候显示html页面,是chat的时候就是接口的东西
19.3请求的是跟地址
但是如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象。这里就需要改刚刚19.2步的代码了
// console.log(req.url);// 来看一下req的url,看他请求的是什么 // 因为如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象 const url = new URL(req.url!,'file:///') // return res.end(url.pathname),// 这里输出url的pathname,也就是请求的接口地址,他就会是一个纯路径 // 做个判断,看看请求的是什么接口 switch (url.pathname) { // 让数据以流的形式返回给客户端 case "/": createReadStream("./index.html").pipe(res); break; }
复制
如图所示,这就是运行的结果了。页面显示的是刚刚html中写的内容
19.4请求的是chat
这里就是再加一个case,然后后面跟的是之前写的东西,就直接把那一部分的代码XC过来就可以
// 做个判断,看看请求的是什么接口 switch (url.pathname) { // 让数据以流的形式返回给客户端 case "/": createReadStream("./index.html").pipe(res); break; // 如果请求的是/chat接口的话,执行的就是之前写的ChatGPT接口那一部分的内容了 case "/chat": // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式 const { data } = await api.post<Stream>( "chat/completions", { // 这里是ChatGPT的JSON数据 model: "gpt-3.5-turbo", messages: [{ role: "user", content: "你可以讲一个爱情故事吗?" }], max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 temperature: 0.7, stream: true, }, { // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据 responseType: "stream", } ); data.pipe(res); // 可以直接流向res响应的对象 break; }
复制
19.5如果请求的是其他地址
如果请求的是根地址和chat之外的地址的话,直接返回一个空的字符串
default: // 如果请求的接口地址不在上面定义的接口地址中,就返回一个空的字符串 res.end('')
复制
19.6注意一点
messages: [{ role: "user", content: "你可以讲一个爱情故事吗?" }]
这一部分的内容应该是由前端那边传过来的,所以我们需要把他设置成变量
// entries()是返回了键值对的一个数组;Object.fromEntries()是将键值对数组转化为一个对象 const query = Object.fromEntries(url.searchParams.entries()); console.log(query);
复制
如下图所示,因为我url后面没有接参数,所以我终端输出的是一个空的对象。为什么会有两个呢?因为一般浏览器还会发一个favicon的请求,所以会多一个空的对象,不用管它
所以下方的请求chat中的message就可以写成query.prompt,但是在写的时候一定要确保query.prompt存在,所以一定要在刚开始加上判断
case "/chat": res.setHeader("Content-Type", "text/event-stream");// 一定要加,要不然浏览器控制台会报错,报错信息看下方图片 if(!query.prompt){ res.statusCode = 400; return res.end(JSON.stringify({ message: "请输入提问内容" })) } // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式 const { data } = await api.post<Stream>( "chat/completions", { // 这里是ChatGPT的JSON数据 model: "gpt-3.5-turbo", messages: [{ role: "user", content: query.prompt}],// 但是一定要确保prompt是字符串格式且存在,所以在上面加一个判断 max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 temperature: 0.7, stream: true, }, { // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据 responseType: "stream", } ); data.pipe(res); // 可以直接流向res响应的对象 break;
复制
19.7html中的代码
<script> // 在这里定义一个函数,方便调用 function send(prompt){ // 定义一个url,根地址是当前页面的地址 const url = new URL('/chat',location.href) // 将prompt添加到url中,直接可以拼接参数,不需要手写。设置一个参数,键名为prompt,值为上面传进来的prompt url.searchParams.set('prompt',prompt) // url定义好之后就直接给es就行了 const es = new EventSource(url) // es就可以监听到服务器的推送了。得到一个e是message响应的事件 es.onmessage = (e) => { // e.data就是服务器返回的数据 console.log(e.data); } } send('你好呀') </script>
复制
但是后面你就会发现浏览器中的控制台会报错,这是因为在TS文件里面中的请求chat的那一块少写了东西res.setHeader("Content-Type", "text/event-stream")
,这个代码我已经在上面写过了,不过你们可以尝试一下把这句话注了,然后看看报错。当把这个代码加上之后,再刷新一下页面,控制台就没有报错了,但是控制台中会输出一些东西(见下方图片)。但是有一个小bug,就是它会持续不断的输出,因为我们没有关闭它。可以翻到控制台的最后,可以看到[DONE],这个是openai返回的一个特殊的标记,表示这个代码已经完成了
所以我们可以在es.onmessage中加一个判断,当输出[DONE]的时候就关闭es,这样就不会有重复的内容了
es.onmessage = (e) => { // e.data就是服务器返回的数据 console.log(e.data); if(e.data ==='[DONE]'){ // 当服务器推送[DONE]时,关闭es return es.close(); }
复制
19.8返回字符串
从浏览器的控制台中可得知,我们每次输出的都是一个对象,所以这里我们把对象中输出的内容拿出来
es.onmessage = (e) => { // 定义一个data变量,将服务器返回的数据转换为JSON格式 const data = JSON.parse(e.data); // 获取content。从浏览器的控制台中可以看到服务器返回的数据,里面的内容就在choices下面的delta中的content里。如果content为空,则给一个空的字符串 const {content = ''} = data.choices[0].delta; console.log(content); } send('你好呀')
复制
如图所示,这就是我们可以看到的一个实时输出的一个效果
19.9在页面输出
在html中用标签输出,给他设一个id。然后在js代码中获取一下这个标签,在进行连接之前需要清空一下之前的记录,然后在收到任何content后,把这些内容加到这个标签当中。
<!-- 这里用了pre标签,因为它可以保留换行符,方便查看 --> <pre id="out"></pre> <script> // 获取输出元素 const out = document.getElementById('out'); // 在连接之前清空之前的记录 out.innerHTML = '' es.onmessage = (e) => { const {content = ''} = data.choices[0].delta; // 然后在收到任何一个content之后把他加进去 out.innerHTML += content; console.log(content); } send('你好呀') </script>
复制
19.10用文本框的形式输出
<!-- 一定要把文本框放到pre的上面,要不然可能会导致答案在上面 --> <input type="text" id="input"> <script> const input = document.getElementById('input'); // 给input加一个事件监听 input.onkeydown = e => { // 按下回车键,就触发事件 if (e.keyCode === 13) { // 获取输入框的值 send(e.target.value) } } </script>
复制
20.全部代码
TS代码
// 从http中导入createServer方法,目的是创建一个web服务 import { createServer } from "http"; // 导入axios import axios from "axios"; // 导入dotenv import "dotenv/config"; // 导入socks-proxy-agent import { SocksProxyAgent } from "socks-proxy-agent"; // 导入stream import { Stream } from "stream"; // 导入fs import { createReadStream } from "fs"; const socksProxy = process.env.SOCKS_PROXY; // 检查 socksProxy 是否为 undefined if (socksProxy === undefined) { throw new Error("SOCKS_PROXY environment variable is not defined"); } // 创建axios的一个api实例 const api = axios.create({ baseURL: "https://api.openai.com/v1", //这里写的就是ChatGPT的接口地址 timeout: 5000, // 可以不写 headers: { "Content-Type": "application/json", // 这个可以不写,重要的是下面的Authorization Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // 这里是咱们自己的token }, // 因为我们请求的是https,所以需要设置代理 httpsAgent: new SocksProxyAgent(socksProxy), }); createServer(async (req, res) => { // console.log(req.url);// 来看一下req的url,看他请求的是什么 // 因为如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象 const url = new URL(req.url!, "file:///"); // entries()是返回了键值对的一个数组;Object.fromEntries()是将键值对数组转化为一个对象 const query = Object.fromEntries(url.searchParams.entries()); console.log(query); // return res.end(url.pathname),// 这里输出url的pathname,也就是请求的接口地址,他就会是一个纯路径 // 做个判断,看看请求的是什么接口,这个url中会包含接口的很多信息 switch (url.pathname) { // 让数据以流的形式返回给客户端 case "/": createReadStream("./index.html").pipe(res); break; // 如果请求的是/chat接口的话,执行的就是之前写的ChatGPT接口那一部分的内容了 case "/chat": res.setHeader("Content-Type", "text/event-stream");// 一定要加,要不然浏览器控制台会报错 if(!query.prompt){ res.statusCode = 400; return res.end(JSON.stringify({ message: "请输入提问内容" })) } // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式 const { data } = await api.post<Stream>( "chat/completions", { // 这里是ChatGPT的JSON数据 model: "gpt-3.5-turbo", messages: [{ role: "user", content: query.prompt}],// 但是一定要确保prompt是字符串格式且存在,所以在上面加一个判断 // max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间 temperature: 0.7, stream: true, }, { // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据 responseType: "stream", } ); data.pipe(res); // 可以直接流向res响应的对象 break; default: // 如果请求的接口地址不在上面定义的接口地址中,就返回一个空的字符串 res.end('') } //res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串。但是因为ChatGPT的返回是stream格式,所以需要用pipe方法将stream转成字符串,这里就把JSON给注销了 // res.end(JSON.stringify(data)); }).listen(9002); // 这个9001是监听的端口号 console.log("http://localhost:9002");
复制
env代码
OPENAI_API_KEY = 你自己的key # 这里我把key放到了.env文件(也就是环境变量)中,这样在代码中就不需要写key了 # 但是如果要在代码中写key,就需要把key写到代码中,这样不好,所以我把key写到了.env文件中,这样在代码中就不需要写key了 # 这里的key是之前在OpenAI官网申请的,也就是群里那个文档中的东西,每个人都有的 # 这里写的是猫的地址 SOCKS_PROXY = socks5://127.0.0.1:7890
复制
html代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流式响应</title> <style> #input { width: 80%; padding: 8px; margin-top: 10px; } #out { white-space: pre-wrap; text-align: left; max-width: 80%; /* margin: 20px auto; */ padding: 10px; /* border: 1px solid #ccc; */ } </style> </head> <body> <!-- 一定要把文本框放到pre的上面,要不然可能会出现问题 --> <input type="text" id="input"> <!-- 这里用了pre标签,因为它可以保留换行符,方便查看 --> <pre id="out"></pre> <script> // 获取输出元素 const out = document.getElementById('out'); const input = document.getElementById('input'); // 给input加一个事件监听 input.onkeydown = e => { // 按下回车键,就触发事件 if (e.keyCode === 13) { // 获取输入框的值 send(e.target.value) } } // 在这里定义一个函数,方便调用 function send(prompt){ // 定义一个url,根地址是当前页面的地址 const url = new URL('/chat',location.href) // 将prompt添加到url中,直接可以拼接参数,不需要手写。设置一个参数,键名为prompt,值为上面传进来的prompt url.searchParams.set('prompt',prompt) // url定义好之后就直接给es就行了,发起请求就是在这一步。new了一个EventSource就是发起 const es = new EventSource(url) // 在连接之前清空之前的记录 out.innerHTML = '' // es就可以监听到服务器的推送了。得到一个e是message响应的事件。onmessage是服务端响应的过程中,每一次响应都会触发onmessage事件;直到输出[DONE],然后return es.close(),把这个页面的监听关掉。 // 注意:这里的e是message事件,不是message响应的事件。 es.onmessage = (e) => { // e.data就是服务器返回的数据 // console.log(e.data); if(e.data ==='[DONE]'){ // 当服务器推送[DONE]时,关闭es。这就意味着服务器EventSource已经推送完毕了。 return es.close(); } // 定义一个data变量,将服务器返回的数据转换为JSON格式 const data = JSON.parse(e.data); // 获取content。从浏览器的控制台中可以看到服务器返回的数据,里面的内容就在choices下面的delta中的content里。如果content为空,则给一个空的字符串 const {content = ''} = data.choices[0].delta; // 然后在收到任何一个content之后把他加进去 out.innerHTML += content; console.log(content); } } // send('你好呀') </script> </body> </html>
复制