@@ -35,7 +35,7 @@
class="role-btn"
@click="showRoleGallery"
aria-label="选择角色"
-:class="route.meta.title === '探索' && route.meta.title !== '对话' ? 'active' : ''"
+ :class="String(route.meta.title) === '探索' ? 'active' : ''"
>
@@ -163,7 +163,7 @@ import { userApi } from '@/api/modules/user'
import { conversationApi } from '@/api/modules/conversation'
import { isMobile } from '@/utils/isMobile'
import { removeToken } from '@/utils/token'
-import { ElMessage, ElMessageBox } from 'element-plus'
+import { ElInput, ElMessage, ElMessageBox } from 'element-plus'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// import type { ChatHistoryItem } from '@/types/common'
@@ -193,7 +193,7 @@ const userInfo = ref({
const searchText = ref('')
const searchInput = ref()
const userMenu = useTemplateRef('userMenu')
-const editInput = useTemplateRef('editInput')
+const editInput = useTemplateRef
[]>('editInput')
const showUserMenu = ref(false)
// 编辑相关状态
@@ -312,26 +312,15 @@ const handleChatAction = async (command: { action: string; chatId: string; title
}
}
-// 删除对话
-const deleteChat = async (conversationUuid: string, event: Event) => {
- event.stopPropagation() // 阻止事件冒泡
-
- try {
- await chatHistoryStore().deleteChatHistory(conversationUuid)
- } catch (error) {
- console.error('删除对话失败:', error)
- ElMessage.error('删除对话失败')
- }
-}
-
// 开始编辑标题
const startEditTitle = (conversationUuid: string, currentTitle: string) => {
editingChatId.value = conversationUuid
editingTitle.value = currentTitle
nextTick(() => {
- if (editInput.value) {
- editInput.value.focus()
- editInput.value.select()
+ const input = editInput.value?.[0]
+ if (input) {
+ input.focus()
+ input.select()
}
})
}
diff --git a/vocata-web/src/types/api.ts b/vocata-web/src/types/api.ts
index f044e0e..33062f4 100644
--- a/vocata-web/src/types/api.ts
+++ b/vocata-web/src/types/api.ts
@@ -34,7 +34,39 @@ export interface RegisterParams {
// 登录响应数据
export interface LoginResponse {
token: string,
- expiresIn: number
+ expiresIn: number,
+ user?: {
+ nickname?: string
+ }
+}
+
+export interface CreateCharacterRequest {
+ name: string
+ description: string
+ greeting: string
+ isPublic: boolean
+ persona: string
+ voiceId: string
+ avatarUrl: string
+}
+
+export interface CreateCharacterResponse {
+ id: string | number
+}
+
+export interface TtsVoiceOption {
+ name: string
+ [key: string]: unknown
+}
+
+export interface AiGenerateRoleRequest {
+ name: string
+ description: string
+ greeting: string
+}
+
+export interface AiGenerateRoleResponse {
+ persona: string
}
// 用户信息响应数据
@@ -108,6 +140,6 @@ export interface MessageResponse {
audioUrl: string | null,
llmModelId: string,
ttsVoiceId: string | null,
- metadata: Record,
+ metadata: Record,
createDate: string
}
diff --git a/vocata-web/src/types/common.ts b/vocata-web/src/types/common.ts
index 0ef20ed..80489e4 100644
--- a/vocata-web/src/types/common.ts
+++ b/vocata-web/src/types/common.ts
@@ -1,11 +1,11 @@
export interface roleInfo {
- "id"?: number,
+ "id": number,
"characterCode"?: string,
"name"?: string,
"description"?: string,
"greeting"?: string,
"avatarUrl"?: string,
- "tags"?: string,
+ "tags"?: string | string[],
"language"?: string,
"status"?: number,
"statusName"?: string,
@@ -42,10 +42,10 @@ export interface ChatMessage {
contentType?: number, // 1=文本, 2=语音, 3=图片, 4=音频
audioUrl?: string | null,
createDate?: string,
- metadata?: Record,
+ metadata?: Record,
// AI对话系统新增字段
isStreaming?: boolean, // 是否为流式显示中的消息
isRecognizing?: boolean, // 是否为语音识别中的消息
characterName?: string, // AI角色名称
confidence?: number // 语音识别置信度
-}
\ No newline at end of file
+}
diff --git a/vocata-web/src/utils/aiChat.ts b/vocata-web/src/utils/aiChat.ts
index e92e069..0a26962 100644
--- a/vocata-web/src/utils/aiChat.ts
+++ b/vocata-web/src/utils/aiChat.ts
@@ -5,10 +5,16 @@
import { getToken } from './token'
+type EventCallback = (data?: unknown) => void
+
+interface WindowWithWebkitAudio extends Window {
+ webkitAudioContext?: typeof AudioContext
+}
+
// WebSocket消息类型定义
-interface WebSocketMessage {
+export interface WebSocketMessage {
type: string
- [key: string]: any
+ [key: string]: unknown
}
interface STTResultMessage extends WebSocketMessage {
@@ -64,7 +70,7 @@ export class VocaTaWebSocketClient {
private conversationUuid: string
private reconnectAttempts = 0
private readonly maxReconnectAttempts = 5
- private callbacks: Map = new Map()
+ private callbacks: Map = new Map()
private manualClose = false
constructor(conversationUuid: string) {
@@ -135,7 +141,7 @@ export class VocaTaWebSocketClient {
const message: WebSocketMessage = JSON.parse(event.data)
console.log(`📨 收到消息:`, message)
this.emit('message', message)
- } catch (e) {
+ } catch {
console.error('❌ 解析消息失败:', event.data)
}
}
@@ -207,14 +213,14 @@ export class VocaTaWebSocketClient {
}
// 事件监听器
- on(event: string, callback: Function): void {
+ on(event: string, callback: (data: T) => void): void {
if (!this.callbacks.has(event)) {
this.callbacks.set(event, [])
}
- this.callbacks.get(event)?.push(callback)
+ this.callbacks.get(event)?.push(callback as EventCallback)
}
- private emit(event: string, data?: any): void {
+ private emit(event: string, data?: unknown): void {
const callbacks = this.callbacks.get(event)
if (callbacks) {
callbacks.forEach(callback => callback(data))
@@ -272,7 +278,7 @@ export class AudioManager {
private currentWsClient: VocaTaWebSocketClient | null = null
private stopRecordingPromise: Promise | null = null
private stopRecordingResolve?: () => void
- private stopRecordingReject?: (reason?: any) => void
+ private stopRecordingReject?: (reason?: unknown) => void
private playbackStateListener?: (isPlaying: boolean) => void
async initialize(): Promise {
@@ -298,7 +304,12 @@ export class AudioManager {
private async ensureAudioContext(): Promise {
if (!this.audioContext) {
console.log('🎵 延迟初始化音频上下文...')
- this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
+ const AudioContextConstructor =
+ window.AudioContext || (window as WindowWithWebkitAudio).webkitAudioContext
+ if (!AudioContextConstructor) {
+ throw new Error('当前浏览器不支持 AudioContext')
+ }
+ this.audioContext = new AudioContextConstructor()
// 检查音频上下文状态
if (this.audioContext.state === 'suspended') {
@@ -613,8 +624,6 @@ export class VocaTaAIChat {
private wsClient: VocaTaWebSocketClient | null = null
private audioManager: AudioManager
private isAudioCallActive = false
- private currentConversation: any = null
- private currentCharacter: any = null
private conversationUuid: string | null = null
private connectingPromise: Promise | null = null
@@ -623,7 +632,7 @@ export class VocaTaAIChat {
private currentSTTText = ''
// 回调函数
- private onMessageCallback?: (message: any) => void
+ private onMessageCallback?: (message: WebSocketMessage) => void
private onSTTResultCallback?: (text: string, isFinal: boolean) => void
private onLLMStreamCallback?: (text: string, isComplete: boolean, characterName?: string) => void
private onAudioPlayCallback?: (isPlaying: boolean) => void
@@ -684,8 +693,12 @@ export class VocaTaAIChat {
this.handleWebSocketMessage(message)
// 如果收到状态消息表示连接已建立,则resolve
- if (!connectionResolved && message.type === 'status' &&
- (message.message?.includes('连接已建立') || message.message?.includes('WebSocket连接已建立'))) {
+ const statusMessage = typeof message.message === 'string' ? message.message : ''
+ if (
+ !connectionResolved &&
+ message.type === 'status' &&
+ (statusMessage.includes('连接已建立') || statusMessage.includes('WebSocket连接已建立'))
+ ) {
console.log('🎉 收到服务器连接确认,连接完全建立')
connectionResolved = true
this.onConnectionStatusCallback?.('connected', 'WebSocket连接已建立')
@@ -707,12 +720,12 @@ export class VocaTaAIChat {
this.handleAudioData(audioBuffer)
})
- this.wsClient.on('error', (error: any) => {
+ this.wsClient.on('error', (error) => {
console.error('❌ WebSocket错误:', error)
this.onConnectionStatusCallback?.('error', 'WebSocket连接错误')
if (!connectionResolved) {
connectionResolved = true
- reject(error)
+ reject(error instanceof Error ? error : new Error('WebSocket连接错误'))
finalize()
}
})
@@ -909,7 +922,7 @@ export class VocaTaAIChat {
}
// 设置回调函数
- onMessage(callback: (message: any) => void): void {
+ onMessage(callback: (message: WebSocketMessage) => void): void {
this.onMessageCallback = callback
}
@@ -953,6 +966,10 @@ export class VocaTaAIChat {
return this.audioManager.playing
}
+ get voiceActive(): boolean {
+ return this.audioManager.recording
+ }
+
// 清理资源
destroy(): void {
console.log('🧹 清理AI对话系统资源')
diff --git a/vocata-web/src/views/ChatPage.vue b/vocata-web/src/views/ChatPage.vue
index d9921ab..83bf9fd 100644
--- a/vocata-web/src/views/ChatPage.vue
+++ b/vocata-web/src/views/ChatPage.vue
@@ -153,7 +153,7 @@ import { ElMessage } from 'element-plus'
import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { ChatMessage } from '@/types/common'
-import type { MessageResponse } from '@/types/api'
+import type { ConversationResponse, MessageResponse } from '@/types/api'
import { VocaTaAIChat } from '@/utils/aiChat'
import { getToken } from '@/utils/token'
@@ -166,7 +166,7 @@ const input = ref('')
const router = useRouter()
const route = useRoute()
const conversationUuid = computed(() => route.params.conversationUuid as string)
-const currentConversation = ref(null)
+const currentConversation = ref(null)
const userAvatar = ref('')
const userNickname = ref('')
@@ -345,7 +345,7 @@ const loadRecentMessages = async (limit: number = 20) => {
senderType: 2,
contentType: 1,
createDate: new Date().toISOString(),
- characterName: currentConversation.value.characterName,
+ characterName: currentConversation.value?.characterName || getCharacterName(),
metadata: { isGreeting: true }
}]
}
@@ -371,40 +371,6 @@ const loadRecentMessages = async (limit: number = 20) => {
}
}
-// 加载更多历史消息
-const loadMoreHistory = async (limit: number = 20) => {
- if (!conversationUuid.value || isLoadingMessages.value || !hasMoreHistory.value) return
-
- try {
- isLoadingMessages.value = true
- const res = await conversationApi.getHistoryMessages(
- conversationUuid.value,
- currentOffset.value,
- limit
- )
- if (res.code === 200) {
- if (res.data.length === 0) {
- hasMoreHistory.value = false
- return
- }
-
- const messages = convertMessagesToChatFormat(res.data)
- // 将历史消息添加到列表开头
- chats.value = [...messages.reverse(), ...chats.value]
- currentOffset.value += res.data.length
-
- if (res.data.length < limit) {
- hasMoreHistory.value = false
- }
- }
- } catch (error) {
- console.error('加载历史消息失败:', error)
- ElMessage.error('加载历史消息失败')
- } finally {
- isLoadingMessages.value = false
- }
-}
-
// 将后端消息转换为前端所需的格式
const convertMessagesToChatFormat = (messages: MessageResponse[]): ChatMessage[] => {
return messages.map(msg => ({
@@ -471,13 +437,11 @@ const loadConversationInfo = async () => {
const res = await conversationApi.getConversationList()
if (res.code === 200) {
// 从最新的对话列表中查找当前对话
- const conversation = res.data.find(
- (conv: any) => conv.conversationUuid === conversationUuid.value
- )
+ const conversation = res.data.find((conv) => conv.conversationUuid === conversationUuid.value)
if (!conversation) {
console.warn('⚠️ 在对话列表中找不到当前对话UUID:', conversationUuid.value)
- console.log('📋 可用的对话列表:', res.data.map((c: any) => ({
+ console.log('📋 可用的对话列表:', res.data.map((c) => ({
uuid: c.conversationUuid,
title: c.title,
characterName: c.characterName
@@ -574,7 +538,7 @@ const setupAIChatCallbacks = () => {
if (!aiChat.value) return
// 连接状态回调
- aiChat.value.onConnectionStatus((status, message) => {
+ aiChat.value.onConnectionStatus((status) => {
switch (status) {
case 'connected':
connectionStatus.value = '已连接到AI服务'
@@ -954,11 +918,6 @@ const scrollToBottomWithRetry = (maxRetries: number = 3) => {
})
}
-// 兼容旧的滚动函数
-const scrollToBottom = () => {
- scrollToBottomWithRetry()
-}
-
// VAD监控相关函数
const startVADMonitoring = () => {
if (vadCheckInterval.value) {
@@ -966,11 +925,7 @@ const startVADMonitoring = () => {
}
vadCheckInterval.value = window.setInterval(() => {
- // 检查aiChat的audioManager是否有VAD状态
- if (aiChat.value && (aiChat.value as any).audioManager) {
- const audioManager = (aiChat.value as any).audioManager
- vadActive.value = audioManager.voiceActive || false
- }
+ vadActive.value = aiChat.value?.voiceActive ?? false
}, 100) // 每100ms检查一次VAD状态
}
diff --git a/vocata-web/src/views/LoginPage.vue b/vocata-web/src/views/LoginPage.vue
index 570484a..253d380 100644
--- a/vocata-web/src/views/LoginPage.vue
+++ b/vocata-web/src/views/LoginPage.vue
@@ -44,7 +44,7 @@
-
+
({
})
const registerForm = ref({
- username: '',
+ nickname: '',
password: '',
confirmPassword: '',
email: '',
@@ -186,9 +186,9 @@ const handleLogin = async (): Promise => {
// 注册表单确认(发送验证码前)
const validateRegisterForm = (): boolean => {
- const { username, email, password, confirmPassword } = registerForm.value
+ const { nickname, email, password, confirmPassword } = registerForm.value
- if (!username.trim()) {
+ if (!nickname.trim()) {
ElMessage.error('请输入用户名')
return false
}
diff --git a/vocata-web/src/views/NewRole.vue b/vocata-web/src/views/NewRole.vue
index 562022d..7524fc2 100644
--- a/vocata-web/src/views/NewRole.vue
+++ b/vocata-web/src/views/NewRole.vue
@@ -66,6 +66,7 @@ import type { UploadProps } from 'element-plus'
import { getToken } from '@/utils/token'
import { roleApi } from '@/api/modules/role'
import { chatHistoryStore } from '@/store'
+import type { CreateCharacterRequest, TtsVoiceOption } from '@/types/api'
import { useRouter } from 'vue-router'
const isM = computed(() => isMobile())
@@ -75,7 +76,7 @@ const imageUrl = ref('')
// AI生成状态
const isGenerating = ref(false)
// 表单数据
-const form = ref({
+const form = ref({
name: '',
description: '',
greeting: '',
@@ -86,7 +87,7 @@ const form = ref({
})
// 音色
-const options = ref()
+const options = ref([])
const router = useRouter()
//获取音色
const getVoice = async () => {
@@ -96,7 +97,7 @@ const getVoice = async () => {
}
getVoice()
-const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response) => {
imageUrl.value = response.data.fileUrl
form.value.avatarUrl = response.data.fileUrl
}
@@ -364,4 +365,4 @@ const startConversation = async (characterId: string | number) => {
transform: none;
}
}
-
\ No newline at end of file
+
diff --git a/vocata-web/src/views/SearchRole.vue b/vocata-web/src/views/SearchRole.vue
index 0354c28..6e07aa8 100644
--- a/vocata-web/src/views/SearchRole.vue
+++ b/vocata-web/src/views/SearchRole.vue
@@ -154,7 +154,7 @@