319 lines
9.6 KiB
TypeScript
319 lines
9.6 KiB
TypeScript
/**
|
||
* 对话 API 服务层
|
||
*
|
||
* 封装所有对话相关的后端 API 调用
|
||
*/
|
||
|
||
import { getAuthHeaders } from './request';
|
||
import { useAuthStore } from '@/stores/auth';
|
||
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> {
|
||
return getAuthHeaders();
|
||
}
|
||
|
||
/**
|
||
* 将后端对话格式转换为前端格式
|
||
*/
|
||
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>, userId?: string): Record<string, unknown> {
|
||
const data: Record<string, unknown> = {};
|
||
|
||
if (conversation.id !== undefined) data.id = conversation.id;
|
||
if (userId !== undefined) data.user_id = userId; // 后端使用下划线命名
|
||
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 authStore = useAuthStore();
|
||
// 等待 authStore 初始化完成
|
||
if (!authStore.isInitialized) {
|
||
await new Promise<void>((resolve) => {
|
||
const unwatch = authStore.$subscribe(() => {
|
||
if (authStore.isInitialized) {
|
||
unwatch();
|
||
resolve();
|
||
}
|
||
});
|
||
// 如果已经初始化了,立即 resolve
|
||
if (authStore.isInitialized) {
|
||
unwatch();
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
|
||
const userId = authStore.userId;
|
||
|
||
// 构建 URL,添加 user_id 查询参数
|
||
const url = userId
|
||
? `${ENDPOINTS.CONVERSATIONS}?user_id=${encodeURIComponent(userId)}`
|
||
: ENDPOINTS.CONVERSATIONS;
|
||
|
||
const response = await fetch(url, {
|
||
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 authStore = useAuthStore();
|
||
const userId = authStore.userId || undefined;
|
||
|
||
const response = await fetch(ENDPOINTS.CONVERSATIONS, {
|
||
method: 'POST',
|
||
headers: getHeaders(),
|
||
body: JSON.stringify(toBackendFormat(data, userId)),
|
||
});
|
||
|
||
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 };
|
||
},
|
||
}; |