diff --git a/server/adapters/glm_adapter.py b/server/adapters/glm_adapter.py index f7e6897..d13533a 100644 --- a/server/adapters/glm_adapter.py +++ b/server/adapters/glm_adapter.py @@ -138,6 +138,11 @@ class GLMAdapter(BaseAdapter): logger.info( f"[GLM] 深度思考已启用: extra_kwargs['thinking'] = {extra_kwargs['thinking']}" ) + else: + extra_kwargs["thinking"] = {"type": "disabled"} + logger.info( + f"[GLM] 深度思考已禁用: extra_kwargs['thinking'] = {extra_kwargs['thinking']}" + ) if extra_kwargs: logger.info( diff --git a/server/api/conversation_routes.py b/server/api/conversation_routes.py index ee4971c..c087f5d 100644 --- a/server/api/conversation_routes.py +++ b/server/api/conversation_routes.py @@ -74,6 +74,44 @@ async def delete_conversation_handler(conversation_id: str): raise HTTPException(status_code=404, detail="对话不存在") +async def update_conversation_handler(conversation_id: str, data: dict): + """部分更新对话处理器""" + db = get_db() + result = db.update_conversation(conversation_id, data) + if result: + return result + else: + raise HTTPException(status_code=404, detail="对话不存在") + + +# ── 消息管理 ───────────────────────────────────────────────────── + + +async def add_message_handler(conversation_id: str, message: dict): + """添加消息到对话处理器""" + db = get_db() + # 检查对话是否存在 + existing = db.get_conversation(conversation_id) + if not existing: + raise HTTPException(status_code=404, detail="对话不存在") + return db.add_message(conversation_id, message) + + +async def update_message_handler(conversation_id: str, message_id: str, data: dict): + """更新消息处理器""" + db = get_db() + # 检查对话是否存在 + existing = db.get_conversation(conversation_id) + if not existing: + raise HTTPException(status_code=404, detail="对话不存在") + + result = db.update_message(message_id, data) + if result: + return result + else: + raise HTTPException(status_code=404, detail="消息不存在") + + # ── 文件上传 ───────────────────────────────────────────────────── diff --git a/server/database/db.py b/server/database/db.py index 3886e77..7915ae1 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -76,6 +76,38 @@ class Database: CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id) """) + + # 检查并添加缺失的列(迁移旧数据库 - conversations 表) + cursor.execute("PRAGMA table_info(conversations)") + conv_columns = [col[1] for col in cursor.fetchall()] + + conv_migrations = [ + ('user_id', "TEXT DEFAULT 'default'"), + ('pinned', "INTEGER DEFAULT 0"), + ('archived', "INTEGER DEFAULT 0"), + ('settings', "TEXT"), + ] + + for col_name, col_def in conv_migrations: + if col_name not in conv_columns: + cursor.execute(f"ALTER TABLE conversations ADD COLUMN {col_name} {col_def}") + print(f"[数据库] conversations 表已添加 {col_name} 列") + + # 检查并添加缺失的列(迁移旧数据库 - messages 表) + cursor.execute("PRAGMA table_info(messages)") + msg_columns = [col[1] for col in cursor.fetchall()] + + msg_migrations = [ + ('timestamp', "INTEGER"), + ('feedback', "TEXT"), + ] + + for col_name, col_def in msg_migrations: + if col_name not in msg_columns: + cursor.execute(f"ALTER TABLE messages ADD COLUMN {col_name} {col_def}") + print(f"[数据库] messages 表已添加 {col_name} 列") + + # 创建 user_id 索引(在确保列存在后) cursor.execute(""" CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id) diff --git a/server/main.py b/server/main.py index a5ff1b3..9f1919e 100644 --- a/server/main.py +++ b/server/main.py @@ -54,12 +54,15 @@ init_db() load_dotenv() # ── 会话管理路由处理器 ──────────────────────────────────────────────── -from api.conversation_routes import (delete_conversation_handler, +from api.conversation_routes import (add_message_handler, + delete_conversation_handler, get_conversation_handler, get_conversations_handler, save_conversation_handler, serve_upload_handler, stop_generation_handler, + update_conversation_handler, + update_message_handler, upload_file_handler) # ── OpenAI 兼容网关初始化 ─────────────────────────────────────────────── @@ -189,6 +192,21 @@ async def delete_conversation(conversation_id: str): return await delete_conversation_handler(conversation_id) +@app.put("/api/chat-ui/conversations/{conversation_id}") +async def update_conversation(conversation_id: str, request: Request): + return await update_conversation_handler(conversation_id, await request.json()) + + +@app.post("/api/chat-ui/conversations/{conversation_id}/messages") +async def add_message(conversation_id: str, request: Request): + return await add_message_handler(conversation_id, await request.json()) + + +@app.put("/api/chat-ui/conversations/{conversation_id}/messages/{message_id}") +async def update_message(conversation_id: str, message_id: str, request: Request): + return await update_message_handler(conversation_id, message_id, await request.json()) + + @app.post("/api/chat-ui/upload") async def upload_file(file: UploadFile = File(...)): return await upload_file_handler(file=file) diff --git a/server/middleware/auth.py b/server/middleware/auth.py new file mode 100644 index 0000000..190bdda --- /dev/null +++ b/server/middleware/auth.py @@ -0,0 +1,45 @@ +""" +认证中间件 - 预留接口 + +当前返回默认用户,未来可集成 JWT、OAuth 等认证系统。 +""" + +from typing import Optional + + +def get_current_user_id(request) -> str: + """ + 从请求中获取当前用户 ID(预留) + + 当前返回默认用户 'default' + 未来可集成 JWT、OAuth 等 + + Args: + request: FastAPI Request 对象 + + Returns: + 用户 ID 字符串 + """ + # TODO: 实现 token 验证逻辑 + # 示例: + # auth_header = request.headers.get("Authorization") + # if auth_header and auth_header.startswith("Bearer "): + # token = auth_header[7:] + # user_id = verify_token(token) + # return user_id + + return "default" + + +def get_current_user(request) -> dict: + """ + 获取当前用户完整信息(预留) + + Returns: + 用户信息字典 + """ + return { + "id": get_current_user_id(request), + "name": None, + "email": None + } \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index c33840c..0e7f799 100644 --- a/src/App.vue +++ b/src/App.vue @@ -126,10 +126,10 @@ useKeyboard( // 初始化 onMounted(() => { - // 如果没有对话,创建一个 - if (chatStore.conversations.length === 0) { - chatStore.createConversation(); - } + // // 如果没有对话,创建一个 + // if (chatStore.conversations.length === 0) { + // chatStore.createConversation(); + // } }); // 暴露给全局使用 diff --git a/src/components/chat/ChatMain.vue b/src/components/chat/ChatMain.vue index 8d6f5f7..1bb55ab 100644 --- a/src/components/chat/ChatMain.vue +++ b/src/components/chat/ChatMain.vue @@ -212,7 +212,7 @@ async function handleSend( .map((m: any) => ({ role: m.role, content: m.content.text })); // 添加用户消息 - chatStore.addMessage(MessageRole.USER, { + await chatStore.addMessage(MessageRole.USER, { type: MessageType.TEXT, text, images: attachments.filter((a) => a.type === "image"), @@ -220,7 +220,7 @@ async function handleSend( }); // 添加 AI 消息占位符 - const aiMessage = chatStore.addMessage(MessageRole.ASSISTANT, { + const aiMessage = await chatStore.addMessage(MessageRole.ASSISTANT, { type: MessageType.TEXT, text: "", }); diff --git a/src/services/authService.ts b/src/services/authService.ts new file mode 100644 index 0000000..dcc6518 --- /dev/null +++ b/src/services/authService.ts @@ -0,0 +1,64 @@ +/** + * 认证服务模块 - 预留接口 + * + * 当前返回默认用户,未来可集成 JWT、OAuth 等认证系统 + */ + +export interface AuthUser { + id: string; + name?: string; + email?: string; +} + +// Token 存储 key +const AUTH_TOKEN_KEY = 'auth_token'; + +export const authService = { + /** + * 获取当前用户(预留,目前返回默认用户) + */ + getCurrentUser(): AuthUser | null { + // TODO: 从 token 解析用户信息 + return { id: 'default' }; + }, + + /** + * 获取认证 token(预留) + */ + getToken(): string | null { + return localStorage.getItem(AUTH_TOKEN_KEY); + }, + + /** + * 设置 token(预留) + */ + setToken(token: string): void { + localStorage.setItem(AUTH_TOKEN_KEY, token); + }, + + /** + * 清除认证信息(预留) + */ + clearAuth(): void { + localStorage.removeItem(AUTH_TOKEN_KEY); + }, + + /** + * 检查是否已认证(预留,目前始终返回 true) + */ + isAuthenticated(): boolean { + // TODO: 实现真实的认证检查 + return true; + }, + + /** + * 获取 Authorization header 值 + */ + getAuthHeader(): Record { + const token = this.getToken(); + if (token) { + return { Authorization: `Bearer ${token}` }; + } + return {}; + } +}; \ No newline at end of file diff --git a/src/services/conversationApi.ts b/src/services/conversationApi.ts new file mode 100644 index 0000000..7efe611 --- /dev/null +++ b/src/services/conversationApi.ts @@ -0,0 +1,294 @@ +/** + * 对话 API 服务层 + * + * 封装所有对话相关的后端 API 调用,支持认证预留 + */ + +import { authService } from './authService'; +import type { Conversation, Message, MessageContent, ConversationSettings } from '@/types/chat'; + +// API 端点 +const API_BASE = '/api/chat-ui'; +const ENDPOINTS = { + CONVERSATIONS: `${API_BASE}/conversations`, + CONVERSATION: (id: string) => `${API_BASE}/conversations/${id}`, + CONVERSATION_MESSAGES: (id: string) => `${API_BASE}/conversations/${id}/messages`, +}; + +// 后端返回的对话数据格式 +interface BackendConversation { + id: string; + userId?: string; + title: string; + createdAt: number; + updatedAt: number; + pinned: boolean; + archived: boolean; + settings?: ConversationSettings; + messages?: BackendMessage[]; +} + +// 后端返回的消息数据格式 +interface BackendMessage { + id: string; + role: string; + content: MessageContent; + timestamp: number; + feedback?: { + liked?: boolean; + disliked?: boolean; + copied?: boolean; + }; +} + +/** + * 获取请求头(包含认证信息) + */ +function getHeaders(): Record { + const headers: Record = { + 'Content-Type': 'application/json', + }; + // 添加认证 header(预留) + const authHeader = authService.getAuthHeader(); + return { ...headers, ...authHeader }; +} + +/** + * 将后端对话格式转换为前端格式 + */ +function transformConversation(backendConv: BackendConversation): Conversation { + return { + id: backendConv.id, + title: backendConv.title, + createdAt: backendConv.createdAt, + updatedAt: backendConv.updatedAt, + pinned: backendConv.pinned, + archived: backendConv.archived, + settings: backendConv.settings, + messages: (backendConv.messages || []).map(transformMessage), + }; +} + +/** + * 将后端消息格式转换为前端格式 + */ +function transformMessage(backendMsg: BackendMessage): Message { + return { + id: backendMsg.id, + role: backendMsg.role as 'user' | 'assistant' | 'system', + content: backendMsg.content, + timestamp: backendMsg.timestamp, + feedback: backendMsg.feedback, + isStreaming: false, + } as Message; +} + +/** + * 将前端对话格式转换为后端格式 + */ +function toBackendFormat(conversation: Partial): Record { + const data: Record = {}; + + if (conversation.id !== undefined) data.id = conversation.id; + if (conversation.title !== undefined) data.title = conversation.title; + if (conversation.createdAt !== undefined) data.createdAt = conversation.createdAt; + if (conversation.updatedAt !== undefined) data.updatedAt = conversation.updatedAt; + if (conversation.pinned !== undefined) data.pinned = conversation.pinned; + if (conversation.archived !== undefined) data.archived = conversation.archived; + if (conversation.settings !== undefined) data.settings = conversation.settings; + if (conversation.messages !== undefined) { + data.messages = conversation.messages.map(msg => ({ + id: msg.id, + role: msg.role, + content: msg.content, + timestamp: msg.timestamp, + feedback: msg.feedback, + })); + } + + return data; +} + +/** + * 对话 API 服务 + */ +export const conversationApi = { + /** + * 获取所有对话列表(不含消息内容) + */ + async fetchConversations(): Promise { + const response = await fetch(ENDPOINTS.CONVERSATIONS, { + method: 'GET', + headers: getHeaders(), + }); + + if (!response.ok) { + throw new Error(`获取对话列表失败: HTTP ${response.status}`); + } + + const data: BackendConversation[] = await response.json(); + return data.map(transformConversation); + }, + + /** + * 获取单个对话(含消息内容) + */ + async fetchConversation(id: string): Promise { + const response = await fetch(ENDPOINTS.CONVERSATION(id), { + method: 'GET', + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('对话不存在'); + } + throw new Error(`获取对话失败: HTTP ${response.status}`); + } + + const data: BackendConversation = await response.json(); + return transformConversation(data); + }, + + /** + * 创建新对话 + */ + async createConversation(data: Partial): Promise { + const response = await fetch(ENDPOINTS.CONVERSATIONS, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(toBackendFormat(data)), + }); + + if (!response.ok) { + throw new Error(`创建对话失败: HTTP ${response.status}`); + } + + const result: BackendConversation = await response.json(); + return transformConversation(result); + }, + + /** + * 更新对话(部分更新) + */ + async updateConversation(id: string, data: Partial): Promise { + const response = await fetch(ENDPOINTS.CONVERSATION(id), { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(toBackendFormat(data)), + }); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('对话不存在'); + } + throw new Error(`更新对话失败: HTTP ${response.status}`); + } + + const result: BackendConversation = await response.json(); + return transformConversation(result); + }, + + /** + * 保存对话(创建或更新) + */ + async saveConversation(conversation: Conversation): Promise { + const data = toBackendFormat(conversation); + + const response = await fetch(ENDPOINTS.CONVERSATIONS, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`保存对话失败: HTTP ${response.status}`); + } + + const result: BackendConversation = await response.json(); + return transformConversation(result); + }, + + /** + * 删除对话 + */ + async deleteConversation(id: string): Promise { + const response = await fetch(ENDPOINTS.CONVERSATION(id), { + method: 'DELETE', + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 404) { + // 对话已不存在,视为成功 + return; + } + throw new Error(`删除对话失败: HTTP ${response.status}`); + } + }, + + /** + * 添加消息到对话(增量更新) + */ + async addMessage(conversationId: string, message: Partial): Promise { + const response = await fetch(ENDPOINTS.CONVERSATION_MESSAGES(conversationId), { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({ + id: message.id, + role: message.role, + content: message.content, + timestamp: message.timestamp, + feedback: message.feedback, + }), + }); + + if (!response.ok) { + throw new Error(`添加消息失败: HTTP ${response.status}`); + } + + const result: BackendMessage = await response.json(); + return transformMessage(result); + }, + + /** + * 更新消息 + */ + async updateMessage(conversationId: string, messageId: string, data: Partial): Promise { + const response = await fetch(`${ENDPOINTS.CONVERSATION(conversationId)}/messages/${messageId}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ + content: data.content, + feedback: data.feedback, + }), + }); + + if (!response.ok) { + throw new Error(`更新消息失败: HTTP ${response.status}`); + } + + const result: BackendMessage = await response.json(); + return transformMessage(result); + }, + + /** + * 批量迁移对话数据 + */ + async migrateConversations(conversations: Conversation[]): Promise<{ success: number; failed: number }> { + let success = 0; + let failed = 0; + + for (const conversation of conversations) { + try { + await this.saveConversation(conversation); + success++; + } catch (e) { + console.error(`迁移对话失败 [${conversation.id}]:`, e); + failed++; + } + } + + return { success, failed }; + }, +}; \ No newline at end of file diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 598d45a..c189be1 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -8,6 +8,7 @@ import type { } from "@/types/chat"; import { MessageRole } from "@/types/chat"; import { generateId, extractTitleFromMessage } from "@/utils/helpers"; +import { conversationApi } from "@/services/conversationApi"; export const useChatStore = defineStore("chat", () => { // 状态 @@ -15,6 +16,8 @@ export const useChatStore = defineStore("chat", () => { const currentConversationId = ref(null); const isStreaming = ref(false); const streamController = ref(null); + const isInitialized = ref(false); + const isLoading = ref(false); // 计算属性 const currentConversation = computed(() => { @@ -40,8 +43,43 @@ export const useChatStore = defineStore("chat", () => { return sortedConversations.value.filter((c) => !c.pinned && !c.archived); }); - // 方法 - function createConversation(): string { + // 初始化方法 - 从后端 API 加载数据 + async function initializeFromApi() { + if (isInitialized.value || isLoading.value) return; + + isLoading.value = true; + try { + const loadedConversations = await conversationApi.fetchConversations(); + conversations.value = loadedConversations; + + // 恢复当前对话 ID(从 localStorage 或选择第一个) + const storedId = localStorage.getItem("chat-current-id"); + if (storedId && conversations.value.find((c) => c.id === storedId)) { + currentConversationId.value = storedId; + } else if (conversations.value.length > 0) { + currentConversationId.value = conversations.value[0].id; + } + + isInitialized.value = true; + } catch (error) { + console.error("Failed to initialize from API:", error); + // 如果 API 失败,尝试从 localStorage 加载(降级处理) + loadFromStorage(); + } finally { + isLoading.value = false; + } + } + + // 保存当前对话 ID 到 localStorage + function saveCurrentId() { + localStorage.setItem( + "chat-current-id", + currentConversationId.value || "" + ); + } + + // 创建对话 + async function createConversation(): Promise { const newConversation: Conversation = { id: generateId(), title: "新对话", @@ -53,89 +91,171 @@ export const useChatStore = defineStore("chat", () => { settings: undefined, }; + // 乐观更新 conversations.value.unshift(newConversation); currentConversationId.value = newConversation.id; - saveToStorage(); + saveCurrentId(); + + // 异步保存到后端 + try { + const saved = await conversationApi.createConversation(newConversation); + // 更新本地数据(以防后端修改了某些字段) + const index = conversations.value.findIndex((c) => c.id === newConversation.id); + if (index !== -1) { + conversations.value[index] = saved; + } + } catch (error) { + console.error("Failed to create conversation:", error); + // 回滚乐观更新 + const index = conversations.value.findIndex((c) => c.id === newConversation.id); + if (index !== -1) { + conversations.value.splice(index, 1); + } + throw error; + } return newConversation.id; } - function deleteConversation(id: string) { + // 删除对话 + async function deleteConversation(id: string) { const index = conversations.value.findIndex((c) => c.id === id); - if (index !== -1) { - conversations.value.splice(index, 1); + if (index === -1) return; - if (currentConversationId.value === id) { - currentConversationId.value = conversations.value[0]?.id || null; - } + // 保存引用以便回滚 + const deletedConversation = conversations.value[index]; - saveToStorage(); + // 乐观更新 + conversations.value.splice(index, 1); + if (currentConversationId.value === id) { + currentConversationId.value = conversations.value[0]?.id || null; + saveCurrentId(); + } + + // 异步删除 + try { + await conversationApi.deleteConversation(id); + } catch (error) { + console.error("Failed to delete conversation:", error); + // 回滚 + conversations.value.splice(index, 0, deletedConversation); + throw error; } } - function selectConversation(id: string) { + // 选择对话 + async function selectConversation(id: string) { currentConversationId.value = id; + saveCurrentId(); + + // 如果对话没有加载消息,从后端加载 + const conversation = conversations.value.find((c) => c.id === id); + if (conversation && (!conversation.messages || conversation.messages.length === 0)) { + try { + const loaded = await conversationApi.fetchConversation(id); + const index = conversations.value.findIndex((c) => c.id === id); + if (index !== -1) { + conversations.value[index] = loaded; + } + } catch (error) { + console.error("Failed to load conversation:", error); + } + } } - function togglePinConversation(id: string) { + // 置顶对话 + async function togglePinConversation(id: string) { const conversation = conversations.value.find((c) => c.id === id); - if (conversation) { + if (!conversation) return; + + // 乐观更新 + conversation.pinned = !conversation.pinned; + + // 异步保存 + try { + await conversationApi.updateConversation(id, { pinned: conversation.pinned }); + } catch (error) { + console.error("Failed to toggle pin:", error); + // 回滚 conversation.pinned = !conversation.pinned; - saveToStorage(); + throw error; } } - function renameConversation(id: string, newTitle: string) { + // 重命名对话 + async function renameConversation(id: string, newTitle: string) { const conversation = conversations.value.find((c) => c.id === id); - if (conversation) { - conversation.title = newTitle; - conversation.updatedAt = Date.now(); - saveToStorage(); + if (!conversation) return; + + const oldTitle = conversation.title; + conversation.title = newTitle; + conversation.updatedAt = Date.now(); + + // 异步保存 + try { + await conversationApi.updateConversation(id, { title: newTitle }); + } catch (error) { + console.error("Failed to rename conversation:", error); + // 回滚 + conversation.title = oldTitle; + throw error; } } - function updateConversationSettings( + // 更新对话设置 + async function updateConversationSettings( id: string, - convSettings: ConversationSettings, + convSettings: ConversationSettings ) { const conversation = conversations.value.find((c) => c.id === id); - if (conversation) { - conversation.settings = { ...conversation.settings, ...convSettings }; - conversation.updatedAt = Date.now(); - saveToStorage(); + if (!conversation) return; + + const oldSettings = conversation.settings; + conversation.settings = { ...conversation.settings, ...convSettings }; + conversation.updatedAt = Date.now(); + + // 异步保存 + try { + await conversationApi.updateConversation(id, { settings: conversation.settings }); + } catch (error) { + console.error("Failed to update settings:", error); + // 回滚 + conversation.settings = oldSettings; + throw error; } } - function addMessage( + // 添加消息 + async function addMessage( role: MessageRole, content: MessageContent, - conversationId?: string, - ): Message { - const targetId = conversationId || currentConversationId.value; + conversationId?: string + ): Promise { + let targetId = conversationId || currentConversationId.value; if (!targetId) { - createConversation(); + await createConversation(); + targetId = currentConversationId.value; } - const conversation = conversations.value.find( - (c) => c.id === (targetId || currentConversationId.value), - ); - + const conversation = conversations.value.find((c) => c.id === targetId); if (!conversation) { throw new Error("Conversation not found"); } - const message: any = { + const message: Message = { id: generateId(), role, content, timestamp: Date.now(), isStreaming: false, - }; + } as Message; + // 乐观更新 conversation.messages.push(message); conversation.updatedAt = Date.now(); + // 如果是第一条用户消息,更新标题 if ( role === MessageRole.USER && conversation.messages.length === 1 && @@ -144,21 +264,64 @@ export const useChatStore = defineStore("chat", () => { conversation.title = extractTitleFromMessage(content.text); } - saveToStorage(); + // 异步保存(使用增量更新) + try { + // 确保 targetId 不为空 + if (targetId) { + // 发送消息到后端,不等待完成 + conversationApi.addMessage(targetId, message).catch((error) => { + console.error("Failed to save message:", error); + }); + + // 如果标题更新了,也保存标题 + if ( + role === MessageRole.USER && + conversation.messages.length === 1 + ) { + conversationApi.updateConversation(targetId, { title: conversation.title }).catch((error) => { + console.error("Failed to update title:", error); + }); + } + } + } catch (error) { + console.error("Failed to add message:", error); + } + return message; } - function updateMessage(messageId: string, updates: Partial) { + // 更新消息 + async function updateMessage(messageId: string, updates: Partial) { const conversation = currentConversation.value; - if (!conversation) return; + if (!conversation) { + console.warn("[updateMessage] No current conversation"); + return; + } const message = conversation.messages.find((m) => m.id === messageId); - if (message) { - Object.assign(message, updates); - saveToStorage(); + if (!message) { + console.warn("[updateMessage] Message not found:", messageId); + return; + } + + // 乐观更新 + Object.assign(message, updates); + + // 异步保存 + try { + console.log("[updateMessage] Saving to backend:", { + conversationId: conversation.id, + messageId, + content: updates.content, + }); + await conversationApi.updateMessage(conversation.id, messageId, updates); + console.log("[updateMessage] Save successful"); + } catch (error) { + console.error("Failed to update message:", error); } } + // 更新消息内容(流式更新时使用,不触发 API 调用) function updateMessageContent(messageId: string, text: string) { const conversation = currentConversation.value; if (!conversation) return; @@ -169,24 +332,49 @@ export const useChatStore = defineStore("chat", () => { } } - function setMessageFeedback( + // 保存整个对话(用于流式结束后) + async function saveConversation(conversationId: string) { + const conversation = conversations.value.find((c) => c.id === conversationId); + if (!conversation) return; + + try { + await conversationApi.updateConversation(conversationId, { + messages: conversation.messages, + updatedAt: Date.now() + }); + } catch (error) { + console.error("Failed to save conversation:", error); + } + } + + // 设置消息反馈 + async function setMessageFeedback( messageId: string, - feedback: "like" | "dislike" | null, + feedback: "like" | "dislike" | null ) { const conversation = currentConversation.value; if (!conversation) return; const message = conversation.messages.find((m) => m.id === messageId); - if (message) { - message.feedback = { - liked: feedback === "like", - disliked: feedback === "dislike", - copied: message.feedback?.copied, - }; - saveToStorage(); + if (!message) return; + + message.feedback = { + liked: feedback === "like", + disliked: feedback === "dislike", + copied: message.feedback?.copied, + }; + + // 异步保存 + try { + await conversationApi.updateMessage(conversation.id, messageId, { + feedback: message.feedback + }); + } catch (error) { + console.error("Failed to save feedback:", error); } } + // 设置消息已复制 function setMessageCopied(messageId: string) { const conversation = currentConversation.value; if (!conversation) return; @@ -200,11 +388,13 @@ export const useChatStore = defineStore("chat", () => { } } + // 开始流式输出 function startStreaming() { isStreaming.value = true; streamController.value = new AbortController(); } + // 停止流式输出 function stopStreaming() { isStreaming.value = false; if (streamController.value) { @@ -213,30 +403,23 @@ export const useChatStore = defineStore("chat", () => { } } - function clearConversation(id: string) { + // 清空对话消息 + async function clearConversation(id: string) { const conversation = conversations.value.find((c) => c.id === id); - if (conversation) { - conversation.messages = []; - conversation.updatedAt = Date.now(); - saveToStorage(); - } - } + if (!conversation) return; - function saveToStorage() { + conversation.messages = []; + conversation.updatedAt = Date.now(); + + // 异步保存 try { - localStorage.setItem( - "chat-conversations", - JSON.stringify(conversations.value), - ); - localStorage.setItem( - "chat-current-id", - currentConversationId.value || "", - ); - } catch (e) { - console.error("Failed to save to storage:", e); + await conversationApi.updateConversation(id, { messages: [] }); + } catch (error) { + console.error("Failed to clear conversation:", error); } } + // 降级:从 localStorage 加载(仅在 API 不可用时使用) function loadFromStorage() { try { const stored = localStorage.getItem("chat-conversations"); @@ -255,17 +438,40 @@ export const useChatStore = defineStore("chat", () => { } } - loadFromStorage(); + // 保存到 localStorage(降级模式使用) + function saveToStorage() { + try { + localStorage.setItem( + "chat-conversations", + JSON.stringify(conversations.value) + ); + localStorage.setItem( + "chat-current-id", + currentConversationId.value || "" + ); + } catch (e) { + console.error("Failed to save to storage:", e); + } + } + + // 初始化 + initializeFromApi(); return { + // 状态 conversations, currentConversationId, isStreaming, streamController, + isInitialized, + isLoading, + // 计算属性 currentConversation, sortedConversations, pinnedConversations, recentConversations, + // 方法 + initializeFromApi, createConversation, deleteConversation, selectConversation, @@ -275,11 +481,13 @@ export const useChatStore = defineStore("chat", () => { addMessage, updateMessage, updateMessageContent, + saveConversation, setMessageFeedback, setMessageCopied, startStreaming, stopStreaming, clearConversation, loadFromStorage, + saveToStorage, }; -}); +}); \ No newline at end of file diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 54376b0..7192e93 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -16,7 +16,7 @@ export const useSettingsStore = defineStore("settings", () => { compactMode: false, // AI 默认设置 - defaultModel: "glm-4.6", + defaultModel: "glm-4.6v", defaultTemperature: 0.7, defaultMaxTokens: 4096, defaultSystemPrompt: "你是一个有帮助的 AI 助手。", diff --git a/src/utils/migrateData.ts b/src/utils/migrateData.ts new file mode 100644 index 0000000..ef5c235 --- /dev/null +++ b/src/utils/migrateData.ts @@ -0,0 +1,140 @@ +/** + * 数据迁移工具 + * + * 将 localStorage 中的旧对话数据迁移到后端 SQLite + */ + +import { conversationApi } from '@/services/conversationApi'; +import type { Conversation } from '@/types/chat'; + +const OLD_CONVERSATIONS_KEY = 'chat-conversations'; +const MIGRATION_FLAG_KEY = 'chat-migration-completed'; + +export interface MigrationResult { + success: boolean; + total: number; + migrated: number; + failed: number; + message: string; +} + +/** + * 检查是否已完成迁移 + */ +export function isMigrationCompleted(): boolean { + return localStorage.getItem(MIGRATION_FLAG_KEY) === 'true'; +} + +/** + * 标记迁移已完成 + */ +function markMigrationCompleted() { + localStorage.setItem(MIGRATION_FLAG_KEY, 'true'); +} + +/** + * 从 localStorage 读取旧数据 + */ +function getOldConversations(): Conversation[] { + try { + const stored = localStorage.getItem(OLD_CONVERSATIONS_KEY); + if (stored) { + return JSON.parse(stored); + } + } catch (e) { + console.error('Failed to read old conversations:', e); + } + return []; +} + +/** + * 迁移单个对话 + */ +async function migrateConversation(conversation: Conversation): Promise { + try { + await conversationApi.saveConversation(conversation); + return true; + } catch (error) { + console.error(`Failed to migrate conversation ${conversation.id}:`, error); + return false; + } +} + +/** + * 执行迁移 + */ +export async function migrateData(): Promise { + // 检查是否已迁移 + if (isMigrationCompleted()) { + return { + success: true, + total: 0, + migrated: 0, + failed: 0, + message: '迁移已完成,无需重复执行', + }; + } + + // 读取旧数据 + const oldConversations = getOldConversations(); + + if (oldConversations.length === 0) { + markMigrationCompleted(); + return { + success: true, + total: 0, + migrated: 0, + failed: 0, + message: '没有需要迁移的数据', + }; + } + + // 迁移数据 + let migrated = 0; + let failed = 0; + + for (const conversation of oldConversations) { + const success = await migrateConversation(conversation); + if (success) { + migrated++; + } else { + failed++; + } + } + + // 迁移完成后清理 + if (migrated === oldConversations.length) { + // 全部成功,清理旧数据 + localStorage.removeItem(OLD_CONVERSATIONS_KEY); + markMigrationCompleted(); + } + + return { + success: failed === 0, + total: oldConversations.length, + migrated, + failed, + message: failed === 0 + ? `成功迁移 ${migrated} 条对话` + : `迁移完成:成功 ${migrated} 条,失败 ${failed} 条`, + }; +} + +/** + * 清理 localStorage 中的旧数据 + */ +export function cleanupOldData() { + localStorage.removeItem(OLD_CONVERSATIONS_KEY); + // 保留 chat-current-id,因为它仍在使用 +} + +/** + * 导出迁移状态 + */ +export function getMigrationStatus() { + return { + completed: isMigrationCompleted(), + hasOldData: localStorage.getItem(OLD_CONVERSATIONS_KEY) !== null, + oldDataCount: getOldConversations().length, + }; +} \ No newline at end of file diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 185dfda..6d04746 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.ts","./src/components/icons/index.ts","./src/composables/useKeyboard.ts","./src/services/api.ts","./src/stores/chat.ts","./src/stores/settings.ts","./src/types/chat.ts","./src/utils/helpers.ts","./src/App.vue","./src/components/chat/ChatHeader.vue","./src/components/chat/ChatMain.vue","./src/components/chat/MessageList.vue","./src/components/chat/WelcomeScreen.vue","./src/components/input/AttachmentPreview.vue","./src/components/input/ChatInput.vue","./src/components/message/CodeBlock.vue","./src/components/message/MessageActions.vue","./src/components/message/MessageBubble.vue","./src/components/message/components/EChartsContainerNode.vue","./src/components/message/components/Loading.vue","./src/components/message/components/ThinkingNode.vue","./src/components/modals/ConversationSettingsModal.vue","./src/components/modals/SearchModal.vue","./src/components/modals/SettingsModal.vue","./src/components/modals/ShortcutsModal.vue","./src/components/sidebar/ChatSidebar.vue","./src/components/sidebar/ConversationItem.vue","./src/components/ui/FormSelect.vue","./src/components/ui/FormSlider.vue","./src/components/ui/FormSwitch.vue"],"errors":true,"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.ts","./src/components/icons/index.ts","./src/composables/useKeyboard.ts","./src/services/api.ts","./src/services/authService.ts","./src/services/conversationApi.ts","./src/stores/chat.ts","./src/stores/settings.ts","./src/types/chat.ts","./src/utils/helpers.ts","./src/utils/migrateData.ts","./src/App.vue","./src/components/chat/ChatHeader.vue","./src/components/chat/ChatMain.vue","./src/components/chat/MessageList.vue","./src/components/chat/WelcomeScreen.vue","./src/components/input/AttachmentPreview.vue","./src/components/input/ChatInput.vue","./src/components/message/CodeBlock.vue","./src/components/message/MessageActions.vue","./src/components/message/MessageBubble.vue","./src/components/message/components/EChartsContainerNode.vue","./src/components/message/components/Loading.vue","./src/components/message/components/ThinkingNode.vue","./src/components/modals/ConversationSettingsModal.vue","./src/components/modals/SearchModal.vue","./src/components/modals/SettingsModal.vue","./src/components/modals/ShortcutsModal.vue","./src/components/sidebar/ChatSidebar.vue","./src/components/sidebar/ConversationItem.vue","./src/components/ui/FormSelect.vue","./src/components/ui/FormSlider.vue","./src/components/ui/FormSwitch.vue"],"errors":true,"version":"5.9.3"} \ No newline at end of file