mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Render provider settings through dnd-kit sortable rows, include official providers in the same order model, and persist providerOrder across server and desktop state. Tested: - bun test src/server/__tests__/providers.test.ts src/server/__tests__/persistence-upgrade.test.ts - cd desktop && bun run test -- --run src/stores/providerStore.test.ts src/__tests__/generalSettings.test.tsx -t "providerStore reorderProviders|Settings > Providers tab" - cd desktop && bun run lint - cd desktop && bun run build - bun run check:persistence-upgrade Not-tested: - bun run check:server (broad suite hit environment failures during this run: MCP stdio zshrc timeout, adapter dependency gap at the time, and e2e cascade) Confidence: high Scope-risk: moderate
229 lines
7.7 KiB
TypeScript
229 lines
7.7 KiB
TypeScript
/**
|
|
* Providers REST API
|
|
*
|
|
* GET /api/providers — list all saved providers + activeId
|
|
* GET /api/providers/presets — list available presets
|
|
* GET /api/providers/auth-status — check whether any usable auth exists
|
|
* GET /api/providers/settings — read cc-haha managed settings.json
|
|
* POST /api/providers — add a provider
|
|
* PUT /api/providers/settings — update cc-haha managed settings.json
|
|
* PUT /api/providers/:id — update a provider
|
|
* DELETE /api/providers/:id — delete a provider
|
|
* POST /api/providers/:id/activate — activate a saved provider
|
|
* POST /api/providers/official — activate official (clear env)
|
|
* POST /api/providers/:id/test — test a saved provider
|
|
* POST /api/providers/test — test unsaved config
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
import { ProviderService } from '../services/providerService.js'
|
|
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
|
import {
|
|
CreateProviderSchema,
|
|
UpdateProviderSchema,
|
|
TestProviderSchema,
|
|
ReorderProvidersSchema,
|
|
} from '../types/provider.js'
|
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
import { diagnosticsService } from '../services/diagnosticsService.js'
|
|
|
|
const providerService = new ProviderService()
|
|
|
|
export async function handleProvidersApi(
|
|
req: Request,
|
|
_url: URL,
|
|
segments: string[],
|
|
): Promise<Response> {
|
|
try {
|
|
const id = segments[2]
|
|
const action = segments[3]
|
|
|
|
// POST /api/providers/test
|
|
if (id === 'test' && req.method === 'POST') {
|
|
return await handleTestUnsaved(req)
|
|
}
|
|
|
|
// GET /api/providers/presets
|
|
if (id === 'presets' && req.method === 'GET') {
|
|
return Response.json({ presets: PROVIDER_PRESETS })
|
|
}
|
|
|
|
// GET /api/providers/auth-status
|
|
if (id === 'auth-status' && req.method === 'GET') {
|
|
const status = await providerService.checkAuthStatus()
|
|
return Response.json(status)
|
|
}
|
|
|
|
// /api/providers/settings
|
|
if (id === 'settings') {
|
|
if (req.method === 'GET') {
|
|
return Response.json(await providerService.getManagedSettings())
|
|
}
|
|
if (req.method === 'PUT') {
|
|
const body = await parseJsonBody(req)
|
|
await providerService.updateManagedSettings(body)
|
|
return Response.json({ ok: true })
|
|
}
|
|
throw methodNotAllowed(req.method)
|
|
}
|
|
|
|
// POST /api/providers/official
|
|
if (id === 'official' && req.method === 'POST') {
|
|
await providerService.activateOfficial()
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
// PUT /api/providers/reorder
|
|
if (id === 'reorder') {
|
|
if (req.method !== 'PUT') throw methodNotAllowed(req.method)
|
|
return await handleReorder(req)
|
|
}
|
|
|
|
// /api/providers (no ID)
|
|
if (!id) {
|
|
if (req.method === 'GET') {
|
|
const { providers, activeId } = await providerService.listProviders()
|
|
return Response.json({ providers, activeId })
|
|
}
|
|
if (req.method === 'POST') {
|
|
return await handleCreate(req)
|
|
}
|
|
throw methodNotAllowed(req.method)
|
|
}
|
|
|
|
// /api/providers/:id/activate
|
|
if (action === 'activate') {
|
|
if (req.method !== 'POST') throw methodNotAllowed(req.method)
|
|
await providerService.activateProvider(id)
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
// /api/providers/:id/test
|
|
if (action === 'test') {
|
|
if (req.method !== 'POST') throw methodNotAllowed(req.method)
|
|
let overrides: { baseUrl?: string; modelId?: string; apiFormat?: string; authStrategy?: string } | undefined
|
|
try {
|
|
const body = await req.json()
|
|
if (body && typeof body === 'object') overrides = body as typeof overrides
|
|
} catch { /* no body is fine — uses saved values */ }
|
|
const result = await providerService.testProvider(id, overrides)
|
|
if (!result.connectivity.success || result.proxy?.success === false) {
|
|
void diagnosticsService.recordEvent({
|
|
type: 'provider_test_failed',
|
|
severity: 'warn',
|
|
summary: result.connectivity.error || result.proxy?.error || 'Provider test failed',
|
|
details: {
|
|
providerId: id,
|
|
httpStatus: result.connectivity.httpStatus ?? result.proxy?.httpStatus,
|
|
apiFormat: overrides?.apiFormat,
|
|
modelId: overrides?.modelId,
|
|
connectivity: result.connectivity,
|
|
proxy: result.proxy,
|
|
},
|
|
})
|
|
}
|
|
return Response.json({ result })
|
|
}
|
|
|
|
// /api/providers/:id
|
|
if (req.method === 'GET') {
|
|
const provider = await providerService.getProvider(id)
|
|
return Response.json({ provider })
|
|
}
|
|
if (req.method === 'PUT') {
|
|
return await handleUpdate(req, id)
|
|
}
|
|
if (req.method === 'DELETE') {
|
|
await providerService.deleteProvider(id)
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
throw methodNotAllowed(req.method)
|
|
} catch (error) {
|
|
return errorResponse(error)
|
|
}
|
|
}
|
|
|
|
async function handleCreate(req: Request): Promise<Response> {
|
|
const body = await parseJsonBody(req)
|
|
try {
|
|
const input = CreateProviderSchema.parse(body)
|
|
const provider = await providerService.addProvider(input)
|
|
return Response.json({ provider }, { status: 201 })
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; '))
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function handleUpdate(req: Request, id: string): Promise<Response> {
|
|
const body = await parseJsonBody(req)
|
|
try {
|
|
const input = UpdateProviderSchema.parse(body)
|
|
const provider = await providerService.updateProvider(id, input)
|
|
return Response.json({ provider })
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; '))
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function handleReorder(req: Request): Promise<Response> {
|
|
const body = await parseJsonBody(req)
|
|
try {
|
|
const input = ReorderProvidersSchema.parse(body)
|
|
const { providers, providerOrder } = await providerService.reorderProviders(input.orderedIds)
|
|
return Response.json({ providers, providerOrder })
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; '))
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function handleTestUnsaved(req: Request): Promise<Response> {
|
|
const body = await parseJsonBody(req)
|
|
try {
|
|
const input = TestProviderSchema.parse(body)
|
|
const result = await providerService.testProviderConfig(input)
|
|
if (!result.connectivity.success || result.proxy?.success === false) {
|
|
void diagnosticsService.recordEvent({
|
|
type: 'provider_test_failed',
|
|
severity: 'warn',
|
|
summary: result.connectivity.error || result.proxy?.error || 'Provider test failed',
|
|
details: {
|
|
providerId: null,
|
|
baseUrl: input.baseUrl,
|
|
apiFormat: input.apiFormat,
|
|
modelId: input.modelId,
|
|
connectivity: result.connectivity,
|
|
proxy: result.proxy,
|
|
},
|
|
})
|
|
}
|
|
return Response.json({ result })
|
|
} catch (err) {
|
|
if (!(err instanceof z.ZodError)) {
|
|
void diagnosticsService.recordEvent({
|
|
type: 'provider_test_failed',
|
|
severity: 'warn',
|
|
summary: err instanceof Error ? err.message : String(err),
|
|
details: { providerId: null, error: err },
|
|
})
|
|
}
|
|
if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; '))
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
|
try {
|
|
return (await req.json()) as Record<string, unknown>
|
|
} catch {
|
|
throw ApiError.badRequest('Invalid JSON body')
|
|
}
|
|
}
|
|
|
|
function methodNotAllowed(method: string): ApiError {
|
|
return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED')
|
|
}
|