api-client.ts 11 KB

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