refactor: replace jobs proxy with local controller and optimize model loading (#174)
* refactor: replace jobs proxy with local controller and optimize model loading - Add local jobs controller that directly fetches upstream gateway with profile support and 30s timeout, replacing unreliable proxy catch-all - Upstream errors (non-200) return 502 instead of leaking to frontend - Switch loadModels() from fetchAvailableModels (slow, fetches all provider APIs) to fetchConfigModels (reads config.yaml only) - Hide logo dance video in sidebar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve TypeScript errors from previous refactor - Remove unused imports (danceVideo, useTheme) in AppSidebar - Map ConfigModelsResponse.groups to AvailableModelGroup[] format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,14 +9,9 @@ import ProfileSelector from "./ProfileSelector.vue";
|
||||
import LanguageSwitch from "./LanguageSwitch.vue";
|
||||
import ThemeSwitch from "./ThemeSwitch.vue";
|
||||
import { useSessionSearch } from '@/composables/useSessionSearch'
|
||||
import danceVideoLight from "@/assets/dance-light.mp4";
|
||||
import danceVideoDark from "@/assets/dance-dark.mp4";
|
||||
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { changelog } from "@/data/changelog";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isDark } = useTheme();
|
||||
const message = useMessage();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -66,7 +61,7 @@ function openChangelog() {
|
||||
<div class="sidebar-logo" @click="router.push('/hermes/chat')">
|
||||
<img :src="logoPath" alt="Hermes" class="logo-img" />
|
||||
<span class="logo-text">Hermes</span>
|
||||
<video class="logo-dance" :src="isDark ? danceVideoDark : danceVideoLight" autoplay loop muted playsinline />
|
||||
<!-- <video class="logo-dance" :src="isDark ? danceVideoDark : danceVideoLight" autoplay loop muted playsinline /> -->
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { checkHealth, fetchAvailableModels, updateDefaultModel, triggerUpdate, type AvailableModelGroup } from '@/api/hermes/system'
|
||||
import { checkHealth, fetchConfigModels, updateDefaultModel, triggerUpdate, type AvailableModelGroup } from '@/api/hermes/system'
|
||||
|
||||
const WEB_UI_VERSION = __APP_VERSION__
|
||||
|
||||
@@ -57,10 +57,16 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetchAvailableModels()
|
||||
modelGroups.value = res.groups
|
||||
const res = await fetchConfigModels()
|
||||
modelGroups.value = res.groups.map(g => ({
|
||||
provider: g.provider,
|
||||
label: g.provider,
|
||||
base_url: '',
|
||||
models: g.models.map(m => typeof m === 'string' ? m : m.id),
|
||||
api_key: '',
|
||||
}))
|
||||
selectedModel.value = res.default
|
||||
selectedProvider.value = res.default_provider || ''
|
||||
selectedProvider.value = ''
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { Context } from 'koa'
|
||||
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
||||
import { config } from '../../config'
|
||||
|
||||
function getUpstream(profile: string): string {
|
||||
const mgr = getGatewayManagerInstance()
|
||||
return mgr ? mgr.getUpstream(profile) : config.upstream.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function getApiKey(profile: string): string | null {
|
||||
const mgr = getGatewayManagerInstance()
|
||||
return mgr?.getApiKey(profile) ?? null
|
||||
}
|
||||
|
||||
function resolveProfile(ctx: Context): string {
|
||||
return ctx.get('x-hermes-profile') || (ctx.query.profile as string) || 'default'
|
||||
}
|
||||
|
||||
function buildHeaders(profile: string): Record<string, string> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
const apiKey = getApiKey(profile)
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
return headers
|
||||
}
|
||||
|
||||
const TIMEOUT_MS = 30_000
|
||||
|
||||
async function proxyRequest(ctx: Context, upstreamPath: string, method?: string): Promise<void> {
|
||||
const profile = resolveProfile(ctx)
|
||||
const upstream = getUpstream(profile)
|
||||
const params = new URLSearchParams(ctx.search || '')
|
||||
params.delete('token')
|
||||
const search = params.toString()
|
||||
const url = `${upstream}${upstreamPath}${search ? `?${search}` : ''}`
|
||||
|
||||
const headers = buildHeaders(profile)
|
||||
const body = ctx.req.method !== 'GET' && ctx.req.method !== 'HEAD'
|
||||
? JSON.stringify(ctx.request.body || {})
|
||||
: undefined
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method || ctx.req.method,
|
||||
headers,
|
||||
body,
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
ctx.status = 502
|
||||
ctx.set('Content-Type', 'application/json')
|
||||
ctx.body = { error: { message: `Upstream error: ${res.status} ${res.statusText}` } }
|
||||
return
|
||||
}
|
||||
|
||||
ctx.status = res.status
|
||||
ctx.set('Content-Type', res.headers.get('content-type') || 'application/json')
|
||||
ctx.body = await res.json()
|
||||
}
|
||||
|
||||
export async function list(ctx: Context) {
|
||||
await proxyRequest(ctx, '/api/jobs')
|
||||
}
|
||||
|
||||
export async function get(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}`)
|
||||
}
|
||||
|
||||
export async function create(ctx: Context) {
|
||||
await proxyRequest(ctx, '/api/jobs')
|
||||
}
|
||||
|
||||
export async function update(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}`)
|
||||
}
|
||||
|
||||
export async function remove(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}`)
|
||||
}
|
||||
|
||||
export async function pause(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}/pause`)
|
||||
}
|
||||
|
||||
export async function resume(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}/resume`)
|
||||
}
|
||||
|
||||
export async function run(ctx: Context) {
|
||||
await proxyRequest(ctx, `/api/jobs/${ctx.params.id}/run`)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Router from '@koa/router'
|
||||
import * as ctrl from '../../controllers/hermes/jobs'
|
||||
|
||||
export const jobRoutes = new Router()
|
||||
|
||||
jobRoutes.get('/api/hermes/jobs', ctrl.list)
|
||||
jobRoutes.get('/api/hermes/jobs/:id', ctrl.get)
|
||||
jobRoutes.post('/api/hermes/jobs', ctrl.create)
|
||||
jobRoutes.patch('/api/hermes/jobs/:id', ctrl.update)
|
||||
jobRoutes.delete('/api/hermes/jobs/:id', ctrl.remove)
|
||||
jobRoutes.post('/api/hermes/jobs/:id/pause', ctrl.pause)
|
||||
jobRoutes.post('/api/hermes/jobs/:id/resume', ctrl.resume)
|
||||
jobRoutes.post('/api/hermes/jobs/:id/run', ctrl.run)
|
||||
@@ -22,6 +22,7 @@ import { gatewayRoutes } from './hermes/gateways'
|
||||
import { weixinRoutes } from './hermes/weixin'
|
||||
import { fileRoutes } from './hermes/files'
|
||||
import { downloadRoutes } from './hermes/download'
|
||||
import { jobRoutes } from './hermes/jobs'
|
||||
import { proxyRoutes, proxyMiddleware } from './hermes/proxy'
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export function registerRoutes(app: any, requireAuth: (ctx: Context, next: Next)
|
||||
app.use(weixinRoutes.routes())
|
||||
app.use(fileRoutes.routes()) // Must be before proxy (proxy catch-all matches everything)
|
||||
app.use(downloadRoutes.routes()) // Must be before proxy
|
||||
app.use(jobRoutes.routes()) // Must be before proxy
|
||||
app.use(proxyRoutes.routes())
|
||||
|
||||
// Proxy catch-all middleware (must be last)
|
||||
|
||||
Reference in New Issue
Block a user