diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md
index e0f497b..dc77ee7 100644
--- a/docs/FLOWCHART.md
+++ b/docs/FLOWCHART.md
@@ -1,5 +1,114 @@
# Dify EE(企业版)Helm Chart Values 生成器流程图
+## 版本特性架构(Feature-based Architecture)
+
+从 3.7.0 版本开始,我们采用了 **特性驱动 + 版本检测** 的混合架构来处理不同版本之间的配置差异。
+
+### 架构图
+
+```mermaid
+flowchart TD
+ subgraph VersionManager["版本管理器"]
+ VM[VersionManager] --> |解析版本| ParseVersion["parse_version()"]
+ VM --> |比较版本| CompareVersion["compare_versions()"]
+ VM --> |检查约束| VersionSatisfies["version_satisfies()"]
+ end
+
+ subgraph FeatureSystem["特性系统 (modules/features/)"]
+ FR[FeatureRegistry] --> |注册| Register["register_feature()"]
+ FR --> |发现| Discover["_discover_features()"]
+ FR --> |应用| Apply["apply_features()"]
+
+ subgraph Features["版本特性模块"]
+ F1["trigger_domain.py
≥3.7.0, global"]
+ F2["trigger_worker.py
≥3.7.0, services"]
+ F3["plugin_metric.py
≥3.7.0, plugins"]
+ F4["external_prometheus.py
≥3.7.0, infrastructure"]
+ end
+
+ Register --> Features
+ end
+
+ subgraph Modules["配置模块 (modules/)"]
+ M1["global_config.py"]
+ M2["networking.py"]
+ M3["plugins.py"]
+ M4["services.py"]
+ end
+
+ Generator["ValuesGenerator"] --> |chart_version| VM
+ Generator --> Modules
+ Modules --> |调用| Apply
+ Apply --> |查询适用特性| FR
+ FR --> |版本匹配| VersionSatisfies
+ VersionSatisfies --> |返回匹配特性| Features
+ Features --> |配置| Generator
+
+ style FeatureSystem fill:#E6FFE6
+ style VersionManager fill:#E6F3FF
+ style Features fill:#FFF4E6
+```
+
+### 目录结构
+
+```
+modules/
+├── __init__.py
+├── global_config.py # 核心配置(所有版本通用)
+├── infrastructure.py
+├── networking.py
+├── mail.py
+├── plugins.py
+├── services.py
+└── features/ # 版本特定特性
+ ├── __init__.py # 自动发现机制
+ ├── base.py # Feature基类、注册器、版本比较
+ ├── trigger_domain.py # 3.7.0+ triggerDomain 配置
+ ├── trigger_worker.py # 3.7.0+ triggerWorker 服务
+ ├── plugin_metric.py # 3.7.0+ 插件指标监控
+ └── external_prometheus.py # 3.7.0+ 外部 Prometheus
+```
+
+### 工作原理
+
+1. **启动时自动发现**:`modules/features/__init__.py` 自动扫描并导入所有特性模块
+2. **装饰器注册**:每个特性使用 `@register_feature()` 装饰器声明版本约束
+3. **配置时应用**:各模块末尾调用 `apply_features(generator, "module_name")`
+4. **版本过滤**:只有满足版本约束的特性才会被执行
+
+### 添加新特性示例
+
+```python
+# modules/features/new_feature.py
+from .base import Feature, register_feature
+from utils import print_section, prompt
+from i18n import get_translator
+
+@register_feature(
+ min_version="3.8.0", # 最小版本(含)
+ max_version=None, # 最大版本(含),None表示无上限
+ module="global", # 所属模块
+ name="New Feature",
+ description="Description of the feature"
+)
+class NewFeature(Feature):
+ def configure(self, generator) -> None:
+ _t = get_translator()
+ # 配置逻辑...
+ generator.values['global']['newField'] = "value"
+```
+
+### 版本比较规则
+
+```
+3.6.0-alpha.1 < 3.6.0-beta.1 < 3.6.0-rc.1 < 3.6.0 < 3.7.0
+```
+
+- 预发布版本(alpha < beta < rc)优先级低于正式版本
+- 支持语义化版本比较
+
+---
+
## 主流程图
```mermaid
@@ -56,13 +165,26 @@ flowchart TD
RAGConfig[配置RAG参数
keywordDataSourceType
topKMaxValue
indexingMaxSegmentationTokensLength]
- RAGConfig --> End([模块1完成])
+ RAGConfig --> ApplyFeatures{应用版本特性}
+
+ ApplyFeatures --> CheckVersion{chart_version
≥ 3.7.0?}
+ CheckVersion -->|是| TriggerDomain[配置 triggerDomain
工作流 Webhook 触发器域名]
+ CheckVersion -->|否| End
+ TriggerDomain --> CheckFuture{更多版本特性?}
+ CheckFuture -->|有| ApplyMore[应用其他特性...]
+ CheckFuture -->|无| End
+ ApplyMore --> End
+
+ End([模块1完成])
style Start fill:#E6F3FF
style End fill:#E6F3FF
style RAGCheck fill:#FFF4E6
style DisableUnstructured fill:#FFE6E6
style EnableUnstructured fill:#E6FFE6
+ style ApplyFeatures fill:#E6FFE6
+ style TriggerDomain fill:#90EE90
+ style CheckVersion fill:#FFF4E6
```
## 模块2: 基础设施配置流程图
@@ -328,6 +450,16 @@ flowchart TD
## 决策点说明
+### 版本特性对照表
+
+| 特性名称 | 最低版本 | 所属模块 | 说明 |
+|---------|---------|---------|------|
+| `triggerDomain` | 3.7.0 | global | 工作流 Webhook 触发器域名配置 |
+| `triggerWorker` | 3.7.0 | services | Trigger Worker 服务配置(副本数、Celery、代码限制) |
+| `plugin_manager.metric` | 3.7.0 | plugins | 插件资源监控配置(CPU、内存、网络 I/O) |
+| `externalPrometheus` | 3.7.0 | infrastructure | 外部 Prometheus 配置(用于插件指标监控) |
+| `ssrfProxy.sandboxHost` | 3.6.x | infrastructure (高级选项) | 自定义 sandbox 主机 FQDN(用于跨命名空间部署) |
+
### 关键决策点
1. **PostgreSQL选择** (互斥)
diff --git a/i18n/translations.py b/i18n/translations.py
index 2851a12..5b142a3 100644
--- a/i18n/translations.py
+++ b/i18n/translations.py
@@ -254,6 +254,12 @@ TRANSLATIONS = {
'minio_root_password': 'MinIO Root Password',
'minio_root_user': 'MinIO Root User',
+ # Advanced configuration
+ 'advanced_config': 'Advanced Configuration',
+ 'config_advanced_options': 'Configure advanced options? (for special deployment scenarios)',
+ 'ssrf_proxy_sandbox_host': 'SSRF Proxy Sandbox Host (leave empty for default)',
+ 'ssrf_proxy_sandbox_host_desc': 'Custom sandbox host FQDN for cross-namespace deployments (e.g.: my-release-dify-sandbox-svc.my-namespace.svc.cluster.local)',
+
'select_mail_service_type': 'Select Mail Service Type',
'default_sender_address': 'Default Sender Address (e.g.: no-reply )',
'resend_api_key': 'Resend API Key',
@@ -358,6 +364,59 @@ TRANSLATIONS = {
'http_selected': 'HTTP protocol selected (not recommended)',
'https_selected': 'HTTPS protocol selected (recommended)',
+ # Version-specific features
+ 'applying_feature': 'Applying version-specific feature',
+ 'trigger_domain_desc': 'Trigger domain is used for workflow webhook triggers (3.7.0+)',
+ 'trigger_domain': 'Trigger Domain (for workflow webhooks)',
+
+ # Trigger Worker Feature (3.7.0+)
+ 'trigger_worker_config': 'Trigger Worker Configuration (3.7.0+)',
+ 'trigger_worker_desc': 'Trigger Worker is a dedicated Celery worker for handling workflow triggers',
+ 'config_trigger_worker': 'Configure Trigger Worker settings?',
+ 'using_default_trigger_worker': 'Using default Trigger Worker settings',
+ 'trigger_worker_replicas': 'Trigger Worker Replicas',
+ 'trigger_worker_celery_amount': 'Celery Worker Amount',
+ 'trigger_worker_code_limits_desc': 'Code execution limits control sandbox execution parameters',
+ 'config_trigger_worker_code_limits': 'Configure code execution limits?',
+ 'max_string_array_length': 'Max String Array Length',
+ 'max_object_array_length': 'Max Object Array Length',
+ 'max_number_array_length': 'Max Number Array Length',
+ 'trigger_worker_configured': 'Trigger Worker configured successfully',
+
+ # Plugin Metric Feature (3.7.0+)
+ 'plugin_metric_config': 'Plugin Metrics Configuration (3.7.0+)',
+ 'plugin_metric_desc': 'Plugin metrics monitoring for CPU, memory, and network I/O',
+ 'config_plugin_metric': 'Configure plugin metrics?',
+ 'using_default_plugin_metric': 'Using default plugin metrics settings (disabled)',
+ 'plugin_metric_source_options': 'Available metric sources:',
+ 'plugin_metric_disabled_desc': 'No metrics collected',
+ 'plugin_metric_cadvisor_desc': 'Collect from cadvisor (requires cluster roles)',
+ 'plugin_metric_prometheus_desc': 'Query from external Prometheus (recommended)',
+ 'plugin_metric_source': 'Metric Source',
+ 'cadvisor_cluster_role_warning': 'Warning: cadvisor requires cluster roles for reading nodes',
+ 'config_cadvisor_scrape': 'Configure cadvisor scrape settings?',
+ 'scrape_interval': 'Scrape Interval (e.g., 20s)',
+ 'scrape_timeout': 'Scrape Timeout (e.g., 10s)',
+ 'retain_period': 'Retain Period (e.g., 604800s)',
+ 'prometheus_external_required': 'Note: prometheus source requires externalPrometheus to be enabled',
+ 'prometheus_config_in_infrastructure': 'External Prometheus will be configured in infrastructure module',
+ 'plugin_metric_configured': 'Plugin metrics configured successfully',
+
+ # External Prometheus Feature (3.7.0+)
+ 'external_prometheus_config': 'External Prometheus Configuration (3.7.0+)',
+ 'external_prometheus_desc': 'External Prometheus for plugin metrics monitoring',
+ 'enable_external_prometheus': 'Enable External Prometheus?',
+ 'external_prometheus_disabled': 'External Prometheus disabled',
+ 'external_prometheus_endpoint_desc': 'Prometheus endpoint for querying metrics',
+ 'prometheus_endpoint': 'Prometheus Endpoint URL',
+ 'prometheus_timeout': 'Query Timeout (e.g., 10s)',
+ 'prometheus_auth_required': 'Prometheus requires authentication?',
+ 'prometheus_username': 'Prometheus Username',
+ 'prometheus_password': 'Prometheus Password',
+ 'prometheus_insecure': 'Skip TLS verification?',
+ 'prometheus_insecure_warning': 'Warning: TLS verification disabled, not recommended for production',
+ 'external_prometheus_configured': 'External Prometheus configured successfully',
+
},
'zh': {
# Common
@@ -733,6 +792,12 @@ TRANSLATIONS = {
'minio_root_password': 'MinIO root 密码',
'minio_root_user': 'MinIO root 用户',
+ # Advanced configuration
+ 'advanced_config': '高级配置',
+ 'config_advanced_options': '是否配置高级选项?(用于特殊部署场景)',
+ 'ssrf_proxy_sandbox_host': 'SSRF 代理 Sandbox 主机 (留空使用默认值)',
+ 'ssrf_proxy_sandbox_host_desc': '自定义 sandbox 主机 FQDN,用于跨命名空间部署 (例如: my-release-dify-sandbox-svc.my-namespace.svc.cluster.local)',
+
'select_mail_service_type': '选择邮件服务类型',
'default_sender_address': '默认发件人地址 (例如: no-reply )',
'resend_api_key': 'Resend API Key',
@@ -837,6 +902,59 @@ TRANSLATIONS = {
'http_selected': '已选择 HTTP 协议(不推荐)',
'https_selected': '已选择 HTTPS 协议(推荐)',
+ # Version-specific features
+ 'applying_feature': '正在应用版本特性',
+ 'trigger_domain_desc': 'Trigger Domain 用于工作流 Webhook 触发器 (3.7.0+)',
+ 'trigger_domain': 'Trigger Domain (工作流 Webhook 触发器)',
+
+ # Trigger Worker Feature (3.7.0+)
+ 'trigger_worker_config': 'Trigger Worker 配置 (3.7.0+)',
+ 'trigger_worker_desc': 'Trigger Worker 是专用于处理工作流触发器的 Celery Worker',
+ 'config_trigger_worker': '是否配置 Trigger Worker 设置?',
+ 'using_default_trigger_worker': '使用默认 Trigger Worker 设置',
+ 'trigger_worker_replicas': 'Trigger Worker 副本数',
+ 'trigger_worker_celery_amount': 'Celery Worker 数量',
+ 'trigger_worker_code_limits_desc': '代码执行限制控制沙箱执行参数',
+ 'config_trigger_worker_code_limits': '是否配置代码执行限制?',
+ 'max_string_array_length': '最大字符串数组长度',
+ 'max_object_array_length': '最大对象数组长度',
+ 'max_number_array_length': '最大数字数组长度',
+ 'trigger_worker_configured': 'Trigger Worker 配置成功',
+
+ # Plugin Metric Feature (3.7.0+)
+ 'plugin_metric_config': '插件指标监控配置 (3.7.0+)',
+ 'plugin_metric_desc': '插件资源监控:CPU、内存、网络 I/O',
+ 'config_plugin_metric': '是否配置插件指标监控?',
+ 'using_default_plugin_metric': '使用默认插件指标设置(已禁用)',
+ 'plugin_metric_source_options': '可用的指标来源:',
+ 'plugin_metric_disabled_desc': '不收集指标',
+ 'plugin_metric_cadvisor_desc': '从 cadvisor 收集(需要集群角色)',
+ 'plugin_metric_prometheus_desc': '从外部 Prometheus 查询(推荐)',
+ 'plugin_metric_source': '指标来源',
+ 'cadvisor_cluster_role_warning': '警告:cadvisor 需要读取节点的集群角色权限',
+ 'config_cadvisor_scrape': '是否配置 cadvisor 抓取设置?',
+ 'scrape_interval': '抓取间隔(如 20s)',
+ 'scrape_timeout': '抓取超时(如 10s)',
+ 'retain_period': '保留周期(如 604800s)',
+ 'prometheus_external_required': '注意:prometheus 来源需要启用 externalPrometheus',
+ 'prometheus_config_in_infrastructure': '外部 Prometheus 将在基础设施模块中配置',
+ 'plugin_metric_configured': '插件指标监控配置成功',
+
+ # External Prometheus Feature (3.7.0+)
+ 'external_prometheus_config': '外部 Prometheus 配置 (3.7.0+)',
+ 'external_prometheus_desc': '用于插件指标监控的外部 Prometheus',
+ 'enable_external_prometheus': '是否启用外部 Prometheus?',
+ 'external_prometheus_disabled': '外部 Prometheus 已禁用',
+ 'external_prometheus_endpoint_desc': '用于查询指标的 Prometheus 端点',
+ 'prometheus_endpoint': 'Prometheus 端点 URL',
+ 'prometheus_timeout': '查询超时(如 10s)',
+ 'prometheus_auth_required': 'Prometheus 是否需要认证?',
+ 'prometheus_username': 'Prometheus 用户名',
+ 'prometheus_password': 'Prometheus 密码',
+ 'prometheus_insecure': '是否跳过 TLS 验证?',
+ 'prometheus_insecure_warning': '警告:已禁用 TLS 验证,不建议在生产环境使用',
+ 'external_prometheus_configured': '外部 Prometheus 配置成功',
+
}
}
diff --git a/modules/features/__init__.py b/modules/features/__init__.py
new file mode 100644
index 0000000..bd48e1b
--- /dev/null
+++ b/modules/features/__init__.py
@@ -0,0 +1,57 @@
+"""
+Version-specific features module
+
+This module provides a framework for managing version-specific configuration features.
+Each feature declares its applicable version range and is automatically loaded when
+the current chart version matches.
+
+Architecture:
+- FeatureRegistry: Central registry for all version-specific features
+- Feature: Base class for defining version-specific configuration logic
+- Each feature module (e.g., trigger_domain.py) registers features with version constraints
+
+Usage:
+1. Create a new feature file in modules/features/
+2. Inherit from Feature base class
+3. Use @register_feature decorator to register with version constraints
+4. Features are automatically discovered and applied during configuration
+
+Example:
+ from modules.features import Feature, register_feature
+
+ @register_feature(min_version="3.7.0", module="global")
+ class TriggerDomainFeature(Feature):
+ def configure(self, generator):
+ # Configuration logic here
+ pass
+"""
+
+from .base import Feature, FeatureRegistry, register_feature, apply_features
+
+__all__ = [
+ 'Feature',
+ 'FeatureRegistry',
+ 'register_feature',
+ 'apply_features',
+]
+
+# Auto-discover and import all feature modules
+def _discover_features():
+ """Automatically discover and import all feature modules"""
+ import os
+ import importlib
+ from pathlib import Path
+
+ features_dir = Path(__file__).parent
+ for file in features_dir.glob("*.py"):
+ if file.name.startswith("_") or file.name == "base.py":
+ continue
+ module_name = file.stem
+ try:
+ importlib.import_module(f".{module_name}", package=__name__)
+ except ImportError as e:
+ print(f"Warning: Failed to import feature module {module_name}: {e}")
+
+# Discover features on module load
+_discover_features()
+
diff --git a/modules/features/base.py b/modules/features/base.py
new file mode 100644
index 0000000..e600cd8
--- /dev/null
+++ b/modules/features/base.py
@@ -0,0 +1,275 @@
+"""
+Feature base classes and registry for version-specific configurations
+
+This module provides the core infrastructure for managing version-specific features:
+- Semantic version comparison
+- Feature registration with version constraints
+- Automatic feature discovery and application
+"""
+
+from abc import ABC, abstractmethod
+from typing import Dict, List, Optional, Callable, Any, Type
+from functools import wraps
+import re
+
+
+def parse_version(version_str: str) -> tuple:
+ """
+ Parse version string into comparable tuple
+
+ Handles versions like:
+ - "3.7.0" -> (3, 7, 0, "", 0)
+ - "3.6.0-beta.1" -> (3, 6, 0, "beta", 1)
+ - "3.6.0-alpha.2" -> (3, 6, 0, "alpha", 2)
+ - "3.6.0-rc.1" -> (3, 6, 0, "rc", 1)
+
+ Pre-release versions are considered less than release versions:
+ 3.6.0-alpha.1 < 3.6.0-beta.1 < 3.6.0-rc.1 < 3.6.0
+ """
+ if not version_str:
+ return (0, 0, 0, "", 0)
+
+ # Split main version and pre-release
+ parts = version_str.split("-", 1)
+ main_version = parts[0]
+ pre_release = parts[1] if len(parts) > 1 else ""
+
+ # Parse main version (major.minor.patch)
+ version_parts = main_version.split(".")
+ major = int(version_parts[0]) if len(version_parts) > 0 else 0
+ minor = int(version_parts[1]) if len(version_parts) > 1 else 0
+ patch = int(version_parts[2]) if len(version_parts) > 2 else 0
+
+ # Parse pre-release (e.g., "beta.1", "alpha.2", "rc.1")
+ pre_type = ""
+ pre_num = 0
+ if pre_release:
+ pre_match = re.match(r"([a-zA-Z]+)\.?(\d+)?", pre_release)
+ if pre_match:
+ pre_type = pre_match.group(1).lower()
+ pre_num = int(pre_match.group(2)) if pre_match.group(2) else 0
+
+ # Pre-release priority: "" (release) > "rc" > "beta" > "alpha"
+ # Use higher number for release (empty string = highest priority)
+ pre_priority = {
+ "": 999, # Release version has highest priority
+ "rc": 3,
+ "beta": 2,
+ "alpha": 1,
+ }
+
+ return (major, minor, patch, pre_priority.get(pre_type, 0), pre_num)
+
+
+def compare_versions(v1: str, v2: str) -> int:
+ """
+ Compare two version strings
+
+ Returns:
+ -1 if v1 < v2
+ 0 if v1 == v2
+ 1 if v1 > v2
+ """
+ parsed_v1 = parse_version(v1)
+ parsed_v2 = parse_version(v2)
+
+ if parsed_v1 < parsed_v2:
+ return -1
+ elif parsed_v1 > parsed_v2:
+ return 1
+ return 0
+
+
+def version_satisfies(version: str, min_version: Optional[str] = None, max_version: Optional[str] = None) -> bool:
+ """
+ Check if version satisfies the given constraints
+
+ Args:
+ version: The version to check
+ min_version: Minimum version (inclusive), None means no minimum
+ max_version: Maximum version (inclusive), None means no maximum
+
+ Returns:
+ True if version satisfies constraints
+ """
+ if min_version and compare_versions(version, min_version) < 0:
+ return False
+ if max_version and compare_versions(version, max_version) > 0:
+ return False
+ return True
+
+
+class Feature(ABC):
+ """
+ Base class for version-specific features
+
+ Each feature must implement:
+ - name: Human-readable feature name
+ - configure(): Configuration logic to apply
+ """
+
+ # Feature metadata (set by decorator or subclass)
+ name: str = ""
+ description: str = ""
+ min_version: Optional[str] = None
+ max_version: Optional[str] = None
+ module: str = "" # Which module this feature belongs to (e.g., "global", "networking")
+
+ @abstractmethod
+ def configure(self, generator) -> None:
+ """
+ Apply feature configuration
+
+ Args:
+ generator: ValuesGenerator instance
+ """
+ pass
+
+ def is_applicable(self, chart_version: str) -> bool:
+ """
+ Check if this feature applies to the given chart version
+
+ Args:
+ chart_version: Helm Chart version string
+
+ Returns:
+ True if feature should be applied
+ """
+ return version_satisfies(chart_version, self.min_version, self.max_version)
+
+
+class FeatureRegistry:
+ """
+ Central registry for all version-specific features
+
+ Features are registered with version constraints and module associations.
+ During configuration, applicable features are automatically applied.
+ """
+
+ _features: Dict[str, List[Type[Feature]]] = {}
+
+ @classmethod
+ def register(cls, feature_class: Type[Feature], module: str = "") -> None:
+ """
+ Register a feature class
+
+ Args:
+ feature_class: Feature class to register
+ module: Module this feature belongs to
+ """
+ if module not in cls._features:
+ cls._features[module] = []
+ cls._features[module].append(feature_class)
+
+ @classmethod
+ def get_features_for_module(cls, module: str, chart_version: str) -> List[Feature]:
+ """
+ Get all applicable features for a module and version
+
+ Args:
+ module: Module name (e.g., "global", "networking")
+ chart_version: Helm Chart version
+
+ Returns:
+ List of Feature instances that apply to this version
+ """
+ features = []
+ for feature_class in cls._features.get(module, []):
+ feature = feature_class()
+ if feature.is_applicable(chart_version):
+ features.append(feature)
+ return features
+
+ @classmethod
+ def get_all_features(cls, chart_version: str) -> Dict[str, List[Feature]]:
+ """
+ Get all applicable features grouped by module
+
+ Args:
+ chart_version: Helm Chart version
+
+ Returns:
+ Dict mapping module names to lists of applicable features
+ """
+ result = {}
+ for module, feature_classes in cls._features.items():
+ applicable = []
+ for feature_class in feature_classes:
+ feature = feature_class()
+ if feature.is_applicable(chart_version):
+ applicable.append(feature)
+ if applicable:
+ result[module] = applicable
+ return result
+
+ @classmethod
+ def clear(cls) -> None:
+ """Clear all registered features (useful for testing)"""
+ cls._features.clear()
+
+
+def register_feature(
+ min_version: Optional[str] = None,
+ max_version: Optional[str] = None,
+ module: str = "global",
+ name: str = "",
+ description: str = ""
+) -> Callable[[Type[Feature]], Type[Feature]]:
+ """
+ Decorator to register a feature with version constraints
+
+ Args:
+ min_version: Minimum chart version (inclusive)
+ max_version: Maximum chart version (inclusive)
+ module: Module this feature belongs to
+ name: Human-readable feature name
+ description: Feature description
+
+ Example:
+ @register_feature(min_version="3.7.0", module="global")
+ class TriggerDomainFeature(Feature):
+ def configure(self, generator):
+ pass
+ """
+ def decorator(cls: Type[Feature]) -> Type[Feature]:
+ cls.min_version = min_version
+ cls.max_version = max_version
+ cls.module = module
+ if name:
+ cls.name = name
+ if description:
+ cls.description = description
+
+ FeatureRegistry.register(cls, module)
+ return cls
+
+ return decorator
+
+
+def apply_features(generator, module: str) -> None:
+ """
+ Apply all applicable features for a module
+
+ This function should be called at the end of each module's configure function
+ to apply version-specific features.
+
+ Args:
+ generator: ValuesGenerator instance
+ module: Module name (e.g., "global", "networking")
+ """
+ from utils import print_info, print_success
+ from i18n import get_translator
+
+ _t = get_translator()
+
+ chart_version = generator.chart_version
+ if not chart_version:
+ return
+
+ features = FeatureRegistry.get_features_for_module(module, chart_version)
+ for feature in features:
+ feature_name = feature.name or feature.__class__.__name__
+ print_info(f" → {_t('applying_feature')}: {feature_name} (>= {feature.min_version})")
+ feature.configure(generator)
+ print_success(f" ✓ {feature_name}")
+
diff --git a/modules/features/external_prometheus.py b/modules/features/external_prometheus.py
new file mode 100644
index 0000000..e6ead4d
--- /dev/null
+++ b/modules/features/external_prometheus.py
@@ -0,0 +1,103 @@
+"""
+External Prometheus Feature - Available from Chart version 3.7.0+
+
+This feature adds externalPrometheus configuration for plugin metrics monitoring.
+When enabled, plugin_manager can query metrics from an external Prometheus server.
+"""
+
+from .base import Feature, register_feature
+from utils import print_section, print_info, print_success, print_warning, prompt, prompt_yes_no
+from i18n import get_translator
+
+
+@register_feature(
+ min_version="3.7.0",
+ module="infrastructure",
+ name="External Prometheus",
+ description="Configure external Prometheus for plugin metrics monitoring"
+)
+class ExternalPrometheusFeature(Feature):
+ """
+ Configure externalPrometheus settings (3.7.0+)
+
+ This feature provides an external Prometheus endpoint for plugin metrics:
+ - Required when plugin_manager.metric.source = "prometheus"
+ - Avoids the need for cluster roles required by cadvisor
+ - Supports authentication (username/password)
+
+ Required metrics:
+ - container_cpu_usage_seconds_total
+ - container_memory_working_set_bytes
+ - container_network_receive_bytes_total
+ - container_network_transmit_bytes_total
+ """
+
+ def configure(self, generator) -> None:
+ """Configure external Prometheus settings"""
+ _t = get_translator()
+
+ print_section(_t('external_prometheus_config'))
+ print_info(_t('external_prometheus_desc'))
+
+ # Check if user wants to enable external Prometheus
+ enable_prometheus = prompt_yes_no(
+ _t('enable_external_prometheus'),
+ default=generator.values.get('externalPrometheus', {}).get('enabled', False)
+ )
+
+ # Ensure externalPrometheus configuration exists
+ if 'externalPrometheus' not in generator.values:
+ generator.values['externalPrometheus'] = {}
+
+ generator.values['externalPrometheus']['enabled'] = enable_prometheus
+
+ if not enable_prometheus:
+ print_info(_t('external_prometheus_disabled'))
+ return
+
+ # Configure Prometheus endpoint
+ print_info(_t('external_prometheus_endpoint_desc'))
+
+ endpoint = prompt(
+ _t('prometheus_endpoint'),
+ default=generator.values.get('externalPrometheus', {}).get('endpoint', 'http://prometheus:9090'),
+ required=True
+ )
+ generator.values['externalPrometheus']['endpoint'] = endpoint
+
+ # Configure timeout
+ timeout = prompt(
+ _t('prometheus_timeout'),
+ default=generator.values.get('externalPrometheus', {}).get('timeout', '10s'),
+ required=False
+ )
+ generator.values['externalPrometheus']['timeout'] = timeout
+
+ # Configure authentication
+ if prompt_yes_no(_t('prometheus_auth_required'), default=False):
+ username = prompt(
+ _t('prometheus_username'),
+ default=generator.values.get('externalPrometheus', {}).get('username', ''),
+ required=False
+ )
+ generator.values['externalPrometheus']['username'] = username
+
+ password = prompt(
+ _t('prometheus_password'),
+ default='',
+ required=False
+ )
+ if password:
+ generator.values['externalPrometheus']['password'] = password
+
+ # Configure insecure (skip TLS verification)
+ insecure = prompt_yes_no(
+ _t('prometheus_insecure'),
+ default=generator.values.get('externalPrometheus', {}).get('insecure', True)
+ )
+ generator.values['externalPrometheus']['insecure'] = insecure
+
+ if insecure:
+ print_warning(_t('prometheus_insecure_warning'))
+
+ print_success(_t('external_prometheus_configured'))
diff --git a/modules/features/plugin_metric.py b/modules/features/plugin_metric.py
new file mode 100644
index 0000000..7add483
--- /dev/null
+++ b/modules/features/plugin_metric.py
@@ -0,0 +1,102 @@
+"""
+Plugin Metric Feature - Available from Chart version 3.7.0+
+
+This feature adds plugin_manager.metric configuration for plugin resource monitoring.
+Supports multiple metric sources: disabled, cadvisor, or prometheus.
+"""
+
+from .base import Feature, register_feature
+from utils import print_section, print_info, print_success, print_warning, prompt, prompt_choice, prompt_yes_no
+from i18n import get_translator
+
+
+@register_feature(
+ min_version="3.7.0",
+ module="plugins",
+ name="Plugin Metrics",
+ description="Configure plugin resource monitoring (CPU, memory, network I/O)"
+)
+class PluginMetricFeature(Feature):
+ """
+ Configure plugin_manager.metric settings (3.7.0+)
+
+ This feature enables monitoring of plugin resources:
+ - CPU usage
+ - Memory usage
+ - Network I/O
+
+ Metric sources:
+ - disabled: No metrics collected
+ - cadvisor: Metrics from cadvisor (requires cluster roles)
+ - prometheus: Query from external Prometheus (recommended)
+ """
+
+ def configure(self, generator) -> None:
+ """Configure plugin metrics settings"""
+ _t = get_translator()
+
+ print_section(_t('plugin_metric_config'))
+ print_info(_t('plugin_metric_desc'))
+
+ # Ensure plugin_manager configuration exists
+ if 'plugin_manager' not in generator.values:
+ generator.values['plugin_manager'] = {}
+
+ # Check if user wants to configure plugin metrics
+ if not prompt_yes_no(_t('config_plugin_metric'), default=False):
+ print_info(_t('using_default_plugin_metric'))
+ return
+
+ # Select metric source
+ print_info("")
+ print_info(_t('plugin_metric_source_options'))
+ print_info(f" • disabled - {_t('plugin_metric_disabled_desc')}")
+ print_info(f" • cadvisor - {_t('plugin_metric_cadvisor_desc')}")
+ print_info(f" • prometheus - {_t('plugin_metric_prometheus_desc')}")
+
+ metric_source = prompt_choice(
+ _t('plugin_metric_source'),
+ ["disabled", "cadvisor", "prometheus"],
+ default=generator.values.get('plugin_manager', {}).get('metric', {}).get('source', 'disabled')
+ )
+
+ # Ensure metric section exists
+ if 'metric' not in generator.values['plugin_manager']:
+ generator.values['plugin_manager']['metric'] = {}
+
+ generator.values['plugin_manager']['metric']['source'] = metric_source
+
+ if metric_source == "cadvisor":
+ print_warning(_t('cadvisor_cluster_role_warning'))
+
+ # Configure scrape settings
+ if prompt_yes_no(_t('config_cadvisor_scrape'), default=False):
+ if 'scrape' not in generator.values['plugin_manager']['metric']:
+ generator.values['plugin_manager']['metric']['scrape'] = {}
+
+ scrape_interval = prompt(
+ _t('scrape_interval'),
+ default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('scrapeInterval', '20s'),
+ required=False
+ )
+ generator.values['plugin_manager']['metric']['scrape']['scrapeInterval'] = scrape_interval
+
+ scrape_timeout = prompt(
+ _t('scrape_timeout'),
+ default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('scrapeTimeout', '10s'),
+ required=False
+ )
+ generator.values['plugin_manager']['metric']['scrape']['scrapeTimeout'] = scrape_timeout
+
+ retain_period = prompt(
+ _t('retain_period'),
+ default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('retainPeriod', '604800s'),
+ required=False
+ )
+ generator.values['plugin_manager']['metric']['scrape']['retainPeriod'] = retain_period
+
+ elif metric_source == "prometheus":
+ print_info(_t('prometheus_external_required'))
+ print_info(_t('prometheus_config_in_infrastructure'))
+
+ print_success(_t('plugin_metric_configured'))
diff --git a/modules/features/trigger_worker.py b/modules/features/trigger_worker.py
new file mode 100644
index 0000000..0431616
--- /dev/null
+++ b/modules/features/trigger_worker.py
@@ -0,0 +1,109 @@
+"""
+Trigger Worker Feature - Available from Chart version 3.7.0+
+
+This feature adds the triggerWorker service configuration for workflow trigger functionality.
+The triggerWorker is a dedicated Celery worker for handling workflow triggers.
+"""
+
+from .base import Feature, register_feature
+from utils import print_section, print_info, print_success, prompt, prompt_yes_no
+from i18n import get_translator
+
+
+@register_feature(
+ min_version="3.7.0",
+ module="services",
+ name="Trigger Worker Service",
+ description="Configure triggerWorker service for workflow trigger processing"
+)
+class TriggerWorkerFeature(Feature):
+ """
+ Configure triggerWorker service (3.7.0+)
+
+ This service is used for:
+ - Processing workflow webhook triggers
+ - Handling scheduled workflow executions
+ - Managing trigger-based workflow invocations
+ """
+
+ def configure(self, generator) -> None:
+ """Configure trigger worker service settings"""
+ _t = get_translator()
+
+ print_section(_t('trigger_worker_config'))
+ print_info(_t('trigger_worker_desc'))
+
+ # Ensure triggerWorker configuration exists
+ if 'triggerWorker' not in generator.values:
+ generator.values['triggerWorker'] = {}
+
+ # Check if user wants to configure trigger worker
+ if not prompt_yes_no(_t('config_trigger_worker'), default=False):
+ print_info(_t('using_default_trigger_worker'))
+ return
+
+ # Configure replicas
+ replicas_input = prompt(
+ _t('trigger_worker_replicas'),
+ default=str(generator.values.get('triggerWorker', {}).get('replicas', 1)),
+ required=False
+ )
+ try:
+ replicas = int(replicas_input)
+ if replicas >= 1:
+ generator.values['triggerWorker']['replicas'] = replicas
+ except ValueError:
+ pass
+
+ # Configure celery worker amount
+ celery_input = prompt(
+ _t('trigger_worker_celery_amount'),
+ default=str(generator.values.get('triggerWorker', {}).get('celeryWorkerAmount', 1)),
+ required=False
+ )
+ try:
+ celery_amount = int(celery_input)
+ if celery_amount >= 1:
+ generator.values['triggerWorker']['celeryWorkerAmount'] = celery_amount
+ except ValueError:
+ pass
+
+ # Configure code execution limits
+ print_info(_t('trigger_worker_code_limits_desc'))
+
+ if prompt_yes_no(_t('config_trigger_worker_code_limits'), default=False):
+ # Ensure code section exists
+ if 'code' not in generator.values['triggerWorker']:
+ generator.values['triggerWorker']['code'] = {}
+
+ max_string = prompt(
+ _t('max_string_array_length'),
+ default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxStringArrayLength', 500)),
+ required=False
+ )
+ try:
+ generator.values['triggerWorker']['code']['maxStringArrayLength'] = int(max_string)
+ except ValueError:
+ pass
+
+ max_object = prompt(
+ _t('max_object_array_length'),
+ default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxObjectArrayLength', 500)),
+ required=False
+ )
+ try:
+ generator.values['triggerWorker']['code']['maxObjectArrayLength'] = int(max_object)
+ except ValueError:
+ pass
+
+ max_number = prompt(
+ _t('max_number_array_length'),
+ default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxNumberArrayLength', 500)),
+ required=False
+ )
+ try:
+ generator.values['triggerWorker']['code']['maxNumberArrayLength'] = int(max_number)
+ except ValueError:
+ pass
+
+ print_success(_t('trigger_worker_configured'))
diff --git a/modules/global_config.py b/modules/global_config.py
index b2c312e..86f8823 100644
--- a/modules/global_config.py
+++ b/modules/global_config.py
@@ -6,6 +6,8 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
+from modules.features import apply_features
+from modules.features.base import version_satisfies
_t = get_translator()
@@ -74,6 +76,16 @@ def configure_global(generator):
required=False
)
+ # Trigger domain (3.7.0+)
+ chart_version = getattr(generator, 'chart_version', None)
+ if chart_version and version_satisfies(chart_version, "3.7.0"):
+ print_info(_t('trigger_domain_desc'))
+ generator.values['global']['triggerDomain'] = prompt(
+ _t('trigger_domain'),
+ default="trigger.dify.local",
+ required=False
+ )
+
# Database migration
generator.values['global']['dbMigrationEnabled'] = prompt_yes_no(
_t('enable_db_migration'),
@@ -132,4 +144,6 @@ def configure_global(generator):
except ValueError:
print_warning(f"{_t('invalid_number_use_default')} 4000")
- # ==================== 模块 2: 基础设施配置 ====================
+ # Apply version-specific features for global module
+ # Features are automatically discovered based on chart_version
+ apply_features(generator, "global")
diff --git a/modules/infrastructure.py b/modules/infrastructure.py
index 6e2dcb7..8c2ca55 100644
--- a/modules/infrastructure.py
+++ b/modules/infrastructure.py
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
+from modules.features import apply_features
_t = get_translator()
@@ -556,4 +557,22 @@ def configure_infrastructure(generator):
required=False
)
+ # Advanced SSRF Proxy configuration
+ print_section(_t('advanced_config'))
+ if prompt_yes_no(_t('config_advanced_options'), default=False):
+ # ssrfProxy.sandboxHost
+ print_info(_t('ssrf_proxy_sandbox_host_desc'))
+ sandbox_host = prompt(
+ _t('ssrf_proxy_sandbox_host'),
+ default="",
+ required=False
+ )
+ if sandbox_host:
+ if 'ssrfProxy' not in generator.values:
+ generator.values['ssrfProxy'] = {}
+ generator.values['ssrfProxy']['sandboxHost'] = sandbox_host
+
+ # Apply version-specific features for infrastructure module
+ apply_features(generator, "infrastructure")
+
# ==================== 模块 3: 网络配置 ====================
diff --git a/modules/plugins.py b/modules/plugins.py
index a9165d1..e3d404f 100644
--- a/modules/plugins.py
+++ b/modules/plugins.py
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
+from modules.features import apply_features
_t = get_translator()
@@ -195,3 +196,7 @@ def configure_plugins(generator):
else:
print_success(_t('https_selected'))
+ # Apply version-specific features for plugins module
+ # Features are automatically discovered based on chart_version
+ apply_features(generator, "plugins")
+
diff --git a/modules/services.py b/modules/services.py
index 09325d6..6593687 100644
--- a/modules/services.py
+++ b/modules/services.py
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
+from modules.features import apply_features
_t = get_translator()
@@ -95,3 +96,7 @@ def configure_services(generator):
# Note: unstructured.enabled is automatically configured in global_config based on RAG etlType
# No need to configure service enablement here
+
+ # Apply version-specific features for services module
+ # Features are automatically discovered based on chart_version
+ apply_features(generator, "services")
diff --git a/test_features.py b/test_features.py
new file mode 100644
index 0000000..081115c
--- /dev/null
+++ b/test_features.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+"""
+Test script for version-specific features
+
+This script tests:
+1. Version parsing and comparison
+2. Feature registration and discovery
+3. Feature applicability based on chart version
+"""
+
+import sys
+from pathlib import Path
+
+# Add project root to path
+sys.path.insert(0, str(Path(__file__).parent))
+
+from modules.features.base import (
+ parse_version,
+ compare_versions,
+ version_satisfies,
+ FeatureRegistry
+)
+
+
+def test_version_parsing():
+ """Test version parsing"""
+ print("\n=== 测试版本解析 ===")
+
+ test_cases = [
+ ("3.6.0", (3, 6, 0, 999, 0)),
+ ("3.7.0", (3, 7, 0, 999, 0)),
+ ("3.6.0-beta.1", (3, 6, 0, 2, 1)),
+ ("3.6.0-alpha.2", (3, 6, 0, 1, 2)),
+ ("3.6.0-rc.1", (3, 6, 0, 3, 1)),
+ ]
+
+ for version_str, expected in test_cases:
+ result = parse_version(version_str)
+ status = "✓" if result == expected else "✗"
+ print(f" {status} parse_version('{version_str}') = {result}")
+ if result != expected:
+ print(f" Expected: {expected}")
+
+
+def test_version_comparison():
+ """Test version comparison"""
+ print("\n=== 测试版本比较 ===")
+
+ test_cases = [
+ ("3.6.0", "3.7.0", -1), # 3.6.0 < 3.7.0
+ ("3.7.0", "3.6.0", 1), # 3.7.0 > 3.6.0
+ ("3.7.0", "3.7.0", 0), # 3.7.0 == 3.7.0
+ ("3.6.0-beta.1", "3.6.0", -1), # beta < release
+ ("3.6.0-alpha.1", "3.6.0-beta.1", -1), # alpha < beta
+ ("3.7.0", "3.6.5", 1), # 3.7.0 > 3.6.5
+ ]
+
+ for v1, v2, expected in test_cases:
+ result = compare_versions(v1, v2)
+ status = "✓" if result == expected else "✗"
+ op = "<" if result < 0 else (">" if result > 0 else "==")
+ print(f" {status} {v1} {op} {v2}")
+
+
+def test_version_satisfies():
+ """Test version constraint checking"""
+ print("\n=== 测试版本约束 ===")
+
+ test_cases = [
+ ("3.7.0", "3.7.0", None, True), # 3.7.0 >= 3.7.0
+ ("3.6.5", "3.7.0", None, False), # 3.6.5 < 3.7.0
+ ("3.7.2", "3.7.0", None, True), # 3.7.2 >= 3.7.0
+ ("3.8.0", "3.7.0", "3.8.0", True), # 3.7.0 <= 3.8.0 <= 3.8.0
+ ("3.9.0", "3.7.0", "3.8.0", False), # 3.9.0 > 3.8.0
+ ]
+
+ for version, min_v, max_v, expected in test_cases:
+ result = version_satisfies(version, min_v, max_v)
+ status = "✓" if result == expected else "✗"
+ constraint = f">= {min_v}" + (f", <= {max_v}" if max_v else "")
+ print(f" {status} {version} satisfies ({constraint}) = {result}")
+
+
+def test_feature_registry():
+ """Test feature registry"""
+ print("\n=== 测试特性注册表 ===")
+
+ # Import features to trigger registration
+ import modules.features # This auto-discovers features
+
+ # Test for different versions
+ test_versions = ["3.6.0", "3.6.5", "3.7.0", "3.7.2"]
+
+ for version in test_versions:
+ print(f"\n Chart 版本: {version}")
+ features = FeatureRegistry.get_all_features(version)
+
+ if not features:
+ print(" (无适用特性)")
+ else:
+ for module, feature_list in features.items():
+ for feature in feature_list:
+ print(f" → [{module}] {feature.name or feature.__class__.__name__}")
+
+
+def test_feature_order():
+ """Test that triggerDomain is configured in domain section"""
+ print("\n=== 检查 triggerDomain 配置位置 ===")
+
+ # Read global_config.py to check where triggerDomain is configured
+ global_config_path = Path(__file__).parent / "modules" / "global_config.py"
+ content = global_config_path.read_text()
+
+ # Check if triggerDomain is in domain config section
+ domain_section_start = content.find("# Domain configuration")
+ domain_section_end = content.find("# Database migration")
+
+ # Check if there's any triggerDomain in domain section
+ domain_section = content[domain_section_start:domain_section_end] if domain_section_start >= 0 and domain_section_end >= 0 else ""
+
+ if "triggerDomain" in domain_section:
+ print(" ✓ triggerDomain 在域名配置区域")
+ # Check for version guard
+ if "version_satisfies" in domain_section and "3.7.0" in domain_section:
+ print(" ✓ triggerDomain 有版本检测 (>= 3.7.0)")
+ else:
+ print(" ✗ triggerDomain 缺少版本检测")
+ else:
+ print(" ✗ triggerDomain 不在域名配置区域")
+ print(" → 建议:应该和其他域名一起在域名配置区域中配置")
+
+
+def test_ssrf_proxy_sandbox_host():
+ """Test that ssrfProxy.sandboxHost is configured as advanced option"""
+ print("\n=== 检查 ssrfProxy.sandboxHost 配置 ===")
+
+ # Read infrastructure.py to check sandboxHost configuration
+ infra_config_path = Path(__file__).parent / "modules" / "infrastructure.py"
+ content = infra_config_path.read_text()
+
+ # Check if sandboxHost is in advanced config section
+ if "sandboxHost" in content:
+ print(" ✓ ssrfProxy.sandboxHost 已配置")
+ if "config_advanced_options" in content:
+ print(" ✓ 作为高级选项配置")
+ else:
+ print(" ✗ 不是高级选项")
+ else:
+ print(" ✗ ssrfProxy.sandboxHost 未配置")
+
+ # Check i18n translations
+ translations_path = Path(__file__).parent / "i18n" / "translations.py"
+ translations_content = translations_path.read_text()
+
+ if "ssrf_proxy_sandbox_host" in translations_content:
+ print(" ✓ i18n 翻译已添加")
+ else:
+ print(" ✗ i18n 翻译缺失")
+
+
+def test_global_config_trigger_domain():
+ """Test that triggerDomain is properly shown based on version"""
+ print("\n=== 测试 triggerDomain 版本检测 ===")
+
+ from modules.features.base import version_satisfies
+
+ test_cases = [
+ ("3.6.0", False, "3.6.0 不应显示 triggerDomain"),
+ ("3.6.5", False, "3.6.5 不应显示 triggerDomain"),
+ ("3.7.0", True, "3.7.0 应显示 triggerDomain"),
+ ("3.7.2", True, "3.7.2 应显示 triggerDomain"),
+ ("3.8.0", True, "3.8.0 应显示 triggerDomain"),
+ ]
+
+ for version, expected, desc in test_cases:
+ result = version_satisfies(version, "3.7.0")
+ status = "✓" if result == expected else "✗"
+ print(f" {status} {desc}: {result}")
+
+
+def test_all_features_for_versions():
+ """Comprehensive test of all features across versions"""
+ print("\n=== 完整版本特性矩阵 ===")
+
+ import modules.features
+ from modules.features.base import FeatureRegistry, version_satisfies
+
+ versions = ["3.5.6", "3.6.0", "3.6.5", "3.7.0", "3.7.2"]
+
+ # Build feature matrix
+ print("\n 版本特性支持矩阵:")
+ print(" " + "-" * 70)
+
+ # Header
+ header = " 特性名称".ljust(35)
+ for v in versions:
+ header += f" {v}".center(8)
+ print(header)
+ print(" " + "-" * 70)
+
+ # Collect unique features across all versions
+ all_feature_info = {}
+ for v in versions:
+ features_dict = FeatureRegistry.get_all_features(v)
+ for module, feature_list in features_dict.items():
+ for feature in feature_list:
+ key = feature.__class__.__name__
+ if key not in all_feature_info:
+ all_feature_info[key] = {
+ 'name': getattr(feature, 'name', feature.__class__.__name__),
+ 'min_version': getattr(feature, 'min_version', None),
+ 'max_version': getattr(feature, 'max_version', None),
+ 'module': module
+ }
+
+ # Display matrix
+ for key, info in all_feature_info.items():
+ name = f"[{info['module']}] {info['name']}"[:32]
+ row = f" {name}".ljust(35)
+
+ for v in versions:
+ supported = version_satisfies(v, info['min_version'], info['max_version'])
+ row += (" ✓".center(8) if supported else " -".center(8))
+ print(row)
+
+ print(" " + "-" * 70)
+
+
+def main():
+ print("=" * 60)
+ print("Dify EE Helm Chart Values Generator - Feature Tests")
+ print("=" * 60)
+
+ test_version_parsing()
+ test_version_comparison()
+ test_version_satisfies()
+ test_feature_registry()
+ test_feature_order()
+ test_global_config_trigger_domain()
+ test_ssrf_proxy_sandbox_host()
+ test_all_features_for_versions()
+
+ print("\n" + "=" * 60)
+ print("测试完成")
+ print("=" * 60)
+
+
+if __name__ == "__main__":
+ main()
+