##2024/9/28 22:12:18:
基于 cloudflare pages functions 与 vite+react+tailwind 实现 openai 聊天界面
仓库地址:
https://github.com/qyzhizi/js-logical/tree/main/2024-09-26-vite-react-tailwind-chatgpt-demo
cloudflare 地址:https://2024-09-26-vite-react-tailwind-chatgpt-demo.pages.dev/chatgpt
之前在本地实现了 openai 的聊天页面但是只能在本地或者服务器环境上运行,因为 openai key 需要存放在环境变量中,但是 cloudflare 是服务环境,没有本地文件的概念,不能把 key 存放在如 .env 这样文件中。 但是 cloudflare 有自己的环境变量与cloudflare worker 运行时,这样可以把需要依赖环境变量的那部分函数交给 cloudflare page functions
这是cloudflare worker 运行时,可以绑定环境变量,运行函数,访问数据库,可以当成一个轻量的 node.js 后台。
项目结构
在 cloudflare page 中包含两部分,一部分是与前端 ui 相关的,一部分是与 functions (worker)相关的,这里主要探讨 function 的使用,首先在 page 项目中新建 functions 文件夹, 项目结果如下:
├── .gitignore
├── dist
│ ├── assets
│ │ ├── favicon-C49brna2.svg
│ │ ├── index-C_ON94fw.css
│ │ └── index-Db-owgSd.js
│ └── index.html
├── functions
│ └── api
│ ├── azurechatgpt.js
│ └── chatgpt.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
│ ├── App.jsx
│ ├── chatgpt.jsx
│ ├── favicon.svg
│ ├── index.css
│ └── main.jsx
├── tailwind.config.js
└── vite.config.js
pages function
以 azurechatgpt.js
例子为例:
export async function onRequestPost(context) {
const { request, env } = context;
try {
// const text = await request.text(); // Correctly read request body
// Check if the content type is JSON
const contentType = request.headers.get("content-type");
let messages = null;
if (contentType?.includes("application/json")) {
// Parse the JSON body
const body = await request.json();
messages = body.messages;
} else {
return new Response('Unsupported Content-Type', { status: 400 });
}
const requestData = { messages: messages };
const azure_openai_key = env.AZURE_OPENAI_API_KEY;
const azure_openai_url = env.AZURE_OPENAI_URL;
const requestOptions = {
method: 'POST',
headers: {
'api-key': `${azure_openai_key}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
// body: requestData,
};
const chatres = await fetch(azure_openai_url, requestOptions);
if (!chatres.ok) {
// Handle non-200 responses
return new Response(JSON.stringify({ error: "Failed to fetch from OpenAI" }), { status: chatres.status });
}
// console.log(chatres)
const responseData = await chatres.json();
// console.log(responseData)
return new Response(JSON.stringify(responseData), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) {
// Catch any errors and return a 500 response
return new Response(JSON.stringify({ error: "Internal Server Error", details: error.message }), { status: 500 });
}
}
整个文件就是 一个函数: export async function onRequestPost(context){...}
函数是 onRequestPost 表示接受 POST 请求的异步函数,返回的类型会是 Promise。
调用的 api url: /api/azurechatgpt
可以看到与 azurechatgpt.js 在 functions 目录下的路径对应的,路径是 /api/azurechatgpt.js
在这个函数中用到了 cloudflare 运行时中的环境变量:
const azure_openai_key = env.AZURE_OPENAI_API_KEY;
const azure_openai_url = env.AZURE_OPENAI_URL;
环境变量的设置可以使用配置文件 也可以先将包含项目的 git仓库上传到 cloudflare page上,然后再项目的配置页面上进行设置。
本项目一开始采用的是直接在配置页面上进行手动设置:
https://openai-75050.gzc.vod.tencent-cloud.com/openaiassets_bdd679c319436fe7dc63ad7f61d9281d_2579861727526268561.png
在函数中引用也非常简单:
const { request, env } = context;
env.AZURE_OPENAI_API_KEY;
获取 请求体的代码片段:
// Check if the content type is JSON
const contentType = request.headers.get("content-type");
let text;
if (contentType && contentType.includes("application/json")) {
// Parse the JSON body
const body = await request.json();
text = body.text; // Access the 'text' field
}else {
return new Response('Unsupported Content-Type', { status: 400 });
}
先判断类型,如果是"application/json", 就进行解析,不过这里依旧采用的是异步方式:const body = await request.json();
text 是请求体中的一个字段
想 azure_openai 发送请求的函数是 fetch, 这是在 cloudflare 中可以使用的 api, 可以比较方便的构造各种请求,在这里发送的是 post 请求
const chatres = await fetch(azure_openai_url, requestOptions);
azure_openai_url 不用多说,是请求的 url 地址, requestOptions 是一个对象,包含了请求的类型,请求头,请求体。
const requestOptions = {
method: 'POST',
headers: {
'api-key': `${azure_openai_key}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
};
${azure_openai_key}
这是一种模板语法
JSON.stringify(requestData)
返回的是 json 化后的字符串
const responseData = await chatres.json();
表示 json 对象,表示请求返回的结果, 这里也是用异步的方式获取 json 对象
return new Response(JSON.stringify(responseData), { status: 200, headers: { 'Content-Type': 'application/json' } });
表示最后返回的结果,是整function api 请求的结果。
调用 function 部分的api
接下来是关于ui 部分如何调用 function 部分的api
import React, { useState } from 'react';
import axios from 'axios';
function Chatgpt() {
const [input, setInput] = useState('');
const [response, setResponse] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!input) return;
try {
const result = await axios.post('/api/azurechatgpt', {text: input },
{headers: {'Content-Type': 'application/json', },}
);
// Log the result to the console for debugging
// console.log('Response from API:', result);
// console.log('Response from API:', result.data.choices[0].message.content);
setResponse(result.data.choices[0].message.content);
setInput(''); // Clear the input after submission
} catch (error) {
console.error('Error fetching response:', error);
setResponse('Error fetching response. Please try again.');
}
};
return (
<div className="text-center">
<h2 className="text-2xl mb-10">ChatGPT</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
className="border rounded p-2"
placeholder="Your question here..."
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" className="ml-2 p-2 bg-blue-500 text-white rounded">Ask</button>
</form>
{response && (
<div className="mt-4 p-4 border rounded bg-gray-100">
<h3 className="font-bold">Response:</h3>
<p>{response}</p>
</div>
)}
</div>
);
}
export default Chatgpt;
const result = await axios.post('/api/azurechatgpt', {text: input },
是核心,这里使用的是 axios 异步调用的方式
整个函数 Chatgpt 就是一个 react 组件,将在 App.jsx 中进行组装
result 是一个 json 对象
setResponse(result.data.choices[0].message.content);
表示将返回的结果通过函数 setResponse 赋值到变量 response 中