diff --git a/changelog.md b/changelog.md
index 9f02742df1105c41fe8acdfc1d4a04403893ad4a..c6b8de08cf08b557261bf64b4ce1ef70306458e6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,10 @@
+## 1.0.21(2023-06-06)
+- 修复 用户输入的内容,会被markdown解析,导致显示错误的问题
+- 修复 web-pc端 发送消息,会失去焦点的问题
+- 修复 当ai回答的问题未完成时,点击换答案会进入异常的问题
+- 修复 部分情况下 ai回复问题时,光标不停地换行和撤销换行 导致界面上下抖动的问题
+- 修复 短时间内多次操作 `停止响应`和`换答案`功能,偶发响应的数据顺序错乱的问题
+- 新增 客户端支持`model`选择功能(仅预置了`openai`的6个model,可以自行在组件路径:`/components/llm-config/llm-config.vue`中,增改)
## 1.0.20(2023-06-05)
- 修复 移动端 部分情况下 代码块的复制按钮,挡住内容的问题
- 修复 微信小程序端 点击“复制按钮”无效的问题
diff --git a/common/unicloud-co-task.js b/common/unicloud-co-task.js
new file mode 100644
index 0000000000000000000000000000000000000000..417102d391a121aaaaff3439a6f6a18acbf317c3
--- /dev/null
+++ b/common/unicloud-co-task.js
@@ -0,0 +1,61 @@
+class Task {
+ constructor({
+ success,
+ fail,
+ complete
+ } = {}) {
+ this.status = 0
+ this.callback = {
+ success,
+ fail,
+ complete
+ }
+ }
+
+ invoke(callbackName, ...args) {
+ if (this.status !== 0) {
+ // console.log('此任务已被终止');
+ return
+ }
+ const callback = this.callback[callbackName]
+ callback && callback(...args)
+ }
+ abort() {
+ this.status = 1
+ }
+}
+
+export default function main({
+ coName,
+ funName,
+ param,
+ success,
+ fail,
+ complete,
+ config
+} = {}) {
+
+ if(!Array.isArray(param)){
+ throw new Error('param的值必须为数组')
+ }
+
+ const task = new Task({
+ success,
+ fail,
+ complete
+ })
+
+ const uniCloudCo = uniCloud.importObject(coName, config||{})
+
+ uniCloudCo[funName](...param)
+ .then(res => {
+ task.invoke('success', res)
+ })
+ .catch(err => {
+ task.invoke('fail', err)
+ })
+ .finally(res => {
+ task.invoke('complete', res)
+ })
+ return task
+}
\ No newline at end of file
diff --git a/components/llm-config/llm-config.vue b/components/llm-config/llm-config.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3ab4d879c3a09b403b2565e36b77663c0f37d63b
--- /dev/null
+++ b/components/llm-config/llm-config.vue
@@ -0,0 +1,167 @@
+
+
+
+ 请选择llm的model
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/uni-ai-msg/uni-ai-msg.vue b/components/uni-ai-msg/uni-ai-msg.vue
index 70c5674f2735311d161429af0e8b310d8d5f783f..d5c43df0a1735aec09894f538cf63d6f40e854a1 100644
--- a/components/uni-ai-msg/uni-ai-msg.vue
+++ b/components/uni-ai-msg/uni-ai-msg.vue
@@ -5,12 +5,16 @@
-
+
-
+
+
+ {{msgContent}}
+
+
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
@@ -151,20 +155,20 @@
default () {
return false
}
- }
+ }
},
computed: {
- md() {
+ msgContent() {
return this.msg.content
},
nodes() {
let htmlString = ''
// 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果
// 判断markdown中代码块标识符的数量是否为偶数
- if (this.md.split("```").length % 2) {
- htmlString = markdownIt.render(this.md + ' \n |');
+ if (this.msgContent.split("```").length % 2) {
+ htmlString = markdownIt.render(this.msgContent + ' |');
} else {
- htmlString = markdownIt.render(this.md) + ' \n |';
+ htmlString = markdownIt.render(this.msgContent) + ' \n |';
}
// #ifndef APP-NVUE
@@ -179,7 +183,7 @@
// #endif
}
},
- methods: {
+ methods: {
// 根据消息状态返回对应的图标
msgStateIcon(msg) {
switch (msg.state) {
@@ -229,7 +233,7 @@
// 复制文本内容到系统剪切板
copy() {
uni.setClipboardData({
- data: this.md,
+ data: this.msgContent,
showToast: false,
success() {
uni.showToast({
@@ -256,7 +260,7 @@
/* #endif */
.userInfo {
- flex-direction: column;
+ flex-direction: column;
}
.msg-item {
@@ -278,8 +282,8 @@
width: 40px;
height: 40px;
border-radius: 2px;
- }
-
+ }
+
.create_time {
font-size: 12px;
padding: 5px 0;
diff --git a/package.json b/package.json
index cd811997934e8e5bb016993b5caa56fb8c5da586..61968add8499b8782976eab10e4a4a6d1b515585 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"id": "uni-ai-chat",
"name": "uni-ai-chat",
- "version": "1.0.20",
+ "version": "1.0.21",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js",
"scripts": {
diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue
index 71a642e42bc031be4d1023dbee7bfea51c1e145a..be3d75b421f6f8560405b9a2471fe8c22f690f4d 100644
--- a/pages/chat/chat.vue
+++ b/pages/chat/chat.vue
@@ -1,5 +1,5 @@
-
+
没有对话记录
@@ -18,15 +18,19 @@
@@ -50,7 +55,17 @@
import {
msgList
- } from '@/pages/chat/msgList.js';
+ } from '@/pages/chat/msgList.js';
+
+ // 导入uniCloud云对象task模块
+ import uniCoTask from '@/common/unicloud-co-task.js';
+ // 收集所有执行云对象的任务列表
+ let uniCoTaskList = []
+ // 定义终止并清空 云对象的任务列表中所有 任务的方法
+ uniCoTaskList.clear = function(){
+ uniCoTaskList.forEach(task=>task.abort())
+ uniCoTaskList.slice(0,0)
+ }
// 获取广告id
const {
@@ -59,9 +74,6 @@
// 初始化sse通道
let sseChannel = false;
- // 是否通过回调,当用户点击清空后应当跳过前一次请求的回调
- let skip_callback = false;
-
// 键盘的shift键是否被按下
let shiftKeyPressed = false
export default {
@@ -83,7 +95,8 @@
isWidescreen: false,
// 广告位id
adpid,
- focus: false
+ focus: false,
+ llmModel:false
}
},
computed: {
@@ -111,14 +124,6 @@
},
// 监听msgList变化,将其存储到本地缓存中
watch: {
- // #ifdef H5
- inputBoxDisabled(val) {
- this.$nextTick(() => {
- this.focus = !val
- // console.log('this.focus', this.focus);
- })
- },
- // #endif
msgList: {
handler(msgList) {
let msgLength = msgList.length
@@ -136,6 +141,22 @@
},
// 深度监听msgList变化
deep: true
+ },
+ llmModel(llmModel){
+ let title = 'uni-ai-chat'
+ if(llmModel){
+ title += ` (${llmModel})`
+ }
+ uni.setNavigationBarTitle({title})
+ // #ifdef H5
+ if(this.isWidescreen){
+ document.querySelector('.header').innerText = title
+ }
+ // #endif
+ uni.setStorage({
+ key:'uni-ai-chat-llmModel',
+ data:llmModel
+ })
}
},
beforeMount() {
@@ -177,13 +198,15 @@
// })
// }
-
+ // 获得历史对话记录
let _msgList = uni.getStorageSync('uni-ai-msg') || [];
if (_msgList.length) {
msgList.push(..._msgList)
- }
-
- this.msgList = msgList
+ }
+ this.msgList = msgList
+
+ // 获得之前设置的llmModel
+ this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel')
// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
let length = this.msgList.length
@@ -232,7 +255,13 @@
}
// #endif
},
- methods: {
+ methods: {
+ setLLMmodel(){
+ this.$refs['llm-config'].open(model=>{
+ console.log('model',model);
+ this.llmModel = model
+ })
+ },
// 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream
async checkIsOpenPush() {
try {
@@ -324,7 +353,14 @@
// 发送消息
this.send()
},
+ // 换一个答案
async changeAnswer(){
+ // 如果问题还在回答中需要先关闭
+ if(this.sseIndex){
+ this.closeSseChannel()
+ }
+
+ //删除旧的回答
this.msgList.pop()
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
@@ -389,9 +425,6 @@
}
}
- // 检查是否开通uni-push;决定是否启用enableStream
- await this.checkIsOpenPush()
-
// 如果内容为空
if (!this.content) {
// 弹出提示框
@@ -419,11 +452,12 @@
this.showLastMsg()
// dom加载完成后 清空文本内容
this.$nextTick(() => {
- this.content = ''
+ this.content = ''
})
this.send() // 发送消息
},
- async send() {
+ async send() {
+
let messages = []
// 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(this.msgList))
@@ -461,7 +495,11 @@
// 在控制台输出 向ai机器人发送的完整消息内容
console.log('send to ai messages:', messages);
-
+
+ // 检查是否开通uni-push;决定是否启用enableStream
+ await this.checkIsOpenPush()
+ // console.log('this.enableStream',this.enableStream);
+
// 判断是否开启了流式响应模式
if (this.enableStream) {
// 创建消息通道
@@ -526,20 +564,20 @@
})
await sseChannel.open() // 等待通道开启
}
-
- // 重置skip_callback为false,以便下一次请求可以正常回调
- skip_callback = false
- // 导入uni-ai-chat模块,并设置customUI为true
- const uniAiChat = uniCloud.importObject("uni-ai-chat", {
- customUI: true
- })
-
- // 发送消息给ai机器人
- uniAiChat.send({
+
+ // 导入uni-ai-chat模块,并设置customUI为true
+ let task = uniCoTask({
+ coName:"uni-ai-chat",
+ funName:"send",
+ param:[{
messages, // 消息列表
- sseChannel // 消息通道
- })
- .then(res => {
+ sseChannel, // 消息通道
+ llmModel:this.llmModel
+ }],
+ config:{
+ customUI: true
+ },
+ success:res => {
// console.log(111,res);
if (!sseChannel) {
if (!res.data) {
@@ -550,49 +588,44 @@
state: 100
})
// console.log(res, res.reply);
- // 判断是否要跳过本次回调,防止请求未返回时,历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
- if (!skip_callback) {
- let {
- "reply": content,
- summarize,
- insufficientScore,
- illegal
- } = res.data
- if (illegal) {
- // 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true
- this.updateLastMsg({
- illegal: true
- })
- }
- // 将从云端接收到的消息添加到消息列表中
- this.msgList.push({
- // 添加消息创建时间
- create_time: Date.now(),
- // 标记消息为来自AI机器人
- isAi: true,
- // 添加消息内容
- content,
- // 添加消息总结
- summarize,
- // 添加消息分数不足标记
- insufficientScore,
- // 添加消息涉敏标记
- illegal
- })
- // 滚动窗口以显示最新的一条消息
- this.$nextTick(() => {
- this.showLastMsg()
- })
- } else {
- console.log('用户点击了清空按钮,跳过前一次请求的回调。内容:', res.data.reply);
- }
+ let {
+ "reply": content,
+ summarize,
+ insufficientScore,
+ illegal
+ } = res.data
+ if (illegal) {
+ // 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true
+ this.updateLastMsg({
+ illegal: true
+ })
+ }
+ // 将从云端接收到的消息添加到消息列表中
+ this.msgList.push({
+ // 添加消息创建时间
+ create_time: Date.now(),
+ // 标记消息为来自AI机器人
+ isAi: true,
+ // 添加消息内容
+ content,
+ // 添加消息总结
+ summarize,
+ // 添加消息分数不足标记
+ insufficientScore,
+ // 添加消息涉敏标记
+ illegal
+ })
+ // 滚动窗口以显示最新的一条消息
+ this.$nextTick(() => {
+ this.showLastMsg()
+ })
} else {
// 处理 sseChannel没结束 云函数提前结束的情况
sseChannel.close()
this.sseIndex = 0
}
- })
- .catch(e => {
+ },
+ fail:e => {
console.log(e);
// 获取消息列表长度
let l = this.msgList.length
@@ -613,15 +646,18 @@
content: JSON.stringify(e.message),
showCancel: false
});
- })
+ }
+ })
+ uniCoTaskList.push(task)
},
closeSseChannel(){
+ // 如果存在消息通道,就关闭消息通道
if(sseChannel){
sseChannel.close()
sseChannel = false
}
- // 将skip_callback设置为true,以便下一次请求可以正常回调
- skip_callback = true
+ // 清空历史网络请求(调用云对象)任务
+ uniCoTaskList.clear()
// 将流式响应计数值归零
this.sseIndex = 0
},
@@ -646,15 +682,9 @@
content: '本操作不可撤销',
complete: (e) => {
// 如果用户确认清空聊天记录
- if (e.confirm) {
- // 如果存在消息通道,就关闭消息通道
- if (sseChannel) {
- sseChannel.close()
- }
- // 将skip_callback设置为true,以便下一次请求可以正常回调
- skip_callback = true
- // 将流式响应计数值归零
- this.sseIndex = 0
+ if (e.confirm) {
+ // 关闭ssh请求
+ this.closeSseChannel()
// 将消息列表清空
this.msgList.splice(0, this.msgLength);
}
@@ -685,7 +715,7 @@
font-size: 14px;
border-radius: 3px;
margin-bottom:15px;
- background-color: #f0a020;
+ background-color: #f0b00a;
color: #FFF;
width: 90px;
height: 30px;
@@ -786,6 +816,16 @@
.trash {
width: 30rpx;
margin-left: 10rpx;
+ }
+
+ .menu {
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+ }
+ .menu-item{
+ width: 30rpx;
+ margin:0 10rpx;
}
.send {
@@ -915,11 +955,11 @@
padding-bottom: 0;
}
- .menu {
+ .pc-menu {
padding: 0 10px;
}
- .menu-item {
+ .pc-menu-item {
height: 20px;
justify-content: center;
align-items: center;
@@ -929,11 +969,11 @@
cursor: pointer;
}
- .trash {
+ .pc-trash {
opacity: 0.8;
}
- .trash image {
+ .pc-trash image {
height: 15px;
}
diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
index a9fbafde038a0c468aff158b9fe3de51ada6b0a7..839c2c1dd46d3bed080157958a8dd548e3129970 100644
--- a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
+++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
@@ -198,7 +198,9 @@ module.exports = {
// 消息内容
messages,
// sse渠道对象
- sseChannel
+ sseChannel,
+ // 语言模型
+ llmModel
}) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
@@ -217,6 +219,10 @@ module.exports = {
// 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数
let {llm,chatCompletionOptions} = config
+ // 如果客户端传了llmModel 就覆盖配置的model
+ if(llmModel){
+ chatCompletionOptions.model = llmModel
+ }
return await chatCompletion({
messages, //消息内容
sseChannel, //sse渠道对象