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:
jxxghp
2026-08-16 07:50:39 +08:00
parent f8f43058c5
commit b65682da53
5 changed files with 54 additions and 37 deletions
+12 -2
View File
@@ -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: [
+17 -14
View File
@@ -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
+3 -2
View File
@@ -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()