feat: 第四轮大版本更新 — 修复4个严重Bug、2个UI Bug,实现自主思考重构与主-子会话架构
## 🐛 Bug 修复 - 修复前端对话无响应:消除 ChatContainer 中的双重 WebSocket 连接,优化 sendMessage 失败提示 - 修复 Memory-Service 数据库迁移失败:ai-core 和 memory-service 均添加 ALTER TABLE ADD COLUMN IF NOT EXISTS 模式演化 - 修复语音/STT 不可用:添加 MediaRecorder API 降级方案,修复 whisper-cli 输出文件名错误 - 修复仪表盘数据库按钮失效:补充按钮 ID 属性,重写 controlDB() 控制逻辑 ## 🎨 UI 修复 - 修正用户消息头像位置:从 flex-row-reverse 改为 justify-end - 移除空聊天列表的 emoji 占位图标 ## ✨ 新功能 - devtools 新增 STT 处理日志面板(环形缓冲区 + WebSocket 广播 + 可视化表格) - 新增 ADMIN_NICKNAME 环境变量,支持自定义管理员昵称 ## 🔧 改进 - 注册流程增加昵称必填字段(前后端同步) ## 🏗️ 架构重构 - 重构自主思考逻辑:从定时器轮询改为事件驱动(对话后触发 + 静默检测),优化提示词使其更自然人性化 - 实现主-子会话架构:新增 4 种子会话类型(general/memory/iot/knowledge),意图分析 → 并行分发 → 结果合成流程 ## 📄 新增文档 - docs/architecture/main-session-sub-session-design.md — 子会话架构设计文档
This commit is contained in:
@@ -37,6 +37,7 @@ export default function App() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [verifyCode, setVerifyCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
@@ -166,7 +167,11 @@ export default function App() {
|
||||
setError('请输入邮箱');
|
||||
return;
|
||||
}
|
||||
const result = await register(username, password, email, verifyCode || '000000');
|
||||
if (!nickname) {
|
||||
setError('请输入昵称');
|
||||
return;
|
||||
}
|
||||
const result = await register(username, password, email, nickname, verifyCode || '000000');
|
||||
if (!result.success) {
|
||||
setError(result.error || '注册失败');
|
||||
} else {
|
||||
@@ -239,6 +244,16 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{authMode === 'register' && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="昵称 (昔涟会这样称呼你)"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400"
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
@@ -277,7 +292,7 @@ export default function App() {
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRegister}
|
||||
disabled={authLoading || !username || !password || !email}
|
||||
disabled={authLoading || !username || !password || !email || !nickname}
|
||||
className="w-full py-2.5 rounded-xl bg-pink-400 hover:bg-pink-500 disabled:bg-pink-300 text-white font-medium text-sm transition-colors"
|
||||
>
|
||||
{authLoading ? '请稍候...' : '注册并进入 ♪'}
|
||||
|
||||
@@ -98,10 +98,10 @@ export async function login(username: string, password: string): Promise<ApiResp
|
||||
return resp;
|
||||
}
|
||||
|
||||
export async function register(username: string, password: string, email: string, verifyCode: string): Promise<ApiResponse<AuthResponse>> {
|
||||
export async function register(username: string, password: string, email: string, nickname: string, verifyCode: string): Promise<ApiResponse<AuthResponse>> {
|
||||
const resp = await request<AuthResponse>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: { username, password, email, verify_code: verifyCode },
|
||||
body: { username, password, email, nickname, verify_code: verifyCode },
|
||||
auth: false,
|
||||
});
|
||||
if (resp.data?.token) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import { MessageList } from './MessageList';
|
||||
import { IoTStatusBar } from './IoTStatusBar';
|
||||
|
||||
export function ChatContainer() {
|
||||
const { messages, isTyping } = useChat();
|
||||
const messages = useChatStore((s) => s.messages);
|
||||
const isTyping = useChatStore((s) => s.isTyping);
|
||||
const continuousMode = useChatStore((s) => s.continuousMode);
|
||||
const backgroundThinkingStatus = useChatStore((s) => s.backgroundThinkingStatus);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) {
|
||||
const {
|
||||
isListening,
|
||||
isSupported,
|
||||
isFallbackMode,
|
||||
interimText,
|
||||
finalText,
|
||||
error,
|
||||
@@ -423,14 +424,14 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) {
|
||||
{/* 语音输入状态提示 */}
|
||||
{isListening && (
|
||||
<p className="text-xs text-red-400 text-center animate-pulse">
|
||||
🎤 正在聆听...
|
||||
{isFallbackMode ? '🎤 后端语音识别中...' : '🎤 正在聆听...'}
|
||||
<span className="text-gray-400 ml-2">(Ctrl+Shift+V 停止)</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{mode !== 'text' && !isListening && (
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
{mode === 'voice_msg' ? '语音消息功能即将上线 ♪' : ''}
|
||||
{mode === 'voice_msg' ? '点击麦克风按钮开始语音输入 ♪' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -154,7 +154,7 @@ export function MessageBubble({ role, content, timestamp, isStreaming, attachmen
|
||||
const imageAttachments = attachments?.filter((a) => a.type === 'image') ?? [];
|
||||
|
||||
return (
|
||||
<div className={`flex px-4 py-2 gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
|
||||
<div className={`flex px-4 py-2 gap-3 ${isUser ? 'justify-end' : ''}`}>
|
||||
{/* 头像 */}
|
||||
{!isUser && (
|
||||
<CyreneAvatar size="sm" className="flex-shrink-0 mt-1" />
|
||||
|
||||
@@ -19,12 +19,8 @@ export function MessageList({ messages, isTyping }: MessageListProps) {
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 p-8">
|
||||
<div className="text-6xl mb-4">🌸</div>
|
||||
<p className="text-lg font-medium text-pink-300 mb-2">
|
||||
昔涟在这里等你哦 ♪
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
无论是开心的事还是烦恼,都可以和人家说~
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||
开始一段新对话吧
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { transcribeAudio } from '@/api/voice';
|
||||
|
||||
/**
|
||||
* 浏览器 Speech Recognition API 的类型声明补充
|
||||
@@ -11,6 +12,8 @@ interface UseSpeechRecognitionReturn {
|
||||
interimText: string;
|
||||
finalText: string;
|
||||
error: string | null;
|
||||
/** 是否正在使用后端 STT 降级模式 */
|
||||
isFallbackMode: boolean;
|
||||
startListening: () => void;
|
||||
stopListening: () => void;
|
||||
resetText: () => void;
|
||||
@@ -30,7 +33,7 @@ const ERROR_MESSAGES: Record<RecognitionError, string> = {
|
||||
'no-speech': '未检测到语音,请再试一次',
|
||||
'aborted': '语音输入已中止',
|
||||
'audio-capture': '无法访问麦克风设备',
|
||||
'network': '网络错误,语音识别需要网络连接',
|
||||
'network': '网络错误,已切换到后端语音识别',
|
||||
'not-allowed': '麦克风权限被拒绝,请在浏览器设置中允许麦克风访问',
|
||||
'service-not-allowed': '语音识别服务不可用',
|
||||
'bad-grammar': '语法配置错误',
|
||||
@@ -46,16 +49,25 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
const [interimText, setInterimText] = useState('');
|
||||
const [finalText, setFinalText] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isFallbackMode, setIsFallbackMode] = useState(false);
|
||||
|
||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
const finalAccRef = useRef<string[]>([]);
|
||||
|
||||
// --- 后端 STT 降级:MediaRecorder ---
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioChunksRef = useRef<Blob[]>([]);
|
||||
const fallbackStreamRef = useRef<MediaStream | null>(null);
|
||||
|
||||
const SpeechRecognitionAPI =
|
||||
typeof window !== 'undefined'
|
||||
? window.SpeechRecognition || window.webkitSpeechRecognition
|
||||
: undefined;
|
||||
|
||||
const isSupported = SpeechRecognitionAPI != null;
|
||||
// 只要浏览器支持 MediaRecorder 就算语音输入可用
|
||||
const isSupported =
|
||||
typeof window !== 'undefined' &&
|
||||
(SpeechRecognitionAPI != null || typeof MediaRecorder !== 'undefined');
|
||||
|
||||
const resetText = useCallback(() => {
|
||||
setInterimText('');
|
||||
@@ -63,14 +75,35 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
finalAccRef.current = [];
|
||||
}, []);
|
||||
|
||||
const stopListening = useCallback(() => {
|
||||
// 清理浏览器 STT
|
||||
const cleanupBrowserSTT = useCallback(() => {
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.stop();
|
||||
recognitionRef.current.abort();
|
||||
recognitionRef.current = null;
|
||||
}
|
||||
setIsListening(false);
|
||||
}, []);
|
||||
|
||||
// 清理 MediaRecorder 降级
|
||||
const cleanupFallback = useCallback(() => {
|
||||
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
|
||||
mediaRecorderRef.current.stop();
|
||||
}
|
||||
mediaRecorderRef.current = null;
|
||||
if (fallbackStreamRef.current) {
|
||||
fallbackStreamRef.current.getTracks().forEach((t) => t.stop());
|
||||
fallbackStreamRef.current = null;
|
||||
}
|
||||
audioChunksRef.current = [];
|
||||
}, []);
|
||||
|
||||
const stopListening = useCallback(() => {
|
||||
cleanupBrowserSTT();
|
||||
cleanupFallback();
|
||||
setIsListening(false);
|
||||
setIsFallbackMode(false);
|
||||
setError(null);
|
||||
}, [cleanupBrowserSTT, cleanupFallback]);
|
||||
|
||||
// 自动静默超时:若 3 秒内无任何结果,自动停止
|
||||
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -83,22 +116,93 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
}, 3000);
|
||||
}, [stopListening]);
|
||||
|
||||
const startListening = useCallback(() => {
|
||||
if (!SpeechRecognitionAPI) {
|
||||
setError('浏览器不支持语音识别');
|
||||
return;
|
||||
}
|
||||
// --- 后端 STT 降级:启动 MediaRecorder 录音 ---
|
||||
const startFallbackRecognition = useCallback(async () => {
|
||||
try {
|
||||
setIsFallbackMode(true);
|
||||
setError(null);
|
||||
|
||||
// 如果已有实例则先停止
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.abort();
|
||||
recognitionRef.current = null;
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
fallbackStreamRef.current = stream;
|
||||
|
||||
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
||||
? 'audio/webm;codecs=opus'
|
||||
: MediaRecorder.isTypeSupported('audio/webm')
|
||||
? 'audio/webm'
|
||||
: 'audio/mp4';
|
||||
|
||||
const recorder = new MediaRecorder(stream, { mimeType });
|
||||
mediaRecorderRef.current = recorder;
|
||||
audioChunksRef.current = [];
|
||||
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
audioChunksRef.current.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: recorder.mimeType });
|
||||
audioChunksRef.current = [];
|
||||
|
||||
// 调用后端 STT
|
||||
const result = await transcribeAudio(audioBlob, 'zh');
|
||||
|
||||
if (result.error) {
|
||||
setError(`后端语音识别失败: ${result.error}`);
|
||||
setIsListening(false);
|
||||
setIsFallbackMode(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data?.text) {
|
||||
setFinalText(result.data.text);
|
||||
} else {
|
||||
setError('未识别到语音内容');
|
||||
}
|
||||
setIsListening(false);
|
||||
setIsFallbackMode(false);
|
||||
};
|
||||
|
||||
recorder.onerror = () => {
|
||||
setError('录音设备出错,请重试');
|
||||
setIsListening(false);
|
||||
setIsFallbackMode(false);
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
setIsListening(true);
|
||||
// 降级模式没有 interim results,所以超时设长一点
|
||||
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
||||
silenceTimerRef.current = setTimeout(() => {
|
||||
stopListening();
|
||||
}, 15000); // 15s 最大录音时长
|
||||
|
||||
} catch (err) {
|
||||
const msg = err instanceof DOMException && err.name === 'NotAllowedError'
|
||||
? '麦克风权限被拒绝'
|
||||
: err instanceof Error ? err.message : '无法启动录音';
|
||||
setError(msg);
|
||||
setIsFallbackMode(false);
|
||||
}
|
||||
}, [stopListening]);
|
||||
|
||||
const startListening = useCallback(() => {
|
||||
// 如果已有实例则先停止
|
||||
cleanupBrowserSTT();
|
||||
cleanupFallback();
|
||||
|
||||
setError(null);
|
||||
setInterimText('');
|
||||
setFinalText('');
|
||||
finalAccRef.current = [];
|
||||
setIsFallbackMode(false);
|
||||
|
||||
if (!SpeechRecognitionAPI) {
|
||||
// 浏览器不支持 SpeechRecognition,直接使用后端 STT
|
||||
startFallbackRecognition();
|
||||
return;
|
||||
}
|
||||
|
||||
const recognition = new SpeechRecognitionAPI();
|
||||
recognition.continuous = true;
|
||||
@@ -124,6 +228,14 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
};
|
||||
|
||||
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
||||
if (event.error === 'network') {
|
||||
// 网络错误 → 自动降级到后端 STT
|
||||
cleanupBrowserSTT();
|
||||
setError('浏览器语音识别网络不可达,正在切换到后端识别...');
|
||||
startFallbackRecognition();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = getRecognitionError(event.error);
|
||||
setError(message);
|
||||
|
||||
@@ -157,7 +269,7 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
|
||||
recognitionRef.current = recognition;
|
||||
recognition.start();
|
||||
}, [SpeechRecognitionAPI, resetSilenceTimer]);
|
||||
}, [SpeechRecognitionAPI, resetSilenceTimer, cleanupBrowserSTT, cleanupFallback, startFallbackRecognition]);
|
||||
|
||||
// cleanup: 组件卸载时停止识别
|
||||
useEffect(() => {
|
||||
@@ -165,12 +277,10 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
if (silenceTimerRef.current) {
|
||||
clearTimeout(silenceTimerRef.current);
|
||||
}
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.abort();
|
||||
recognitionRef.current = null;
|
||||
}
|
||||
cleanupBrowserSTT();
|
||||
cleanupFallback();
|
||||
};
|
||||
}, []);
|
||||
}, [cleanupBrowserSTT, cleanupFallback]);
|
||||
|
||||
return {
|
||||
isListening,
|
||||
@@ -178,6 +288,7 @@ export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
interimText,
|
||||
finalText,
|
||||
error,
|
||||
isFallbackMode,
|
||||
startListening,
|
||||
stopListening,
|
||||
resetText,
|
||||
|
||||
@@ -8,22 +8,30 @@ import type { WSClientMessage, WSServerMessage } from '@/types/chat';
|
||||
const WS_BASE_URL =
|
||||
import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws/chat';
|
||||
|
||||
let wsInstanceCounter = 0;
|
||||
|
||||
export function useWebSocket() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const shouldReconnectRef = useRef(true);
|
||||
const activeSessionRef = useRef<string | null>(null);
|
||||
const instanceIdRef = useRef(++wsInstanceCounter);
|
||||
|
||||
// 订阅 sessionStore 中的 currentSessionId 变化
|
||||
const currentSessionId = useSessionStore((s) => s.currentSessionId);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
const instanceId = instanceIdRef.current;
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
if (!token) {
|
||||
console.warn(`[WS#${instanceId}] connect: 无 token,跳过连接`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭旧连接
|
||||
if (wsRef.current) {
|
||||
console.log(`[WS#${instanceId}] 关闭旧连接`);
|
||||
shouldReconnectRef.current = false;
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
@@ -34,12 +42,13 @@ export function useWebSocket() {
|
||||
? `${WS_BASE_URL}?token=${token}&session_id=${sessionID}`
|
||||
: `${WS_BASE_URL}?token=${token}`;
|
||||
|
||||
console.log(`[WS#${instanceId}] 正在连接, session_id=${sessionID || '(无)'}`);
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
setIsConnected(true);
|
||||
shouldReconnectRef.current = true;
|
||||
console.log('[WS] 已连接, session_id:', sessionID);
|
||||
console.log(`[WS#${instanceId}] 已连接, session_id:`, sessionID);
|
||||
|
||||
// 连接后发送会话恢复消息,恢复后端上下文
|
||||
const sid = useSessionStore.getState().currentSessionId;
|
||||
@@ -50,15 +59,15 @@ export function useWebSocket() {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
ws.send(JSON.stringify(resumeMsg));
|
||||
console.log('[WS] 已发送会话恢复请求, session_id:', sid);
|
||||
console.log(`[WS#${instanceId}] 已发送会话恢复请求, session_id:`, sid);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setIsConnected(false);
|
||||
console.log('[WS] 已断开');
|
||||
console.log(`[WS#${instanceId}] 已断开`);
|
||||
if (shouldReconnectRef.current) {
|
||||
console.log('[WS] 3秒后重连...');
|
||||
console.log(`[WS#${instanceId}] 3秒后重连...`);
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
}
|
||||
@@ -67,7 +76,7 @@ export function useWebSocket() {
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('[WS] 连接错误:', err);
|
||||
console.error(`[WS#${instanceId}] 连接错误:`, err);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -75,7 +84,7 @@ export function useWebSocket() {
|
||||
const msg: WSServerMessage = JSON.parse(event.data);
|
||||
handleServerMessage(msg);
|
||||
} catch (err) {
|
||||
console.error('[WS] 消息解析失败:', err);
|
||||
console.error(`[WS#${instanceId}] 消息解析失败:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,6 +107,7 @@ export function useWebSocket() {
|
||||
}, [connect, currentSessionId]);
|
||||
|
||||
const sendMessage = useCallback((msg: WSClientMessage) => {
|
||||
const instanceId = instanceIdRef.current;
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
const sessionID = useSessionStore.getState().currentSessionId;
|
||||
wsRef.current.send(
|
||||
@@ -106,6 +116,24 @@ export function useWebSocket() {
|
||||
session_id: msg.session_id || sessionID || undefined,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const stateLabels = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
||||
const state = wsRef.current
|
||||
? (stateLabels[wsRef.current.readyState] || `UNKNOWN(${wsRef.current.readyState})`)
|
||||
: 'NO_SOCKET';
|
||||
console.error(
|
||||
`[WS#${instanceId}] sendMessage 失败: WebSocket 未就绪 (readyState=${state}),消息被丢弃:`,
|
||||
msg.type,
|
||||
msg.content?.slice(0, 50)
|
||||
);
|
||||
// 通知用户连接已断开
|
||||
useChatStore.getState().setTyping(false);
|
||||
useChatStore.getState().addMessage({
|
||||
id: 'err_' + Date.now(),
|
||||
role: 'system',
|
||||
content: '⚠️ 连接未建立,请刷新页面后重试',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -228,6 +256,13 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
case 'error':
|
||||
console.error('[WS] 服务端错误:', msg.error);
|
||||
setTyping(false);
|
||||
// 在聊天界面显示错误提示,让用户知情
|
||||
addMessage({
|
||||
id: 'err_' + Date.now(),
|
||||
role: 'system',
|
||||
content: '❌ 服务端错误: ' + (msg.error || '未知错误'),
|
||||
timestamp: msg.timestamp || Date.now(),
|
||||
});
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
|
||||
@@ -11,7 +11,7 @@ interface AuthStore {
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
login: (username: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
||||
register: (username: string, password: string, email: string, verifyCode: string) => Promise<{ success: boolean; error?: string }>;
|
||||
register: (username: string, password: string, email: string, nickname: string, verifyCode: string) => Promise<{ success: boolean; error?: string }>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
@@ -42,14 +42,18 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username: string, password: string, email: string, verifyCode: string) => {
|
||||
register: async (username: string, password: string, email: string, nickname: string, verifyCode: string) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const resp = await apiRegister(username, password, email, verifyCode);
|
||||
const resp = await apiRegister(username, password, email, nickname, verifyCode);
|
||||
if (resp.error) {
|
||||
set({ loading: false });
|
||||
return { success: false, error: resp.error };
|
||||
}
|
||||
// 保存昵称到 localStorage
|
||||
if (resp.data?.nickname) {
|
||||
localStorage.setItem('user_nickname', resp.data.nickname);
|
||||
}
|
||||
set({
|
||||
isLoggedIn: true,
|
||||
userId: resp.data?.user_id || null,
|
||||
@@ -65,6 +69,7 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
|
||||
logout: () => {
|
||||
clearToken();
|
||||
localStorage.removeItem('user_nickname');
|
||||
set({
|
||||
isLoggedIn: false,
|
||||
userId: null,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface AuthResponse {
|
||||
user_id: string;
|
||||
token: string;
|
||||
expires: number;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface LoginParams {
|
||||
@@ -53,5 +54,6 @@ export interface RegisterParams {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
nickname: string;
|
||||
verify_code: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user