fix: Phase 6联调 — 插件管理器端口修正 + 多模型配置系统整合 + 历史消息刷新修复
## 调试日志
### 1. 插件管理器启动失败
- **症状**: DevTools 显示插件管理器一直"已停止",手动启动正常
- **排查**: 对比 process-manager.js 传入的环境变量 vs plugin-manager config.go 读取的变量
- **根因**: config.js 传入 PLUGIN_MANAGER_PORT=8094,但 config.go 读取 os.Getenv("PORT"),env 名不匹配。且 process.env 中 PORT 泄露时被误读为 9090,与 DevTools 端口冲突
- **修复**: config.js 将 PLUGIN_MANAGER_PORT → PORT,使 env 名与代码一致 (c3055f4)
### 2. 历史消息刷新后消失
- **症状**: 浏览器刷新后聊天历史清空
- **排查**: WebSocket history_response handler 中 if (msg.messages) 对空数组 [] 为 truthy
- **根因**: 后端返回空的 history_response (缓存为空) 时,空数组覆盖了 HTTP 已加载的消息
- **修复**: useWebSocket.ts 改为 if (msg.messages && msg.messages.length > 0),空数组走 else-if 分支仅打日志,不覆盖已有消息
### 3. Phase 6 多模型配置系统
- Gateway: ModelsConfigStore (JSON文件持久化) + Admin CRUD API (providers/models/routing)
- ai-core: ModelSelector 支持按 purpose 选择 + fallback_chain,无配置时回退 .env
- DevTools: 模型配置管理面板 (Providers/Models/Routing 三Tab)、在线模型查询代理、路由表单 checkbox 多选、关键词搜索过滤
- .gitignore: models.json + platform_configs.json
### 4. 多端客户端追踪
- Hub 新增 knownClients 映射 (clientID → KnownClient),在线/离线状态追踪
- 客户端备注持久化到 PostgreSQL
- DevTools 客户端管理面板
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import { useSpeechSynthesis } from '@/hooks/useSpeechSynthesis';
|
||||
import type { MessageAttachment, MultiMessageItem, StreamSegment, MessageDisplayType } from '@/types/chat';
|
||||
import { ImageLightbox } from './ImageLightbox';
|
||||
|
||||
interface MessageBubbleProps {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system' | 'action';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
@@ -141,6 +143,7 @@ function AIMessageActions({ content }: { content: string }) {
|
||||
}
|
||||
|
||||
export function MessageBubble({
|
||||
id,
|
||||
role,
|
||||
content,
|
||||
timestamp,
|
||||
@@ -203,6 +206,19 @@ export function MessageBubble({
|
||||
// 判断是否还有未显示完的字符
|
||||
const hasMoreChars = isStreaming && displayedContent.length < content.length;
|
||||
|
||||
// When typewriter animation finishes, notify store to process next queued message.
|
||||
const typewriterComplete = isStreaming && content.length > 0 && displayedContent.length >= content.length;
|
||||
const doneNotifiedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (typewriterComplete && !doneNotifiedRef.current) {
|
||||
doneNotifiedRef.current = true;
|
||||
const timer = setTimeout(() => {
|
||||
useChatStore.getState().onTypewriterDone(id);
|
||||
}, 200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [typewriterComplete, id]);
|
||||
|
||||
// 图片附件
|
||||
const imageAttachments = attachments?.filter((a) => a.type === 'image') ?? [];
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export function MessageList({
|
||||
{messages.map((msg) => (
|
||||
<MessageBubble
|
||||
key={msg.id}
|
||||
id={msg.id}
|
||||
role={msg.role}
|
||||
content={msg.content}
|
||||
timestamp={msg.timestamp}
|
||||
|
||||
@@ -249,7 +249,8 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
case 'history_response':
|
||||
// 防御性检查:仅当当前消息为空时才加载 WebSocket 历史响应
|
||||
// 避免 WebSocket 的 history_response (可能来自后端空缓存) 覆盖 HTTP loadMessagesFromServer 已加载的消息
|
||||
if (msg.messages) {
|
||||
// 注意:空数组 [] 在 JS 中是 truthy,必须显式检查 length > 0
|
||||
if (msg.messages && msg.messages.length > 0) {
|
||||
const sessionState = useSessionStore.getState();
|
||||
// 如果 sessionStore 或 chatStore 中已有消息,说明 HTTP 已加载完成,忽略 WS 的历史响应
|
||||
if (sessionState.messages.length > 0 || chatState.messages.length > 0) {
|
||||
@@ -263,9 +264,13 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
const msgsWithIds: Message[] = msg.messages.map((m, i) => ({
|
||||
...m,
|
||||
id: m.id || `hist_${i}_${Date.now()}`,
|
||||
// 规范化 msg_type → msgType 以保持与 HTTP 加载路径一致
|
||||
msgType: (m as any).msg_type || m.msgType,
|
||||
}));
|
||||
setMessages(msgsWithIds);
|
||||
useChatStore.getState().setMessages(msgsWithIds);
|
||||
} else if (msg.messages && msg.messages.length === 0) {
|
||||
console.log('[WS] 忽略空的 history_response(后端缓存和数据库均无消息),等待 HTTP 加载');
|
||||
}
|
||||
setTyping(false);
|
||||
break;
|
||||
|
||||
@@ -21,9 +21,16 @@ interface ChatStore {
|
||||
isLoadingHistory: boolean;
|
||||
historyPage: number;
|
||||
|
||||
// 多气泡消息队列:确保气泡依次出现 + 逐字动画
|
||||
messageQueue: Message[];
|
||||
|
||||
addMessage: (message: Message) => void;
|
||||
appendToLastMessage: (content: string) => void;
|
||||
finishStreaming: () => void;
|
||||
/** 将一个消息加入队列,等待上一个气泡的逐字动画完成后再展示 */
|
||||
enqueueMessage: (message: Message) => void;
|
||||
/** 当前气泡逐字动画完成后调用:关闭 isStreaming,出队下一个 */
|
||||
onTypewriterDone: (messageId: string) => void;
|
||||
setMessages: (messages: Message[]) => void;
|
||||
setTyping: (typing: boolean) => void;
|
||||
clearMessages: () => void;
|
||||
@@ -49,6 +56,7 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
hasMoreMessages: false,
|
||||
isLoadingHistory: false,
|
||||
historyPage: 1,
|
||||
messageQueue: [],
|
||||
|
||||
addMessage: (message) =>
|
||||
set((state) => ({
|
||||
@@ -84,14 +92,48 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
isStreaming: false,
|
||||
};
|
||||
}
|
||||
return { messages: msgs, isTyping: false };
|
||||
// Process queued messages after streaming finishes
|
||||
const queue = [...state.messageQueue];
|
||||
if (queue.length > 0) {
|
||||
const next = queue.shift()!;
|
||||
return {
|
||||
messages: [...msgs, { ...next, isStreaming: true }],
|
||||
messageQueue: queue,
|
||||
isTyping: false,
|
||||
};
|
||||
}
|
||||
return { messages: msgs, isTyping: false, messageQueue: queue };
|
||||
}),
|
||||
|
||||
enqueueMessage: (message) =>
|
||||
set((state) => ({
|
||||
messageQueue: [...state.messageQueue, message],
|
||||
})),
|
||||
|
||||
onTypewriterDone: (messageId) =>
|
||||
set((state) => {
|
||||
// Mark this specific message as no longer streaming
|
||||
const msgs = state.messages.map((m) =>
|
||||
m.id === messageId ? { ...m, isStreaming: false } : m
|
||||
);
|
||||
// Process next queued message
|
||||
const queue = [...state.messageQueue];
|
||||
if (queue.length > 0) {
|
||||
const next = queue.shift()!;
|
||||
return {
|
||||
messages: [...msgs, { ...next, isStreaming: true }],
|
||||
messageQueue: queue,
|
||||
isTyping: false,
|
||||
};
|
||||
}
|
||||
return { messages: msgs, isTyping: false, messageQueue: queue };
|
||||
}),
|
||||
|
||||
setMessages: (messages) => set({ messages, isTyping: false }),
|
||||
|
||||
setTyping: (typing) => set({ isTyping: typing }),
|
||||
|
||||
clearMessages: () => set({ messages: [], isTyping: false, hasMoreMessages: false, historyPage: 1 }),
|
||||
clearMessages: () => set({ messages: [], isTyping: false, hasMoreMessages: false, historyPage: 1, messageQueue: [] }),
|
||||
|
||||
setContinuousMode: (enabled) => set({ continuousMode: enabled }),
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ export interface ReviewMessage {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** 客户端信息 (多端区分) */
|
||||
export interface ClientInfo {
|
||||
client_id: string;
|
||||
device_name?: string;
|
||||
user_agent?: string;
|
||||
}
|
||||
|
||||
/** 单条消息 */
|
||||
export interface Message {
|
||||
id: string;
|
||||
@@ -50,6 +57,8 @@ export interface Message {
|
||||
isStreaming?: boolean;
|
||||
/** 消息显示类型: 区分聊天消息与动作消息 */
|
||||
msgType?: MessageDisplayType;
|
||||
/** 消息来源客户端信息 (多端区分) */
|
||||
client_info?: ClientInfo;
|
||||
}
|
||||
|
||||
/** IoT 设备类型定义 */
|
||||
@@ -99,6 +108,9 @@ export interface WSClientMessage {
|
||||
audio_data?: string; // base64
|
||||
attachments?: MessageAttachment[];
|
||||
timestamp: number;
|
||||
client_id?: string;
|
||||
device_name?: string;
|
||||
user_agent?: string;
|
||||
}
|
||||
|
||||
/** 通知类型 */
|
||||
@@ -146,6 +158,7 @@ export interface WSServerMessage {
|
||||
system_info?: SystemInfoPayload;
|
||||
protocol_version?: number;
|
||||
timestamp: number;
|
||||
client_info?: ClientInfo;
|
||||
}
|
||||
|
||||
/** 工具进度信息 */
|
||||
|
||||
Reference in New Issue
Block a user