Files
hermes-ui/packages/client/src/api/hermes/system.ts
T
toor 3ca960f388
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
优化页面交互并增强 Provider 配置能力
- 表单下拉框不再用 label 包裹,改为 div + aria-label,避免点击后
  下拉面板瞬间关闭
- Provider 卡片新增「从上游获取模型」和「修改」入口,支持编辑
  base URL、API Key 与 api_mode,凭据写入对应的环境变量
- 服务端抽出 provider-model-discovery 统一不同 api_mode 的模型发现,
  旧的 custom_providers 与新的 providers 字典结构都能正确更新
- Agent Bridge 改为后台启动,HTTP 接口在 Python 运行时就绪前即可用;
  worker 连接被拒时按类型重试,不重放可能已产生副作用的请求
- 断线重连后恢复 profiles、models 与会话列表,健康检查改为自调度
  的 setTimeout,离线时 2 秒后重试
- 批量删除确认文案改用 Popconfirm content,避免变成侧栏触发器文本
2026-10-22 14:29:48 +08:00

328 lines
9.3 KiB
TypeScript

import { request } from '../client'
export interface HealthResponse {
status: string
platform?: string
version?: string
gateway?: string
webui_version?: string
webui_latest?: string
webui_update_available?: boolean
node_version?: string
is_docker?: boolean
agent_bridge?: {
status: string
reachable: boolean
ready?: boolean
running?: boolean
attached?: boolean
starting?: boolean
stopping?: boolean
restart_scheduled?: boolean
restart_attempts?: number
endpoint_kind?: 'ipc' | 'tcp' | 'unknown'
pid?: number
error?: string
}
}
export interface PreviewTag {
name: string
sha: string
}
export interface PreviewStatus {
preview_dir: string
exists: boolean
has_package: boolean
installed: boolean
running: boolean
pid: number | null
current_tag: string
frontend_url: string
agent_bridge_endpoint: string
log_path: string
webui_home: string
action_log_path: string
dev_log_path: string
active_action: string | null
active_action_started_at: string | null
last_action: string | null
last_action_completed_at: string | null
last_action_success: boolean | null
last_action_message: string
last_action_code: string
action_log: string
dev_log: string
}
export interface PreviewActionResponse extends PreviewStatus {
success: boolean
accepted?: boolean
message?: string
code?: string
}
// Config-based model types
export interface ModelInfo {
id: string
label: string
}
export interface ModelGroup {
provider: string
models: ModelInfo[]
}
export interface ConfigModelsResponse {
default: string
groups: ModelGroup[]
}
export interface ModelVisibilityRule {
mode: 'all' | 'include'
models: string[]
}
export type ModelVisibility = Record<string, ModelVisibilityRule>
export type CustomModels = Record<string, string[]>
export type ProviderApiMode = 'chat_completions' | 'codex_responses' | 'anthropic_messages' | 'bedrock_converse' | 'codex_app_server'
export interface AvailableModelGroup {
provider: string // credential pool key (e.g. "zai", "custom:subrouter.ai")
label: string // display name (e.g. "zai", "subrouter.ai")
base_url: string
models: string[]
/** Full unfiltered model catalog for this provider, used to restore hidden WUI models. */
available_models?: string[]
api_key: string
api_mode?: ProviderApiMode
builtin?: boolean
/** Env var used by Hermes to override this provider's base URL. If present, the preset URL is editable. */
base_url_env?: string
credentials_editable?: boolean
/** Config source for custom providers. Dict-backed providers can be deleted from providers:<key>. */
provider_source?: 'custom_providers' | 'providers'
provider_key?: string
/** 可选:模型 ID -> 元数据(preview/disabled/alias)。alias 仅用于 Web UI 展示。 */
model_meta?: Record<string, { preview?: boolean; disabled?: boolean; alias?: string }>
}
export interface ProfileAvailableModels {
profile: string
default: string
default_provider: string
groups: AvailableModelGroup[]
}
export interface AvailableModelsResponse {
default: string
default_provider: string
groups: AvailableModelGroup[]
allProviders: AvailableModelGroup[]
profiles?: ProfileAvailableModels[]
/** Web UI-only display aliases keyed by provider -> canonical model ID. */
model_aliases?: Record<string, Record<string, string>>
model_visibility?: ModelVisibility
custom_models?: CustomModels
}
export interface CustomProvider {
name: string
base_url: string
api_key: string
model: string
context_length?: number
api_mode?: ProviderApiMode
providerKey?: string | null
}
export async function checkHealth(): Promise<HealthResponse> {
return request<HealthResponse>('/health')
}
export interface HermesCliStatus {
available: boolean
bin?: string
}
let cliProbePromise: Promise<boolean> | null = null
async function probeHermesCli(): Promise<boolean> {
try {
const res = await request<HermesCliStatus>('/api/hermes/cli-status')
return res.available !== false
} catch {
// Fail open: probe errors (older servers, mocked backends) must not block the app.
return true
}
}
/** Cached per page load; the router guard probes once before showing the app. */
export function checkHermesCliAvailable(): Promise<boolean> {
if (!cliProbePromise) {
cliProbePromise = probeHermesCli()
}
return cliProbePromise
}
export function resetHermesCliProbe(): void {
cliProbePromise = null
}
export async function triggerUpdate(): Promise<{ success: boolean; message: string }> {
return request<{ success: boolean; message: string }>('/api/hermes/update', { method: 'POST' })
}
export async function fetchPreviewStatus(): Promise<PreviewStatus> {
return request<PreviewStatus>('/api/hermes/update/preview')
}
export async function fetchPreviewTags(): Promise<{ tags: PreviewTag[] }> {
return request<{ tags: PreviewTag[] }>('/api/hermes/update/preview/tags')
}
export async function preparePreview(tag: string): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/prepare', {
method: 'POST',
body: JSON.stringify({ tag }),
})
}
export async function installPreview(): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/install', { method: 'POST' })
}
export async function startPreview(tag?: string): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/start', {
method: 'POST',
body: JSON.stringify({ tag }),
})
}
export async function stopPreview(): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/stop', { method: 'POST' })
}
export async function fetchConfigModels(): Promise<ConfigModelsResponse> {
return request<ConfigModelsResponse>('/api/hermes/config/models')
}
export async function fetchAvailableModels(): Promise<AvailableModelsResponse> {
return request<AvailableModelsResponse>('/api/hermes/available-models')
}
export async function fetchAvailableModelsForProfile(profile: string): Promise<AvailableModelsResponse> {
const params = new URLSearchParams()
params.set('profile', profile || 'default')
return request<AvailableModelsResponse>(`/api/hermes/available-models?${params.toString()}`)
}
export async function fetchProviderModels(data: {
base_url: string
api_key?: string
freeOnly?: boolean
provider?: string
label?: string
update_cache?: boolean
api_mode?: ProviderApiMode
}): Promise<{ models: string[] }> {
return request<{ models: string[] }>('/api/hermes/provider-models', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function refreshProviderModelCache(): Promise<{ success: boolean }> {
return request<{ success: boolean }>('/api/hermes/provider-models/cache/refresh', {
method: 'POST',
})
}
export async function updateDefaultModel(data: {
default: string
provider?: string
base_url?: string
api_key?: string
}): Promise<void> {
await request('/api/hermes/config/model', {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function updateModelAlias(data: {
provider: string
model: string
alias: string
}): Promise<void> {
await request('/api/hermes/model-alias', {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function addCustomProvider(data: CustomProvider): Promise<void> {
await request('/api/hermes/config/providers', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function removeCustomProvider(name: string, options: { source?: 'custom_providers' | 'providers'; providerKey?: string } = {}): Promise<void> {
const query = new URLSearchParams()
if (options.source) query.set('source', options.source)
if (options.providerKey) query.set('providerKey', options.providerKey)
await request(`/api/hermes/config/providers/${encodeURIComponent(name)}${query.size ? `?${query}` : ''}`, {
method: 'DELETE',
})
}
export async function updateProvider(poolKey: string, data: {
source?: 'custom_providers' | 'providers'
providerKey?: string
name?: string
base_url?: string
api_key?: string
model?: string
api_mode?: ProviderApiMode
}): Promise<void> {
await request(`/api/hermes/config/providers/${encodeURIComponent(poolKey)}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function updateModelVisibility(data: {
provider: string
mode: 'all' | 'include'
models: string[]
}): Promise<{ success: boolean; model_visibility: ModelVisibility }> {
return request<{ success: boolean; model_visibility: ModelVisibility }>('/api/hermes/model-visibility', {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function addCustomModel(data: {
provider: string
model: string
}): Promise<{ success: boolean; custom_models: CustomModels }> {
return request<{ success: boolean; custom_models: CustomModels }>('/api/hermes/custom-model', {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function removeCustomModel(data: {
provider: string
model: string
}): Promise<{ success: boolean; custom_models: CustomModels }> {
const params = new URLSearchParams()
params.set('provider', data.provider)
params.set('model', data.model)
return request<{ success: boolean; custom_models: CustomModels }>(`/api/hermes/custom-model?${params.toString()}`, {
method: 'DELETE',
})
}