2
0

api-client.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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. };
  208. const response = await fetch(url, {
  209. method: 'POST',
  210. headers: this.getHeaders(),
  211. body: JSON.stringify(requestBody),
  212. signal: controller.signal,
  213. });
  214. if (!response.ok) {
  215. throw new Error(`HTTP error! status: ${response.status}`);
  216. }
  217. const reader = response.body?.getReader();
  218. const decoder = new TextDecoder();
  219. if (!reader) {
  220. throw new Error('Response body is null');
  221. }
  222. let buffer = '';
  223. let currentEvent = 'message'; // 默认事件类型
  224. while (true) {
  225. const { done, value } = await reader.read();
  226. if (done) {
  227. onComplete?.();
  228. break;
  229. }
  230. buffer += decoder.decode(value, { stream: true });
  231. // 处理 SSE 事件
  232. const lines = buffer.split('\n');
  233. buffer = lines.pop() || '';
  234. for (const line of lines) {
  235. if (line.startsWith('event: ')) {
  236. // 提取事件类型
  237. currentEvent = line.slice(7).trim();
  238. } else if (line.startsWith('data: ')) {
  239. // 提取数据并使用当前事件类型
  240. const data = line.slice(6);
  241. try {
  242. onEvent(new MessageEvent(currentEvent, { data }));
  243. } catch (e) {
  244. console.error('Error parsing SSE data:', e);
  245. }
  246. // 重置事件类型为默认值
  247. currentEvent = 'message';
  248. }
  249. }
  250. }
  251. } catch (error) {
  252. if (error instanceof Error && error.name !== 'AbortError') {
  253. onError?.(error);
  254. }
  255. }
  256. return () => controller.abort();
  257. }
  258. // 获取 MCP 服务器列表
  259. async listMcpServers(): Promise<{ servers: MCPServer[] }> {
  260. return this.request('/mcp/servers');
  261. }
  262. // 获取 MCP 工具列表
  263. async listMcpTools(): Promise<MCPToolsResponse> {
  264. return this.request('/mcp/tools');
  265. }
  266. // 用户登录
  267. async login(email: string, password: string): Promise<LoginResponse> {
  268. const response = await this.request<LoginResponse>('/auth/login', {
  269. method: 'POST',
  270. body: JSON.stringify({ email, password }),
  271. });
  272. if (response.session_id) {
  273. this.setSession(response.session_id);
  274. // 保存用户信息
  275. this.setUserInfo({
  276. username: response.username,
  277. role: response.role || 'reader',
  278. });
  279. }
  280. return response;
  281. }
  282. // 管理员登录
  283. async adminLogin(email: string, password: string): Promise<LoginResponse> {
  284. const response = await this.request<LoginResponse>('/auth/admin-login', {
  285. method: 'POST',
  286. body: JSON.stringify({ email, password }),
  287. });
  288. if (response.session_id) {
  289. this.setSession(response.session_id);
  290. // 保存用户信息
  291. this.setUserInfo({
  292. username: response.username,
  293. role: response.role || 'admin',
  294. });
  295. }
  296. return response;
  297. }
  298. // 用户注册
  299. async register(email: string, username: string, password: string): Promise<{ success: boolean; message: string; user: unknown }> {
  300. return this.request('/auth/register', {
  301. method: 'POST',
  302. body: JSON.stringify({ email, username, password }),
  303. });
  304. }
  305. // 登出(同时清除 localStorage)
  306. async logout(): Promise<{ success: boolean }> {
  307. const response = await this.request<{ success: boolean }>('/auth/logout', {
  308. method: 'POST',
  309. body: JSON.stringify({ session_id: this.sessionId }),
  310. });
  311. this.sessionId = null;
  312. this.mcpTokens = {};
  313. this.userInfo = null;
  314. // 清除 localStorage
  315. if (typeof window !== 'undefined') {
  316. localStorage.removeItem('session_id');
  317. localStorage.removeItem('username');
  318. localStorage.removeItem('userInfo');
  319. }
  320. return response;
  321. }
  322. // 检查认证状态
  323. async authStatus(): Promise<AuthStatusResponse> {
  324. return this.request('/auth/status');
  325. }
  326. }
  327. // EventSource polyfill for POST requests
  328. class EventSourcePolyfill {
  329. private url: string;
  330. private headers: Record<string, string>;
  331. private onmessage: ((event: MessageEvent) => void) | null = null;
  332. private onerror: ((event: Event) => void) | null = null;
  333. private eventSource: EventSource | null = null;
  334. constructor(url: string, options: { headers: Record<string, string>; method?: string; body?: string }) {
  335. this.url = url;
  336. this.headers = options.headers;
  337. // 简化实现 - 实际应用中需要更完整的 polyfill
  338. // 这里仅作为类型占位
  339. }
  340. addEventListener(type: string, listener: (event: MessageEvent) => void) {
  341. if (type === 'message') {
  342. this.onmessage = listener;
  343. }
  344. }
  345. close() {
  346. this.eventSource?.close();
  347. }
  348. }
  349. // 单例实例
  350. export const apiClient = new ApiClient();