增加一键重启所有网关按钮
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-10-21 17:21:11 +08:00
parent 8fdec02a1c
commit 421ffa0714
12 changed files with 553 additions and 1 deletions
@@ -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