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:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
@@ -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">
+12
View File
@@ -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()}`,
+16
View File
@@ -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);
}
}
+38 -2
View File
@@ -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();
}