diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java index 95a5e6c8..3dac6583 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java @@ -152,6 +152,19 @@ public class NodeStreamingChatHelper { || msg.contains("invalid_request_error") || msg.contains("unsupported")) { return ErrorType.CLIENT_ERROR; } + // DashScope-specific "model name does not map to a valid endpoint" — reported as + // "[InvalidParameter] url error, please check url" (see + // https://help.aliyun.com/zh/model-studio/error-code#error-url). Despite the wording + // it's not a URL issue — it's the provider rejecting an unknown/unsupported model id + // on the native protocol. Treat as client error so we do NOT retry. + if (msg.contains("[InvalidParameter]") + || msg.contains("InvalidParameter") + || msg.contains("url error") + || msg.contains("Model not exist") + || msg.contains("model_not_found") + || msg.contains("Model not found")) { + return ErrorType.CLIENT_ERROR; + } // Server errors if (msg.contains("500") || msg.contains("502") || msg.contains("503") || msg.contains("504") || msg.contains("APITimeoutError") || msg.contains("APIConnectionError") @@ -211,10 +224,15 @@ public class NodeStreamingChatHelper { if (lastResult.errorType() == ErrorType.THINKING_BLOCK_ERROR) { return lastResult; // 已经重试过了 } - // 成功或不可重试 + // 成功 if (lastResult.errorMessage() == null || lastResult.errorType() == ErrorType.NONE) { return lastResult; } + // Any other non-null errored result with a classified type that doStreamCall + // chose NOT to retry (i.e. UNKNOWN, or RATE_LIMIT/SERVER_ERROR past MAX_RETRIES) + // must exit — otherwise we silently spin through attempts and waste seconds + // per turn on unrecoverable errors like DashScope's "url error" / unknown model. + return lastResult; } // lastResult == null 表示需要重试 } @@ -640,6 +658,13 @@ public class NodeStreamingChatHelper { private static String extractUserFriendlyError(Throwable error) { String msg = error.getMessage(); if (msg == null) return error.getClass().getSimpleName(); + // DashScope "url error" is really "model name not mapped to any valid endpoint". + // Translate it so users see the real cause and the actionable next step. + if (msg.contains("url error") || msg.contains("[InvalidParameter]") + || msg.contains("Model not exist") || msg.contains("model_not_found") + || msg.contains("Model not found")) { + return "Model name not available on this provider — verify the model exists and is supported (Settings → Models)"; + } // 对 Jackson 反序列化错误,提取关键信息 if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later"; if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead"; diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java index 9604f026..db8bcc52 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java @@ -51,6 +51,15 @@ public class ModelDiscoveryService { "qwen3.5-plus" ); + /** + * Pattern matching DashScope model ids that use a dot-versioned family (e.g. + * "qwen3.5-max", "qwen3.6-plus"). These are only offered on compatible-mode + * and consistently fail on the native endpoint with + * "[InvalidParameter] url error". Block them regardless of exact name. + */ + private static final java.util.regex.Pattern DASHSCOPE_NATIVE_UNSUPPORTED_PATTERN = + java.util.regex.Pattern.compile("^qwen\\d+\\.\\d+.*", java.util.regex.Pattern.CASE_INSENSITIVE); + /** * Allow-list prefixes for DashScope models that are known to work on the native * protocol. An empty set means "no prefix filter" (we still apply DENY). @@ -120,15 +129,7 @@ public class ModelDiscoveryService { } int before = discovered.size(); List filtered = discovered.stream() - .filter(m -> { - String id = m.getId(); - if (id == null || id.isBlank()) return false; - String lower = id.toLowerCase(); - if (DASHSCOPE_NATIVE_DENY.contains(lower)) return false; - // Allow if any allowed prefix matches; if allow-list is empty, permit everything - if (DASHSCOPE_NATIVE_ALLOW_PREFIXES.isEmpty()) return true; - return DASHSCOPE_NATIVE_ALLOW_PREFIXES.stream().anyMatch(lower::startsWith); - }) + .filter(m -> isDashScopeModelIdAcceptable(m.getId())) .toList(); if (filtered.size() < before) { log.info("[ModelDiscovery] Filtered {} -> {} DashScope models for provider={} (allow/deny rules)", @@ -137,6 +138,39 @@ public class ModelDiscoveryService { return filtered; } + /** + * Return true if a DashScope model id is allowed on the native protocol: + * not in the explicit DENY set, doesn't match the dot-version unsupported + * pattern, and matches at least one ALLOW prefix (or the allow list is empty). + */ + private static boolean isDashScopeModelIdAcceptable(String modelId) { + if (modelId == null || modelId.isBlank()) return false; + String lower = modelId.toLowerCase(); + if (DASHSCOPE_NATIVE_DENY.contains(lower)) return false; + if (DASHSCOPE_NATIVE_UNSUPPORTED_PATTERN.matcher(lower).matches()) return false; + if (DASHSCOPE_NATIVE_ALLOW_PREFIXES.isEmpty()) return true; + return DASHSCOPE_NATIVE_ALLOW_PREFIXES.stream().anyMatch(lower::startsWith); + } + + /** + * Defensive guard for code paths that persist a model id without going through + * discovery (e.g. the manual "Add model" form). Throws a MateClawException with + * a user-friendly message if the id is known to be unusable under the provider's + * runtime protocol. + */ + public static void assertModelIdAcceptable(String providerId, ModelProviderEntity provider, String modelId) { + if (provider == null) return; + ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel()); + if (protocol == ModelProtocol.DASHSCOPE_NATIVE && !isDashScopeModelIdAcceptable(modelId)) { + throw new MateClawException( + "err.llm.model_not_supported", + "Model id '" + modelId + "' is not supported on DashScope native protocol. " + + "Dot-versioned families (e.g. qwen3.5-*, qwen3.6-*) are only available via compatible-mode. " + + "Use an allowed id such as qwen-max / qwen-plus / qwen3-max." + ); + } + } + /** * Probe each discovered model in parallel (bounded concurrency) using the same * protocol the runtime will use. Populates {@code probeOk}/{@code probeError} @@ -253,10 +287,9 @@ public class ModelDiscoveryService { for (String modelId : modelIds) { if (modelId == null || modelId.isBlank()) continue; if (existingIds.contains(modelId)) continue; - // Defense-in-depth: never add a DashScope model that is on the native protocol deny list - if (protocol == ModelProtocol.DASHSCOPE_NATIVE - && DASHSCOPE_NATIVE_DENY.contains(modelId.toLowerCase())) { - log.warn("[ModelDiscovery] Refusing to add {} — on DashScope native deny list", modelId); + // Defense-in-depth: never add a DashScope model that fails the protocol-aware check + if (protocol == ModelProtocol.DASHSCOPE_NATIVE && !isDashScopeModelIdAcceptable(modelId)) { + log.warn("[ModelDiscovery] Refusing to add {} — blocked by DashScope native protocol filter", modelId); skipped++; continue; } diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java index 951eb3ec..621eed9b 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java @@ -121,7 +121,15 @@ public class ModelProviderService { public ProviderInfoDTO addModel(String providerId, AddProviderModelRequest request) { getProvider(providerId); - modelConfigService.addModelToProvider(providerId, request.getId(), request.getName(), false); + // Defense-in-depth: the manual "Add model" form must apply the same + // protocol-level safety as auto-discovery — otherwise users can freely + // type an unknown model id (e.g. "qwen3.6-plus") that DashScope native + // rejects at runtime with the opaque "[InvalidParameter] url error". + String modelId = request.getId(); + if (modelId != null && !modelId.isBlank()) { + ModelDiscoveryService.assertModelIdAcceptable(providerId, this.getProvider(providerId), modelId); + } + modelConfigService.addModelToProvider(providerId, modelId, request.getName(), false); return toProviderInfo(getProvider(providerId), modelConfigService.listModelsByProvider(providerId)); } diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V16__purge_dot_version_dashscope_models.sql b/mateclaw-server/src/main/resources/db/migration/h2/V16__purge_dot_version_dashscope_models.sql new file mode 100644 index 00000000..69a23b44 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V16__purge_dot_version_dashscope_models.sql @@ -0,0 +1,13 @@ +-- V16: Broaden the DashScope native-protocol purge beyond the two explicit ids in V15. +-- Any model whose name matches "qwenN.M-*" (dot-versioned family like qwen3.5-*, qwen3.6-*) +-- only exists on compatible-mode and fails on the native endpoint with +-- "[InvalidParameter] url error". Users who manually added such ids through the +-- "Add model" form before V16's runtime guard was in place end up with conversations +-- that silently fail. Clear them here so those users get a fresh start. +DELETE FROM mate_model_config +WHERE provider = 'dashscope' + AND (model_name LIKE 'qwen1.%' + OR model_name LIKE 'qwen2.%' + OR model_name LIKE 'qwen3.%' + OR model_name LIKE 'qwen4.%' + OR model_name LIKE 'qwen5.%'); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V16__purge_dot_version_dashscope_models.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V16__purge_dot_version_dashscope_models.sql new file mode 100644 index 00000000..d5898491 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V16__purge_dot_version_dashscope_models.sql @@ -0,0 +1,8 @@ +-- V16: Broaden the DashScope native-protocol purge (see V15). +DELETE FROM mate_model_config +WHERE provider = 'dashscope' + AND (model_name LIKE 'qwen1.%' + OR model_name LIKE 'qwen2.%' + OR model_name LIKE 'qwen3.%' + OR model_name LIKE 'qwen4.%' + OR model_name LIKE 'qwen5.%');