feat: chat message ui (#3275)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run

This commit is contained in:
shaohuzhang1 2025-06-17 11:58:35 +08:00 committed by GitHub
parent 6fc0558f63
commit 00e486c3fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 269 additions and 143 deletions

View File

@ -315,7 +315,7 @@ class ChatSerializers(serializers.Serializer):
class OpenChatSerializers(serializers.Serializer):
workspace_id = serializers.CharField(required=True)
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
application_id = serializers.UUIDField(required=True)
chat_user_id = serializers.CharField(required=True, label=_("Client id"))
chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
@ -325,7 +325,10 @@ class OpenChatSerializers(serializers.Serializer):
super().is_valid(raise_exception=True)
workspace_id = self.data.get('workspace_id')
application_id = self.data.get('application_id')
if not QuerySet(Application).filter(id=application_id, workspace_id=workspace_id).exists():
query_set = QuerySet(Application).filter(id=application_id)
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
raise AppApiException(500, gettext('Application does not exist'))
def open(self):

View File

@ -126,6 +126,8 @@ class ApplicationProfileSerializer(serializers.Serializer):
'user_avatar': application_setting.user_avatar,
'show_user_avatar': application_setting.show_user_avatar,
'float_location': application_setting.float_location}
base_node = [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
node.get('id') == 'base-node']
return {**ApplicationSerializerModel(application).data,
'stt_model_id': application.stt_model_id,
'tts_model_id': application.tts_model_id,
@ -136,8 +138,7 @@ class ApplicationProfileSerializer(serializers.Serializer):
'stt_autosend': application.stt_autosend,
'file_upload_enable': application.file_upload_enable,
'file_upload_setting': application.file_upload_setting,
'work_flow': {'nodes': [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
node.get('id') == 'base-node']},
'work_flow': {'nodes': base_node} if base_node else None,
'show_source': application_access_token.show_source,
'language': application_access_token.language,
**application_setting_dict}

View File

@ -5,10 +5,10 @@ from . import views
app_name = 'chat'
urlpatterns = [
path('chat/embed', views.ChatEmbedView.as_view()),
path('application/anonymous_authentication', views.AnonymousAuthentication.as_view()),
path('auth/profile', views.AuthProfile.as_view()),
path('profile', views.ApplicationProfile.as_view()),
path('embed', views.ChatEmbedView.as_view()),
path('auth/anonymous', views.AnonymousAuthentication.as_view()),
path('profile', views.AuthProfile.as_view()),
path('application/profile', views.ApplicationProfile.as_view()),
path('chat_message/<str:chat_id>', views.ChatView.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/open', views.OpenView.as_view())
path('open', views.OpenView.as_view())
]

View File

@ -100,7 +100,8 @@ class ChatView(APIView):
return ChatSerializers(data={'chat_id': chat_id,
'chat_user_id': request.auth.chat_user_id,
'chat_user_type': request.auth.chat_user_type,
'application_id': request.auth.application_id}
'application_id': request.auth.application_id,
'debug': False}
).chat(request.data)
@ -116,7 +117,8 @@ class OpenView(APIView):
responses=None,
tags=[_('Chat')] # type: ignore
)
def get(self, request: Request, workspace_id: str, application_id: str):
def get(self, request: Request):
return result.success(OpenChatSerializers(
data={'workspace_id': workspace_id, 'application_id': application_id,
'chat_user_id': request.auth.chat_user_id, 'chat_user_type': request.auth.chat_user_type}).open())
data={'application_id': request.auth.application_id,
'chat_user_id': request.auth.chat_user_id, 'chat_user_type': request.auth.chat_user_type,
'debug': False}).open())

View File

@ -9,7 +9,7 @@ import {
download,
exportFile,
} from '@/request/chat/index'
import { type ChatProfile } from '@/api/type/chat'
import { type Ref } from 'vue'
import useStore from '@/stores'
@ -27,11 +27,9 @@ Object.defineProperty(prefix, 'value', {
* @param loading
* @returns
*/
const open: (application_id: string, loading?: Ref<boolean>) => Promise<Result<string>> = (
application_id,
loading,
) => {
return get(`${prefix.value}/${application_id}/open`, {}, loading)
const open: (loading?: Ref<boolean>) => Promise<Result<string>> = (loading) => {
return get('/open', {}, loading)
}
/**
*
@ -40,22 +38,38 @@ const open: (application_id: string, loading?: Ref<boolean>) => Promise<Result<s
* data
*/
const chat: (chat_id: string, data: any) => Promise<any> = (chat_id, data) => {
return postStream(`/api/chat_message/${chat_id}`, data)
return postStream(`/chat/api/chat_message/${chat_id}`, data)
}
const chatProfile: (assessToken: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
const chatProfile: (assessToken: string, loading?: Ref<boolean>) => Promise<Result<ChatProfile>> = (
assessToken,
loading,
) => {
return get('/auth/profile', { access_token: assessToken }, loading)
return get('/profile', { access_token: assessToken }, loading)
}
const applicationProfile: (assessToken: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
assessToken,
loading,
) => {
return get('/chat/api/profile')
/**
*
* @param assessToken
* @param loading
* @returns
*/
const anonymousAuthentication: (
assessToken: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (assessToken, loading) => {
return post('/auth/anonymous', { access_token: assessToken }, {}, loading)
}
/**
*
* @param loading
* @returns
*/
const applicationProfile: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) => {
return get('/application/profile', {}, loading)
}
export default {
open,
chat,
chatProfile,
anonymousAuthentication,
applicationProfile,
}

15
ui/src/api/type/chat.ts Normal file
View File

@ -0,0 +1,15 @@
interface ChatProfile {
// 是否开启认证
authentication: boolean
// icon
icon?: string
// 应用名称
application_name?: string
// 背景图
bg_icon?: string
// 认证类型
authentication_type?: 'password' | 'login'
// 登录类型
login_value?: Array<string>
}
export { type ChatProfile }

View File

@ -85,9 +85,10 @@
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
import { type Ref, ref, nextTick, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application/application'
import chatAPI from '@/api/chat/chat'
import chatLogApi from '@/api/application/chat-log'
import { ChatManagement, type chatType } from '@/api/type/application'
import { randomId } from '@/utils/utils'
@ -280,23 +281,35 @@ const handleDebounceClick = debounce((val, other_params_data?: any, chat?: chatT
*/
const openChatId: () => Promise<string> = () => {
const obj = props.applicationDetails
return getOpenChatAPI()(obj.id)
.then((res) => {
chartOpenId.value = res.data
return res.data
})
.catch((res) => {
if (res.response.status === 403) {
return application.asyncAppAuthentication(accessToken).then(() => {
return openChatId()
})
}
return Promise.reject(res)
})
}
const getChatMessageAPI = () => {
if (props.type === 'debug-ai-chat') {
return applicationApi
.open(obj.id)
.then((res) => {
chartOpenId.value = res.data
return res.data
})
.catch((res) => {
if (res.response.status === 403) {
return application.asyncAppAuthentication(accessToken).then(() => {
return openChatId()
})
}
return Promise.reject(res)
})
return applicationApi.chat
} else {
return Promise.reject('暂不支持')
return chatAPI.chat
}
}
const getOpenChatAPI = () => {
if (props.type === 'debug-ai-chat') {
return applicationApi.open
} else {
return (a?: string, loading?: Ref<boolean>) => {
return chatAPI.open(loading)
}
}
}
/**
@ -453,8 +466,7 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_para
},
}
//
applicationApi
.chat(chartOpenId.value, obj)
getChatMessageAPI()(chartOpenId.value, obj)
.then((response) => {
if (response.status === 401) {
application

View File

@ -23,10 +23,10 @@ instance.interceptors.request.use(
if (config.headers === undefined) {
config.headers = new AxiosHeaders()
}
const { user, login } = useStore()
const token = login.getToken()
const language = user.getLanguage()
config.headers['Accept-Language'] = `${language}`
const { chatUser } = useStore()
const token = chatUser.getToken()
// const language = chatUser.getLanguage()
// config.headers['Accept-Language'] = `${language}`
if (token) {
config.headers['AUTHORIZATION'] = `Bearer ${token}`
}
@ -203,14 +203,14 @@ export const postStream: (url: string, data?: unknown) => Promise<Result<any> |
url,
data,
) => {
const { user, login } = useStore()
const token = login.getToken()
const language = user.getLanguage()
const { chatUser } = useStore()
const token = chatUser.getToken()
// const language = user.getLanguage()
const headers: HeadersInit = { 'Content-Type': 'application/json' }
if (token) {
headers['AUTHORIZATION'] = `Bearer ${token}`
}
headers['Accept-Language'] = `${language}`
// headers['Accept-Language'] = `${language}`
return fetch(url, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,

View File

@ -20,26 +20,39 @@ const router = createRouter({
router.beforeEach(
async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
NProgress.start()
if (to.name === '404') {
if (to.path === '/404') {
next()
return
}
const { user, login } = useStore()
const notAuthRouteNameList = ['login', 'ForgotPassword', 'ResetPassword', 'Chat', 'UserLogin']
const { chatUser } = useStore()
const notAuthRouteNameList = ['UserLogin']
if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) {
if (to.query && to.query.token) {
localStorage.setItem('token', to.query.token.toString())
}
const token = login.getToken()
if (!token) {
if (to.params && to.params.accessToken) {
chatUser.setAccessToken(to.params.accessToken.toString())
} else {
next({
path: '/login',
path: '/404',
})
return
}
if (!user.userInfo) {
await user.profile()
const token = chatUser.getToken()
const authentication = await chatUser.isAuthentication()
if (authentication) {
if (!token) {
next({
name: 'UserLogin',
params: {
accessToken: to.params.accessToken,
},
})
return
}
} else {
await chatUser.anonymousAuthentication()
}
if (!chatUser.application) {
await chatUser.applicationProfile()
}
}
// 判断是否有菜单权限

View File

@ -4,13 +4,19 @@ export const routes: Array<RouteRecordRaw> = [
// 对话
{
path: '/:accessToken',
name: 'Chat',
name: 'chat',
component: () => import('@/views/chat/index.vue'),
},
// 对话用户登录
{
path: '/user-login/:accessToken',
name: 'UserLogin',
name: 'user_login',
component: () => import('@/views/chat/user-login/index.vue'),
},
// 对话用户登录
{
path: '/404',
name: '404',
component: () => import('@/views/404/index.vue'),
},
]

View File

@ -11,6 +11,7 @@ import useParagraphStore from './modules/paragraph'
import useDocumentStore from './modules/document'
import useApplicationStore from './modules/application'
import useChatLogStore from './modules/chat-log'
import useChatUserStore from './modules/chat-user'
const useStore = () => ({
common: useCommonStore(),
login: useLoginStore(),
@ -25,6 +26,7 @@ const useStore = () => ({
document: useDocumentStore(),
application: useApplicationStore(),
chatLog: useChatLogStore(),
chatUser: useChatUserStore(),
})
export default useStore

View File

@ -0,0 +1,74 @@
import { defineStore } from 'pinia'
import ChatAPI from '@/api/chat/chat'
import { type ChatProfile } from '@/api/type/chat'
interface ChatUser {
// 用户id
id: string
}
interface Application {}
interface Chat {
chat_profile?: ChatProfile
application?: Application
token?: string
accessToken?: string
}
const useChatUserStore = defineStore('chat-user', {
state: (): Chat => ({
chat_profile: undefined,
application: undefined,
accessToken: undefined,
}),
actions: {
setAccessToken(accessToken: string) {
this.accessToken = accessToken
},
getChatProfile() {
return ChatAPI.chatProfile(this.accessToken as string).then((ok) => {
this.chat_profile = ok.data
return this.chat_profile
})
},
applicationProfile() {
return ChatAPI.applicationProfile().then((ok) => {
this.application = ok.data
})
},
isAuthentication() {
if (this.chat_profile) {
return Promise.resolve(this.chat_profile.authentication)
} else {
return this.getChatProfile().then((ok) => {
return ok.authentication
})
}
},
getToken() {
if (this.token) {
return this.token
}
const token = sessionStorage.getItem(`${this.accessToken}-accessToken`)
if (token) {
this.token = token
return token
}
const local_token = localStorage.getItem(`${this.accessToken}-accessToken`)
if (local_token) {
this.token = local_token
return local_token
}
return localStorage.getItem(`accessToken`)
},
/**
*
*/
anonymousAuthentication() {
return ChatAPI.anonymousAuthentication(this.accessToken as string).then((ok) => {
this.token = ok.data
return this.token
})
},
},
})
export default useChatUserStore

View File

@ -0,0 +1,51 @@
<template>
<el-row class="not-found-container">
<el-col class="img" :xs="0" :sm="0" :md="12" :lg="12" :xl="12"> </el-col>
<el-col class="message-container" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="title">{{ $t('views.notFound.title') }}</div>
<div class="message">{{ $t('views.notFound.message') }}</div>
<!-- TODO 暂时不处理 -->
<!-- <div class="operate"><el-button type="primary" @click="router.push('/')">{{ $t('views.notFound.operate') }}</el-button></div> -->
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<style lang="scss" scoped>
.not-found-container {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
.img {
background-image: url('@/assets/404.png');
background-size: 100% 100%;
width: 50%;
height: 100%;
}
.message-container {
color: var(--app-text-color);
.title {
font-size: 50px;
font-weight: 500;
}
.message {
font-size: 20px;
margin: 30px 0 20px 0;
}
}
}
@media only screen and (max-width: 1000px) {
.not-found-container .message-container {
text-align: center;
}
}
</style>

View File

@ -1,35 +1,19 @@
<template>
<component
v-if="chat_show && init_data_end"
:applicationAvailable="applicationAvailable"
:is="currentTemplate"
:application_profile="application_profile"
:application_profile="chatUser.application"
:key="route.fullPath"
v-loading="loading"
/>
<Auth
v-else
:application_profile="application_profile"
:auth_type="application_profile.authentication_type"
v-model="is_auth"
:style="{
'--el-color-primary': application_profile?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(application_profile?.custom_theme?.theme_color, 0.1),
}"
></Auth>
</template>
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import useStore from '@/stores'
import Auth from '@/views/chat/auth/index.vue'
import { hexToRgba } from '@/utils/theme'
import { useI18n } from 'vue-i18n'
import { getBrowserLang } from '@/locales/index'
import ChatAPI from '@/api/chat/chat.ts'
const { locale } = useI18n({ useScope: 'global' })
const route = useRoute()
const { application, user } = useStore()
const { chatUser } = useStore()
const components: any = import.meta.glob('@/views/chat/**/index.vue', {
eager: true,
@ -39,71 +23,20 @@ const {
query: { mode },
params: { accessToken },
} = route as any
const is_auth = ref<boolean>(false)
const currentTemplate = computed(() => {
let modeName = ''
if (!mode || mode === 'pc') {
modeName = show_history.value || !user.isEnterprise() ? 'pc' : 'base'
modeName = 'pc'
} else {
modeName = mode
}
const name = `/src/views/chat/${modeName}/index.vue`
return components[name].default
})
/**
* 是否显示对话
*/
const chat_show = computed(() => {
if (init_data_end.value) {
if (!applicationAvailable.value) {
return true
}
if (application_profile.value) {
if (application_profile.value.authentication && is_auth.value) {
return true
} else if (!application_profile.value.authentication) {
return true
}
}
}
return false
})
const loading = ref(false)
const show_history = ref(false)
const application_profile = ref<any>({})
/**
* 初始化结束
*/
const init_data_end = ref<boolean>(false)
const applicationAvailable = ref<boolean>(true)
function getAppProfile() {
return application.asyncGetAppProfile(loading).then((res: any) => {
locale.value = res.data?.language || getBrowserLang()
show_history.value = res.data?.show_history
application_profile.value = res.data
})
}
function getAccessToken(token: string) {
return application.asyncAppAuthentication(token, loading).then(() => {
return getAppProfile()
})
}
const getChatProfile = () => {
return ChatAPI.chatProfile(accessToken).then((ok: any) => {
console.log(ok)
})
}
onBeforeMount(() => {
user.changeUserType(2, accessToken)
Promise.all([user.asyncGetProfile(), getChatProfile()])
.catch(() => {
applicationAvailable.value = false
})
.finally(() => (init_data_end.value = true))
})
</script>
<style lang="scss"></style>

View File

@ -13,6 +13,7 @@ const envDir = './env'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const ENV = loadEnv(mode, envDir)
console.log(ENV)
const prefix = process.env.VITE_DYNAMIC_PREFIX || ENV.VITE_BASE_PATH
const proxyConf: Record<string, string | ProxyOptions> = {}
proxyConf['/api'] = {
@ -25,10 +26,9 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
rewrite: (path: string) => path.replace(ENV.VITE_BASE_PATH, '/'),
}
proxyConf['/chat'] = {
proxyConf['/chat/api'] = {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
rewrite: (path: string) => path.replace(ENV.VITE_BASE_PATH, '/'),
}
proxyConf['/doc'] = {
target: 'http://127.0.0.1:8080',