- 表单下拉框不再用 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,避免变成侧栏触发器文本
328 lines
9.3 KiB
TypeScript
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',
|
|
})
|
|
}
|