Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
DCloud
uni-ai-chat
提交
5784ce64
U
uni-ai-chat
项目概览
DCloud
/
uni-ai-chat
通知
893
Star
11
Fork
8
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
2
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
U
uni-ai-chat
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
2
Issue
2
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
提交
5784ce64
编写于
5月 11, 2023
作者:
DCloud_JSON
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
添加注释
上级
2713e2a2
变更
5
展开全部
显示空白变更内容
内联
并排
Showing
5 changed file
with
1292 addition
and
1027 deletion
+1292
-1027
components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue
components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue
+35
-34
components/uni-ai-msg/uni-ai-msg.vue
components/uni-ai-msg/uni-ai-msg.vue
+32
-15
package.json
package.json
+1
-1
pages/chat/chat.vue
pages/chat/chat.vue
+966
-821
uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
+258
-156
未找到文件。
components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue
浏览文件 @
5784ce64
...
...
@@ -16,18 +16,19 @@
return
{}
},
props
:
{
// 广告位唯一标识
adpid
:
{
type
:
String
,
default
(){
return
'
1053355918
'
return
'
'
}
},
},
computed
:
{
// 回调URL
urlCallback
()
{
// 拿到当前用户的id值
let
uid
=
uniCloud
.
getCurrentUserInfo
().
uid
return
{
userId
:
uid
,
extra
:
JSON
.
stringify
({
...
...
@@ -85,7 +86,7 @@
},
onAdClose
(
e
)
{
console
.
log
(
'
onAdClose
'
,
e
)
this
.
$emit
(
'
onAdClose
'
,
e
)
this
.
$emit
(
'
onAdClose
'
,
e
)
// 触发onAdClose事件
},
onAdError
(
e
)
{
console
.
log
(
'
onAdError
'
,
e
)
...
...
components/uni-ai-msg/uni-ai-msg.vue
浏览文件 @
5784ce64
...
...
@@ -12,18 +12,23 @@
</
template
>
<
script
>
// 引入markdown-it库
import
MarkdownIt
from
'
@/lib/markdown-it.min.js
'
;
// hljs是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题
import
hljs
from
"
@/lib/highlight/highlight-uni.min.js
"
;
// 引入html-parser.js库
import
parseHtml
from
'
@/lib/html-parser.js
'
;
// console.log('hljs--',hljs);
// console.log('hljs--',hljs.getLanguage('js'));
// 初始化 MarkdownIt库
const
markdownIt
=
MarkdownIt
({
// 在源码中启用 HTML 标签
html
:
true
,
// 如果结果以
<
pre
...
开头
,
内部包装器则会跳过
。
highlight
:
function
(
str
,
lang
)
{
// if (lang && hljs.getLanguage(lang)) {
// console.error('lang', lang)
...
...
@@ -49,7 +54,9 @@
name: "msg",
data() {
return {
// 悬浮的复制按钮的左边距
left: "-100px",
// 悬浮的复制按钮的上边距
top: "-100px"
};
},
...
...
@@ -58,32 +65,37 @@
// web端限制不选中文字时出现系统右键菜单
let richTextBox = this.$refs[
'
rich
-
text
-
box
'
]
if (richTextBox) {
// 监听鼠标右键事件
richTextBox.$el.addEventListener(
'
contextmenu
'
, (e) => {
// 判断是否选中了文字内容,如果没有则限制系统默认行为(禁止弹出右键菜单)
if (!document.getSelection().toString()) {
// console.log(e);
// 设置悬浮的复制按钮的坐标值
this.top = e.y +
'
px
'
this.left = e.x +
'
px
'
// console.log(e.x);
// console.log(e.y);
// 禁止系统默认行为(禁止弹出右键菜单)
e.preventDefault()
}
})
}
// 监听全局点击事件,隐藏悬浮的复制按钮的坐标
document.addEventListener(
'
click
'
, () => {
this.left = "-100px"
})
// #endif
},
props: {
// 传入的markdown内容
md: {
type: String,
default () {
return
''
}
},
// 是否显示鼠标闪烁的效果
showCursor: {
type: [Boolean, Number],
default () {
...
...
@@ -92,7 +104,9 @@
}
},
computed: {
// 修改转换结果的html值 用于正确给界面增加鼠标闪烁的效果
html() {
// 判断markdown中代码块标识符的数量是否为偶数
if(this.md.split("```").length%2){
return markdownIt.render(this.md +
'
\
n
<
span
class
=
"
cursor
"
>|<
/span>'
)
;
}
else
{
...
...
@@ -102,16 +116,19 @@
nodes
()
{
// return this.html
// console.log('parseHtml(this.html)',parseHtml(this.html));
// HTML String 类型转换 避免内部转换导致的性能下降。
return
parseHtml
(
this
.
html
)
}
},
methods
:
{
// #ifdef H5
// 复制文本内容到系统剪切板
copy
()
{
uni
.
setClipboardData
({
data
:
this
.
md
,
showToast
:
false
,
})
// 设置悬浮的复制按钮的坐标值,使其隐藏
this
.
left
=
"
-100px
"
}
// #endif
...
...
package.json
浏览文件 @
5784ce64
{
"id"
:
"uni-ai-chat"
,
"name"
:
"uni-ai-chat"
,
"version"
:
"1.0.
3
"
,
"version"
:
"1.0.
4
"
,
"description"
:
"基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体"
,
"main"
:
"main.js"
,
"scripts"
:
{
...
...
pages/chat/chat.vue
浏览文件 @
5784ce64
此差异已折叠。
点击以展开。
uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js
浏览文件 @
5784ce64
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
// 引入utils模块中的safeRequire和checkContentSecurityEnable函数
const
{
safeRequire
,
checkContentSecurityEnable
}
=
require
(
'
./utils
'
)
// 引入uni-config-center模块,并创建config对象
const
createConfig
=
safeRequire
(
'
uni-config-center
'
)
const
config
=
createConfig
({
pluginId
:
'
uni-ai-chat
'
}).
config
()
// 引入uniCloud.database()方法,并创建db对象
const
db
=
uniCloud
.
database
();
// 创建userscollection对象
const
userscollection
=
db
.
collection
(
'
uni-id-users
'
)
// 引入uni-id-common模块
const
uniIdCommon
=
require
(
'
uni-id-common
'
)
module
.
exports
=
{
_before
:
async
function
()
{
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如:
/*
例如:使用uni-id-pages(链接地址:https://ext.dcloud.net.cn/plugin?id=8577)搭建账户体系。
然后再使用uni-id-common的uniIdCommon.checkToken判断用户端身份,验证不通过你可以直接`throw new Error(“token无效”)`抛出异常拦截访问。
如果验证通过了可以获得用户id,可以记录每一个用户id的调用次数来限制,调用多少次后必须充值(推荐用uni-pay,下载地址:https://ext.dcloud.net.cn/plugin?id=1835)
或者看一个激励视频广告(详情:https://uniapp.dcloud.net.cn/uni-ad/ad-rewarded-video.html)后才能继续使用
*** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 ***
*/
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑
// 判断否调用量本云对象的send方法
if
(
this
.
getMethodName
()
==
'
send
'
){
// 从配置中心获取是否需要销毁积分
if
(
config
.
spentScore
){
...
...
@@ -33,6 +33,7 @@ module.exports = {
this
.
uniIdCommon
=
uniIdCommon
.
createInstance
({
clientInfo
:
this
.
clientInfo
})
// 校验token(用户身份令牌)是否有效,并获得用户的_id
let
res
=
await
this
.
uniIdCommon
.
checkToken
(
this
.
clientInfo
.
uniIdToken
)
if
(
res
.
errCode
)
{
// 如果token校验出错,则抛出错误
...
...
@@ -44,9 +45,12 @@ module.exports = {
/* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */
let
{
data
:[{
score
}]}
=
await
userscollection
.
doc
(
this
.
current_uid
).
field
({
'
score
'
:
1
}).
get
()
console
.
log
(
'
score----
'
,
score
);
if
(
score
==
0
||
score
<
0
){
//并发的情况下可能花超过
// 如果积分数小于等于0 则抛出错误提醒客户端
// 注意需要判断小于0 因为特殊的情况下可能花超过
if
(
score
==
0
||
score
<
0
){
throw
"
insufficientScore
"
}
// 扣除对应的积分值
await
userscollection
.
doc
(
this
.
current_uid
)
.
update
({
score
:
db
.
command
.
inc
(
-
1
*
config
.
spentScore
)
...
...
@@ -56,13 +60,18 @@ module.exports = {
// 从配置中心获取内容安全配置
console
.
log
(
'
config.contentSecurity
'
,
config
.
contentSecurity
);
if
(
config
.
contentSecurity
)
{
// 引入uni-sec-check模块
const
UniSecCheck
=
safeRequire
(
'
uni-sec-check
'
)
// 创建uniSecCheck对象
const
uniSecCheck
=
new
UniSecCheck
({
provider
:
'
mp-weixin
'
,
requestId
:
this
.
getUniCloudRequestId
()
})
// 定义文本安全检测函数
this
.
textSecCheck
=
async
(
content
)
=>
{
// 获取sseChannel
let
{
sseChannel
}
=
this
.
getParams
()[
0
]
||
{}
// 如果存在sseChannel,则抛出错误
if
(
sseChannel
){
throw
{
errSubject
:
'
uni-ai-chat
'
,
...
...
@@ -72,12 +81,17 @@ module.exports = {
}
// 检测文本
const
checkRes
=
await
uniSecCheck
.
textSecCheck
({
// 文本内容,不可超过500KB
content
,
// 微信小程序端 开放的唯一用户标识符
// openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene
:
4
,
version
:
1
//后续:支持微信登录后,微信小程序端 改用模式2 详情:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#%E4%BD%BF%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B
// 接口版本号,可选1或2,但1的检测能力很弱 支持微信登录的项目,微信小程序端 可改用模式2 详情:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#%E4%BD%BF%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B
version
:
1
})
console
.
log
(
'
checkRes检测文本
'
,
checkRes
);
// 如果检测到风险内容,则抛出错误
if
(
checkRes
.
errCode
===
uniSecCheck
.
ErrorCode
.
RISK_CONTENT
)
{
console
.
error
({
errCode
:
checkRes
.
errCode
,
...
...
@@ -85,6 +99,7 @@ module.exports = {
result
:
checkRes
.
result
});
throw
"
uni-sec-check:illegalData
"
// 如果检测出错,则抛出错误
}
else
if
(
checkRes
.
errCode
)
{
console
.
log
(
`其他原因导致此文件未完成自动审核(错误码:
${
checkRes
.
errCode
}
,错误信息:
${
checkRes
.
errMsg
}
),需要人工审核`
);
console
.
error
({
...
...
@@ -96,17 +111,25 @@ module.exports = {
}
}
// 获取messages参数
let
{
messages
}
=
this
.
getParams
()[
0
]
||
{
"
messages
"
:[]}
// 将messages中的content拼接成字符串
let
contentString
=
messages
.
map
(
i
=>
i
.
content
).
join
(
'
'
)
console
.
log
(
'
contentString
'
,
contentString
);
// 对contentString进行文本安全检测
await
this
.
textSecCheck
(
contentString
)
}
}
},
async
_after
(
error
,
result
)
{
// 打印错误和结果
console
.
log
(
'
_after
'
,{
error
,
result
});
// 如果有错误
if
(
error
){
// 如果是内容安全检测错误
if
(
error
.
errCode
==
"
60004
"
||
error
==
"
uni-sec-check:illegalData
"
)
{
// 返回一个包含敏感内容提示和标记的响应体
return
{
"
data
"
:
{
"
reply
"
:
"
内容涉及敏感
"
,
...
...
@@ -115,20 +138,29 @@ module.exports = {
"
errCode
"
:
0
}
}
else
if
(
error
.
errCode
&&
error
.
errMsg
)
{
// 如果是符合响应体规范的错误
// 符合响应体规范的错误,直接返回
return
error
}
// 如果是积分不足错误
else
if
(
error
==
'
insufficientScore
'
){
// 设置回复内容
let
reply
=
"
积分不足,请看完激励视频广告后再试
"
// 获取sseChannel
let
{
sseChannel
}
=
this
.
getParams
()[
0
]
||
{}
// 如果存在sseChannel
if
(
sseChannel
){
// 反序列化sseChannel
const
channel
=
uniCloud
.
deserializeSSEChannel
(
sseChannel
)
// 向sseChannel写入回复内容
await
channel
.
write
(
reply
)
// 结束sseChannel
await
channel
.
end
({
"
insufficientScore
"
:
true
})
}
else
{
// 如果不存在sseChannel 返回一个包含回复内容和标记的响应体
return
{
"
data
"
:
{
reply
,
...
...
@@ -138,14 +170,18 @@ module.exports = {
}
}
}
else
{
// 如果是其他错误
throw
error
// 直接抛出异常
}
}
// 如果是send方法且开启了内容安全检测
if
(
this
.
getMethodName
()
==
'
send
'
&&
config
.
contentSecurity
)
{
try
{
// 对回复内容进行文本安全检测
await
this
.
textSecCheck
(
result
.
data
.
reply
)
}
catch
(
e
){
// 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体
return
{
"
data
"
:
{
"
reply
"
:
"
内容涉及敏感
"
,
...
...
@@ -155,12 +191,19 @@ module.exports = {
}
}
}
// 返回处理后的结果
return
result
},
// 发送消息
async
send
({
// 消息内容
messages
,
// sse渠道对象
sseChannel
})
{
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
// messages = [{
// role: 'user',
...
...
@@ -168,91 +211,123 @@ module.exports = {
// }]
// 校验客户端提交的参数
// 检查消息是否符合规范
let
res
=
checkMessages
(
messages
)
if
(
res
.
errCode
)
{
throw
new
Error
(
res
.
errMsg
)
}
// 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数
let
{
llm
,
chatCompletionOptions
}
=
config
return
await
chatCompletion
({
messages
,
//消息内容
sseChannel
,
//sse渠道对象
llm
})
// chatCompletion函数:对话完成
async
function
chatCompletion
({
// 消息列表
messages
,
// 是否需要总结
summarize
=
false
,
// sse渠道对象
sseChannel
=
false
,
// 语言模型
llm
})
{
// 获取语言模型管理器
const
llmManager
=
uniCloud
.
ai
.
getLLMManager
(
llm
)
// 调用chatCompletion方法,传入参数
let
res
=
await
llmManager
.
chatCompletion
({
...
chatCompletionOptions
,
messages
,
stream
:
sseChannel
!==
false
})
// 如果存在sseChannel
if
(
sseChannel
)
{
let
reply
=
""
return
new
Promise
((
resolve
,
reject
)
=>
{
// 反序列化sseChannel
const
channel
=
uniCloud
.
deserializeSSEChannel
(
sseChannel
)
// 判断如果是open-ai按字返回,否则按行返回
if
(
llm
&&
llm
.
provider
&&
llm
.
provider
==
"
openai
"
){
// 按字返回
res
.
on
(
'
message
'
,
async
(
message
)
=>
{
reply
+=
message
await
channel
.
write
(
message
)
// console.log('---message----', message)
})
}
else
{
// 按行返回
res
.
on
(
'
line
'
,
async
(
line
)
=>
{
await
channel
.
write
(
reply
?
(
"
\n\n
"
+
line
)
:
line
)
reply
+=
line
// console.log('---line----', line)
})
}
// 结束返回
res
.
on
(
'
end
'
,
async
()
=>
{
// console.log('---end----',reply)
// 将回复内容添加到消息列表中
messages
.
push
({
"
content
"
:
reply
,
"
role
"
:
"
assistant
"
})
// 计算消息总长度
let
totalTokens
=
messages
.
map
(
i
=>
i
.
content
).
join
(
''
).
length
;
// console.log('totalTokens',totalTokens);
// 如果不需要总结且消息总长度超过500
if
(
!
summarize
&&
totalTokens
>
500
)
{
// 获取总结
let
replySummarize
=
await
getSummarize
(
messages
)
// console.log('replySummarize',replySummarize)
// 结束sseChannel并返回总结
await
channel
.
end
({
summarize
:
replySummarize
})
}
else
{
// 结束sseChannel
await
channel
.
end
()
}
// 返回处理结果
resolve
({
errCode
:
0
})
})
// 返回错误
res
.
on
(
'
error
'
,
(
err
)
=>
{
console
.
error
(
'
---error----
'
,
err
)
reject
(
err
)
})
})
}
else
{
// 如果不需要总结
if
(
summarize
==
false
)
{
// 将回复内容添加到消息列表中
messages
.
push
({
"
content
"
:
res
.
reply
,
"
role
"
:
"
assistant
"
})
// 计算消息总长度
let
totalTokens
=
messages
.
map
(
i
=>
i
.
content
).
join
(
''
).
length
;
// 如果消息总长度超过500
if
(
totalTokens
>
500
)
{
// 获取总结
let
replySummarize
=
await
getSummarize
(
messages
)
// 将总结添加到返回结果中
res
.
summarize
=
replySummarize
}
}
// 如果存在错误
if
(
res
.
errCode
){
// 抛出错误
throw
res
}
// 返回处理结果
return
{
data
:
res
,
errCode
:
0
...
...
@@ -260,46 +335,73 @@ module.exports = {
}
}
//获总结
async
function
getSummarize
(
messages
)
{
messages
.
push
({
"
content
"
:
"
请简要总结上述全部对话
"
,
"
role
"
:
"
user
"
})
//
获取总结不需要再总结summarize和stream
//
调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数
let
res
=
await
chatCompletion
({
// 消息内容
messages
,
// 是否需要总结
summarize
:
true
,
// 是否需要流式返回
stream
:
false
,
// sse渠道对象
sseChannel
:
false
})
// 返回总结的文字内容
return
res
.
reply
}
/**
* 校验消息内容是否符合规范
* @param {Array} messages - 消息列表
* @returns {Object} - 返回校验结果
*/
function
checkMessages
(
messages
)
{
try
{
// 如果messages未定义
if
(
messages
===
undefined
)
{
// 抛出异常
throw
"
messages为必传参数
"
// 如果messages不是数组
}
else
if
(
!
Array
.
isArray
(
messages
))
{
// 抛出异常
throw
"
参数messages的值类型必须是[object,object...]
"
}
else
{
// 否则 遍历messages
messages
.
forEach
(
item
=>
{
// 如果item不是对象
if
(
typeof
item
!=
'
object
'
)
{
// 抛出异常
throw
"
参数messages的值类型必须是[object,object...]
"
}
// 定义itemRoleArr数组
let
itemRoleArr
=
[
"
assistant
"
,
"
user
"
,
"
system
"
]
// 如果item的role属性不在itemRoleArr数组中
if
(
!
itemRoleArr
.
includes
(
item
.
role
))
{
// 抛出异常
throw
"
参数messages[{role}]的值只能是:
"
+
itemRoleArr
.
join
(
'
或
'
)
}
// 如果item的content属性不是字符串
if
(
typeof
item
.
content
!=
'
string
'
)
{
// 抛出异常
throw
"
参数messages[{content}]的值类型必须是字符串
"
}
})
}
// 返回校验结果
return
{
errCode
:
0
,
}
// 捕获异常
}
catch
(
errMsg
)
{
// 返回异常信息
return
{
errSubject
:
'
ai-demo
'
,
errCode
:
'
param-error
'
,
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录