增加一键重启所有网关按钮
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
getGatewayRuntimeStatusForProfile,
|
||||
prepareGatewayForProfileDelete,
|
||||
restartGatewayForProfile as restartGatewayRuntimeForProfile,
|
||||
restartGatewaysForProfiles,
|
||||
} from '../../services/hermes/gateway-autostart'
|
||||
import { logger } from '../../services/logger'
|
||||
import { smartCloneCleanup, copyModelProviderAuthForClone } from '../../services/hermes/profile-credentials'
|
||||
@@ -334,6 +335,14 @@ function setRuntimeStatusCache(status: RuntimeStatus, checkedAt = Date.now()): v
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重启后让缓存立即过期,下一次状态查询会重新探测真实运行状态。
|
||||
*/
|
||||
function setRuntimeStatusCacheDirty(): void {
|
||||
runtimeStatusMinimumFreshAt = Date.now()
|
||||
scheduleRuntimeStatusRefresh()
|
||||
}
|
||||
|
||||
function listProfilesForStatusFast(): HermesProfile[] {
|
||||
return filterVisibleProfiles(listProfilesFromDisk(getActiveProfileName()))
|
||||
}
|
||||
@@ -644,6 +653,67 @@ export async function restartProfileRuntime(ctx: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartAllGateways(ctx: any) {
|
||||
const profiles = filterProfilesForUser(ctx, listProfilesForStatusFast())
|
||||
const names = profiles.map(profile => profile.name).filter(name => !isForbiddenProfileName(name))
|
||||
if (names.length === 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'No profiles available for restart' }
|
||||
return
|
||||
}
|
||||
try {
|
||||
const results = await restartGatewaysForProfiles(names)
|
||||
for (const result of results) {
|
||||
if (!result.running) continue
|
||||
try {
|
||||
await bridgeCleanupClient().destroyProfile(result.profile)
|
||||
} catch (err) {
|
||||
logger.warn(err, '[profiles] failed to destroy bridge sessions after batch gateway restart profile=%s', result.profile)
|
||||
}
|
||||
}
|
||||
const failed = results.filter(result => !result.running)
|
||||
if (failed.length > 0) {
|
||||
setRuntimeStatusCacheDirty()
|
||||
}
|
||||
ctx.body = {
|
||||
success: failed.length === 0,
|
||||
restarted: results.filter(result => result.running).map(result => result.profile),
|
||||
failed: failed.map(result => ({ profile: result.profile, error: result.error })),
|
||||
}
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartAllProfiles(ctx: any) {
|
||||
const profiles = filterProfilesForUser(ctx, listProfilesForStatusFast())
|
||||
const names = profiles.map(profile => profile.name).filter(name => !isForbiddenProfileName(name))
|
||||
if (names.length === 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'No profiles available for restart' }
|
||||
return
|
||||
}
|
||||
try {
|
||||
const restarted: string[] = []
|
||||
const failed: Array<{ profile: string; error: string }> = []
|
||||
for (const name of names) {
|
||||
try {
|
||||
await bridgeCleanupClient().destroyProfile(name)
|
||||
restarted.push(name)
|
||||
} catch (err: any) {
|
||||
logger.warn(err, '[profiles] failed to destroy bridge sessions after batch profile restart profile=%s', name)
|
||||
failed.push({ profile: name, error: err?.message || String(err) })
|
||||
}
|
||||
}
|
||||
setRuntimeStatusCacheDirty()
|
||||
ctx.body = { success: failed.length === 0, restarted, failed }
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(ctx: any) {
|
||||
const { name } = ctx.params
|
||||
if (denyProfile(ctx, name)) return
|
||||
|
||||
@@ -7,6 +7,9 @@ export const profileRoutes = new Router()
|
||||
profileRoutes.get('/api/hermes/profiles', ctrl.list)
|
||||
profileRoutes.post('/api/hermes/profiles', ctrl.create)
|
||||
profileRoutes.get('/api/hermes/profiles/runtime-statuses', ctrl.runtimeStatuses)
|
||||
// 批量路由必须排在 /:name 之前,否则 "gateways" 会被当成 profile 名。
|
||||
profileRoutes.post('/api/hermes/profiles/gateways/restart-all', ctrl.restartAllGateways)
|
||||
profileRoutes.post('/api/hermes/profiles/restart-all', ctrl.restartAllProfiles)
|
||||
profileRoutes.get('/api/hermes/profiles/:name/runtime-status', ctrl.runtimeStatus)
|
||||
profileRoutes.post('/api/hermes/profiles/:name/restart', ctrl.restartProfileRuntime)
|
||||
profileRoutes.post('/api/hermes/profiles/:name/gateway/restart', ctrl.restartGatewayForProfile)
|
||||
|
||||
@@ -516,6 +516,85 @@ export async function restartGatewayForProfile(profile: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export interface BatchGatewayRestartResult {
|
||||
profile: string
|
||||
targetProfile: string
|
||||
running: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 把请求的 profile 列表映射成需要重启的网关 target 列表。
|
||||
* 统一网关模式下多个 profile 指向同一个 target,这里按 target 去重,
|
||||
* 避免同一个网关被反复重启。
|
||||
*/
|
||||
export function resolveGatewayRestartTargets(
|
||||
profiles: string[],
|
||||
unified: boolean,
|
||||
): Array<{ targetProfile: string; requestedProfile: string }> {
|
||||
const targets = new Map<string, string>()
|
||||
for (const profile of profiles) {
|
||||
const requested = String(profile || '').trim() || 'default'
|
||||
const target = resolveGatewayTargetProfile(requested, unified)
|
||||
if (!targets.has(target.targetProfile)) {
|
||||
targets.set(target.targetProfile, requested)
|
||||
}
|
||||
}
|
||||
return Array.from(targets, ([targetProfile, requestedProfile]) => ({ targetProfile, requestedProfile }))
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启多个 profile 的网关。统一网关模式下多个 profile 会指向同一个
|
||||
* target profile,这里按 target 去重,避免同一个网关被反复重启。
|
||||
*/
|
||||
export async function restartGatewaysForProfiles(
|
||||
profiles: string[],
|
||||
opts: {
|
||||
unified?: boolean
|
||||
stopGateway?: (profile: string, profileDir: string) => Promise<void>
|
||||
startGateway?: (profile: string, profileDir: string) => Promise<void>
|
||||
waitForGateway?: (profile: string, profileDir: string) => Promise<boolean>
|
||||
} = {},
|
||||
): Promise<BatchGatewayRestartResult[]> {
|
||||
const hermesBin = resolveHermesBin()
|
||||
let unified = opts.unified
|
||||
if (unified === undefined) {
|
||||
const { gatewayAutoStart } = await readAppConfig()
|
||||
unified = shouldUseUnifiedGatewayManagement(gatewayAutoStart)
|
||||
}
|
||||
|
||||
const stopGateway = opts.stopGateway || ((profile: string, profileDir: string) =>
|
||||
stopGatewayForProfile(hermesBin, profile, profileDir))
|
||||
const startGateway = opts.startGateway || ((profile: string, profileDir: string) =>
|
||||
startGatewayForProfile(hermesBin, profile, profileDir, { managedRun: shouldUseManagedGatewayRun() }))
|
||||
const waitForGateway = opts.waitForGateway || ((profile: string, profileDir: string) =>
|
||||
waitForGatewayRunning(hermesBin, profile, profileDir))
|
||||
|
||||
const results: BatchGatewayRestartResult[] = []
|
||||
for (const { targetProfile, requestedProfile } of resolveGatewayRestartTargets(profiles, unified)) {
|
||||
const profileDir = getProfileDir(targetProfile)
|
||||
try {
|
||||
await stopGateway(targetProfile, profileDir)
|
||||
await startGateway(targetProfile, profileDir)
|
||||
const running = await waitForGateway(targetProfile, profileDir)
|
||||
if (!running) {
|
||||
throw new Error('Hermes gateway start completed but gateway did not report running within timeout')
|
||||
}
|
||||
results.push({ profile: requestedProfile, targetProfile, running: true })
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
err,
|
||||
'[gateway-autostart] batch gateway restart failed requestedProfile=%s targetProfile=%s home=%s',
|
||||
requestedProfile,
|
||||
targetProfile,
|
||||
profileDir,
|
||||
)
|
||||
results.push({ profile: requestedProfile, targetProfile, running: false, error: err?.message || String(err) })
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function ensureProfileGatewaysRunning(): Promise<void> {
|
||||
const hermesBin = resolveHermesBin()
|
||||
const discoveredProfiles = listProfileNamesFromDisk()
|
||||
|
||||
Reference in New Issue
Block a user