feat(storage): 更换持久存储策略为 SQLite 数据库,提升数据存储性能与结构化能力 [原因:localStorage 存储能力有限,SQLite 支持更复杂的数据结构]

This commit is contained in:
肖应宇 2026-03-09 10:13:11 +08:00
parent 633e5101a2
commit fe4ee53c38
13 changed files with 924 additions and 80 deletions

View File

@ -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(

View File

@ -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="消息不存在")
# ── 文件上传 ─────────────────────────────────────────────────────

View File

@ -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)

View File

@ -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)

45
server/middleware/auth.py Normal file
View File

@ -0,0 +1,45 @@
"""
认证中间件 - 预留接口
当前返回默认用户未来可集成 JWTOAuth 等认证系统
"""
from typing import Optional
def get_current_user_id(request) -> str:
"""
从请求中获取当前用户 ID预留
当前返回默认用户 'default'
未来可集成 JWTOAuth
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
}

View File

@ -126,10 +126,10 @@ useKeyboard(
//
onMounted(() => {
//
if (chatStore.conversations.length === 0) {
chatStore.createConversation();
}
// //
// if (chatStore.conversations.length === 0) {
// chatStore.createConversation();
// }
});
// 使

View File

@ -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: "",
});

View File

@ -0,0 +1,64 @@
/**
* -
*
* JWTOAuth
*/
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<string, string> {
const token = this.getToken();
if (token) {
return { Authorization: `Bearer ${token}` };
}
return {};
}
};

View File

@ -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<string, string> {
const headers: Record<string, string> = {
'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<Conversation>): Record<string, unknown> {
const data: Record<string, unknown> = {};
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<Conversation[]> {
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<Conversation> {
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<Conversation>): Promise<Conversation> {
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<Conversation>): Promise<Conversation> {
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<Conversation> {
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<void> {
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<Message>): Promise<Message> {
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<Message>): Promise<Message> {
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 };
},
};

View File

@ -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<string | null>(null);
const isStreaming = ref(false);
const streamController = ref<AbortController | null>(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<string> {
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<Message> {
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<Message>) {
// 更新消息
async function updateMessage(messageId: string, updates: Partial<Message>) {
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,
};
});

View File

@ -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 助手。",

140
src/utils/migrateData.ts Normal file
View File

@ -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<boolean> {
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<MigrationResult> {
// 检查是否已迁移
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,
};
}

View File

@ -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"}
{"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"}