feat: DevTools 数据库监看面板 + 隧道控制 + 多项 Bug 修复
**DevTools 新增功能 (Tasks 13-14):** - 首页仪表盘添加数据库实时监看卡片 (5端口状态 + 记忆数) - 侧边栏新增数据库面板,支持自动 5 秒刷新 - 数据库面板显示 PostgreSQL/Redis/Qdrant/MinIO/NATS 端口状态 - 隧道控制按钮 (启动/停止/重启/查看状态) - 新增 API 端点: GET /api/database/status, POST /api/tunnel/:action - 更新 docs/api-reference/ API 文档 **Bug 修复 (Task 15):** - 修复 pgrep -f 自匹配导致隧道状态误判 (添加 ^ssh 锚点) - devtools/src/index.js (dashboard + database/status) - scripts/tunnel.sh (is_tunnel_running + show_status) - 修复数据库面板缺少自动刷新定时器 - 修复侧边栏数据库徽章永远 display:none - 修复僵尸进程场景下按钮死锁问题 **其他改进:** - .gitignore 添加 backend/cmd, backend/iot-debug-service/main - 前端多项改进 (登录/注册/会话/流式动画等)
This commit is contained in:
+94
-15
@@ -6,12 +6,16 @@ import { useAuth } from '@/hooks/useAuth';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
|
||||
export default function App() {
|
||||
const { isLoggedIn, login, loading: authLoading } = useAuth();
|
||||
const { isLoggedIn, login, register, loading: authLoading } = useAuth();
|
||||
const { send } = useChat();
|
||||
|
||||
const [authMode, setAuthMode] = useState<'login' | 'register'>('login');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [verifyCode, setVerifyCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError('');
|
||||
@@ -21,7 +25,28 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// 登录页面 (开发阶段暂时禁用注册)
|
||||
const handleRegister = async () => {
|
||||
setError('');
|
||||
setSuccessMsg('');
|
||||
if (!email) {
|
||||
setError('请输入邮箱');
|
||||
return;
|
||||
}
|
||||
const result = await register(username, password, email, verifyCode || '000000');
|
||||
if (!result.success) {
|
||||
setError(result.error || '注册失败');
|
||||
} else {
|
||||
setSuccessMsg('注册成功!正在进入...');
|
||||
}
|
||||
};
|
||||
|
||||
const switchMode = (mode: 'login' | 'register') => {
|
||||
setAuthMode(mode);
|
||||
setError('');
|
||||
setSuccessMsg('');
|
||||
};
|
||||
|
||||
// 登录/注册页面
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#FFFAF5] dark:bg-[#1a1a2e] flex items-center justify-center p-4">
|
||||
@@ -35,10 +60,30 @@ export default function App() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 登录表单 */}
|
||||
{/* 登录/注册表单 */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-lg p-6 space-y-4 border border-pink-100 dark:border-pink-900">
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm font-medium text-pink-500">登录</span>
|
||||
{/* 模式切换 */}
|
||||
<div className="flex rounded-xl bg-pink-50 dark:bg-pink-900/20 p-1">
|
||||
<button
|
||||
onClick={() => switchMode('login')}
|
||||
className={`flex-1 py-1.5 text-sm rounded-lg font-medium transition-colors ${
|
||||
authMode === 'login'
|
||||
? 'bg-white dark:bg-gray-800 text-pink-500 shadow-sm'
|
||||
: 'text-gray-400 hover:text-pink-400'
|
||||
}`}
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
onClick={() => switchMode('register')}
|
||||
className={`flex-1 py-1.5 text-sm rounded-lg font-medium transition-colors ${
|
||||
authMode === 'register'
|
||||
? 'bg-white dark:bg-gray-800 text-pink-500 shadow-sm'
|
||||
: 'text-gray-400 hover:text-pink-400'
|
||||
}`}
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -46,33 +91,67 @@ export default function App() {
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())}
|
||||
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"
|
||||
/>
|
||||
|
||||
{authMode === 'register' && (
|
||||
<input
|
||||
type="email"
|
||||
placeholder="邮箱"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(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="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())}
|
||||
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"
|
||||
/>
|
||||
|
||||
{authMode === 'register' && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="验证码 (开发环境输入 000000)"
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value)}
|
||||
maxLength={6}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 text-center">{error}</p>
|
||||
)}
|
||||
{successMsg && (
|
||||
<p className="text-xs text-green-400 text-center">{successMsg}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
disabled={authLoading || !username || !password}
|
||||
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 ? '请稍候...' : '进入昔涟的世界 ♪'}
|
||||
</button>
|
||||
{authMode === 'login' ? (
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
disabled={authLoading || !username || !password}
|
||||
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 ? '请稍候...' : '进入昔涟的世界 ♪'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRegister}
|
||||
disabled={authLoading || !username || !password || !email}
|
||||
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 ? '请稍候...' : '注册并进入 ♪'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
开发阶段 · 管理员凭据: admin / cyrene-dev-admin
|
||||
{authMode === 'register' ? '开发阶段 · 验证码统一使用 000000' : '欢迎回来 ♪'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,10 +98,10 @@ export async function login(username: string, password: string): Promise<ApiResp
|
||||
return resp;
|
||||
}
|
||||
|
||||
export async function register(username: string, password: string): Promise<ApiResponse<AuthResponse>> {
|
||||
export async function register(username: string, password: string, email: string, verifyCode: string): Promise<ApiResponse<AuthResponse>> {
|
||||
const resp = await request<AuthResponse>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: { username, password },
|
||||
body: { username, password, email, verify_code: verifyCode },
|
||||
auth: false,
|
||||
});
|
||||
if (resp.data?.token) {
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
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 continuousMode = useChatStore((s) => s.continuousMode);
|
||||
const backgroundThinkingStatus = useChatStore((s) => s.backgroundThinkingStatus);
|
||||
|
||||
return <MessageList messages={messages} isTyping={isTyping} />;
|
||||
const thinkingLabel = backgroundThinkingStatus === 'thinking' ? '昔涟正在回忆中...' : '';
|
||||
const statusLabel = continuousMode ? '主对话 · 进行中' : '';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 状态指示器栏 */}
|
||||
<div className="flex items-center justify-between px-4 py-1.5 border-b border-pink-100 dark:border-pink-900 bg-pink-50/50 dark:bg-pink-950/20">
|
||||
<div className="flex items-center gap-2">
|
||||
{statusLabel && (
|
||||
<span className="text-xs font-medium text-pink-500 dark:text-pink-400 bg-pink-100 dark:bg-pink-900/50 px-2 py-0.5 rounded-full">
|
||||
{statusLabel}
|
||||
</span>
|
||||
)}
|
||||
{thinkingLabel && (
|
||||
<span className="text-xs text-pink-400 dark:text-pink-500 animate-pulse flex items-center gap-1">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-pink-400 rounded-full animate-bounce" />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-pink-400 rounded-full animate-bounce" style={{ animationDelay: '0.15s' }} />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-pink-400 rounded-full animate-bounce" style={{ animationDelay: '0.3s' }} />
|
||||
{thinkingLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{continuousMode && (
|
||||
<button
|
||||
onClick={() => useChatStore.getState().setContinuousMode(!continuousMode)}
|
||||
className="text-[10px] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
title="切换连续对话模式"
|
||||
>
|
||||
{continuousMode ? '🔗 连续对话' : '⏸️ 暂停中'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MessageList messages={messages} isTyping={isTyping} />
|
||||
</div>
|
||||
|
||||
{/* IoT 状态栏(底部) */}
|
||||
<IoTStatusBar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +1,6 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { login as apiLogin, register as apiRegister, clearToken, isAuthenticated, getToken } from '@/api/client';
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
userId: string | null;
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
}
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
/** 兼容旧代码的 Hook 导出 — 现在基于共享 Zustand store */
|
||||
export function useAuth() {
|
||||
const [state, setState] = useState<AuthState>({
|
||||
isLoggedIn: isAuthenticated(),
|
||||
userId: localStorage.getItem('user_id'),
|
||||
token: getToken(),
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
setState(prev => ({ ...prev, loading: true }));
|
||||
try {
|
||||
const resp = await apiLogin(username, password);
|
||||
if (resp.error) {
|
||||
return { success: false, error: resp.error };
|
||||
}
|
||||
setState({
|
||||
isLoggedIn: true,
|
||||
userId: resp.data?.user_id || null,
|
||||
token: resp.data?.token || null,
|
||||
loading: false,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
setState(prev => ({ ...prev, loading: false }));
|
||||
return { success: false, error: err instanceof Error ? err.message : '登录失败' };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (username: string, password: string) => {
|
||||
setState(prev => ({ ...prev, loading: true }));
|
||||
try {
|
||||
const resp = await apiRegister(username, password);
|
||||
if (resp.error) {
|
||||
return { success: false, error: resp.error };
|
||||
}
|
||||
setState({
|
||||
isLoggedIn: true,
|
||||
userId: resp.data?.user_id || null,
|
||||
token: resp.data?.token || null,
|
||||
loading: false,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
setState(prev => ({ ...prev, loading: false }));
|
||||
return { success: false, error: err instanceof Error ? err.message : '注册失败' };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearToken();
|
||||
setState({
|
||||
isLoggedIn: false,
|
||||
userId: null,
|
||||
token: null,
|
||||
loading: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
};
|
||||
return useAuthStore();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 认证状态管理 (Zustand Store)
|
||||
* 用于跨组件共享登录/退出状态
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { login as apiLogin, register as apiRegister, clearToken, isAuthenticated, getToken } from '@/api/client';
|
||||
|
||||
interface AuthStore {
|
||||
isLoggedIn: boolean;
|
||||
userId: string | null;
|
||||
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 }>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStore>((set) => ({
|
||||
isLoggedIn: isAuthenticated(),
|
||||
userId: localStorage.getItem('user_id'),
|
||||
token: getToken(),
|
||||
loading: false,
|
||||
|
||||
login: async (username: string, password: string) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const resp = await apiLogin(username, password);
|
||||
if (resp.error) {
|
||||
set({ loading: false });
|
||||
return { success: false, error: resp.error };
|
||||
}
|
||||
set({
|
||||
isLoggedIn: true,
|
||||
userId: resp.data?.user_id || null,
|
||||
token: resp.data?.token || null,
|
||||
loading: false,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
set({ loading: false });
|
||||
return { success: false, error: err instanceof Error ? err.message : '登录失败' };
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username: string, password: string, email: string, verifyCode: string) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const resp = await apiRegister(username, password, email, verifyCode);
|
||||
if (resp.error) {
|
||||
set({ loading: false });
|
||||
return { success: false, error: resp.error };
|
||||
}
|
||||
set({
|
||||
isLoggedIn: true,
|
||||
userId: resp.data?.user_id || null,
|
||||
token: resp.data?.token || null,
|
||||
loading: false,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
set({ loading: false });
|
||||
return { success: false, error: err instanceof Error ? err.message : '注册失败' };
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearToken();
|
||||
set({
|
||||
isLoggedIn: false,
|
||||
userId: null,
|
||||
token: null,
|
||||
loading: false,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
/** 兼容旧代码的 Hook 导出 */
|
||||
export function useAuth() {
|
||||
return useAuthStore();
|
||||
}
|
||||
@@ -6,6 +6,9 @@ export type MessageRole = 'user' | 'assistant' | 'system';
|
||||
/** 对话模式 */
|
||||
export type ChatMode = 'text' | 'voice_msg' | 'voice_assistant';
|
||||
|
||||
/** 后台思考状态 */
|
||||
export type BackgroundThinkingStatus = 'idle' | 'thinking' | 'done';
|
||||
|
||||
/** 语音片段 */
|
||||
export interface VoiceSegment {
|
||||
index: number;
|
||||
@@ -25,6 +28,32 @@ export interface Message {
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
/** IoT 设备类型定义 */
|
||||
export type IoTDeviceType = 'light' | 'ac' | 'curtain' | 'sensor' | 'lock';
|
||||
|
||||
/** IoT 设备 */
|
||||
export interface IoTDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
type: IoTDeviceType;
|
||||
status: string;
|
||||
brightness?: number;
|
||||
color?: string;
|
||||
temperature?: number;
|
||||
mode?: string;
|
||||
position?: number;
|
||||
value?: number;
|
||||
unit?: string;
|
||||
battery?: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
/** IoT 设备更新消息 */
|
||||
export interface IoTDeviceUpdate {
|
||||
devices: IoTDevice[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** WebSocket 客户端消息 */
|
||||
export interface WSClientMessage {
|
||||
type: 'message' | 'voice_input' | 'ping' | 'history';
|
||||
@@ -37,7 +66,7 @@ export interface WSClientMessage {
|
||||
|
||||
/** WebSocket 服务端消息 */
|
||||
export interface WSServerMessage {
|
||||
type: 'response' | 'segment' | 'audio' | 'error' | 'device_update' | 'pong' | 'history_response' | 'stream_chunk' | 'stream_end';
|
||||
type: 'response' | 'segment' | 'audio' | 'error' | 'device_update' | 'pong' | 'history_response' | 'stream_chunk' | 'stream_end' | 'background_thinking';
|
||||
message_id?: string;
|
||||
text?: string;
|
||||
content?: string;
|
||||
@@ -49,6 +78,8 @@ export interface WSServerMessage {
|
||||
tool_calls?: ToolCall[];
|
||||
error?: string;
|
||||
messages?: Message[];
|
||||
devices?: IoTDevice[];
|
||||
thinking_status?: BackgroundThinkingStatus;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -64,4 +95,6 @@ export interface ChatState {
|
||||
messages: Message[];
|
||||
isTyping: boolean;
|
||||
currentMode: ChatMode;
|
||||
continuousMode: boolean;
|
||||
backgroundThinkingStatus: BackgroundThinkingStatus;
|
||||
}
|
||||
|
||||
@@ -41,4 +41,6 @@ export interface LoginParams {
|
||||
export interface RegisterParams {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
verify_code: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user