fix: 兼容插件自由 API 响应

This commit is contained in:
jxxghp
2026-08-18 00:29:14 +08:00
parent 0518817dd0
commit b8b8fabe15
4 changed files with 26 additions and 8 deletions
+13
View File
@@ -154,6 +154,19 @@ describe('MoviePilot API client', () => {
expect(notifier.error).toHaveBeenCalledWith('Plugin rejected request') 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 () => { it('Blob 成功响应绕过 envelope 解包', async () => {
const blob = new Blob(['moviepilot'], { type: 'application/octet-stream' }) const blob = new Blob(['moviepilot'], { type: 'application/octet-stream' })
const { api } = createApiClients({ adapter: resolveWith(blob), notifier }) const { api } = createApiClients({ adapter: resolveWith(blob), notifier })
+1 -1
View File
@@ -74,7 +74,7 @@ describe('API application wiring', () => {
vi.useRealTimers() vi.useRealTimers()
}) })
it('向 window 暴露插件 envelope 客户端,而内部默认导出数据客户端', async () => { it('向 window 暴露插件最终 payload 客户端,而内部默认导出数据客户端', async () => {
const module = await import('@/api') const module = await import('@/api')
expect(window.MoviePilotAPI).toBe(module.pluginApi) expect(window.MoviePilotAPI).toBe(module.pluginApi)
+8 -4
View File
@@ -68,6 +68,9 @@ export interface DataApiClient extends Omit<
): Promise<R> ): Promise<R>
} }
/** 插件与联邦组件接收 endpoint 最终 payload,方法签名不暴露 AxiosResponse 外壳。 */
export type PluginApiClient = DataApiClient
/** 请求层使用的反馈出口,由应用入口接到全局 Toast。 */ /** 请求层使用的反馈出口,由应用入口接到全局 Toast。 */
export interface ApiFeedbackNotifier { export interface ApiFeedbackNotifier {
error(message: string): void error(message: string): void
@@ -83,7 +86,7 @@ export interface ApiClientHooks {
reportConnectionFailure?(reason: 'network-error' | 'timeout' | 'server-unreachable'): void reportConnectionFailure?(reason: 'network-error' | 'timeout' | 'server-unreachable'): void
} }
/** 创建内部数据客户端与插件原始协议客户端时所需的配置。 */ /** 创建内部数据客户端与插件最终 payload 客户端时所需的配置。 */
export interface CreateApiClientsOptions extends CreateAxiosDefaults { export interface CreateApiClientsOptions extends CreateAxiosDefaults {
hooks?: ApiClientHooks hooks?: ApiClientHooks
notifier?: ApiFeedbackNotifier notifier?: ApiFeedbackNotifier
@@ -181,10 +184,10 @@ export function resolveConnectionFailureReason(
return null return null
} }
/** 创建普通数据客户端和保持插件 ABI 的原始 envelope 客户端。 */ /** 创建普通数据客户端和保持插件自由响应 ABI 的最终 payload 客户端。 */
export function createApiClients(options: CreateApiClientsOptions = {}): { export function createApiClients(options: CreateApiClientsOptions = {}): {
api: DataApiClient api: DataApiClient
pluginApi: AxiosInstance pluginApi: PluginApiClient
} { } {
const { hooks, notifier, resolveFallbackMessage, setupInstance, ...axiosConfig } = options const { hooks, notifier, resolveFallbackMessage, setupInstance, ...axiosConfig } = options
const api = axios.create(axiosConfig) const api = axios.create(axiosConfig)
@@ -202,7 +205,7 @@ export function createApiClients(options: CreateApiClientsOptions = {}): {
return { return {
api: api as DataApiClient, api: api as DataApiClient,
pluginApi, pluginApi: pluginApi as PluginApiClient,
} }
} }
@@ -226,6 +229,7 @@ function installResponseInterceptors(
const payload: unknown = response.data const payload: unknown = response.data
if (!isApiResponse(payload)) { if (!isApiResponse(payload)) {
// 联邦插件自行声明 API 响应;非宿主 envelope 必须原样交给插件调用方。
if (responseMode === 'envelope') return payload if (responseMode === 'envelope') return payload
const error = new ApiRequestError(resolveFallback('invalid-envelope', resolveFallbackMessage), { const error = new ApiRequestError(resolveFallback('invalid-envelope', resolveFallbackMessage), {
code: AxiosError.ERR_BAD_RESPONSE, code: AxiosError.ERR_BAD_RESPONSE,
+4 -3
View File
@@ -14,6 +14,7 @@ import {
type ApiFeedbackMode, type ApiFeedbackMode,
type ApiFallbackMessageKey, type ApiFallbackMessageKey,
type DataApiClient, type DataApiClient,
type PluginApiClient,
} from './client' } from './client'
/** 带连接探测和反馈策略的 MoviePilot 请求配置。 */ /** 带连接探测和反馈策略的 MoviePilot 请求配置。 */
@@ -70,7 +71,7 @@ const { api, pluginApi } = createApiClients({
declare global { declare global {
interface Window { 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 if (typeof window !== 'undefined') window.MoviePilotAPI = pluginApi
export { ApiRequestError, getApiBusinessErrorMessage, isApiBusinessFailure, isApiResponse, pluginApi } export { ApiRequestError, getApiBusinessErrorMessage, isApiBusinessFailure, isApiResponse, pluginApi }
export type { ApiFeedbackMode, DataApiClient } export type { ApiFeedbackMode, DataApiClient, PluginApiClient }
export default api export default api