api-client.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /**
  2. * API 客户端 - 与 FastAPI 后端通信
  3. * 使用 SuperJSON 处理序列化
  4. */
  5. import { serialize, deserialize } from './superjson';
  6. import { mcpTokenManager } from './mcp-token-manager';
  7. // 配置 - 使用 Next.js rewrites 反向代理到 FastAPI
  8. const BACKEND_URL = '/api';
  9. // 类型定义
  10. export interface ChatMessage {
  11. role: 'user' | 'assistant';
  12. content: string;
  13. }
  14. export interface ChatRequest {
  15. message: string;
  16. history: ChatMessage[];
  17. }
  18. export interface ChatResponse {
  19. response: string;
  20. model: string;
  21. tool_calls: Array<{
  22. tool: string;
  23. result: unknown;
  24. }>;
  25. has_tools: boolean;
  26. }
  27. export interface MCPServer {
  28. id: string;
  29. name: string;
  30. url: string;
  31. auth_type: 'none' | 'jwt';
  32. enabled: boolean;
  33. }
  34. export interface MCPToolsResponse {
  35. tools: Array<{
  36. name: string;
  37. description: string;
  38. input_schema: Record<string, unknown>;
  39. }>;
  40. count: number;
  41. }
  42. export interface LoginRequest {
  43. email: string;
  44. password: string;
  45. }
  46. // 用户角色类型
  47. export type UserRole = 'reader' | 'author' | 'admin';
  48. // 用户信息接口
  49. export interface UserInfo {
  50. id?: string;
  51. username: string;
  52. email?: string;
  53. role: UserRole;
  54. }
  55. export interface LoginResponse {
  56. success: boolean;
  57. session_id: string;
  58. username: string;
  59. server: string;
  60. role: UserRole;
  61. token?: string;
  62. }
  63. export interface RegisterRequest {
  64. email: string;
  65. username: string;
  66. password: string;
  67. }
  68. export interface AuthStatusResponse {
  69. authenticated: boolean;
  70. username?: string;
  71. server?: string;
  72. role?: string;
  73. }
  74. // API 客户端类
  75. export class ApiClient {
  76. private baseUrl: string;
  77. private sessionId: string | null = null;
  78. private mcpTokens: Record<string, string> = {};
  79. private userInfo: UserInfo | null = null;
  80. constructor(baseUrl: string = BACKEND_URL) {
  81. this.baseUrl = baseUrl;
  82. }
  83. // 设置会话(同时持久化到 localStorage)
  84. setSession(sessionId: string) {
  85. this.sessionId = sessionId;
  86. // 持久化到 localStorage
  87. if (typeof window !== 'undefined') {
  88. localStorage.setItem('session_id', sessionId);
  89. }
  90. }
  91. // 设置用户信息
  92. setUserInfo(userInfo: UserInfo) {
  93. this.userInfo = userInfo;
  94. // 持久化到 localStorage
  95. if (typeof window !== 'undefined') {
  96. localStorage.setItem('userInfo', JSON.stringify(userInfo));
  97. }
  98. }
  99. // 获取用户信息
  100. getUserInfo(): UserInfo | null {
  101. return this.userInfo;
  102. }
  103. // 根据角色获取 MCP URL
  104. getMcpUrl(): string {
  105. if (!this.userInfo) {
  106. return '/mcp/'; // 默认使用 User MCP
  107. }
  108. // admin 使用 Admin MCP,其他角色使用 User MCP
  109. return this.userInfo.role === 'admin' ? '/admin-mcp/' : '/mcp/';
  110. }
  111. // 设置 MCP Tokens
  112. setMcpTokens(tokens: Record<string, string>) {
  113. this.mcpTokens = { ...tokens };
  114. }
  115. // 获取请求头
  116. private getHeaders(): HeadersInit {
  117. const headers: HeadersInit = {
  118. 'Content-Type': 'application/json',
  119. };
  120. // 携带所有已登录的 MCP Token
  121. const allMcpTokens = mcpTokenManager.getAllTokens();
  122. if (Object.keys(allMcpTokens).length > 0) {
  123. headers['X-MCP-Tokens'] = JSON.stringify(allMcpTokens);
  124. }
  125. return headers;
  126. }
  127. // 通用请求方法
  128. private async request<T>(
  129. endpoint: string,
  130. options: RequestInit = {}
  131. ): Promise<T> {
  132. const url = `${this.baseUrl}${endpoint}`;
  133. const response = await fetch(url, {
  134. ...options,
  135. headers: {
  136. ...this.getHeaders(),
  137. ...options.headers,
  138. },
  139. });
  140. if (!response.ok) {
  141. const error = await response.json().catch(() => ({ error: response.statusText }));
  142. throw new Error(error.error || error.detail || 'API request failed');
  143. }
  144. return response.json();
  145. }
  146. // 健康检查
  147. async health(): Promise<{ status: string; model: string; mcp_servers: string[] }> {
  148. return this.request('/health');
  149. }
  150. // 聊天(非流式)
  151. async chat(message: string, history: ChatMessage[] = []): Promise<ChatResponse> {
  152. return this.request<ChatResponse>('/chat', {
  153. method: 'POST',
  154. body: serialize({ message, history }),
  155. });
  156. }
  157. // 聊天(流式)- 返回 EventSource
  158. chatStream(message: string, history: ChatMessage[] = []): any {
  159. const url = new URL(`${this.baseUrl}/chat/stream`);
  160. const headers: Record<string, string> = {};
  161. if (this.sessionId) {
  162. headers['X-Session-ID'] = this.sessionId;
  163. }
  164. if (Object.keys(this.mcpTokens).length > 0) {
  165. headers['X-MCP-Tokens'] = JSON.stringify(this.mcpTokens);
  166. }
  167. // 构建请求体
  168. const body = { message, history };
  169. // 使用 fetch 创建流式连接
  170. const eventSource = new EventSourcePolyfill(url.toString(), {
  171. headers,
  172. method: 'POST',
  173. body: JSON.stringify(body),
  174. });
  175. return eventSource;
  176. }
  177. // 使用 fetch 的流式请求
  178. async chatStreamFetch(
  179. message: string,
  180. history: ChatMessage[] = [],
  181. onEvent: (event: MessageEvent) => void,
  182. onError?: (error: Error) => void,
  183. onComplete?: () => void
  184. ): Promise<() => void> {
  185. const url = `${this.baseUrl}/chat/stream`;
  186. const controller = new AbortController();
  187. try {
  188. const response = await fetch(url, {
  189. method: 'POST',
  190. headers: this.getHeaders(),
  191. body: JSON.stringify({ message, history }),
  192. signal: controller.signal,
  193. });
  194. if (!response.ok) {
  195. throw new Error(`HTTP error! status: ${response.status}`);
  196. }
  197. const reader = response.body?.getReader();
  198. const decoder = new TextDecoder();
  199. if (!reader) {
  200. throw new Error('Response body is null');
  201. }
  202. let buffer = '';
  203. let currentEvent = 'message'; // 默认事件类型
  204. while (true) {
  205. const { done, value } = await reader.read();
  206. if (done) {
  207. onComplete?.();
  208. break;
  209. }
  210. buffer += decoder.decode(value, { stream: true });
  211. // 处理 SSE 事件
  212. const lines = buffer.split('\n');
  213. buffer = lines.pop() || '';
  214. for (const line of lines) {
  215. if (line.startsWith('event: ')) {
  216. // 提取事件类型
  217. currentEvent = line.slice(7).trim();
  218. } else if (line.startsWith('data: ')) {
  219. // 提取数据并使用当前事件类型
  220. const data = line.slice(6);
  221. try {
  222. onEvent(new MessageEvent(currentEvent, { data }));
  223. } catch (e) {
  224. console.error('Error parsing SSE data:', e);
  225. }
  226. // 重置事件类型为默认值
  227. currentEvent = 'message';
  228. }
  229. }
  230. }
  231. } catch (error) {
  232. if (error instanceof Error && error.name !== 'AbortError') {
  233. onError?.(error);
  234. }
  235. }
  236. return () => controller.abort();
  237. }
  238. // 获取 MCP 服务器列表
  239. async listMcpServers(): Promise<{ servers: MCPServer[] }> {
  240. return this.request('/mcp/servers');
  241. }
  242. // 获取 MCP 工具列表
  243. async listMcpTools(): Promise<MCPToolsResponse> {
  244. return this.request('/mcp/tools');
  245. }
  246. // 用户登录
  247. async login(email: string, password: string): Promise<LoginResponse> {
  248. const response = await this.request<LoginResponse>('/auth/login', {
  249. method: 'POST',
  250. body: JSON.stringify({ email, password }),
  251. });
  252. if (response.session_id) {
  253. this.setSession(response.session_id);
  254. // 保存用户信息
  255. this.setUserInfo({
  256. username: response.username,
  257. role: response.role || 'reader',
  258. });
  259. }
  260. return response;
  261. }
  262. // 管理员登录
  263. async adminLogin(email: string, password: string): Promise<LoginResponse> {
  264. const response = await this.request<LoginResponse>('/auth/admin-login', {
  265. method: 'POST',
  266. body: JSON.stringify({ email, password }),
  267. });
  268. if (response.session_id) {
  269. this.setSession(response.session_id);
  270. // 保存用户信息
  271. this.setUserInfo({
  272. username: response.username,
  273. role: response.role || 'admin',
  274. });
  275. }
  276. return response;
  277. }
  278. // 用户注册
  279. async register(email: string, username: string, password: string): Promise<{ success: boolean; message: string; user: unknown }> {
  280. return this.request('/auth/register', {
  281. method: 'POST',
  282. body: JSON.stringify({ email, username, password }),
  283. });
  284. }
  285. // 登出(同时清除 localStorage)
  286. async logout(): Promise<{ success: boolean }> {
  287. const response = await this.request<{ success: boolean }>('/auth/logout', {
  288. method: 'POST',
  289. body: JSON.stringify({ session_id: this.sessionId }),
  290. });
  291. this.sessionId = null;
  292. this.mcpTokens = {};
  293. this.userInfo = null;
  294. // 清除 localStorage
  295. if (typeof window !== 'undefined') {
  296. localStorage.removeItem('session_id');
  297. localStorage.removeItem('username');
  298. localStorage.removeItem('userInfo');
  299. }
  300. return response;
  301. }
  302. // 检查认证状态
  303. async authStatus(): Promise<AuthStatusResponse> {
  304. return this.request('/auth/status');
  305. }
  306. }
  307. // EventSource polyfill for POST requests
  308. class EventSourcePolyfill {
  309. private url: string;
  310. private headers: Record<string, string>;
  311. private onmessage: ((event: MessageEvent) => void) | null = null;
  312. private onerror: ((event: Event) => void) | null = null;
  313. private eventSource: EventSource | null = null;
  314. constructor(url: string, options: { headers: Record<string, string>; method?: string; body?: string }) {
  315. this.url = url;
  316. this.headers = options.headers;
  317. // 简化实现 - 实际应用中需要更完整的 polyfill
  318. // 这里仅作为类型占位
  319. }
  320. addEventListener(type: string, listener: (event: MessageEvent) => void) {
  321. if (type === 'message') {
  322. this.onmessage = listener;
  323. }
  324. }
  325. close() {
  326. this.eventSource?.close();
  327. }
  328. }
  329. // 单例实例
  330. export const apiClient = new ApiClient();