我是跟着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>