From 137d906ef080913d12f4c531c97662eff2529189 Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Thu, 25 Jun 2026 17:24:23 +0800 Subject: [PATCH] fix: Fixed issue with openclaw config telegram failed --- agent/app/service/agents_channels.go | 33 ++++++-- agent/app/service/vllm_upgrade.go | 19 ++++- frontend/src/lang/modules/en.ts | 5 ++ frontend/src/lang/modules/es-es.ts | 6 ++ frontend/src/lang/modules/ja.ts | 4 + frontend/src/lang/modules/ko.ts | 4 + frontend/src/lang/modules/ms.ts | 5 ++ frontend/src/lang/modules/pt-br.ts | 6 ++ frontend/src/lang/modules/ru.ts | 5 ++ frontend/src/lang/modules/tr.ts | 6 ++ frontend/src/lang/modules/zh-Hant.ts | 4 + frontend/src/lang/modules/zh.ts | 4 + .../src/views/ai/agents/model/add/index.vue | 77 +++++++++++++++++++ 13 files changed, 169 insertions(+), 9 deletions(-) diff --git a/agent/app/service/agents_channels.go b/agent/app/service/agents_channels.go index 7ee0b82d51d3..60077441844a 100644 --- a/agent/app/service/agents_channels.go +++ b/agent/app/service/agents_channels.go @@ -811,9 +811,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig result.RequireMention = result.GroupPolicy == "allowlist" result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"]) result.Proxy = extractStringValue(telegram["proxy"]) - if streaming := extractStringValue(telegram["streaming"]); streaming != "" { - result.Streaming = streaming - } + result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming) accounts := childMap(telegram, "accounts") if len(accounts) == 0 { botToken := extractStringValue(telegram["botToken"]) @@ -839,7 +837,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig BotToken: extractStringValue(account["botToken"]), DmPolicy: extractStringValue(account["dmPolicy"]), GroupPolicy: extractStringValue(account["groupPolicy"]), - Streaming: extractStringValue(account["streaming"]), + Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming), }) } result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots)) @@ -879,7 +877,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf } else { delete(telegram, "proxy") } - telegram["streaming"] = config.Streaming + telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming) accounts := make(map[string]interface{}, len(config.Bots)) for _, bot := range config.Bots { account := map[string]interface{}{ @@ -888,7 +886,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf "botToken": bot.BotToken, "dmPolicy": bot.DmPolicy, "groupPolicy": bot.GroupPolicy, - "streaming": bot.Streaming, + "streaming": buildTelegramStreamingConfig(bot.Streaming), } if bot.DmPolicy == "open" { account["allowFrom"] = []string{"*"} @@ -899,6 +897,29 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf delete(telegram, "botToken") } +func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string { + mode := defaultMode + switch typed := value.(type) { + case string: + mode = typed + case map[string]interface{}: + mode = extractStringValue(typed["mode"]) + } + mode = strings.ToLower(strings.TrimSpace(mode)) + switch mode { + case "off", "partial", "block", "progress": + return mode + default: + return "partial" + } +} + +func buildTelegramStreamingConfig(mode string) map[string]interface{} { + return map[string]interface{}{ + "mode": normalizeTelegramStreamingMode(mode, "partial"), + } +} + func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"} discord := getChannelConfig(conf, "discord") diff --git a/agent/app/service/vllm_upgrade.go b/agent/app/service/vllm_upgrade.go index dd25a8c97fad..b13775af90ac 100644 --- a/agent/app/service/vllm_upgrade.go +++ b/agent/app/service/vllm_upgrade.go @@ -12,6 +12,7 @@ const ( vllmImageEnvKey = "IMAGE" vllmImageTypeNvidia = "nvidia" vllmImageTypeIntel = "intel" + vllmImageTypeAscend = "ascend" ) func resolveVllmVersionFamily(version, image string) string { @@ -19,6 +20,9 @@ func resolveVllmVersionFamily(version, image string) string { if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") { return vllmImageTypeIntel } + if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") { + return vllmImageTypeAscend + } if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") { return vllmImageTypeNvidia } @@ -26,13 +30,16 @@ func resolveVllmVersionFamily(version, image string) string { if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") { return vllmImageTypeIntel } + if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") { + return vllmImageTypeAscend + } return vllmImageTypeNvidia } func trimVllmVersionFamily(version string) string { trimmed := strings.TrimSpace(version) normalized := strings.ToLower(trimmed) - for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel} { + for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} { prefix := family + "-" if strings.HasPrefix(normalized, prefix) { return strings.TrimSpace(trimmed[len(prefix):]) @@ -43,12 +50,16 @@ func trimVllmVersionFamily(version string) string { func buildDefaultVllmImageByVersion(version string) string { tag := trimVllmVersionFamily(version) - if resolveVllmVersionFamily(version, "") == vllmImageTypeIntel { + family := resolveVllmVersionFamily(version, "") + if family == vllmImageTypeIntel { return "intel/llm-scaler-vllm:" + tag } if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") { tag = "v" + tag } + if family == vllmImageTypeAscend { + return "quay.io/ascend/vllm-ascend:" + tag + } return "vllm/vllm-openai:" + tag } @@ -60,7 +71,9 @@ func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage str func hasVllmVersionFamilyPrefix(version string) bool { normalized := strings.ToLower(strings.TrimSpace(version)) - return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") || strings.HasPrefix(normalized, vllmImageTypeIntel+"-") + return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") || + strings.HasPrefix(normalized, vllmImageTypeIntel+"-") || + strings.HasPrefix(normalized, vllmImageTypeAscend+"-") } func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index ba872c111f8d..077fe44fa488 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -855,6 +855,9 @@ const message = { channelAutoRestartHelper: 'Saving will automatically restart the container so the changes take effect.', channelDeleteConfirm: 'Delete the {0} channel configuration?', customProviderHelper: 'Custom model providers do not validate whether the account is available.', + apiTypeBaseURLHelper: 'This type sends requests to {0}. Recommended Base URL: {1}', + apiTypeBaseURLMismatch: + 'The current Base URL looks like a {0} path, but {1} is selected. Recommended URL: {2}', }, model: { model: 'Models', @@ -1025,6 +1028,8 @@ const message = { healthUnknown: 'Unknown', validationModelMapEmpty: 'Model mapping names cannot be empty', validationModelMapDuplicate: 'Request model {0} is duplicated', + validateAvailability: 'Validate Account Availability', + validateAvailabilityHelper: 'Send a minimal message before saving to validate API availability', modelGroup: 'Model Group', availableModels: 'Available Models', modelGroupModels: 'Request Models', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index fee362cff76b..82b25f62067c 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -866,6 +866,9 @@ const message = { 'Al guardar, el contenedor se reiniciará automáticamente para que la configuración surta efecto.', channelDeleteConfirm: '¿Eliminar la configuración del canal {0}?', customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible', + apiTypeBaseURLHelper: 'Este tipo envía solicitudes a {0}. Base URL recomendada: {1}', + apiTypeBaseURLMismatch: + 'La Base URL actual parece una ruta {0}, pero está seleccionado {1}. URL recomendada: {2}', }, model: { model: 'Modelo', @@ -1040,6 +1043,9 @@ const message = { healthUnknown: 'Desconocido', validationModelMapEmpty: 'Los nombres del mapeo de modelos no pueden estar vacíos', validationModelMapDuplicate: 'El modelo solicitado {0} está duplicado', + validateAvailability: 'Validar disponibilidad de la cuenta', + validateAvailabilityHelper: + 'Enviar un mensaje mínimo antes de guardar para validar la disponibilidad de la API', modelGroup: 'Grupo de modelos', availableModels: 'Modelos disponibles', modelGroupModels: 'Modelos solicitados', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 0a735fb8c3ef..5c82210ee339 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -858,6 +858,8 @@ const message = { channelAutoRestartHelper: '保存後、設定を反映するためにコンテナが自動で再起動されます。', channelDeleteConfirm: '{0} チャンネルの設定を削除しますか?', customProviderHelper: 'カスタムモデルプロバイダーでは、アカウントの有効性を検証しません', + apiTypeBaseURLHelper: 'このタイプは最終的に {0} にリクエストします。推奨 Base URL: {1}', + apiTypeBaseURLMismatch: '現在の Base URL は {0} のパスに見えますが、{1} が選択されています。推奨 URL: {2}', }, model: { model: 'モデル', @@ -1030,6 +1032,8 @@ const message = { healthUnknown: '不明', validationModelMapEmpty: 'モデルマッピングのモデル名は空にできません', validationModelMapDuplicate: 'リクエストモデル {0} が重複しています', + validateAvailability: 'アカウントの可用性を検証', + validateAvailabilityHelper: '保存前に最小メッセージを送信して API の可用性を検証します', modelGroup: 'モデルグループ', availableModels: '利用可能なモデル', modelGroupModels: 'リクエストモデル', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index caa955bdfa3c..d5afa793906b 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -843,6 +843,8 @@ const message = { channelAutoRestartHelper: '저장하면 설정 적용을 위해 컨테이너가 자동으로 재시작됩니다.', channelDeleteConfirm: '{0} 채널 구성을 삭제하시겠습니까?', customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다', + apiTypeBaseURLHelper: '이 유형은 최종적으로 {0}에 요청합니다. 권장 Base URL: {1}', + apiTypeBaseURLMismatch: '현재 Base URL은 {0} 경로처럼 보이지만 {1}이 선택되었습니다. 권장 URL: {2}', }, model: { model: '모델', @@ -1015,6 +1017,8 @@ const message = { healthUnknown: '알 수 없음', validationModelMapEmpty: '모델 매핑의 모델 이름은 비워 둘 수 없습니다', validationModelMapDuplicate: '요청 모델 {0}이(가) 중복되었습니다', + validateAvailability: '계정 사용 가능 여부 검증', + validateAvailabilityHelper: '저장 전에 최소 메시지를 보내 API 사용 가능 여부를 검증합니다', modelGroup: '모델 그룹', availableModels: '사용 가능한 모델', modelGroupModels: '요청 모델', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 5cd1b0aa12a9..8b8c92665f7c 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -865,6 +865,9 @@ const message = { 'Menyimpan akan memulakan semula bekas secara automatik supaya konfigurasi berkuat kuasa.', channelDeleteConfirm: 'Padam konfigurasi saluran {0}?', customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan', + apiTypeBaseURLHelper: 'Jenis ini menghantar permintaan ke {0}. Base URL yang disyorkan: {1}', + apiTypeBaseURLMismatch: + 'Base URL semasa kelihatan seperti laluan {0}, tetapi {1} dipilih. URL yang disyorkan: {2}', }, model: { model: 'Model', @@ -1038,6 +1041,8 @@ const message = { healthUnknown: 'Tidak diketahui', validationModelMapEmpty: 'Nama model dalam pemetaan model tidak boleh kosong', validationModelMapDuplicate: 'Model permintaan {0} berulang', + validateAvailability: 'Sahkan Ketersediaan Akaun', + validateAvailabilityHelper: 'Hantar mesej minimum sebelum menyimpan untuk mengesahkan ketersediaan API', modelGroup: 'Kumpulan Model', availableModels: 'Model Tersedia', modelGroupModels: 'Model Permintaan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 88cf8b4996f5..ffedcc1227a2 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -861,6 +861,9 @@ const message = { 'Ao salvar, o contêiner será reiniciado automaticamente para que a configuração entre em vigor.', channelDeleteConfirm: 'Excluir a configuração do canal {0}?', customProviderHelper: 'Provedores de modelo personalizados não validam se a conta está disponível', + apiTypeBaseURLHelper: 'Este tipo envia requisições para {0}. Base URL recomendada: {1}', + apiTypeBaseURLMismatch: + 'A Base URL atual parece um caminho {0}, mas {1} está selecionado. URL recomendada: {2}', }, model: { model: 'Modelo', @@ -1034,6 +1037,9 @@ const message = { healthUnknown: 'Desconhecido', validationModelMapEmpty: 'Os nomes no mapeamento de modelos não podem ficar vazios', validationModelMapDuplicate: 'O modelo solicitado {0} está duplicado', + validateAvailability: 'Validar disponibilidade da conta', + validateAvailabilityHelper: + 'Envie uma mensagem mínima antes de salvar para validar a disponibilidade da API', modelGroup: 'Grupo de modelos', availableModels: 'Modelos disponíveis', modelGroupModels: 'Modelos solicitados', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f9dfc5963b15..0174b7ed27f2 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -856,6 +856,8 @@ const message = { 'После сохранения контейнер будет автоматически перезапущен, чтобы настройки вступили в силу.', channelDeleteConfirm: 'Удалить конфигурацию канала {0}?', customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется', + apiTypeBaseURLHelper: 'Этот тип отправляет запросы в {0}. Рекомендуемый Base URL: {1}', + apiTypeBaseURLMismatch: 'Текущий Base URL похож на путь {0}, но выбран {1}. Рекомендуемый URL: {2}', }, model: { model: 'Модель', @@ -1029,6 +1031,9 @@ const message = { healthUnknown: 'Неизвестно', validationModelMapEmpty: 'Имена моделей в сопоставлении не могут быть пустыми', validationModelMapDuplicate: 'Запрошенная модель {0} дублируется', + validateAvailability: 'Проверить доступность аккаунта', + validateAvailabilityHelper: + 'Отправить минимальное сообщение перед сохранением для проверки доступности API', modelGroup: 'Группа моделей', availableModels: 'Доступные модели', modelGroupModels: 'Запрошенные модели', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 40039d4d67c5..af8c9f092b16 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -864,6 +864,9 @@ const message = { 'Kaydettiğinizde ayarların etkili olması için konteyner otomatik olarak yeniden başlatılır.', channelDeleteConfirm: '{0} kanal yapılandırması silinsin mi?', customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz', + apiTypeBaseURLHelper: 'Bu tür istekleri {0} yoluna gönderir. Önerilen Base URL: {1}', + apiTypeBaseURLMismatch: + 'Geçerli Base URL bir {0} yolu gibi görünüyor, ancak {1} seçildi. Önerilen URL: {2}', }, model: { model: 'Model', @@ -1037,6 +1040,9 @@ const message = { healthUnknown: 'Bilinmiyor', validationModelMapEmpty: 'Model eşlemesindeki model adları boş olamaz', validationModelMapDuplicate: 'İstek modeli {0} yineleniyor', + validateAvailability: 'Hesap Kullanılabilirliğini Doğrula', + validateAvailabilityHelper: + 'Kaydetmeden önce API kullanılabilirliğini doğrulamak için en küçük iletiyi gönder', modelGroup: 'Model Grubu', availableModels: 'Kullanılabilir Modeller', modelGroupModels: 'İstek Modelleri', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 0430ffc50c79..5dc595f478f1 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -809,6 +809,8 @@ const message = { channelAutoRestartHelper: '保存後將自動重新啟動容器以使設定生效。', channelDeleteConfirm: '確認刪除 {0} 頻道設定?', customProviderHelper: '自訂模型供應商不驗證帳號是否可用', + apiTypeBaseURLHelper: '目前類型最終請求 {0},Base URL 建議填寫 {1}', + apiTypeBaseURLMismatch: '目前 Base URL 看起來是 {0} 路徑,和已選擇的 {1} 不一致,建議改為 {2}', }, model: { model: '模型', @@ -977,6 +979,8 @@ const message = { healthUnknown: '未知', validationModelMapEmpty: '模型映射的模型名稱不能為空', validationModelMapDuplicate: '請求模型 {0} 重複', + validateAvailability: '驗證帳號可用性', + validateAvailabilityHelper: '儲存前發送最小訊息驗證介面可用性', modelGroup: '模型組', availableModels: '可用模型', modelGroupModels: '請求模型', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 6d897e7108fe..14ee4d48696c 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -802,6 +802,8 @@ const message = { channelAutoRestartHelper: '保存后将自动重启容器以使配置生效。', channelDeleteConfirm: '确认删除 {0} 频道配置?', customProviderHelper: '自定义模型供应商不验证账号是否可用', + apiTypeBaseURLHelper: '当前类型最终请求 {0},Base URL 建议填写 {1}', + apiTypeBaseURLMismatch: '当前 Base URL 看起来是 {0} 路径,和已选择的 {1} 不一致,建议改为 {2}', }, model: { model: '模型', @@ -968,6 +970,8 @@ const message = { healthUnknown: '未知', validationModelMapEmpty: '模型映射的模型名不能为空', validationModelMapDuplicate: '请求模型 {0} 重复', + validateAvailability: '验证账号可用性', + validateAvailabilityHelper: '保存前发送最小消息验证接口可用性', modelGroup: '模型组', availableModels: '可用模型', modelGroupModels: '请求模型', diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/agents/model/add/index.vue index 4f44bd4948f6..7a2ce9c55137 100644 --- a/frontend/src/views/ai/agents/model/add/index.vue +++ b/frontend/src/views/ai/agents/model/add/index.vue @@ -30,6 +30,18 @@ + + {{ apiTypeBaseURLHelper }} + + + {{ baseURLAPITypeMismatchTip }} + @@ -133,6 +145,67 @@ const apiTypeOptions = computed(() => { } return ['openai-completions', 'openai-responses']; }); +const apiTypeURLHints: Record = { + 'openai-completions': { + requestPath: '/v1/chat/completions', + example: 'http://127.0.0.1:8000/v1', + endpointSuffixes: ['/v1/chat/completions', '/v1beta/chat/completions', '/chat/completions'], + }, + 'openai-responses': { + requestPath: '/v1/responses', + example: 'http://127.0.0.1:8000/v1', + endpointSuffixes: ['/v1/responses', '/responses'], + }, + 'anthropic-messages': { + requestPath: '/v1/messages', + example: 'http://127.0.0.1:8000', + endpointSuffixes: ['/v1/messages', '/messages'], + }, +}; +const showAPITypeBaseURLTips = computed(() => form.provider === 'vllm'); +const apiTypeBaseURLHelper = computed(() => { + const hint = apiTypeURLHints[form.apiType]; + if (!showAPITypeBaseURLTips.value || !hint) { + return ''; + } + return i18n.global.t('aiTools.agents.apiTypeBaseURLHelper', [hint.requestPath, hint.example]); +}); + +const normalizeBaseURLPath = (baseURL: string) => { + const value = baseURL.trim().replace(/\/+$/, ''); + if (!value) { + return ''; + } + try { + return (new URL(value).pathname || '/').replace(/\/+$/, '').toLowerCase(); + } catch { + return value.split(/[?#]/)[0].replace(/\/+$/, '').toLowerCase(); + } +}; + +const detectAPITypeFromBaseURL = (baseURL: string) => { + const path = normalizeBaseURLPath(baseURL); + if (!path) { + return ''; + } + for (const [apiType, hint] of Object.entries(apiTypeURLHints)) { + if (hint.endpointSuffixes.some((suffix) => path.endsWith(suffix))) { + return apiType; + } + } + return ''; +}; +const baseURLAPITypeMismatchTip = computed(() => { + if (!showAPITypeBaseURLTips.value || !form.baseURL) { + return ''; + } + const detectedAPIType = detectAPITypeFromBaseURL(form.baseURL); + const expected = apiTypeURLHints[form.apiType]; + if (!detectedAPIType || detectedAPIType === form.apiType || !expected) { + return ''; + } + return i18n.global.t('aiTools.agents.apiTypeBaseURLMismatch', [detectedAPIType, form.apiType, expected.example]); +}); const rules = reactive({ provider: [Rules.requiredSelect], @@ -376,4 +449,8 @@ defineExpose({ white-space: nowrap; } } + +.base-url-warning { + margin-top: 8px; +}