前言
最近写的开源项目核心功能跑通了,前两天突发奇想。关于项目可否介入大模型来辅助用户使用平台,就跑去研究了最近较火的国内大模型--讯飞星火大模型。
大模型api获取
控制台登录
地址:console.xfyun.cn/app/myapp 新建应用后点进去:
获取api地址和其key
左侧选用大模型版本,右侧圈起来的地方就是咱api要调用的数据了
如果没有正常的token或key可能没有实名认证,需要先实名认证下!
下面有关于web的调用接口,这是咱们后面要调用的api接口地址:
技术栈
react hooks + TypeScript + semi-ui(组件库,可选)
下载工具包:
npm i crypto-js base-64 -d
实现
api和key都获取到了,咱就直接开始上代码操作吧!
目录
utils(工具类)
以下工具类getWebsocketUrl方法,负责构建api的URL地址,具体原因可以查阅对应的官方文档说明
typescript
import * as base64 from 'base-64';
import CryptoJs from 'crypto-js';
import { requestObj } from '../config';
export const getWebsocketUrl = () => {
return new Promise<string>((resovle, reject) => {
let url = 'ws://spark-api.xf-yun.com/v1.1/chat';
let host = 'spark-api.xf-yun.com';
let apiKeyName = 'api_key';
// let date = new Date().toGMTString();
let date = new Date().toUTCString();
let algorithm = 'hmac-sha256';
let headers = 'host date request-line';
let signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v1.1/chat HTTP/1.1`;
let signatureSha = CryptoJs.HmacSHA256(signatureOrigin, requestObj.APISecret);
let signature = CryptoJs.enc.Base64.stringify(signatureSha);
let authorizationOrigin = `${apiKeyName}="${requestObj.APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
let authorization = base64.encode(authorizationOrigin);
// 将空格编码
url = `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;
resovle(url);
});
};
config(配置类)
前言说的 key 在此处分别填入即可,Uid无关紧要
server/AiTool(ws服务工具)
该工具类负责接收父类组件传来的问题,并对相关数据信息进行返回。
- **forwardRef,useImperativeHandle,props **实现父子通信
- 通过 **getWebsocketUrl **返回url地址构建ws通信
- **websocket **返回相关ai回应数据,并对数据加载状态进行实时通信
typescript
import { FC, forwardRef, useImperativeHandle, useState } from 'react';
import { requestObj } from '../config';
import { getWebsocketUrl } from '../utils';
interface AiToolProps {
isText?: boolean;
respondHoodle: (result: string) => void; //关联数据
loadHoodle?: (isLoading: boolean) => void; //加载状态
errorHoodle?: (isLoading: boolean) => void; //失败调用
}
interface CropperRef {
submitHoodle: (v: any) => void; //父类调用
}
const AiTool = forwardRef<CropperRef, AiToolProps>(function AiTool(
{ isText, respondHoodle, loadHoodle, errorHoodle },
ref
) {
let result: string = '';
const [historyMessage, setHistoryMessage] = useState<any[]>([
{ role: 'user', content: '你是谁' }, //# 用户的历史问题
{ role: 'assistant', content: '我是AI助手' }
]);
useImperativeHandle(ref, () => ({
submitHoodle: sendMsg
}));
const sendMsg = async (questionText: string) => {
result = ' ';
// 获取请求地址
let myUrl = await getWebsocketUrl();
// 获取输入框中的内容
// 每次发送问题 都是一个新的websocket请求
let socket = new WebSocket(myUrl);
// 监听websocket的各阶段事件 并做相应处理
socket.addEventListener('open', (event) => {
if (loadHoodle) loadHoodle(true);
// 发送消息
let params = {
header: {
app_id: requestObj.APPID,
uid: 'wzz'
},
parameter: {
chat: {
domain: 'general',
temperature: 0.5,
max_tokens: 1024
}
},
payload: {
message: {
// 如果想获取结合上下文的回答,需要开发者每次将历史问答信息一起传给服务端,如下示例
// 注意:text里面的所有content内容加一起的tokens需要控制在8192以内,开发者如有较长对话需求,需要适当裁剪历史信息
text: [
...historyMessage,
// ....... 省略的历史对话
{ role: 'user', content: questionText } //# 最新的一条问题,如无需上下文,可只传最新一条问题
]
}
}
};
socket.send(JSON.stringify(params));
});
socket.addEventListener('message', (event) => {
let data = JSON.parse(event.data);
if (!data.payload) {
socket.close();
return;
}
result += data.payload.choices.text[0].content;
respondHoodle(result);
if (data.header.code !== 0) {
console.log('出错了', data.header.code, ':', data.header.message);
// 出错了"手动关闭连接"
socket.close();
}
if (data.header.code === 0) {
// 对话已经完成
if (data.payload.choices.text && data.header.status === 2) {
setTimeout(() => {
// "对话完成,手动关闭连接"
socket.close();
}, 1000);
}
}
});
socket.addEventListener('close', (event) => {
setHistoryMessage([
...historyMessage,
{ role: 'user', content: questionText },
{ role: 'assistant', content: result }
]);
if (loadHoodle) loadHoodle(false);
// 对话完成后socket会关闭,将聊天记录换行处理
});
socket.addEventListener('error', (event) => {
if (errorHoodle) errorHoodle(true);
console.log('连接发送错误!!', event);
});
};
// return result;
return '';
});
export default AiTool;
Chat.tsx(具体组件)
chat组件的具体实现
- messageList 记录信息数据
- submit 发送问题函数
- overRespond 介绍信息函数
- **moveY **返回底部
typescript
import { memo, useRef, useState } from 'react';
//type
import type { FC } from 'react';
import styles from './index.module.scss';
import { Button, Spin } from '@douyinfe/semi-ui';
import AiTool from '@/ai/server/AiTool';
interface IProps {
datas?: any[];
}
//user:true代表用户信息,反之ai
interface messageInfo {
text: string;
user: boolean;
}
const Chat: FC<IProps> = () => {
const [question, setQuestion] = useState<string>('');
// const [result, setResult] = useState<string>('');
let result = '';
const [isLoading, setIsLoading] = useState<boolean>(false);
const [messageList, setMessageList] = useState<messageInfo[]>([]);
const ref = useRef<any>(null);
const messageContainerRef = useRef<any>(null);
const loadingRef = useRef<any>(null);
const submit = () => {
setQuestion('');
console.log(messageList);
if (!messageList.length) {
setMessageList([
{
user: true,
text: question
}
]);
console.log(messageList);
} else {
setMessageList([
...messageList,
{
user: true,
text: question
}
]);
console.log(messageList);
}
moveY();
if (ref.current) {
ref.current.submitHoodle(question);
}
};
const respondHoodle = (respond: string) => {
result = respond;
loadingRef.current.innerText = result;
moveY();
// loadingRef.current
};
const overRespond = (v: boolean) => {
if (!v) {
setMessageList((prevList) => [...prevList, { user: false, text: result }]);
console.log(messageList);
moveY();
}
setIsLoading(v);
};
const handleSendMessage = (e: any) => {
e.preventDefault();
};
const handleKeyPress = (e: any) => {
if (e.keyCode === 13) {
submit();
}
};
//返回底部
const moveY = () => {
const h = messageContainerRef.current.scrollHeight;
messageContainerRef.current.scrollTop = h + 20;
};
return (
<div className={styles.chat}>
<div className={styles.chat__main}>
<header className={styles.chat__mainHeader}>
<p>欢迎使用青邮AI助手!</p>
<div>
<Button onClick={moveY} style={{ marginRight: 4 }}>
返回底部
</Button>
<Button type="danger" theme="solid" onClick={() => setMessageList([])}>
清除聊天记录
</Button>
</div>
</header>
{/* 显示你发送消息的内容 */}
<div className={styles.message__container} ref={messageContainerRef}>
{messageList.map((item, index) => {
return item.user ? (
<div key={item.user.toString() + index} className={styles.message__chats}>
<p className={styles.sender__name}>You</p>
<div className={styles.message__sender}>
<p>{item.text}</p>
</div>
</div>
) : (
<div className={styles.message__chats}>
<p>Ai</p>
<div className={styles.message__recipient}>
<p>{item.text}</p>
</div>
</div>
);
})}
{isLoading ? (
<div className={styles.message__chats}>
<p>Ai</p>
<div className={styles.message__recipient}>
<p ref={loadingRef}>{result}</p>
<Spin />
</div>
</div>
) : (
''
)}
</div>
<div className={styles.chat__footer}>
<form className="form" onSubmit={handleSendMessage}>
<input
type="text"
placeholder="编写消息"
className={styles.message}
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyUp={handleKeyPress}
/>
<Button onClick={submit} type="primary" theme="solid" className={styles.sendBtn}>
发送
</Button>
</form>
</div>
<AiTool loadHoodle={overRespond} respondHoodle={respondHoodle} ref={ref} />
</div>
</div>
);
};
export default memo(Chat);
scss就不v了,如需要可以在评论区喊我
结果
效果图
后言
关于使用server的AiTool.tsx工具,其实还能产生不少其他的扩展,接下来着重研究下!加油加油!