From bf2bc70794174d5c6aea23e3464ab377940e038f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 14 Aug 2025 23:55:14 +0800 Subject: [PATCH] feat: refactor webui httpclient --- .../plugin-market/PluginMarketComponent.tsx | 6 +- web/src/app/infra/http/BackendClient.ts | 292 ++++++++++ web/src/app/infra/http/BaseHttpClient.ts | 195 +++++++ web/src/app/infra/http/CloudServiceClient.ts | 39 ++ web/src/app/infra/http/HttpClient.ts | 526 +----------------- web/src/app/infra/http/README.md | 68 +++ web/src/app/infra/http/index.ts | 86 +++ 7 files changed, 701 insertions(+), 511 deletions(-) create mode 100644 web/src/app/infra/http/BackendClient.ts create mode 100644 web/src/app/infra/http/BaseHttpClient.ts create mode 100644 web/src/app/infra/http/CloudServiceClient.ts create mode 100644 web/src/app/infra/http/README.md create mode 100644 web/src/app/infra/http/index.ts diff --git a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx index 3db0b290..054836e1 100644 --- a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx @@ -4,7 +4,7 @@ import { useEffect, useState, useRef } from 'react'; import styles from '@/app/home/plugins/plugins.module.css'; import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent'; -import { spaceClient } from '@/app/infra/http/HttpClient'; +import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { Input } from '@/components/ui/input'; import { @@ -41,6 +41,8 @@ export default function PluginMarketComponent({ const searchTimeout = useRef(null); const pageSize = 10; + const cloudServiceClient = getCloudServiceClientSync(); + useEffect(() => { initData(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -72,7 +74,7 @@ export default function PluginMarketComponent({ sortOrder: string = sortOrderValue, ) { setLoading(true); - spaceClient + cloudServiceClient .getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder) .then((res) => { setMarketPluginList( diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts new file mode 100644 index 00000000..a30a5380 --- /dev/null +++ b/web/src/app/infra/http/BackendClient.ts @@ -0,0 +1,292 @@ +import { BaseHttpClient } from './BaseHttpClient'; +import { + ApiRespProviderRequesters, + ApiRespProviderRequester, + ApiRespProviderLLMModels, + ApiRespProviderLLMModel, + LLMModel, + ApiRespPipelines, + Pipeline, + ApiRespPlatformAdapters, + ApiRespPlatformAdapter, + ApiRespPlatformBots, + ApiRespPlatformBot, + Bot, + ApiRespPlugins, + ApiRespPlugin, + ApiRespPluginConfig, + PluginReorderElement, + AsyncTaskCreatedResp, + ApiRespSystemInfo, + ApiRespAsyncTasks, + ApiRespUserToken, + GetPipelineResponseData, + GetPipelineMetadataResponseData, + AsyncTask, + ApiRespWebChatMessage, + ApiRespWebChatMessages, +} from '@/app/infra/entities/api'; +import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; +import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; + +/** + * 后端服务客户端 + * 负责与后端 API 的所有交互 + */ +export class BackendClient extends BaseHttpClient { + constructor(baseURL: string) { + super(baseURL, false); + } + + // ============ Provider API ============ + public getProviderRequesters(): Promise { + return this.get('/api/v1/provider/requesters'); + } + + public getProviderRequester(name: string): Promise { + return this.get(`/api/v1/provider/requesters/${name}`); + } + + public getProviderRequesterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/provider/requesters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + + `/api/v1/provider/requesters/${name}/icon` + ); + } + + // ============ Provider Model LLM ============ + public getProviderLLMModels(): Promise { + return this.get('/api/v1/provider/models/llm'); + } + + public getProviderLLMModel(uuid: string): Promise { + return this.get(`/api/v1/provider/models/llm/${uuid}`); + } + + public createProviderLLMModel(model: LLMModel): Promise { + return this.post('/api/v1/provider/models/llm', model); + } + + public deleteProviderLLMModel(uuid: string): Promise { + return this.delete(`/api/v1/provider/models/llm/${uuid}`); + } + + public updateProviderLLMModel( + uuid: string, + model: LLMModel, + ): Promise { + return this.put(`/api/v1/provider/models/llm/${uuid}`, model); + } + + public testLLMModel(uuid: string, model: LLMModel): Promise { + return this.post(`/api/v1/provider/models/llm/${uuid}/test`, model); + } + + // ============ Pipeline API ============ + public getGeneralPipelineMetadata(): Promise { + // as designed, this method will be deprecated, and only for developer to check the prefered config schema + return this.get('/api/v1/pipelines/_/metadata'); + } + + public getPipelines(): Promise { + return this.get('/api/v1/pipelines'); + } + + public getPipeline(uuid: string): Promise { + return this.get(`/api/v1/pipelines/${uuid}`); + } + + public createPipeline(pipeline: Pipeline): Promise<{ + uuid: string; + }> { + return this.post('/api/v1/pipelines', pipeline); + } + + public updatePipeline(uuid: string, pipeline: Pipeline): Promise { + return this.put(`/api/v1/pipelines/${uuid}`, pipeline); + } + + public deletePipeline(uuid: string): Promise { + return this.delete(`/api/v1/pipelines/${uuid}`); + } + + // ============ Debug WebChat API ============ + public sendWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + timeout: number = 15000, + ): Promise { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/send`, + { + session_type: sessionType, + message: messageChain, + }, + { + timeout, + }, + ); + } + + public getWebChatHistoryMessages( + pipelineId: string, + sessionType: string, + ): Promise { + return this.get( + `/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`, + ); + } + + public resetWebChatSession( + pipelineId: string, + sessionType: string, + ): Promise<{ message: string }> { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`, + ); + } + + // ============ Platform API ============ + public getAdapters(): Promise { + return this.get('/api/v1/platform/adapters'); + } + + public getAdapter(name: string): Promise { + return this.get(`/api/v1/platform/adapters/${name}`); + } + + public getAdapterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/platform/adapters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + `/api/v1/platform/adapters/${name}/icon` + ); + } + + // ============ Platform Bots ============ + public getBots(): Promise { + return this.get('/api/v1/platform/bots'); + } + + public getBot(uuid: string): Promise { + return this.get(`/api/v1/platform/bots/${uuid}`); + } + + public createBot(bot: Bot): Promise<{ uuid: string }> { + return this.post('/api/v1/platform/bots', bot); + } + + public updateBot(uuid: string, bot: Bot): Promise { + return this.put(`/api/v1/platform/bots/${uuid}`, bot); + } + + public deleteBot(uuid: string): Promise { + return this.delete(`/api/v1/platform/bots/${uuid}`); + } + + public getBotLogs( + botId: string, + request: GetBotLogsRequest, + ): Promise { + return this.post(`/api/v1/platform/bots/${botId}/logs`, request); + } + + // ============ Plugins API ============ + public getPlugins(): Promise { + return this.get('/api/v1/plugins'); + } + + public getPlugin(author: string, name: string): Promise { + return this.get(`/api/v1/plugins/${author}/${name}`); + } + + public getPluginConfig( + author: string, + name: string, + ): Promise { + return this.get(`/api/v1/plugins/${author}/${name}/config`); + } + + public updatePluginConfig( + author: string, + name: string, + config: object, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/config`, config); + } + + public togglePlugin( + author: string, + name: string, + target_enabled: boolean, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { + target_enabled, + }); + } + + public reorderPlugins(plugins: PluginReorderElement[]): Promise { + return this.put('/api/v1/plugins/reorder', { plugins }); + } + + public updatePlugin( + author: string, + name: string, + ): Promise { + return this.post(`/api/v1/plugins/${author}/${name}/update`); + } + + public installPluginFromGithub( + source: string, + ): Promise { + return this.post('/api/v1/plugins/install/github', { source }); + } + + public removePlugin( + author: string, + name: string, + ): Promise { + return this.delete(`/api/v1/plugins/${author}/${name}`); + } + + // ============ System API ============ + public getSystemInfo(): Promise { + return this.get('/api/v1/system/info'); + } + + public getAsyncTasks(): Promise { + return this.get('/api/v1/system/tasks'); + } + + public getAsyncTask(id: number): Promise { + return this.get(`/api/v1/system/tasks/${id}`); + } + + // ============ User API ============ + public checkIfInited(): Promise<{ initialized: boolean }> { + return this.get('/api/v1/user/init'); + } + + public initUser(user: string, password: string): Promise { + return this.post('/api/v1/user/init', { user, password }); + } + + public authUser(user: string, password: string): Promise { + return this.post('/api/v1/user/auth', { user, password }); + } + + public checkUserToken(): Promise { + return this.get('/api/v1/user/check-token'); + } +} diff --git a/web/src/app/infra/http/BaseHttpClient.ts b/web/src/app/infra/http/BaseHttpClient.ts new file mode 100644 index 00000000..dc440799 --- /dev/null +++ b/web/src/app/infra/http/BaseHttpClient.ts @@ -0,0 +1,195 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + AxiosError, +} from 'axios'; + +type JSONValue = string | number | boolean | JSONObject | JSONArray | null; +interface JSONObject { + [key: string]: JSONValue; +} +type JSONArray = Array; + +export interface ResponseData { + code: number; + message: string; + data: T; + timestamp: number; +} + +export interface RequestConfig extends AxiosRequestConfig { + isSSR?: boolean; // 服务端渲染标识 + retry?: number; // 重试次数 +} + +/** + * 基础 HTTP 客户端类 + * 提供通用的 HTTP 请求方法和拦截器配置 + */ +export abstract class BaseHttpClient { + protected instance: AxiosInstance; + protected disableToken: boolean = false; + protected baseURL: string; + + constructor(baseURL: string, disableToken?: boolean) { + this.baseURL = baseURL; + this.disableToken = disableToken || false; + + this.instance = axios.create({ + baseURL: baseURL, + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.initInterceptors(); + } + + // 外部获取baseURL的方法 + public getBaseUrl(): string { + return this.baseURL; + } + + // 更新 baseURL + public updateBaseURL(newBaseURL: string): void { + this.baseURL = newBaseURL; + this.instance.defaults.baseURL = newBaseURL; + } + + // 同步获取Session + protected getSessionSync(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; + } + + // 拦截器配置 + protected initInterceptors(): void { + // 请求拦截 + this.instance.interceptors.request.use( + async (config) => { + // 客户端添加认证头 + if (typeof window !== 'undefined' && !this.disableToken) { + const session = this.getSessionSync(); + if (session) { + config.headers.Authorization = `Bearer ${session}`; + } + } + + return config; + }, + (error) => Promise.reject(error), + ); + + // 响应拦截 + this.instance.interceptors.response.use( + (response: AxiosResponse) => { + return response; + }, + (error: AxiosError) => { + // 统一错误处理 + if (error.response) { + const { status, data } = error.response; + const errMessage = data?.message || error.message; + + switch (status) { + case 401: + console.log('401 error: ', errMessage, error.request); + console.log('responseURL', error.request.responseURL); + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + if (!error.request.responseURL.includes('/check-token')) { + window.location.href = '/login'; + } + } + break; + case 403: + console.error('Permission denied:', errMessage); + break; + case 500: + console.error('Server error:', errMessage); + break; + } + + return Promise.reject({ + code: data?.code || status, + message: errMessage, + data: data?.data || null, + }); + } + + return Promise.reject({ + code: -1, + message: error.message || 'Network Error', + data: null, + }); + }, + ); + } + + // 转换下划线为驼峰 + protected convertKeysToCamel(obj: JSONValue): JSONValue { + if (Array.isArray(obj)) { + return obj.map((v) => this.convertKeysToCamel(v)); + } else if (obj !== null && typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const camelKey = key.replace(/_([a-z])/g, (_, letter) => + letter.toUpperCase(), + ); + acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]); + return acc; + }, {} as JSONObject); + } + return obj; + } + + // 错误处理 + protected handleError(error: object): never { + if (axios.isCancel(error)) { + throw { code: -2, message: 'Request canceled', data: null }; + } + throw error; + } + + // 核心请求方法 + public async request(config: RequestConfig): Promise { + try { + const response = await this.instance.request>(config); + return response.data.data; + } catch (error) { + return this.handleError(error as object); + } + } + + // 快捷方法 + public get( + url: string, + params?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'get', url, params, ...config }); + } + + public post( + url: string, + data?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'post', url, data, ...config }); + } + + public put( + url: string, + data?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'put', url, data, ...config }); + } + + public delete(url: string, config?: RequestConfig): Promise { + return this.request({ method: 'delete', url, ...config }); + } +} diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts new file mode 100644 index 00000000..668808ab --- /dev/null +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -0,0 +1,39 @@ +import { BaseHttpClient } from './BaseHttpClient'; +import { MarketPluginResponse } from '@/app/infra/entities/api'; + +/** + * 云服务客户端 + * 负责与 cloud service 的所有交互 + */ +export class CloudServiceClient extends BaseHttpClient { + constructor(baseURL: string = '') { + // cloud service 不需要 token 认证 + super(baseURL, true); + } + + /** + * 获取插件市场插件列表 + * @param page 页码 + * @param page_size 每页大小 + * @param query 搜索关键词 + * @param sort_by 排序字段 + * @param sort_order 排序顺序 + */ + public getMarketPlugins( + page: number, + page_size: number, + query: string, + sort_by: string = 'stars', + sort_order: string = 'DESC', + ): Promise { + return this.post(`/api/v1/market/plugins`, { + page, + page_size, + query, + sort_by, + sort_order, + }); + } + + // 未来可以在这里添加更多 cloud service 相关的方法 +} diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index e5192063..4e6f864f 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -1,509 +1,17 @@ -import axios, { - AxiosInstance, - AxiosRequestConfig, - AxiosResponse, - AxiosError, -} from 'axios'; -import { - ApiRespProviderRequesters, - ApiRespProviderRequester, - ApiRespProviderLLMModels, - ApiRespProviderLLMModel, - LLMModel, - ApiRespPipelines, - Pipeline, - ApiRespPlatformAdapters, - ApiRespPlatformAdapter, - ApiRespPlatformBots, - ApiRespPlatformBot, - Bot, - ApiRespPlugins, - ApiRespPlugin, - ApiRespPluginConfig, - PluginReorderElement, - AsyncTaskCreatedResp, - ApiRespSystemInfo, - ApiRespAsyncTasks, - ApiRespUserToken, - MarketPluginResponse, - GetPipelineResponseData, - GetPipelineMetadataResponseData, - AsyncTask, - ApiRespWebChatMessage, - ApiRespWebChatMessages, -} from '@/app/infra/entities/api'; -import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; -import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; - -type JSONValue = string | number | boolean | JSONObject | JSONArray | null; -interface JSONObject { - [key: string]: JSONValue; -} -type JSONArray = Array; - -export interface ResponseData { - code: number; - message: string; - data: T; - timestamp: number; -} - -export interface RequestConfig extends AxiosRequestConfig { - isSSR?: boolean; // 服务端渲染标识 - retry?: number; // 重试次数 -} - -export let systemInfo: ApiRespSystemInfo = { - debug: false, - version: '', - cloud_service_url: '', -}; - -class HttpClient { - private instance: AxiosInstance; - private disableToken: boolean = false; - private baseURL: string; - // 暂不需要SSR - // private ssrInstance: AxiosInstance | null = null - - constructor(baseURL: string, disableToken?: boolean) { - this.baseURL = baseURL; - this.instance = axios.create({ - baseURL: baseURL, - timeout: 15000, - headers: { - 'Content-Type': 'application/json', - }, - }); - this.disableToken = disableToken || false; - this.initInterceptors(); - - if ( - systemInfo.cloud_service_url === '' && - baseURL != 'https://space.langbot.app' - ) { - this.getSystemInfo().then((res) => { - systemInfo = res; - }); - } - } - - // 外部获取baseURL的方法 - getBaseUrl(): string { - return this.baseURL; - } - - // 同步获取Session - private getSessionSync() { - // NOT IMPLEMENT - return localStorage.getItem('token'); - } - - // 拦截器配置 - private initInterceptors() { - // 请求拦截 - this.instance.interceptors.request.use( - async (config) => { - // 服务端请求自动携带 cookie, Langbot暂时用不到SSR相关 - // if (typeof window === 'undefined' && config.isSSR) { } - // cookie not required - // const { cookies } = await import('next/headers') - // config.headers.Cookie = cookies().toString() - - // 客户端添加认证头 - if (typeof window !== 'undefined' && !this.disableToken) { - const session = this.getSessionSync(); - config.headers.Authorization = `Bearer ${session}`; - } - - return config; - }, - (error) => Promise.reject(error), - ); - - // 响应拦截 - this.instance.interceptors.response.use( - (response: AxiosResponse) => { - // 响应拦截处理写在这里,暂无业务需要 - - return response; - }, - (error: AxiosError) => { - // 统一错误处理 - if (error.response) { - const { status, data } = error.response; - const errMessage = data?.message || error.message; - - switch (status) { - case 401: - console.log('401 error: ', errMessage, error.request); - console.log('responseURL', error.request.responseURL); - localStorage.removeItem('token'); - if (!error.request.responseURL.includes('/check-token')) { - window.location.href = '/login'; - } - break; - case 403: - console.error('Permission denied:', errMessage); - break; - case 500: - // NOTE: move to component layer for customized message? - // toast.error(errMessage); - console.error('Server error:', errMessage); - break; - } - - return Promise.reject({ - code: data?.code || status, - message: errMessage, - data: data?.data || null, - }); - } - - return Promise.reject({ - code: -1, - message: error.message || 'Network Error', - data: null, - }); - }, - ); - } - - // 转换下划线为驼峰 - private convertKeysToCamel(obj: JSONValue): JSONValue { - if (Array.isArray(obj)) { - return obj.map((v) => this.convertKeysToCamel(v)); - } else if (obj !== null && typeof obj === 'object') { - return Object.keys(obj).reduce((acc, key) => { - const camelKey = key.replace(/_([a-z])/g, (_, letter) => - letter.toUpperCase(), - ); - acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]); - return acc; - }, {} as JSONObject); - } - return obj; - } - - // 核心请求方法 - public async request(config: RequestConfig): Promise { - try { - // 这里未来如果需要SSR可以将前面替换为SSR的instance - const instance = config.isSSR ? this.instance : this.instance; - const response = await instance.request>(config); - return response.data.data; - } catch (error) { - return this.handleError(error as object); - } - } - - private handleError(error: object): never { - if (axios.isCancel(error)) { - throw { code: -2, message: 'Request canceled', data: null }; - } - throw error; - } - - // 快捷方法 - public get( - url: string, - params?: object, - config?: RequestConfig, - ) { - return this.request({ method: 'get', url, params, ...config }); - } - - public post(url: string, data?: object, config?: RequestConfig) { - return this.request({ method: 'post', url, data, ...config }); - } - - public put(url: string, data?: object, config?: RequestConfig) { - return this.request({ method: 'put', url, data, ...config }); - } - - public delete(url: string, config?: RequestConfig) { - return this.request({ method: 'delete', url, ...config }); - } - - // real api request implementation - // ============ Provider API ============ - public getProviderRequesters(): Promise { - return this.get('/api/v1/provider/requesters'); - } - - public getProviderRequester(name: string): Promise { - return this.get(`/api/v1/provider/requesters/${name}`); - } - - public getProviderRequesterIconURL(name: string): string { - if (this.instance.defaults.baseURL === '/') { - // 获取用户访问的URL - const url = window.location.href; - const baseURL = url.split('/').slice(0, 3).join('/'); - return `${baseURL}/api/v1/provider/requesters/${name}/icon`; - } - return ( - this.instance.defaults.baseURL + - `/api/v1/provider/requesters/${name}/icon` - ); - } - - // ============ Provider Model LLM ============ - public getProviderLLMModels(): Promise { - return this.get('/api/v1/provider/models/llm'); - } - - public getProviderLLMModel(uuid: string): Promise { - return this.get(`/api/v1/provider/models/llm/${uuid}`); - } - - public createProviderLLMModel(model: LLMModel): Promise { - return this.post('/api/v1/provider/models/llm', model); - } - - public deleteProviderLLMModel(uuid: string): Promise { - return this.delete(`/api/v1/provider/models/llm/${uuid}`); - } - - public updateProviderLLMModel( - uuid: string, - model: LLMModel, - ): Promise { - return this.put(`/api/v1/provider/models/llm/${uuid}`, model); - } - - public testLLMModel(uuid: string, model: LLMModel): Promise { - return this.post(`/api/v1/provider/models/llm/${uuid}/test`, model); - } - - // ============ Pipeline API ============ - public getGeneralPipelineMetadata(): Promise { - // as designed, this method will be deprecated, and only for developer to check the prefered config schema - return this.get('/api/v1/pipelines/_/metadata'); - } - - public getPipelines(): Promise { - return this.get('/api/v1/pipelines'); - } - - public getPipeline(uuid: string): Promise { - return this.get(`/api/v1/pipelines/${uuid}`); - } - - public createPipeline(pipeline: Pipeline): Promise<{ - uuid: string; - }> { - return this.post('/api/v1/pipelines', pipeline); - } - - public updatePipeline(uuid: string, pipeline: Pipeline): Promise { - return this.put(`/api/v1/pipelines/${uuid}`, pipeline); - } - - public deletePipeline(uuid: string): Promise { - return this.delete(`/api/v1/pipelines/${uuid}`); - } - - // ============ Debug WebChat API ============ - public sendWebChatMessage( - sessionType: string, - messageChain: object[], - pipelineId: string, - timeout: number = 15000, - ): Promise { - return this.post( - `/api/v1/pipelines/${pipelineId}/chat/send`, - { - session_type: sessionType, - message: messageChain, - }, - { - timeout, - }, - ); - } - - public getWebChatHistoryMessages( - pipelineId: string, - sessionType: string, - ): Promise { - return this.get( - `/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`, - ); - } - - public resetWebChatSession( - pipelineId: string, - sessionType: string, - ): Promise<{ message: string }> { - return this.post( - `/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`, - ); - } - - // ============ Platform API ============ - public getAdapters(): Promise { - return this.get('/api/v1/platform/adapters'); - } - - public getAdapter(name: string): Promise { - return this.get(`/api/v1/platform/adapters/${name}`); - } - - public getAdapterIconURL(name: string): string { - if (this.instance.defaults.baseURL === '/') { - // 获取用户访问的URL - const url = window.location.href; - const baseURL = url.split('/').slice(0, 3).join('/'); - return `${baseURL}/api/v1/platform/adapters/${name}/icon`; - } - return ( - this.instance.defaults.baseURL + `/api/v1/platform/adapters/${name}/icon` - ); - } - - // ============ Platform Bots ============ - public getBots(): Promise { - return this.get('/api/v1/platform/bots'); - } - - public getBot(uuid: string): Promise { - return this.get(`/api/v1/platform/bots/${uuid}`); - } - - public createBot(bot: Bot): Promise<{ uuid: string }> { - return this.post('/api/v1/platform/bots', bot); - } - - public updateBot(uuid: string, bot: Bot): Promise { - return this.put(`/api/v1/platform/bots/${uuid}`, bot); - } - - public deleteBot(uuid: string): Promise { - return this.delete(`/api/v1/platform/bots/${uuid}`); - } - - public getBotLogs( - botId: string, - request: GetBotLogsRequest, - ): Promise { - return this.post(`/api/v1/platform/bots/${botId}/logs`, request); - } - - // ============ Plugins API ============ - public getPlugins(): Promise { - return this.get('/api/v1/plugins'); - } - - public getPlugin(author: string, name: string): Promise { - return this.get(`/api/v1/plugins/${author}/${name}`); - } - - public getPluginConfig( - author: string, - name: string, - ): Promise { - return this.get(`/api/v1/plugins/${author}/${name}/config`); - } - - public updatePluginConfig( - author: string, - name: string, - config: object, - ): Promise { - return this.put(`/api/v1/plugins/${author}/${name}/config`, config); - } - - public togglePlugin( - author: string, - name: string, - target_enabled: boolean, - ): Promise { - return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { - target_enabled, - }); - } - - public reorderPlugins(plugins: PluginReorderElement[]): Promise { - return this.put('/api/v1/plugins/reorder', { plugins }); - } - - public updatePlugin( - author: string, - name: string, - ): Promise { - return this.post(`/api/v1/plugins/${author}/${name}/update`); - } - - public getMarketPlugins( - page: number, - page_size: number, - query: string, - sort_by: string = 'stars', - sort_order: string = 'DESC', - ): Promise { - return this.post(`/api/v1/market/plugins`, { - page, - page_size, - query, - sort_by, - sort_order, - }); - } - - public installPluginFromGithub( - source: string, - ): Promise { - return this.post('/api/v1/plugins/install/github', { source }); - } - - public removePlugin( - author: string, - name: string, - ): Promise { - return this.delete(`/api/v1/plugins/${author}/${name}`); - } - - // ============ System API ============ - public getSystemInfo(): Promise { - return this.get('/api/v1/system/info'); - } - - public getAsyncTasks(): Promise { - return this.get('/api/v1/system/tasks'); - } - - public getAsyncTask(id: number): Promise { - return this.get(`/api/v1/system/tasks/${id}`); - } - - // ============ User API ============ - public checkIfInited(): Promise<{ initialized: boolean }> { - return this.get('/api/v1/user/init'); - } - - public initUser(user: string, password: string): Promise { - return this.post('/api/v1/user/init', { user, password }); - } - - public authUser(user: string, password: string): Promise { - return this.post('/api/v1/user/auth', { user, password }); - } - - public checkUserToken(): Promise { - return this.get('/api/v1/user/check-token'); - } -} - -const getBaseURL = (): string => { - if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) { - return process.env.NEXT_PUBLIC_API_BASE_URL; - } - - return '/'; -}; - -export const httpClient = new HttpClient(getBaseURL()); - -// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 -export const spaceClient = new HttpClient(systemInfo.cloud_service_url); +/** + * @deprecated 此文件仅用于向后兼容。请使用新的 client: + * - import { backendClient } from '@/app/infra/http' + * - import { getCloudServiceClient } from '@/app/infra/http' + */ + +// 重新导出新的客户端实现,保持向后兼容 +export { + backendClient as httpClient, + systemInfo, + type ResponseData, + type RequestConfig, +} from './index'; + +// 为了兼容性,重新导出 BackendClient 作为 HttpClient +import { BackendClient } from './BackendClient'; +export const HttpClient = BackendClient; diff --git a/web/src/app/infra/http/README.md b/web/src/app/infra/http/README.md new file mode 100644 index 00000000..b305d8a9 --- /dev/null +++ b/web/src/app/infra/http/README.md @@ -0,0 +1,68 @@ +# HTTP Client 架构说明 + +## 概述 + +HTTP Client 已经重构为更清晰的架构,将通用方法与业务逻辑分离,并为不同的服务创建了独立的客户端。 + +## 文件结构 + +- **BaseHttpClient.ts** - 基础 HTTP 客户端类,包含所有通用的 HTTP 方法和拦截器配置 +- **BackendClient.ts** - 后端服务客户端,处理与后端 API 的所有交互 +- **CloudServiceClient.ts** - 云服务客户端,处理与 cloud service 的交互(如插件市场) +- **index.ts** - 主入口文件,管理客户端实例的创建和导出 +- **HttpClient.ts** - 仅用于向后兼容的文件(已废弃) + +## 使用方法 + +### 新的推荐用法 + +```typescript +// 使用后端客户端 +import { backendClient } from '@/app/infra/http'; + +// 获取模型列表 +const models = await backendClient.getProviderLLMModels(); + +// 使用云服务客户端(异步方式,确保 URL 已初始化) +import { getCloudServiceClient } from '@/app/infra/http'; + +const cloudClient = await getCloudServiceClient(); +const marketPlugins = await cloudClient.getMarketPlugins(1, 10, 'search term'); + +// 使用云服务客户端(同步方式,可能使用默认 URL) +import { cloudServiceClient } from '@/app/infra/http'; + +const marketPlugins = await cloudServiceClient.getMarketPlugins(1, 10, 'search term'); +``` + +### 向后兼容(不推荐) + +```typescript +// 旧的用法仍然可以工作 +import { httpClient, spaceClient } from '@/app/infra/http/HttpClient'; + +// httpClient 现在指向 backendClient +const models = await httpClient.getProviderLLMModels(); + +// spaceClient 现在指向 cloudServiceClient +const marketPlugins = await spaceClient.getMarketPlugins(1, 10, 'search term'); +``` + +## 特点 + +1. **清晰的职责分离** + - BaseHttpClient:通用 HTTP 功能 + - BackendClient:后端 API 业务逻辑 + - CloudServiceClient:云服务 API 业务逻辑 + +2. **自动初始化** + - 应用启动时自动从后端获取 cloud service URL + - 云服务客户端会自动更新 baseURL + +3. **类型安全** + - 所有方法都有完整的 TypeScript 类型定义 + - 请求和响应类型都从 `@/app/infra/entities/api` 导入 + +4. **向后兼容** + - 旧代码无需修改即可继续工作 + - 逐步迁移到新的 API diff --git a/web/src/app/infra/http/index.ts b/web/src/app/infra/http/index.ts new file mode 100644 index 00000000..dad2b68c --- /dev/null +++ b/web/src/app/infra/http/index.ts @@ -0,0 +1,86 @@ +import { BackendClient } from './BackendClient'; +import { CloudServiceClient } from './CloudServiceClient'; +import { ApiRespSystemInfo } from '@/app/infra/entities/api'; + +// 系统信息 +export let systemInfo: ApiRespSystemInfo = { + debug: false, + version: '', + cloud_service_url: '', +}; + +/** + * 获取基础 URL + */ +const getBaseURL = (): string => { + if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) { + return process.env.NEXT_PUBLIC_API_BASE_URL; + } + return '/'; +}; + +// 创建后端客户端实例 +export const backendClient = new BackendClient(getBaseURL()); + +// 创建云服务客户端实例(初始化时使用默认 URL) +export const cloudServiceClient = new CloudServiceClient( + 'https://space.langbot.app', +); + +// 应用启动时自动初始化系统信息 +if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') { + backendClient + .getSystemInfo() + .then((info) => { + systemInfo = info; + cloudServiceClient.updateBaseURL(info.cloud_service_url); + }) + .catch((error) => { + console.error('Failed to initialize system info on startup:', error); + }); +} + +/** + * 获取云服务客户端 + * 如果 cloud service URL 尚未初始化,会自动从后端获取 + */ +export const getCloudServiceClient = async (): Promise => { + if (systemInfo.cloud_service_url === '') { + try { + systemInfo = await backendClient.getSystemInfo(); + // 更新 cloud service client 的 baseURL + cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url); + } catch (error) { + console.error('Failed to get system info:', error); + // 如果获取失败,继续使用默认 URL + } + } + return cloudServiceClient; +}; + +/** + * 获取云服务客户端(同步版本) + * 注意:如果 cloud service URL 尚未初始化,将使用默认 URL + */ +export const getCloudServiceClientSync = (): CloudServiceClient => { + return cloudServiceClient; +}; + +/** + * 手动初始化系统信息 + * 可以在应用启动时调用此方法预先获取系统信息 + */ +export const initializeSystemInfo = async (): Promise => { + try { + systemInfo = await backendClient.getSystemInfo(); + cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url); + } catch (error) { + console.error('Failed to initialize system info:', error); + } +}; + +// 导出类型,以便其他地方使用 +export type { ResponseData, RequestConfig } from './BaseHttpClient'; +export { BaseHttpClient } from './BaseHttpClient'; +export { BackendClient } from './BackendClient'; +export { CloudServiceClient } from './CloudServiceClient';