优化页面交互并增强 Provider 配置能力
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

- 表单下拉框不再用 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,避免变成侧栏触发器文本
This commit is contained in:
2026-10-22 14:29:48 +08:00
committed by trae-solo-agent
parent 90c759c760
commit 3ca960f388
35 changed files with 876 additions and 78 deletions
@@ -194,23 +194,35 @@ export async function create(ctx: any) {
export async function update(ctx: any) {
const poolKey = decodeURIComponent(ctx.params.poolKey)
const { name, base_url, api_key, model, api_mode } = ctx.request.body as {
name?: string; base_url?: string; api_key?: string; model?: string; api_mode?: ProviderApiMode
const { name, base_url, api_key, model, api_mode, source, providerKey } = ctx.request.body as {
name?: string; base_url?: string; api_key?: string; model?: string; api_mode?: ProviderApiMode; source?: string; providerKey?: string
}
const customApiMode = normalizeApiMode(api_mode)
if (base_url !== undefined) {
try {
const parsed = new URL(base_url)
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Invalid protocol')
} catch {
ctx.status = 400; ctx.body = { error: 'base_url must be a valid HTTP or HTTPS URL' }; return
}
}
try {
const profile = requestedProfile(ctx)
const isCustom = poolKey.startsWith('custom:')
if (isCustom) {
const found = await updateConfigYamlForProfile(profile, (config) => {
if (!Array.isArray(config.custom_providers)) return { data: config, result: false, write: false }
const entry = (config.custom_providers as any[]).find((e: any) => {
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
})
if (!entry) return { data: config, result: false, write: false }
const found = await updateConfigYamlForProfile(profile, async (config) => {
const legacyIndex = source === 'providers' ? -1 : findLegacyCustomProviderIndex(config, poolKey)
const dictKey = source === 'custom_providers' || legacyIndex !== -1
? '' : findProviderDictKey(config, poolKey, providerKey)
const entry = legacyIndex !== -1 ? config.custom_providers[legacyIndex] : dictKey ? config.providers[dictKey] : null
const normalized = normalizeCustomProviderEntry(entry, dictKey, dictKey ? 'providers' : 'custom_providers')
if (!entry || !normalized || providerKeyForCustomName(normalized.name) !== poolKey) return { data: config, result: false, write: false }
if (name !== undefined) entry.name = name
if (base_url !== undefined) entry.base_url = base_url
if (api_key !== undefined) entry.api_key = api_key
if (base_url !== undefined) entry.base_url = base_url.trim()
if (api_key !== undefined) {
if (normalized.key_env) await saveEnvValueForProfile(profile, normalized.key_env, api_key)
entry.api_key = api_key
}
if (model !== undefined) entry.model = model
if (customApiMode !== undefined) entry.api_mode = customApiMode
return { data: config, result: true }
@@ -220,10 +232,17 @@ export async function update(ctx: any) {
}
} else {
const envMapping = PROVIDER_ENV_MAP[poolKey]
if (!envMapping?.api_key_env) {
if (!envMapping || (!envMapping.api_key_env && !envMapping.base_url_env)) {
ctx.status = 400; ctx.body = { error: `Cannot update credentials for "${poolKey}"` }; return
}
if (api_key !== undefined) { await saveEnvValueForProfile(profile, envMapping.api_key_env, api_key) }
if (base_url !== undefined && !envMapping.base_url_env) {
ctx.status = 400; ctx.body = { error: `Cannot update base URL for "${poolKey}"` }; return
}
if (api_key !== undefined && !envMapping.api_key_env) {
ctx.status = 400; ctx.body = { error: `Cannot update API key for "${poolKey}"` }; return
}
if (api_key !== undefined) await saveEnvValueForProfile(profile, envMapping.api_key_env, api_key)
if (base_url !== undefined) await saveEnvValueForProfile(profile, envMapping.base_url_env, base_url.trim())
}
// TODO: Test if provider works without gateway restart
// try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }