ai-chat-ui/src/services/conversationApi.ts

319 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 对话 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 };
},
};