chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
export default `
|
||||
1. 这是1.1 *斜体*
|
||||
- 这是1.1.1
|
||||
- 这是1.1.2
|
||||
2. 这是1.2
|
||||
- 这是1.2.1
|
||||
- 这是1.2.1.1
|
||||
- 这是1.2.1.2
|
||||
- 这是1.2.2
|
||||
- 这是1.2.2.1
|
||||
- 这是1.2.2.2
|
||||
3. 这是1.3
|
||||
- 这是1.3.1
|
||||
- 这是1.3.2
|
||||
|
||||
---
|
||||
|
||||
[**加粗的链接**](https://example2.com)
|
||||
|
||||
[*斜体的链接*](https://example2.com)
|
||||
|
||||
[~~删除的链接~~](https://example2.com)
|
||||
|
||||
\`\`\` html
|
||||
<div>这是一个html代码块</div>
|
||||
\`\`\`
|
||||
|
||||
\`\`\` javascript
|
||||
function example() {
|
||||
return 'Hello, World!';
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`\`\` css
|
||||
.example {
|
||||
color: red;
|
||||
color: #666;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**加粗**
|
||||
|
||||
*斜体*
|
||||
|
||||
1. *斜体* 这是一个有序列表的第一个
|
||||
- 团队选择本 Sprint 要完成的任务,拆解为具体工作。
|
||||
2. **加粗** 这是一个有序列表的第二个
|
||||
3. ## 列表 + 一个井号的标题(H1)
|
||||
|
||||
|
||||
这是一个段落,链接前面的文字 [link](https://example.com) 这是链接后面的文字
|
||||
|
||||
# *井号+斜体*
|
||||
|
||||
[link2](https://example2.com)
|
||||
|
||||
# 一个井号的标题(H1)
|
||||
123
|
||||
## 两个井号的标题(H2)
|
||||
|
||||
### 三个井号的标题
|
||||
|
||||
#### 四个井号的标题
|
||||
|
||||
##### 五个井号的标题
|
||||
|
||||
**加粗**
|
||||
|
||||
*斜体*
|
||||
|
||||
~~删除线~~
|
||||
|
||||
# **加粗**
|
||||
|
||||
#### 无序列表
|
||||
- 这是一个无序列表的第一个
|
||||
- 这是一个无序列表的第二个
|
||||
|
||||
## 有序列表
|
||||
1. 这是一个有序列表的第一个
|
||||
2. 这是一个有序列表的第二个
|
||||
3. 这是一个有序列表的第二个
|
||||
|
||||
> 这是一个引用
|
||||
|
||||
| 表格标题 1 | 表格标题 2 |
|
||||
| ---- | ---- |
|
||||
| 表格内容 1 | 表格内容 2表格内容 2表格内容 2表格内容 2表格内容 2表格内容 2表格内容 2 |
|
||||
| 表格内容 1 | 表格内容 2 |
|
||||
| 表格内容 1 | 表格内容 2 |
|
||||
|
||||
`
|
||||
114
qiming-mobile/uni_modules/uni-ai-x/components/msg-tool-bar.uvue
Normal file
114
qiming-mobile/uni_modules/uni-ai-x/components/msg-tool-bar.uvue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view class="tool-bar-container">
|
||||
<text class="iconfont icon-Copy btn" @tap="copy"></text>
|
||||
<text class="time">{{createTime}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { MsgItem } from '@/uni_modules/uni-ai-x/sdk';
|
||||
import { formatTimeAgo } from '@/utils/common';
|
||||
import { useI18n } from '@/utils/i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 使用 ref 来存储时间,自动更新时间
|
||||
const createTime = ref<string>('')
|
||||
|
||||
const props = defineProps<{
|
||||
msg: MsgItem
|
||||
}>()
|
||||
|
||||
const msg = computed<MsgItem>(() => props.msg as MsgItem)
|
||||
|
||||
// 更新时间的函数
|
||||
const updateTime = () => {
|
||||
createTime.value = formatTimeAgo(msg.value.create_time)
|
||||
}
|
||||
|
||||
// 定时器ID
|
||||
let timerId: number | null = null
|
||||
|
||||
// 启动定时器,每分钟更新一次
|
||||
const startTimer = () => {
|
||||
// 立即更新一次
|
||||
updateTime()
|
||||
// 每分钟更新一次(60000毫秒)
|
||||
timerId = setInterval(() => {
|
||||
updateTime()
|
||||
}, 60000)
|
||||
}
|
||||
|
||||
// 停止定时器
|
||||
const stopTimer = () => {
|
||||
if (timerId !== null) {
|
||||
clearInterval(timerId)
|
||||
timerId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startTimer()
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
const copy = () => {
|
||||
// 兼容性处理
|
||||
const text = msg.value.text || msg.value.body
|
||||
if (typeof text != 'string') {
|
||||
uni.showToast({
|
||||
title: t('Mobile.ThirdParty.UniAiX.invalidMessageContent'),
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: text as string,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('Mobile.Common.copySuccess'),
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: t('Mobile.Common.copyFailed'),
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tool-bar-container {
|
||||
width: 100%;
|
||||
padding: 8rpx 0 16rpx 0;
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
font-size: 36rpx;
|
||||
margin-right: 32rpx;
|
||||
// #ifdef WEB
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: auto;
|
||||
font-size: 28rpx;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<text :decode="true" :style="{ color: color, 'font-size': iconSize }" @click="_onClick" class="uni-ai-icon">{{unicode}}</text>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
// #ifdef APP
|
||||
function decodeUnicode(code: string): string {
|
||||
// 将字符串分割成 Unicode 编码的字符
|
||||
let codes = code.split('%u');
|
||||
// 遍历字符数组
|
||||
let chars = codes.map(code => {
|
||||
// 转换为十六进制
|
||||
let hexCode = parseInt(code, 16);
|
||||
// 将 UTF-16 编码转换为字符
|
||||
return String.fromCharCode(hexCode);
|
||||
});
|
||||
// 将字符数组转换成字符串
|
||||
return chars.join('');
|
||||
}
|
||||
// #endif
|
||||
const getVal = (val: string) :string => {
|
||||
const reg = /^[0-9]*$/g
|
||||
return (typeof val === 'number' || reg.test(val) )? val + 'px' : val;
|
||||
}
|
||||
|
||||
type UniAiIconProps = {
|
||||
code?: string
|
||||
color?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<UniAiIconProps>(), {
|
||||
code: '',
|
||||
color: '#333333',
|
||||
size: '16'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'click': [e: UniEvent]
|
||||
}>()
|
||||
|
||||
const unicode = computed<string>(() => {
|
||||
// #ifdef APP
|
||||
return decodeUnicode(props.code)
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
return unescape(`%u${props.code}`) as string
|
||||
// #endif
|
||||
})
|
||||
|
||||
const iconSize = computed<string>(() => {
|
||||
return getVal(props.size)
|
||||
})
|
||||
|
||||
const _onClick = (e: UniEvent) => {
|
||||
emit('click', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
// #ifdef WEB
|
||||
font-display: swap;
|
||||
// #endif
|
||||
font-family: "uni-ai-icon";
|
||||
src: url('/uni_modules/uni-ai-x/static/font/iconfont.ttf');
|
||||
}
|
||||
.uni-ai-icon {
|
||||
font-family: uni-ai-icon !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
/* #ifdef H5 */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<view class="msg-root">
|
||||
<text v-if="msg.error_msg != null" class="error-msg">
|
||||
{{ msg.error_msg }}
|
||||
</text>
|
||||
<template v-else>
|
||||
<!-- AI 思考模式 -->
|
||||
<view class="think-container" v-if="!!msg.thinkContent || !!msg.think">
|
||||
<view class="think-header">
|
||||
<view class="think-header-left" @tap="changeThinkContent">
|
||||
<text class="iconfont icon-BulbOutlined thinking-icon"></text>
|
||||
<text class="thinking-title">
|
||||
{{
|
||||
isThinkingFinished
|
||||
? t("Mobile.Chat.thought")
|
||||
: t("Mobile.Chat.thinking")
|
||||
}}
|
||||
</text>
|
||||
<text
|
||||
class="iconfont icon-a-Chevrondown expand-icon"
|
||||
:class="{ 'is-expanded': !hideThinkContent }"
|
||||
></text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="think-content"
|
||||
:class="{ 'hide-think-content': hideThinkContent }"
|
||||
>
|
||||
<view class="text-box">
|
||||
<mp-html
|
||||
:content="msg.thinkContent || msg.think"
|
||||
:markdown="true"
|
||||
:selectable="true"
|
||||
:container-style="`font-size: 28rpx; color: #888; line-height: 1.6;`"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 答复内容 -->
|
||||
<view class="answer-container">
|
||||
<!-- AI校验通过后,需要水平滚动表格时,需要设置 scroll-table="true" -->
|
||||
<mp-html
|
||||
:content="processedText"
|
||||
:processing-list="msg.processingList"
|
||||
:conversation-id="msg.chat_id"
|
||||
:markdown="true"
|
||||
:selectable="true"
|
||||
:container-style="`width: 100%;`"
|
||||
:tag-style="tagStyle"
|
||||
:scroll-table="true"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<image
|
||||
v-if="
|
||||
isConversationActive && isLastMsg && taskStatus !== TaskStatus.EXECUTING
|
||||
"
|
||||
class="icon-loading-image"
|
||||
src="@/static/assets/icon_loading.svg"
|
||||
:show-menu-by-longpress="false"
|
||||
@longtap.stop.prevent
|
||||
@contextmenu.stop.prevent="true"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<msg-tool-bar
|
||||
v-if="
|
||||
((!isConversationActive || !isLastMsg) && (msg.body || msg.text)) ||
|
||||
msg.state === 3
|
||||
"
|
||||
:msg="msg"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { MsgItem } from "@/uni_modules/uni-ai-x/sdk";
|
||||
import msgToolBar from "@/uni_modules/uni-ai-x/components/msg-tool-bar.uvue";
|
||||
import mpHtml from "@/subpackages/uni_modules/mp-html/components/mp-html/mp-html.vue";
|
||||
import {
|
||||
replaceMathBracket,
|
||||
groupMarkdownContainers,
|
||||
} from "@/utils/markdown.uts";
|
||||
import { TaskStatus } from "@/types/enums/agent";
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
msg: MsgItem;
|
||||
// 会话是否正在进行中(有消息正在处理)
|
||||
isConversationActive?: boolean;
|
||||
taskStatus?: TaskStatus;
|
||||
// 是否是最后一条消息
|
||||
isLastMsg?: boolean;
|
||||
}>();
|
||||
|
||||
const msg = ref<MsgItem>(props.msg as MsgItem); // 转化内部状态为响应式
|
||||
const isLastMsg = computed(() => props.isLastMsg);
|
||||
const isConversationActive = computed(() => props.isConversationActive);
|
||||
|
||||
// 思考内容是否隐藏(默认隐藏)
|
||||
const hideThinkContent = ref<boolean>(true);
|
||||
|
||||
// 思考是否已完成:如果正文已经有内容,或者会话已结束,或者不是最后一条消息,则认为思考已完成
|
||||
const isThinkingFinished = computed((): boolean => {
|
||||
return (
|
||||
msg.value.body.trim().length > 0 ||
|
||||
!props.isConversationActive ||
|
||||
!props.isLastMsg
|
||||
);
|
||||
});
|
||||
|
||||
// 标签自定义样式,解决长文本换行问题
|
||||
const tagStyle = {
|
||||
p: "word-break: break-all; overflow-wrap: break-word;",
|
||||
li: "word-break: break-all; overflow-wrap: break-word;",
|
||||
div: "word-break: break-all; overflow-wrap: break-word;",
|
||||
span: "word-break: break-all; overflow-wrap: break-word;",
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
changeThinkContent: [hideThinkContent: boolean];
|
||||
}>();
|
||||
|
||||
const getRenderKey = (...args: any[]) => {
|
||||
return msg.value.chat_id + "-" + args.join("-");
|
||||
};
|
||||
|
||||
const genImageHtml = (src: string) => {
|
||||
return `<img src="${src}" />`;
|
||||
};
|
||||
|
||||
const changeThinkContent = () => {
|
||||
hideThinkContent.value = !hideThinkContent.value;
|
||||
emit("changeThinkContent", hideThinkContent.value);
|
||||
};
|
||||
|
||||
// 添加updateMessage方法,支持动态更新消息内容
|
||||
const updateMessage = (newData: any) => {
|
||||
try {
|
||||
// 使用响应式数据而不是直接操作props
|
||||
if (newData) {
|
||||
const updatedMsg = {
|
||||
...msg.value,
|
||||
...newData,
|
||||
};
|
||||
msg.value = updatedMsg;
|
||||
// console.log('更新消息成功:', newData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("updateMessage 执行失败:", error);
|
||||
}
|
||||
};
|
||||
// 处理文本内容,应用数学公式格式转换及组件合并
|
||||
const processedText = computed(() => {
|
||||
if (!msg.value || (msg.value.body || "").trim().length === 0) {
|
||||
return "";
|
||||
}
|
||||
const groupedText = groupMarkdownContainers(msg.value.body);
|
||||
return replaceMathBracket(groupedText);
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
updateMessage,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.msg-root {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// #ifdef WEB || MP-WEIXIN
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
// #endif
|
||||
padding: 0 32rpx;
|
||||
min-height: 90rpx;
|
||||
|
||||
.think-container {
|
||||
width: 100%;
|
||||
// margin-bottom: 24rpx;
|
||||
|
||||
.think-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
|
||||
.think-header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.thinking-icon {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.thinking-title {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
margin-left: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.think-content {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
padding-top: 8rpx;
|
||||
|
||||
&.hide-think-content {
|
||||
height: 0;
|
||||
padding-top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
border-left: 4rpx solid #efefef;
|
||||
padding-left: 20rpx;
|
||||
background-color: rgba(245, 245, 245, 0.3);
|
||||
border-radius: 0 8rpx 8rpx 0;
|
||||
padding-top: 8rpx;
|
||||
padding-bottom: 8rpx;
|
||||
|
||||
.text {
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.answer-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ff4d4f;
|
||||
font-size: 28rpx;
|
||||
line-height: 52rpx;
|
||||
}
|
||||
|
||||
.loging-icon {
|
||||
margin-top: 16rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
.stop-text {
|
||||
color: #aaa;
|
||||
font-size: 28rpx;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
.need-try {
|
||||
color: #ff4d4f;
|
||||
font-size: 28rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
.error-msg,
|
||||
.stop-text,
|
||||
.need-try {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #15171f;
|
||||
font-weight: 400;
|
||||
line-height: 56rpx;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 100%;
|
||||
}
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.del {
|
||||
text-decoration-line: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 10px;
|
||||
}
|
||||
.link {
|
||||
color: #0066cc !important;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
// #ifdef WEB
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
.paragraph-box {
|
||||
display: block;
|
||||
.text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
line-height: 56rpx;
|
||||
// 自动换行并显示 \n
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 100%;
|
||||
/* #ifdef H5 */
|
||||
width: auto;
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
.image-box {
|
||||
width: 100%;
|
||||
}
|
||||
.blockquote-box {
|
||||
padding: 5px;
|
||||
background: #f5f5f5;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
.container-box {
|
||||
width: 100%;
|
||||
}
|
||||
.heading-box {
|
||||
.text {
|
||||
color: #15171f;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
.depth-1 {
|
||||
font-size: 50rpx;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.depth-2 {
|
||||
font-size: 42rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
|
||||
.depth-3 {
|
||||
font-size: 34rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
|
||||
.depth-4 {
|
||||
font-size: 30rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
|
||||
.depth-5 {
|
||||
font-size: 26rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
|
||||
.depth-6 {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.hr {
|
||||
height: 2rpx;
|
||||
background: #ddd;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
.br {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list-box {
|
||||
padding-left: 10rpx;
|
||||
.list-content {
|
||||
width: 100%;
|
||||
}
|
||||
.text {
|
||||
line-height: 56rpx;
|
||||
}
|
||||
.list-item-content-box {
|
||||
flex: 1;
|
||||
.list {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.list-item-br {
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-index {
|
||||
width: 40rpx;
|
||||
&.unordered {
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
}
|
||||
&.task {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-inline-image-box {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
.list-content {
|
||||
width: 100%;
|
||||
}
|
||||
.text {
|
||||
line-height: 56rpx;
|
||||
}
|
||||
.list-item-content-box {
|
||||
flex: 1;
|
||||
.list {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.list-item-br {
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-index {
|
||||
width: 40rpx;
|
||||
&.unordered {
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
}
|
||||
&.task {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hr-box {
|
||||
background-color: #ddd;
|
||||
height: 2rpx;
|
||||
}
|
||||
|
||||
.tool-call-box {
|
||||
width: 100%;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
|
||||
.table-box,
|
||||
.code-box,
|
||||
.hr-box,
|
||||
.paragraph-box,
|
||||
.blockquote-box,
|
||||
.heading-box,
|
||||
.list-box,
|
||||
.loging-icon,
|
||||
.math-box {
|
||||
width: 100%;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
.paragraph-box {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.space-box {
|
||||
height: 30rpx;
|
||||
}
|
||||
|
||||
.codespan {
|
||||
color: #666;
|
||||
background-color: #f5f5f5;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.icon-loading-image {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 10rpx;
|
||||
-webkit-touch-callout: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<view class="rotate-icon" id="rotate-icon" @transitionend="updateRotate"
|
||||
:style="{
|
||||
transform: `rotate(${times * 360}deg)`
|
||||
}"
|
||||
></view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
const times = ref<number>(0)
|
||||
|
||||
const updateRotate = () => {
|
||||
times.value++
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
updateRotate()
|
||||
}, 300)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.rotate-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 100px;
|
||||
border-top: solid 1px #AAA;
|
||||
border-right: solid 1px #AAA;
|
||||
margin:0 5px;
|
||||
/* 旋转动画 */
|
||||
transition-property: transform;
|
||||
transition-timing-function: linear;
|
||||
transform: rotate(0deg);
|
||||
transition-duration: 1000ms;
|
||||
}
|
||||
</style>
|
||||
105
qiming-mobile/uni_modules/uni-ai-x/config.uts
Normal file
105
qiming-mobile/uni_modules/uni-ai-x/config.uts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {reactive} from 'vue';
|
||||
import {LLMModel, Provider, DefaultLLM, BailianTokenInfo,Userinfo} from '@/uni_modules/uni-ai-x/types.uts';
|
||||
// 1. 定义左侧菜单区域下方的用户头像信息
|
||||
export const currentUser: Userinfo = reactive<Userinfo>({})
|
||||
// 这里以uni-id的用户信息绑定为例,你自己的系统如果不是基于 unicloud 的,请参考此方法修改
|
||||
// import {watch} from 'vue';
|
||||
// 导入 uni-id 中的用户信息
|
||||
// import {state as uniIdState} from '@/uni_modules/uni-id-pages-x/store.uts';
|
||||
// 监听用户信息变化,更新 currentUser 对象
|
||||
// watch((): UTSJSONObject => uniIdState.userInfo,(userInfo: UTSJSONObject)=>{
|
||||
// currentUser._id = userInfo.getString('_id')
|
||||
// currentUser.nickname = userInfo.getString('nickname')
|
||||
// // 根据 file_id 获取文件的 url
|
||||
// const avatarFileUrl = userInfo.getString('avatar_file.url')
|
||||
// if (avatarFileUrl != null) {
|
||||
// uniCloud.getTempFileURL({fileList: [avatarFileUrl]}).then((res) => {
|
||||
// currentUser.avatar_file = {url: res.fileList[0].tempFileURL}
|
||||
// })
|
||||
// } else {
|
||||
// currentUser.avatar_file = null
|
||||
// }
|
||||
// }, {deep: true, immediate: true})
|
||||
|
||||
// 2. 点击左侧菜单区域下方的用户头像信息后跳转的页面路径
|
||||
// export const userCenterPage = '/uni_modules/uni-id-pages-x/pages/userinfo/userinfo?showLoginManage=true'
|
||||
|
||||
// 3. 配置大语言模型
|
||||
/**
|
||||
* 配置大语言模型
|
||||
* 注意需要在,目录:`/uni_modules/uni-ai-x/static/ai-provider/${provider}.png` 中添加对应的logo图片
|
||||
* 默认以第一个为默认模型
|
||||
*/
|
||||
const bailianTokenInfo: BailianTokenInfo = {token: '', expireTime: 0}
|
||||
const llmModelMap = new Map<string, Provider>([
|
||||
// 阿里云百炼
|
||||
[
|
||||
'bailian', {
|
||||
models: [
|
||||
{ name: 'deepseek-v3'},
|
||||
{ name: 'deepseek-r1', thinking: true },
|
||||
{ name: 'qwen-turbo'},
|
||||
{ name: 'qwen-plus', thinking: true }
|
||||
],
|
||||
description: '阿里云百炼 - 专注AI技术研究和应用',
|
||||
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
async getToken(): Promise<string> {
|
||||
// 如果token存在且未过期,则直接返回
|
||||
if (bailianTokenInfo.expireTime > Date.now()) {
|
||||
return bailianTokenInfo.token
|
||||
}
|
||||
|
||||
// unicloud项目 通过uni-ai-x-co 获取临时token
|
||||
const uniAiChatCo = uniCloud.importObject("uni-ai-x-co", { customUI: true })
|
||||
try {
|
||||
const getTmpTokenRes = await uniAiChatCo.getBailianTmpToken()
|
||||
if (getTmpTokenRes.errCode != 0) {
|
||||
throw new Error('获取临时token失败')
|
||||
}
|
||||
const tmpToken = getTmpTokenRes.getString('data.token', '')
|
||||
const expiresAt = getTmpTokenRes.getNumber('data.expiresAt', 0)
|
||||
bailianTokenInfo.token = tmpToken
|
||||
bailianTokenInfo.expireTime = expiresAt
|
||||
return tmpToken
|
||||
} catch (e: any) {
|
||||
// const error = JSON.parse<UniCloudError>(JSON.stringify(e)) as UniCloudError
|
||||
// console.error('uni-ai-x-co 请求失败', error, error.message)
|
||||
throw new Error('获取临时token失败')
|
||||
}
|
||||
|
||||
|
||||
// 非 unicloud项目 请求自己的服务器的接口得到临时 token 例如
|
||||
/*
|
||||
const res = await uni.request({
|
||||
url: 'https://your-server.com/get-token',
|
||||
method: 'GET'
|
||||
})
|
||||
if (res.statusCode != 200) {
|
||||
throw new Error('获取临时token失败')
|
||||
}
|
||||
const tmpToken = res.getString('data.token', '')
|
||||
const expiresAt = res.getNumber('data.expiresAt', 0)
|
||||
bailianTokenInfo.token = tmpToken
|
||||
bailianTokenInfo.expireTime = expiresAt
|
||||
return tmpToken
|
||||
*/
|
||||
}
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const providerNameList: string[] = []
|
||||
llmModelMap.forEach((_, key: string) => {
|
||||
providerNameList.push(key)
|
||||
})
|
||||
|
||||
if (providerNameList.length < 1) {
|
||||
throw new Error('至少需要配置一个大语言模型提供商')
|
||||
}
|
||||
const defaultLLM: DefaultLLM = {
|
||||
provider: providerNameList[0]!,
|
||||
model: llmModelMap.get(providerNameList[0])?.models?.[0].name!
|
||||
}
|
||||
|
||||
export default llmModelMap
|
||||
export { llmModelMap, type LLMModel, providerNameList, defaultLLM }
|
||||
106
qiming-mobile/uni_modules/uni-ai-x/package.json
Normal file
106
qiming-mobile/uni_modules/uni-ai-x/package.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "uni-ai-x",
|
||||
"displayName": "uni-ai x",
|
||||
"version": "1.0.7",
|
||||
"description": "一个开源的、全平台的、原生的、云端一体的ai聊天套件",
|
||||
"keywords": [
|
||||
"uni-ai-x",
|
||||
"uni-ai-chat",
|
||||
"ai",
|
||||
"deepseek"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ai-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "unicloud-template-page",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "√"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "-",
|
||||
"vue3": "-"
|
||||
},
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"vue": "-",
|
||||
"nvue": "-",
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
qiming-mobile/uni_modules/uni-ai-x/sdk/index.uts
Normal file
4
qiming-mobile/uni_modules/uni-ai-x/sdk/index.uts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 只导出类型定义,不导出完整 SDK 功能
|
||||
// 项目只使用了 uni-ai-x 的类型定义,完整功能已移除以减小包体积
|
||||
import { MsgItem, ChatItem, BaseMsgItem, Userinfo, markdownElItem } from '@/uni_modules/uni-ai-x/types.uts';
|
||||
export { MsgItem, ChatItem, BaseMsgItem, Userinfo, markdownElItem }
|
||||
BIN
qiming-mobile/uni_modules/uni-ai-x/static/font/iconfont.ttf
Normal file
BIN
qiming-mobile/uni_modules/uni-ai-x/static/font/iconfont.ttf
Normal file
Binary file not shown.
154
qiming-mobile/uni_modules/uni-ai-x/types.uts
Normal file
154
qiming-mobile/uni_modules/uni-ai-x/types.uts
Normal file
@@ -0,0 +1,154 @@
|
||||
// 定义简化的 MarkdownToken 类型,不依赖 kux-marked
|
||||
export type MarkdownToken = {
|
||||
type: string,
|
||||
raw?: string,
|
||||
text?: string | null,
|
||||
href?: string | null,
|
||||
title?: string | null,
|
||||
depth?: number,
|
||||
lang?: string | null,
|
||||
tokens?: MarkdownToken[] | null,
|
||||
ordered?: boolean,
|
||||
start?: number | null,
|
||||
loose?: boolean,
|
||||
items?: MarkdownToken[],
|
||||
task?: boolean,
|
||||
checked?: boolean | null,
|
||||
codeBlockStyle?: string | null,
|
||||
escaped?: boolean,
|
||||
[key: string]: any // 允许其他属性
|
||||
}
|
||||
|
||||
export type LLMModel = {
|
||||
name: string,
|
||||
thinking?: boolean,
|
||||
webSearch?: boolean
|
||||
}
|
||||
|
||||
export type Provider = {
|
||||
models: LLMModel[],
|
||||
description?: string,
|
||||
baseURL?: string,
|
||||
getToken?: () => Promise<string>,
|
||||
}
|
||||
|
||||
export type DefaultLLM = {
|
||||
provider: string,
|
||||
model: string
|
||||
}
|
||||
|
||||
export type BailianTokenInfo = {
|
||||
token: string,
|
||||
expireTime: number
|
||||
}
|
||||
|
||||
|
||||
export type Llm = {
|
||||
provider: string
|
||||
model: string
|
||||
webSearch: boolean
|
||||
}
|
||||
|
||||
// 对话
|
||||
export type Chat = {
|
||||
datas: ChatItem[],
|
||||
activeId: string
|
||||
}
|
||||
|
||||
// 对话
|
||||
export type ChatItem = {
|
||||
id: string,
|
||||
title: string,
|
||||
state: string,
|
||||
inputContent: string,
|
||||
msgList: MsgItem[],
|
||||
update_time: number
|
||||
}
|
||||
|
||||
// 流式响应分块
|
||||
type ChunkChoiceDelta = {
|
||||
content: string,
|
||||
reasoning_content?: string
|
||||
}
|
||||
type ChunkMessage = {
|
||||
content: string
|
||||
}
|
||||
type ChunkChoice = {
|
||||
delta?: ChunkChoiceDelta
|
||||
message?: ChunkMessage
|
||||
}
|
||||
type Output = {
|
||||
text: string
|
||||
}
|
||||
export type Chunk = {
|
||||
choices?: ChunkChoice[],
|
||||
output?: Output,
|
||||
DONE?: boolean,
|
||||
}
|
||||
|
||||
export type markdownElItem = {
|
||||
uniqueId: string,
|
||||
type: string,
|
||||
datasList: MarkdownToken[][]
|
||||
}
|
||||
// 消息
|
||||
export type MsgItem = {
|
||||
_id: string
|
||||
body: string
|
||||
thinkContent?: string
|
||||
from_uid: string
|
||||
state: number
|
||||
create_time: number
|
||||
// 如果是ai回复的,则需要关联的msg_id
|
||||
about_msg_id?: string
|
||||
// 所属对话id
|
||||
chat_id: string,
|
||||
markdownElList: markdownElItem[]
|
||||
error_msg?: string
|
||||
rendered: boolean
|
||||
// 自定义组件处理列表(用于历史消息中的工具调用等)
|
||||
processingList?: any[]
|
||||
}
|
||||
export type BaseMsgItem = {
|
||||
body: string
|
||||
from_uid: string
|
||||
about_msg_id?: string
|
||||
}
|
||||
|
||||
// 消息对象,含:消息列表、是否还有更多消息、是否正在加载更多消息、加载更多消息方法
|
||||
export type Msg = {
|
||||
dataList: MsgItem[],
|
||||
hasMore: boolean,
|
||||
loading: boolean,
|
||||
loadMore: () => void
|
||||
}
|
||||
|
||||
// 请求ai对话消息
|
||||
export type RequestAiCoMessage = {
|
||||
// 来自谁:用户或ai
|
||||
role : "user" | "assistant"
|
||||
// 内容
|
||||
content : string
|
||||
}
|
||||
|
||||
// 用户
|
||||
export type Userinfo = {
|
||||
_id?: string
|
||||
nickname?: string
|
||||
avatar_file?: AvatarFile
|
||||
}
|
||||
|
||||
export type AvatarFile = {
|
||||
url?: string
|
||||
file_id?: string
|
||||
// 略...
|
||||
}
|
||||
|
||||
// 用户
|
||||
export type Theme = "auto" | "dark" | "light"
|
||||
export type Setting = {
|
||||
// 主题
|
||||
theme: Theme
|
||||
// 语言
|
||||
language: "zh-CN" | "en-US"
|
||||
}
|
||||
Reference in New Issue
Block a user