mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +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>>(
|
function manageTarget<T = Record<string, unknown>>(
|
||||||
endpoint: 'notification/manage' | 'storage/manage',
|
endpoint: 'notification/manage' | 'storage/manage' | 'llm/manage',
|
||||||
request: ManageRequest,
|
request: ManageRequest,
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@@ -41,3 +41,13 @@ export function manageStorage<T = Record<string, unknown>>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return manageTarget<T>('storage/manage', { target: storage, action, params }, config)
|
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'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
apiGet: vi.fn(),
|
apiPost: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/api', () => ({
|
vi.mock('@/api', () => ({
|
||||||
default: createDataApiMock({
|
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', () => {
|
describe('useLlmProviderDirectory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.apiGet.mockReset()
|
mocks.apiPost.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('只为 OpenAI 兼容 runtime 或声明 Responses 工具能力的模型显示 API 协议字段', async () => {
|
it('只为 OpenAI 兼容 runtime 或声明 Responses 工具能力的模型显示 API 协议字段', async () => {
|
||||||
mocks.apiGet.mockResolvedValue({
|
mocks.apiPost.mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
data: [
|
data: [
|
||||||
createProvider('openai', 'openai_compatible'),
|
createProvider('openai', 'openai_compatible'),
|
||||||
@@ -83,7 +83,7 @@ describe('useLlmProviderDirectory', () => {
|
|||||||
|
|
||||||
expect(wrapper.vm.showApiProtocolField).toBe(false)
|
expect(wrapper.vm.showApiProtocolField).toBe(false)
|
||||||
|
|
||||||
mocks.apiGet.mockResolvedValueOnce({
|
mocks.apiPost.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
models: [
|
models: [
|
||||||
@@ -110,7 +110,7 @@ describe('useLlmProviderDirectory', () => {
|
|||||||
|
|
||||||
wrapper.vm.selectProvider('google')
|
wrapper.vm.selectProvider('google')
|
||||||
await nextTick()
|
await nextTick()
|
||||||
mocks.apiGet.mockResolvedValueOnce({
|
mocks.apiPost.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
models: [
|
models: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, onBeforeUnmount, ref, type Ref } from 'vue'
|
import { computed, onBeforeUnmount, ref, type Ref } from 'vue'
|
||||||
import api from '@/api'
|
import { manageLlmProvider } from '@/api/manage'
|
||||||
|
|
||||||
export interface LlmProviderAuthMethod {
|
export interface LlmProviderAuthMethod {
|
||||||
id: string
|
id: string
|
||||||
@@ -248,7 +248,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
async function loadProviders(preserveBaseUrl = true) {
|
async function loadProviders(preserveBaseUrl = true) {
|
||||||
loadingProviders.value = true
|
loadingProviders.value = true
|
||||||
try {
|
try {
|
||||||
const result = await api.get<LlmProvider[]>('llm/providers')
|
const result = await manageLlmProvider<LlmProvider[]>('', 'list_providers')
|
||||||
providers.value = Array.isArray(result) ? result : []
|
providers.value = Array.isArray(result) ? result : []
|
||||||
if (!selectedProvider.value && providers.value.length > 0) {
|
if (!selectedProvider.value && providers.value.length > 0) {
|
||||||
options.provider.value = providers.value[0].id
|
options.provider.value = providers.value[0].id
|
||||||
@@ -266,9 +266,10 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
|
|
||||||
loadingModels.value = true
|
loadingModels.value = true
|
||||||
try {
|
try {
|
||||||
const payload = await api.get<{ auth_status?: LlmProviderAuthStatus; models?: LlmModel[] }>('llm/models', {
|
const payload = await manageLlmProvider<{ auth_status?: LlmProviderAuthStatus; models?: LlmModel[] }>(
|
||||||
params: {
|
normalizeValue(options.provider.value),
|
||||||
provider: normalizeValue(options.provider.value),
|
'list_models',
|
||||||
|
{
|
||||||
api_key: normalizeValue(options.apiKey.value) || undefined,
|
api_key: normalizeValue(options.apiKey.value) || undefined,
|
||||||
base_url: normalizeValue(options.baseUrl.value) || undefined,
|
base_url: normalizeValue(options.baseUrl.value) || undefined,
|
||||||
base_url_preset: normalizeValue(options.baseUrlPreset?.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,
|
user_agent: normalizeValue(options.userAgent?.value) || undefined,
|
||||||
force_refresh: forceRefresh,
|
force_refresh: forceRefresh,
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
models.value = Array.isArray(payload.models) ? payload.models : []
|
models.value = Array.isArray(payload.models) ? payload.models : []
|
||||||
updateProviderAuthStatus(normalizeValue(options.provider.value), payload.auth_status)
|
updateProviderAuthStatus(normalizeValue(options.provider.value), payload.auth_status)
|
||||||
|
|
||||||
@@ -311,9 +312,10 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
authPolling.value = true
|
authPolling.value = true
|
||||||
clearPollTimer()
|
clearPollTimer()
|
||||||
try {
|
try {
|
||||||
const result = await api.post<Partial<LlmProviderAuthSession>>(
|
const result = await manageLlmProvider<Partial<LlmProviderAuthSession>>(
|
||||||
`llm/provider-auth/${authSession.value.session_id}/poll`,
|
normalizeValue(options.provider.value),
|
||||||
undefined,
|
'poll_auth',
|
||||||
|
{ session_id: authSession.value.session_id },
|
||||||
{ feedback: 'silent' },
|
{ feedback: 'silent' },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -347,10 +349,11 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
throw new Error('LLM provider is required')
|
throw new Error('LLM provider is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.post<LlmProviderAuthSession>('llm/provider-auth/start', {
|
const result = await manageLlmProvider<LlmProviderAuthSession>(
|
||||||
provider: normalizeValue(options.provider.value),
|
normalizeValue(options.provider.value),
|
||||||
method: methodId,
|
'start_auth',
|
||||||
})
|
{ method: methodId },
|
||||||
|
)
|
||||||
|
|
||||||
authSession.value = result
|
authSession.value = result
|
||||||
authDialogVisible.value = true
|
authDialogVisible.value = true
|
||||||
@@ -363,7 +366,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
async function disconnectAuth() {
|
async function disconnectAuth() {
|
||||||
if (!selectedProvider.value) return false
|
if (!selectedProvider.value) return false
|
||||||
|
|
||||||
await api.delete(`llm/provider-auth/${normalizeValue(options.provider.value)}`)
|
await manageLlmProvider(normalizeValue(options.provider.value), 'disconnect')
|
||||||
|
|
||||||
await loadProviders()
|
await loadProviders()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { manageLlmProvider } from '@/api/manage'
|
||||||
import { useGlobalSettingsStore } from '@/stores'
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
import { DownloaderConf, MediaServerConf } from '@/api/types'
|
import { DownloaderConf, MediaServerConf } from '@/api/types'
|
||||||
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
|
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
|
||||||
@@ -817,7 +818,7 @@ async function testLlmConnection() {
|
|||||||
|
|
||||||
const snapshot = buildLlmSnapshot()
|
const snapshot = buildLlmSnapshot()
|
||||||
const snapshotKey = buildLlmSnapshotKey(snapshot)
|
const snapshotKey = buildLlmSnapshotKey(snapshot)
|
||||||
const payload = buildLlmTestPayload(snapshot)
|
const { provider: testProvider, ...testParams } = buildLlmTestPayload(snapshot)
|
||||||
const requestId = ++llmTestRequestId
|
const requestId = ++llmTestRequestId
|
||||||
if (llmTestAbortController) llmTestAbortController.abort()
|
if (llmTestAbortController) llmTestAbortController.abort()
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
@@ -825,7 +826,7 @@ async function testLlmConnection() {
|
|||||||
|
|
||||||
testingLlm.value = true
|
testingLlm.value = true
|
||||||
try {
|
try {
|
||||||
await api.post('llm/test', payload, {
|
await manageLlmProvider(testProvider, 'test', testParams, {
|
||||||
feedback: 'silent',
|
feedback: 'silent',
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -445,20 +445,23 @@ describe('AccountSettingSystem', () => {
|
|||||||
await fireEvent.click(testLlm)
|
await fireEvent.click(testLlm)
|
||||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('LLM 调用测试失败:provider rejected'))
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('LLM 调用测试失败:provider rejected'))
|
||||||
const firstCall = mocks.apiPost.mock.calls[0]
|
const firstCall = mocks.apiPost.mock.calls[0]
|
||||||
expect(firstCall?.[0]).toBe('llm/test')
|
expect(firstCall?.[0]).toBe('llm/manage')
|
||||||
expect(firstCall?.[1]).toEqual({
|
expect(firstCall?.[1]).toEqual({
|
||||||
api_key: 'secret-key',
|
target: 'openai',
|
||||||
api_protocol: 'responses',
|
action: 'test',
|
||||||
base_url: 'https://llm.example/v1',
|
params: {
|
||||||
base_url_preset: 'custom',
|
api_key: 'secret-key',
|
||||||
enabled: true,
|
api_protocol: 'responses',
|
||||||
model: 'gpt-5',
|
base_url: 'https://llm.example/v1',
|
||||||
provider: 'openai',
|
base_url_preset: 'custom',
|
||||||
thinking_level: 'high',
|
enabled: true,
|
||||||
temperature: 0.7,
|
model: 'gpt-5',
|
||||||
use_proxy: false,
|
thinking_level: 'high',
|
||||||
user_agent: 'MoviePilot-Test',
|
temperature: 0.7,
|
||||||
web_search_mode: 'builtin',
|
use_proxy: false,
|
||||||
|
user_agent: 'MoviePilot-Test',
|
||||||
|
web_search_mode: 'builtin',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(firstCall?.[2]).toEqual({ signal: expect.any(AbortSignal) })
|
expect(firstCall?.[2]).toEqual({ signal: expect.any(AbortSignal) })
|
||||||
expect(testLlm).toBeEnabled()
|
expect(testLlm).toBeEnabled()
|
||||||
|
|||||||
Reference in New Issue
Block a user