From b8b8fabe156f9ee73794a85e05f8d9b73a536406 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 18 Aug 2026 00:29:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E8=87=AA=E7=94=B1=20API=20=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/__tests__/client.spec.ts | 13 +++++++++++++ src/api/__tests__/index.spec.ts | 2 +- src/api/client.ts | 12 ++++++++---- src/api/index.ts | 7 ++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/api/__tests__/client.spec.ts b/src/api/__tests__/client.spec.ts index 6d858379..72569751 100644 --- a/src/api/__tests__/client.spec.ts +++ b/src/api/__tests__/client.spec.ts @@ -154,6 +154,19 @@ describe('MoviePilot API client', () => { expect(notifier.error).toHaveBeenCalledWith('Plugin rejected request') }) + it.each([ + ['object', { plugin: 'demo', count: 2 }], + ['array', [{ id: 1 }, { id: 2 }]], + ['scalar', 'ready'], + ['null', null], + ])('插件客户端原样交付非 envelope payload:%s', async (_label, payload) => { + const { pluginApi } = createApiClients({ adapter: resolveWith(payload), notifier }) + + await expect(pluginApi.get('/plugin/demo')).resolves.toEqual(payload) + expect(notifier.error).not.toHaveBeenCalled() + expect(notifier.success).not.toHaveBeenCalled() + }) + it('Blob 成功响应绕过 envelope 解包', async () => { const blob = new Blob(['moviepilot'], { type: 'application/octet-stream' }) const { api } = createApiClients({ adapter: resolveWith(blob), notifier }) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 2ac5a4c6..a201e058 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -74,7 +74,7 @@ describe('API application wiring', () => { vi.useRealTimers() }) - it('向 window 暴露插件 envelope 客户端,而内部默认导出数据客户端', async () => { + it('向 window 暴露插件最终 payload 客户端,而内部默认导出数据客户端', async () => { const module = await import('@/api') expect(window.MoviePilotAPI).toBe(module.pluginApi) diff --git a/src/api/client.ts b/src/api/client.ts index 37b086b9..1e34c636 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -68,6 +68,9 @@ export interface DataApiClient extends Omit< ): Promise } +/** 插件与联邦组件接收 endpoint 最终 payload,方法签名不暴露 AxiosResponse 外壳。 */ +export type PluginApiClient = DataApiClient + /** 请求层使用的反馈出口,由应用入口接到全局 Toast。 */ export interface ApiFeedbackNotifier { error(message: string): void @@ -83,7 +86,7 @@ export interface ApiClientHooks { reportConnectionFailure?(reason: 'network-error' | 'timeout' | 'server-unreachable'): void } -/** 创建内部数据客户端与插件原始协议客户端时所需的配置。 */ +/** 创建内部数据客户端与插件最终 payload 客户端时所需的配置。 */ export interface CreateApiClientsOptions extends CreateAxiosDefaults { hooks?: ApiClientHooks notifier?: ApiFeedbackNotifier @@ -181,10 +184,10 @@ export function resolveConnectionFailureReason( return null } -/** 创建普通数据客户端和保持插件 ABI 的原始 envelope 客户端。 */ +/** 创建普通数据客户端和保持插件自由响应 ABI 的最终 payload 客户端。 */ export function createApiClients(options: CreateApiClientsOptions = {}): { api: DataApiClient - pluginApi: AxiosInstance + pluginApi: PluginApiClient } { const { hooks, notifier, resolveFallbackMessage, setupInstance, ...axiosConfig } = options const api = axios.create(axiosConfig) @@ -202,7 +205,7 @@ export function createApiClients(options: CreateApiClientsOptions = {}): { return { api: api as DataApiClient, - pluginApi, + pluginApi: pluginApi as PluginApiClient, } } @@ -226,6 +229,7 @@ function installResponseInterceptors( const payload: unknown = response.data if (!isApiResponse(payload)) { + // 联邦插件自行声明 API 响应;非宿主 envelope 必须原样交给插件调用方。 if (responseMode === 'envelope') return payload const error = new ApiRequestError(resolveFallback('invalid-envelope', resolveFallbackMessage), { code: AxiosError.ERR_BAD_RESPONSE, diff --git a/src/api/index.ts b/src/api/index.ts index 7ca76f7e..2b6a352e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -14,6 +14,7 @@ import { type ApiFeedbackMode, type ApiFallbackMessageKey, type DataApiClient, + type PluginApiClient, } from './client' /** 带连接探测和反馈策略的 MoviePilot 请求配置。 */ @@ -70,7 +71,7 @@ const { api, pluginApi } = createApiClients({ declare global { interface Window { - MoviePilotAPI: AxiosInstance + MoviePilotAPI: PluginApiClient } } @@ -88,10 +89,10 @@ function initializeClient(instance: AxiosInstance | DataApiClient) { }) } -// 插件远程组件依赖完整 envelope ABI,内部页面则默认使用已解包的数据客户端。 +// 插件远程组件接收 endpoint 的最终 payload,内部页面默认使用严格 envelope 解包客户端。 if (typeof window !== 'undefined') window.MoviePilotAPI = pluginApi export { ApiRequestError, getApiBusinessErrorMessage, isApiBusinessFailure, isApiResponse, pluginApi } -export type { ApiFeedbackMode, DataApiClient } +export type { ApiFeedbackMode, DataApiClient, PluginApiClient } export default api