From 13ed6c8d46271188d863cfef691078d4eba9bfec Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 14:04:40 +0800 Subject: [PATCH] feat: add configurable webhook display prefix (#1797) * Initial plan * Add webhook_display_prefix configuration option Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: change config field name --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin --- src/langbot/pkg/api/http/service/bot.py | 4 +- src/langbot/templates/config.yaml | 1 + .../config/test_webhook_display_prefix.py | 143 ++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/config/test_webhook_display_prefix.py diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index 2942521c..13ef3393 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -59,10 +59,10 @@ class BotService: adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id if persistence_bot['adapter'] in ['wecom', 'wecombot', 'officialaccount', 'qqofficial', 'slack', 'wecomcs']: - api_port = self.ap.instance_config.data['api']['port'] + webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300') webhook_url = f'/bots/{bot_uuid}' adapter_runtime_values['webhook_url'] = webhook_url - adapter_runtime_values['webhook_full_url'] = f'http://:{api_port}{webhook_url}' + adapter_runtime_values['webhook_full_url'] = f'{webhook_prefix}{webhook_url}' else: adapter_runtime_values['webhook_url'] = None adapter_runtime_values['webhook_full_url'] = None diff --git a/src/langbot/templates/config.yaml b/src/langbot/templates/config.yaml index 28c4d57b..c5a72022 100644 --- a/src/langbot/templates/config.yaml +++ b/src/langbot/templates/config.yaml @@ -1,6 +1,7 @@ admins: [] api: port: 5300 + webhook_prefix: 'http://127.0.0.1:5300' command: enable: true prefix: diff --git a/tests/unit_tests/config/test_webhook_display_prefix.py b/tests/unit_tests/config/test_webhook_display_prefix.py new file mode 100644 index 00000000..8331befc --- /dev/null +++ b/tests/unit_tests/config/test_webhook_display_prefix.py @@ -0,0 +1,143 @@ +""" +Tests for webhook_prefix configuration +""" + +import os +import pytest +from typing import Any + + +def _apply_env_overrides_to_config(cfg: dict) -> dict: + """Apply environment variable overrides to data/config.yaml + + Environment variables should be uppercase and use __ (double underscore) + to represent nested keys. For example: + - CONCURRENCY__PIPELINE overrides concurrency.pipeline + - PLUGIN__RUNTIME_WS_URL overrides plugin.runtime_ws_url + + Arrays and dict types are ignored. + + Args: + cfg: Configuration dictionary + + Returns: + Updated configuration dictionary + """ + + def convert_value(value: str, original_value: Any) -> Any: + """Convert string value to appropriate type based on original value + + Args: + value: String value from environment variable + original_value: Original value to infer type from + + Returns: + Converted value (falls back to string if conversion fails) + """ + if isinstance(original_value, bool): + return value.lower() in ('true', '1', 'yes', 'on') + elif isinstance(original_value, int): + try: + return int(value) + except ValueError: + # If conversion fails, keep as string (user error, but non-breaking) + return value + elif isinstance(original_value, float): + try: + return float(value) + except ValueError: + # If conversion fails, keep as string (user error, but non-breaking) + return value + else: + return value + + # Process environment variables + for env_key, env_value in os.environ.items(): + # Check if the environment variable is uppercase and contains __ + if not env_key.isupper(): + continue + if '__' not in env_key: + continue + + # Convert environment variable name to config path + # e.g., CONCURRENCY__PIPELINE -> ['concurrency', 'pipeline'] + keys = [key.lower() for key in env_key.split('__')] + + # Navigate to the target value and validate the path + current = cfg + + for i, key in enumerate(keys): + if not isinstance(current, dict) or key not in current: + break + + if i == len(keys) - 1: + # At the final key - check if it's a scalar value + if isinstance(current[key], (dict, list)): + # Skip dict and list types + pass + else: + # Valid scalar value - convert and set it + converted_value = convert_value(env_value, current[key]) + current[key] = converted_value + else: + # Navigate deeper + current = current[key] + + return cfg + + +class TestWebhookDisplayPrefix: + """Test webhook_prefix configuration functionality""" + + def test_default_webhook_prefix(self): + """Test that the default webhook display prefix is correctly set""" + cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}} + + # Should have the default value + assert cfg['api']['webhook_prefix'] == 'http://127.0.0.1:5300' + + def test_webhook_prefix_env_override(self): + """Test overriding webhook_prefix via environment variable""" + cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}} + + # Set environment variable + os.environ['API__WEBHOOK_PREFIX'] = 'https://example.com:8080' + + result = _apply_env_overrides_to_config(cfg) + + assert result['api']['webhook_prefix'] == 'https://example.com:8080' + + # Cleanup + del os.environ['API__WEBHOOK_PREFIX'] + + def test_webhook_prefix_with_custom_domain(self): + """Test webhook_prefix with custom domain""" + cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}} + + # Set to a custom domain + os.environ['API__WEBHOOK_PREFIX'] = 'https://bot.mycompany.com' + + result = _apply_env_overrides_to_config(cfg) + + assert result['api']['webhook_prefix'] == 'https://bot.mycompany.com' + + # Cleanup + del os.environ['API__WEBHOOK_PREFIX'] + + def test_webhook_prefix_with_subdirectory(self): + """Test webhook_prefix with subdirectory path""" + cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}} + + # Set to a URL with subdirectory + os.environ['API__WEBHOOK_PREFIX'] = 'https://example.com/langbot' + + result = _apply_env_overrides_to_config(cfg) + + assert result['api']['webhook_prefix'] == 'https://example.com/langbot' + + # Cleanup + del os.environ['API__WEBHOOK_PREFIX'] + + +if __name__ == '__main__': + pytest.main([__file__, '-v'])