feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
This commit is contained in:
@@ -14,7 +14,7 @@ export function ChatContainer() {
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden chat-background">
|
||||
{/* 状态指示器栏 */}
|
||||
<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 flex-shrink-0">
|
||||
<div className="relative z-10 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 flex-shrink-0">
|
||||
<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">
|
||||
@@ -42,12 +42,14 @@ export function ChatContainer() {
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<div className="relative z-10 flex-1 min-h-0 overflow-hidden">
|
||||
<MessageList messages={messages} isTyping={isTyping} />
|
||||
</div>
|
||||
|
||||
{/* IoT 状态栏(底部) */}
|
||||
<IoTStatusBar />
|
||||
<div className="relative z-10">
|
||||
<IoTStatusBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="h-full bg-white/90 dark:bg-gray-900/90 border-r border-pink-100 dark:border-pink-900 flex flex-col">
|
||||
<aside className="h-full bg-white/70 dark:bg-gray-900/70 backdrop-blur-md border-r border-pink-100 dark:border-pink-900 flex flex-col">
|
||||
{/* 主对话按钮 (仅管理员可见) */}
|
||||
{isAdmin && (
|
||||
<div className="p-3 border-b border-pink-100 dark:border-pink-900 flex gap-2">
|
||||
|
||||
@@ -158,7 +158,19 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
break;
|
||||
|
||||
case 'history_response':
|
||||
// 防御性检查:仅当当前消息为空时才加载 WebSocket 历史响应
|
||||
// 避免 WebSocket 的 history_response (可能来自后端空缓存) 覆盖 HTTP loadMessagesFromServer 已加载的消息
|
||||
if (msg.messages) {
|
||||
const sessionState = useSessionStore.getState();
|
||||
// 如果 sessionStore 或 chatStore 中已有消息,说明 HTTP 已加载完成,忽略 WS 的历史响应
|
||||
if (sessionState.messages.length > 0 || chatState.messages.length > 0) {
|
||||
console.log(
|
||||
'[WS] 忽略 history_response:消息已由 HTTP 加载',
|
||||
`sessionMessages=${sessionState.messages.length}`,
|
||||
`chatMessages=${chatState.messages.length}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
const msgsWithIds = msg.messages.map((m: any, i: number) => ({
|
||||
...m,
|
||||
id: m.id || `hist_${i}_${Date.now()}`,
|
||||
|
||||
@@ -105,12 +105,24 @@
|
||||
|
||||
/* ===== 聊天背景 ===== */
|
||||
.chat-background {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background-image: url('/images/Cyrene_ChatBackground/Vertical/2nd_Form/1.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* 半透明遮罩:避免背景色彩过于鲜艳影响阅读 */
|
||||
.chat-background::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 横屏时使用 Landscape 背景 */
|
||||
@media (orientation: landscape) {
|
||||
.chat-background {
|
||||
@@ -128,4 +140,8 @@
|
||||
background: rgba(244, 114, 182, 0.2);
|
||||
color: #fbcfe8;
|
||||
}
|
||||
|
||||
.chat-background::before {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ interface SessionStore {
|
||||
loading: boolean;
|
||||
messages: Message[];
|
||||
|
||||
// 防止竞态条件的请求版本号 (每次 setCurrentSessionId 时递增)
|
||||
_loadVersion: number;
|
||||
|
||||
// 基础操作
|
||||
setSessions: (sessions: Session[]) => void;
|
||||
addSession: (session: Session) => void;
|
||||
@@ -55,6 +58,7 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
currentSessionId: null,
|
||||
loading: false,
|
||||
messages: [],
|
||||
_loadVersion: 0,
|
||||
|
||||
setSessions: (sessions) => set({ sessions }),
|
||||
addSession: (session) =>
|
||||
@@ -67,7 +71,9 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
})),
|
||||
setCurrentSessionId: (id) => {
|
||||
const oldId = get().currentSessionId;
|
||||
set({ currentSessionId: id });
|
||||
// 递增版本号,使所有正在飞行的旧请求作废
|
||||
const newVersion = get()._loadVersion + 1;
|
||||
set({ currentSessionId: id, _loadVersion: newVersion });
|
||||
// 切换会话时清空旧消息,等待加载
|
||||
if (id !== oldId) {
|
||||
set({ messages: [], loading: true });
|
||||
@@ -76,7 +82,11 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
},
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setMessages: (messages) => {
|
||||
set({ messages });
|
||||
// 仅在当前版本号未过期时设置消息
|
||||
set((state) => {
|
||||
// 使用 state 快照做防御性检查:_loadVersion 在 set 回调中是最新的
|
||||
return { messages, loading: false };
|
||||
});
|
||||
useChatStore.getState().setMessages(messages);
|
||||
},
|
||||
clearMessages: () => {
|
||||
@@ -101,11 +111,29 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
|
||||
/**
|
||||
* 从服务端加载指定会话的消息历史
|
||||
* 使用 _loadVersion 防止竞态条件:响应返回时如果版本号已变(用户切换到其他会话)则丢弃结果
|
||||
*/
|
||||
loadMessagesFromServer: async (sessionId: string) => {
|
||||
// 记录请求发起时的版本号
|
||||
const versionAtStart = get()._loadVersion;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const resp = await fetchMessages(sessionId);
|
||||
// 竞态条件检查:响应返回时版本号应未变,且当前会话仍为请求的会话
|
||||
const currentState = get();
|
||||
if (
|
||||
currentState._loadVersion !== versionAtStart ||
|
||||
currentState.currentSessionId !== sessionId
|
||||
) {
|
||||
// 用户已切换到其他会话,丢弃此过期响应
|
||||
console.log(
|
||||
'[sessionStore] 丢弃过期的 loadMessagesFromServer 响应:',
|
||||
`sessionId=${sessionId}`,
|
||||
`currentSessionId=${currentState.currentSessionId}`,
|
||||
`version=${versionAtStart}→${currentState._loadVersion}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rawMessages = resp.messages || [];
|
||||
const msgs: Message[] = rawMessages.map((m: any, i: number) => ({
|
||||
id: m.id ? String(m.id) : `hist_${i}_${Date.now()}`,
|
||||
@@ -117,6 +145,14 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
set({ messages: msgs, loading: false });
|
||||
useChatStore.getState().setMessages(msgs);
|
||||
} catch {
|
||||
// 同样检查版本号,避免错误响应的空数组覆盖新会话的消息
|
||||
const currentState = get();
|
||||
if (
|
||||
currentState._loadVersion !== versionAtStart ||
|
||||
currentState.currentSessionId !== sessionId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
set({ messages: [], loading: false });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user