背景:
如果大家不了解AppBuilder的话,可以先到这里了解一下:https://cloud.baidu.com/doc/AppBuilder/s/6lq7s8lli
直接用JavaScript调用AppBuilder API的话,肯定是不可避免的会遇到跨域的问题:
那么就可以换个思路,用nginx来将跨域转化为同域。
步骤大致就这么几步:
-
找一台能访问公网的机器,在上面装个nginx
-
开发一个html页面,将这个页面放到这台装了nginx的服务器上
-
用nginx代理这个html页面,这样就可以用这台机器的hostname访问这个html页面了
-
用nginx代理咱AppBuilder的API接口,在配置中添加允许跨域的请求头
-
将html中访问AppBuilder接口的请求,域名改为这台机器的hostname,这样我们的html页面和AppBuilder API的请求就被nginx代理为同域请求了
AppBuilder的API接口:
可以拿postman测测,具体就不展开了,主要就是两个接口:
https://qianfan.baidubce.com/v2/app/conversation:这个接口通过bear token和appid去生成一个conversationid
https://qianfan.baidubce.com/v2/app/conversation/runs:这个接口基于appid,conversationid,query去调用AB应用进行对话
开发页面:
讲真javascript我已经忘得差不多了,所以其实本文中大部分的代码基本都是用大模型生成的,大家如果对javascript不熟悉,也可以用这种方式来做,我也会把用于生成代码的prompt贴一下方便大家参考,但是在大模型加持下,真的没有啥技术含量,基本上就是有条理的把要实现的逻辑实现描述清楚让大模型生成代码,然后对代码做一些测试,对其中有问题的地方在让大模型纠正代码。
好了,话不多说,咱们先把html页面开发好。
咱们先让大模型把AppBuilder这俩接口给封装好:
prompt(这个curl是从官网文档里面copy过来的,https://cloud.baidu.com/doc/AppBuilder/s/klv2eywua):
帮我实现一段javascript代码,定义一个方法,方法名为get_conversation_id,方法实现了下面的curl:
curl --location 'https://qianfan.baidubce.com/v2/app/conversation' \
--header 'Authorization: Bearer authorization string' \
--header 'Content-Type: application/json' \
--data '{ "app_id": "你的app_id" }'
大模型给出的代码确实质量不错(最后会把完整代码贴给大家,这里就拿这个举个例子):
async function get_conversation_id() {
const authorizationString = 'Bearer authorization string'; // 替换为你的授权字符串
const appId = '你的app_id'; // 替换为你的app_id
const response = await fetch('https://qianfan.baidubce.com/v2/app/conversation', {
method: 'POST',
headers: {
'Authorization': authorizationString,
'Content-Type': 'application/json'
},
body: JSON.stringify({ app_id: appId })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data); // 根据返回的数据结构,你可能需要调整这里来获取conversation_id
// 假设conversation_id在返回的data中,你可以这样获取它:
// const conversationId = data.conversation_id;
// return conversationId;
}
另一个接口类似,用于生成代码的prompt:
使用上面的get_conversation_id方法获取到的conversation_id,构造一个新方法,叫run_prompt,实现下面的http请求:
POST /v2/app/conversation/runs HTTP/1.1
HOST: qianfan.baidubce.com
Authorization: authorization string
Content-Type: application/json
{
"app_id": "85xxxxxx-239c-469c-b342-b6xxxxxxxxf6",
"query": "根据文件中的数据,统计这几所学校小学生有多少",
"stream": true,
"conversation_id": "35xxxx4e-a6d8-4dec-b840-70xxxxxxxx22",
"file_ids": []
}
这里就不贴生成的代码了。
接着我们让大模型基于这两个方法,给咱把html页面开发出来:
基于上面的 get_conversation_id 和 run_prompt,帮我实现一个简单的html页面,这个页面有一个输入框用于输入prompt,输入框下方有一个文本展示区域,用于展示历史对话结果。
页面的执行逻辑:
1. 用户在prompt输入框中输入内容
2. 将用户输入的内容显示在历史对话结果中
3. 调用run_prompt,传入用户输入的prompt,得到answer
4. 将answer也展示在历史对话结果中
注意:在历史对话结果中,prompt左对齐,answer右对齐,以区分prompt和answer
基于这个,大模型把html和javascript的代码初版就生成好了。
但是生成的代码还有很多需要让大模型调整的地方,比如下面这些(这些问题如果你知道问题所在,在反馈大模型的时候,就直接指出问题和解决思路,大模型会根据你的提示去优化代码的):
-
我想在输出answer的时候,用打字机的方式输出。
-
调试的时候,answer没有回显到界面中。
-
rum_prompt代码解析流式数据的时候报错了,(index):174 Error: SyntaxError: Unexpected token ‘d’, "data: {“re”… is not valid JSON,因为解析data的时候,没有去掉SSE的data前缀
-
样式上面希望调整一下,输入prompt和answer分别以浅蓝色气泡和浅绿色气泡作为背景,二者都是左对齐,上下左右间距10px
-
sendBtn走样了,加一个按钮样式
-
……
经过了一顿操作之后,基本上我们想要实现的功能大致都写完了,虽然大模型基本上把99%的代码都写好了,但是还是需要自己微调一些CSS和javascript的。
这里贴一下最终的html+javascript代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt and Answer</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
#container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
#history {
overflow-y: auto;
height: 500px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
width: 100%;
text-align: right; /* 设置文本右对齐 */
}
.bubble {
display: inline-block;
max-width: 200px;
padding: 10px;
margin-bottom: 10px;
border-radius: 10px;
}
.prompt {
background-color: #e6f7ff; /* 浅蓝色 */
color: #000;
text-align: left; /* 设置文本左对齐 */
margin: 10px;
}
.answer {
background-color: #d9f7be; /* 浅绿色 */
color: #000;
text-align: left; /* 设置文本左对齐 */
margin: 10px;
}
#promptInput {
width: 100%;
padding: 10px;
font-size: 16px;
}
#sendButton {
background-color: #007bff; /* 深蓝色 */
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
#sendButton:hover {
background-color: #0056b3; /* 深蓝色(较深) */
}
</style>
</head>
<body>
<h1>Prompt and Answer</h1>
<div id="container">
<div id="history"></div>
<input type="text" id="promptInput" placeholder="输入 prompt">
<button id="sendButton">发送</button>
</div>
<script>
async function get_conversation_id(authorizationString, appId) {
const url = 'https://qianfan.baidubce.com/v2/app/conversation';
// const url = 'http://<your hostname>/v2/app/conversation';
const headers = {
'Authorization': `Bearer ${authorizationString}`,
'Content-Type': 'application/json'
};
const body = JSON.stringify({
"app_id": appId
});
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data.conversation_id;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
async function run_prompt(authorizationString, appId, query, conversationId, fileIds, answerElement) {
const url = 'https://qianfan.baidubce.com/v2/app/conversation/runs';
// const url = 'http://<your hostname>/v2/app/conversation/runs';
const headers = {
'Authorization': `Bearer ${authorizationString}`,
'Content-Type': 'application/json'
};
const body = JSON.stringify({
"app_id": appId,
"query": query,
"stream": true,
"conversation_id": conversationId,
"file_ids": fileIds
});
let answer = '';
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let partialData = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
partialData += decoder.decode(value, { stream: true });
// Split partialData by lines
const lines = partialData.split('\n');
// If partialData does not end with a new line, keep the incomplete line for the next iteration
partialData = !partialData.endsWith('\n') ? lines.pop() : '';
for (const line of lines) {
// Remove the "data: " prefix from the line
const data = line.trim().replace(/^data: /, '');
if (data !== '') {
try {
const jsonData = JSON.parse(data);
if (jsonData.answer !== '') {
answer = jsonData.answer;
// Display answer with typing effect
await typeEffect(answer, answerElement);
}
} catch (error) {
console.error('Error parsing JSON:', error);
}
}
}
}
} catch (error) {
console.error('Error:', error);
throw error;
}
}
document.addEventListener('DOMContentLoaded', async () => {
const authorizationString = 'XXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const appId = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX';
const fileIds = [];
const conversationId = await get_conversation_id(authorizationString, appId);
const promptInput = document.getElementById('promptInput');
const sendButton = document.getElementById('sendButton');
const history = document.getElementById('history');
sendButton.addEventListener('click', async () => {
const userPrompt = promptInput.value;
if (userPrompt.trim() === '') return;
// Display user prompt
const promptElement = document.createElement('div');
promptElement.className = 'prompt';
promptElement.textContent = userPrompt;
history.appendChild(promptElement);
promptInput.value = '';
// Scroll to bottom
history.scrollTop = history.scrollHeight;
try {
// Display answer with typing effect
const answerElement = document.createElement('div');
answerElement.className = 'answer';
history.appendChild(answerElement);
await run_prompt(
authorizationString,
appId,
userPrompt,
conversationId,
fileIds,
answerElement
);
} catch (error) {
console.error('Error:', error);
}
});
});
async function typeEffect(text, element) {
const typingDelay = 70; // milliseconds between each character
const history = document.getElementById('history');
for (let i = 0; i < text.length; i++) {
if (text[i] === '\n') {
element.appendChild(document.createElement('br'));
} else {
element.innerHTML += text[i];
}
await sleep(typingDelay);
history.scrollTop = history.scrollHeight;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
</script>
</body>
</html>
配置Nginx:
上面页面开发完成之后,我们接着就来配置nginx了,如果你对nginx不熟悉,当然也可以用相同的套路,让大模型帮你生成nginx配置:
我想实际运行这个html页面的时候,使用javascript调用https://qianfan.baidubce.com接口时会报跨域错误,我想通过nginx解决这个跨域问题,思路如下:
1. 将html页面部署到一台主机名为 "xxxxxxxx" 的机器上,路径为/home/users/xxxx/tmp/index.html
2.使用nginx代理这个html页面
3.使用nginx代理https://qianfan.baidubce.com的接口
4.修改html页面中接口的指向,改成对xxxxxxxx的访问
请帮我看一下是否正确,以及具体应该如何写nginx配置
大模型不负所托,最终给出了nginx配置(需要结合你自己的实际情况改一下hostname和root路径):
server {
listen 80;
server_name xxxxxxxxxxxx;
location / {
# 设置html页面所在的目录
root /home/users/xxxx/tmp;
index index.html;
}
location /v2/ {
# Handle preflight requests
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass https://qianfan.baidubce.com/v2/;
proxy_set_header Host qianfan.baidubce.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Add CORS headers to the actual response
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With';
}
}
在之前的html源码中我们已经把两个AppBuilder的API接口换成了这台nginx服务器的hostname,所以重新加载nginx配置之后,就可以直接进行测试了。测试过程中,还需要根据实际的一些报错情况精调一下代码。最终的运行效果(我们指向的AppBuilder的App是这款:金庸群侠传-https://console.bce.baidu.com/ai_apaas/examplePage/6c87c772-56ff-49c9-ba70-93082fe4253d,当然你需要使用你自己的App):
好了,就介绍到这里,如果有更多问题,欢迎留言交流。