fix: Fixed issue with openclaw config telegram failed (#13114)

This commit is contained in:
CityFun
2026-06-25 18:40:34 +08:00
committed by GitHub
parent 56cccdf7c0
commit 677b47dcdb
13 changed files with 169 additions and 9 deletions
+27 -6
View File
@@ -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")
+16 -3
View File
@@ -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 {
+5
View File
@@ -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',
+6
View File
@@ -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',
+4
View File
@@ -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: 'リクエストモデル',
+4
View File
@@ -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: '요청 모델',
+5
View File
@@ -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',
+6
View File
@@ -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',
+5
View File
@@ -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: 'Запрошенные модели',
+6
View File
@@ -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',
+4
View File
@@ -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: '請求模型',
+4
View File
@@ -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: '请求模型',
@@ -30,6 +30,18 @@
</el-form-item>
<el-form-item label="Base URL" prop="baseURL">
<el-input v-model="form.baseURL" :disabled="!editableBaseURLProviders.includes(form.provider)" />
<span v-if="apiTypeBaseURLHelper" class="input-help">
{{ apiTypeBaseURLHelper }}
</span>
<el-alert
v-if="baseURLAPITypeMismatchTip"
class="base-url-warning"
type="warning"
:closable="false"
show-icon
>
{{ baseURLAPITypeMismatchTip }}
</el-alert>
</el-form-item>
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType">
<el-select v-model="form.apiType" :disabled="apiTypeOptions.length === 1">
@@ -133,6 +145,67 @@ const apiTypeOptions = computed(() => {
}
return ['openai-completions', 'openai-responses'];
});
const apiTypeURLHints: Record<string, { requestPath: string; example: string; endpointSuffixes: string[] }> = {
'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;
}
</style>