feat(storage): 更换持久存储策略为 SQLite 数据库,提升数据存储性能与结构化能力 [原因:localStorage 存储能力有限,SQLite 支持更复杂的数据结构]
This commit is contained in:
parent
633e5101a2
commit
fe4ee53c38
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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="消息不存在")
|
||||
|
||||
|
||||
# ── 文件上传 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -126,10 +126,10 @@ useKeyboard(
|
|||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果没有对话,创建一个
|
||||
if (chatStore.conversations.length === 0) {
|
||||
chatStore.createConversation();
|
||||
}
|
||||
// // 如果没有对话,创建一个
|
||||
// if (chatStore.conversations.length === 0) {
|
||||
// chatStore.createConversation();
|
||||
// }
|
||||
});
|
||||
|
||||
// 暴露给全局使用
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, string> {
|
||||
const token = this.getToken();
|
||||
if (token) {
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
@ -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 助手。",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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"}
|
||||
Loading…
Reference in New Issue