mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
refactor(api): LLM 提供商管理调用适配后端通用 manage 接口
- manage.ts 扩展 manageLlmProvider,端点联合类型纳入 llm/manage - useLlmProviderDirectory 的目录/模型查询、授权启动/轮询/断开 全部改走 POST /llm/manage(target+action+params 透传) - 系统设置页 LLM 测试调用改走 manage,provider 字段转为 target - 同步更新 composable 与系统设置页测试断言(GET mock 转 POST)
This commit is contained in:
+12
-2
@@ -9,13 +9,13 @@ export interface ManageRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用统一管理端点(通知渠道 / 网盘存储)
|
||||
* 调用统一管理端点(通知渠道 / 网盘存储 / LLM 提供商)
|
||||
*
|
||||
* 端点层不定义任何目标特定的名称与参数,
|
||||
* 目标标识、管理动作与表单参数原样透传给后端模块
|
||||
*/
|
||||
function manageTarget<T = Record<string, unknown>>(
|
||||
endpoint: 'notification/manage' | 'storage/manage',
|
||||
endpoint: 'notification/manage' | 'storage/manage' | 'llm/manage',
|
||||
request: ManageRequest,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<T> {
|
||||
@@ -41,3 +41,13 @@ export function manageStorage<T = Record<string, unknown>>(
|
||||
): Promise<T> {
|
||||
return manageTarget<T>('storage/manage', { target: storage, action, params }, config)
|
||||
}
|
||||
|
||||
/** 对指定 LLM 提供商执行管理动作,返回响应中的业务数据。 */
|
||||
export function manageLlmProvider<T = Record<string, unknown>>(
|
||||
provider: string,
|
||||
action: string,
|
||||
params: Record<string, unknown> = {},
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<T> {
|
||||
return manageTarget<T>('llm/manage', { target: provider, action, params }, config)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { defineComponent, nextTick, ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
post: (...args: unknown[]) => mocks.apiPost(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -33,11 +33,11 @@ function createProvider(id: string, runtime: string) {
|
||||
|
||||
describe('useLlmProviderDirectory', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
mocks.apiPost.mockReset()
|
||||
})
|
||||
|
||||
it('只为 OpenAI 兼容 runtime 或声明 Responses 工具能力的模型显示 API 协议字段', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
mocks.apiPost.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
createProvider('openai', 'openai_compatible'),
|
||||
@@ -83,7 +83,7 @@ describe('useLlmProviderDirectory', () => {
|
||||
|
||||
expect(wrapper.vm.showApiProtocolField).toBe(false)
|
||||
|
||||
mocks.apiGet.mockResolvedValueOnce({
|
||||
mocks.apiPost.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: {
|
||||
models: [
|
||||
@@ -110,7 +110,7 @@ describe('useLlmProviderDirectory', () => {
|
||||
|
||||
wrapper.vm.selectProvider('google')
|
||||
await nextTick()
|
||||
mocks.apiGet.mockResolvedValueOnce({
|
||||
mocks.apiPost.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: {
|
||||
models: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, onBeforeUnmount, ref, type Ref } from 'vue'
|
||||
import api from '@/api'
|
||||
import { manageLlmProvider } from '@/api/manage'
|
||||
|
||||
export interface LlmProviderAuthMethod {
|
||||
id: string
|
||||
@@ -248,7 +248,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
async function loadProviders(preserveBaseUrl = true) {
|
||||
loadingProviders.value = true
|
||||
try {
|
||||
const result = await api.get<LlmProvider[]>('llm/providers')
|
||||
const result = await manageLlmProvider<LlmProvider[]>('', 'list_providers')
|
||||
providers.value = Array.isArray(result) ? result : []
|
||||
if (!selectedProvider.value && providers.value.length > 0) {
|
||||
options.provider.value = providers.value[0].id
|
||||
@@ -266,9 +266,10 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const payload = await api.get<{ auth_status?: LlmProviderAuthStatus; models?: LlmModel[] }>('llm/models', {
|
||||
params: {
|
||||
provider: normalizeValue(options.provider.value),
|
||||
const payload = await manageLlmProvider<{ auth_status?: LlmProviderAuthStatus; models?: LlmModel[] }>(
|
||||
normalizeValue(options.provider.value),
|
||||
'list_models',
|
||||
{
|
||||
api_key: normalizeValue(options.apiKey.value) || undefined,
|
||||
base_url: normalizeValue(options.baseUrl.value) || undefined,
|
||||
base_url_preset: normalizeValue(options.baseUrlPreset?.value) || undefined,
|
||||
@@ -276,7 +277,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
user_agent: normalizeValue(options.userAgent?.value) || undefined,
|
||||
force_refresh: forceRefresh,
|
||||
},
|
||||
})
|
||||
)
|
||||
models.value = Array.isArray(payload.models) ? payload.models : []
|
||||
updateProviderAuthStatus(normalizeValue(options.provider.value), payload.auth_status)
|
||||
|
||||
@@ -311,9 +312,10 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
authPolling.value = true
|
||||
clearPollTimer()
|
||||
try {
|
||||
const result = await api.post<Partial<LlmProviderAuthSession>>(
|
||||
`llm/provider-auth/${authSession.value.session_id}/poll`,
|
||||
undefined,
|
||||
const result = await manageLlmProvider<Partial<LlmProviderAuthSession>>(
|
||||
normalizeValue(options.provider.value),
|
||||
'poll_auth',
|
||||
{ session_id: authSession.value.session_id },
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
|
||||
@@ -347,10 +349,11 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
throw new Error('LLM provider is required')
|
||||
}
|
||||
|
||||
const result = await api.post<LlmProviderAuthSession>('llm/provider-auth/start', {
|
||||
provider: normalizeValue(options.provider.value),
|
||||
method: methodId,
|
||||
})
|
||||
const result = await manageLlmProvider<LlmProviderAuthSession>(
|
||||
normalizeValue(options.provider.value),
|
||||
'start_auth',
|
||||
{ method: methodId },
|
||||
)
|
||||
|
||||
authSession.value = result
|
||||
authDialogVisible.value = true
|
||||
@@ -363,7 +366,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
||||
async function disconnectAuth() {
|
||||
if (!selectedProvider.value) return false
|
||||
|
||||
await api.delete(`llm/provider-auth/${normalizeValue(options.provider.value)}`)
|
||||
await manageLlmProvider(normalizeValue(options.provider.value), 'disconnect')
|
||||
|
||||
await loadProviders()
|
||||
return true
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { manageLlmProvider } from '@/api/manage'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { DownloaderConf, MediaServerConf } from '@/api/types'
|
||||
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
|
||||
@@ -817,7 +818,7 @@ async function testLlmConnection() {
|
||||
|
||||
const snapshot = buildLlmSnapshot()
|
||||
const snapshotKey = buildLlmSnapshotKey(snapshot)
|
||||
const payload = buildLlmTestPayload(snapshot)
|
||||
const { provider: testProvider, ...testParams } = buildLlmTestPayload(snapshot)
|
||||
const requestId = ++llmTestRequestId
|
||||
if (llmTestAbortController) llmTestAbortController.abort()
|
||||
const abortController = new AbortController()
|
||||
@@ -825,7 +826,7 @@ async function testLlmConnection() {
|
||||
|
||||
testingLlm.value = true
|
||||
try {
|
||||
await api.post('llm/test', payload, {
|
||||
await manageLlmProvider(testProvider, 'test', testParams, {
|
||||
feedback: 'silent',
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
@@ -445,20 +445,23 @@ describe('AccountSettingSystem', () => {
|
||||
await fireEvent.click(testLlm)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('LLM 调用测试失败:provider rejected'))
|
||||
const firstCall = mocks.apiPost.mock.calls[0]
|
||||
expect(firstCall?.[0]).toBe('llm/test')
|
||||
expect(firstCall?.[0]).toBe('llm/manage')
|
||||
expect(firstCall?.[1]).toEqual({
|
||||
api_key: 'secret-key',
|
||||
api_protocol: 'responses',
|
||||
base_url: 'https://llm.example/v1',
|
||||
base_url_preset: 'custom',
|
||||
enabled: true,
|
||||
model: 'gpt-5',
|
||||
provider: 'openai',
|
||||
thinking_level: 'high',
|
||||
temperature: 0.7,
|
||||
use_proxy: false,
|
||||
user_agent: 'MoviePilot-Test',
|
||||
web_search_mode: 'builtin',
|
||||
target: 'openai',
|
||||
action: 'test',
|
||||
params: {
|
||||
api_key: 'secret-key',
|
||||
api_protocol: 'responses',
|
||||
base_url: 'https://llm.example/v1',
|
||||
base_url_preset: 'custom',
|
||||
enabled: true,
|
||||
model: 'gpt-5',
|
||||
thinking_level: 'high',
|
||||
temperature: 0.7,
|
||||
use_proxy: false,
|
||||
user_agent: 'MoviePilot-Test',
|
||||
web_search_mode: 'builtin',
|
||||
},
|
||||
})
|
||||
expect(firstCall?.[2]).toEqual({ signal: expect.any(AbortSignal) })
|
||||
expect(testLlm).toBeEnabled()
|
||||
|
||||
Reference in New Issue
Block a user