refactor: make explore localization request-driven (#41798)

This commit is contained in:
非法操作 2026-09-04 06:03:46 +00:00 committed by GitHub
parent 8504aaa0ef
commit cea89f784f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 62 additions and 73 deletions

View File

@ -7,17 +7,17 @@ from pydantic import BaseModel, Field, computed_field, field_validator
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.explore.error import RecommendedAppNotFoundError
from controllers.console.wraps import account_initialization_required, model_validate, with_current_user
from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import model_validate
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.helper import build_icon_url, dump_response
from libs.login import login_required
from models import Account
from machinery.context import RequestContext
from services.recommended_app_query_service import RecommendedAppNotFoundError as RecommendedAppQueryNotFoundError
class RecommendedAppsQuery(BaseModel):
language: str | None = Field(default=None, description="Language code for recommended app localization")
language: str = Field(default="en-US", description="Language code for recommended app localization")
class RecommendedAppInfoResponse(ResponseModel):
@ -97,16 +97,13 @@ register_response_schema_models(
class RecommendedAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
@console_ns.response(200, "Success", console_ns.models[RecommendedAppListResponse.__name__])
@login_required
@account_initialization_required
@with_current_user
@console_account_admission()
@model_validate(RecommendedAppsQuery)
def get(self, req_data: RecommendedAppsQuery, current_user: Account):
def get(self, req_data: RecommendedAppsQuery, _request_context: RequestContext):
return dump_response(
RecommendedAppListResponse,
application_services().recommended_app_queries.list_recommended(
requested_language=req_data.language,
interface_language=current_user.interface_language,
language=req_data.language,
),
)
@ -115,16 +112,13 @@ class RecommendedAppListApi(Resource):
class LearnDifyAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
@console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__])
@login_required
@account_initialization_required
@with_current_user
@console_account_admission()
@model_validate(RecommendedAppsQuery)
def get(self, req_data: RecommendedAppsQuery, current_user: Account):
def get(self, req_data: RecommendedAppsQuery, _request_context: RequestContext):
return dump_response(
LearnDifyAppListResponse,
application_services().recommended_app_queries.list_learn_dify(
requested_language=req_data.language,
interface_language=current_user.interface_language,
language=req_data.language,
),
)
@ -133,9 +127,8 @@ class LearnDifyAppListApi(Resource):
class RecommendedAppApi(Resource):
@console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__])
@console_ns.response(404, "Recommended app not found")
@login_required
@account_initialization_required
def get(self, app_id: UUID):
@console_account_admission()
def get(self, _request_context: RequestContext, app_id: UUID):
try:
result = application_services().recommended_app_queries.get_detail(str(app_id))
except RecommendedAppQueryNotFoundError:

View File

@ -6507,7 +6507,7 @@ Check if dataset is in use
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Language code for recommended app localization | No | string |
| language | query | Language code for recommended app localization | No | string, <br>**Default:** en-US |
#### Responses
@ -6520,7 +6520,7 @@ Check if dataset is in use
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Language code for recommended app localization | No | string |
| language | query | Language code for recommended app localization | No | string, <br>**Default:** en-US |
#### Responses
@ -21331,7 +21331,7 @@ Model class for provider quota configuration.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| language | string | Language code for recommended app localization | No |
| language | string, <br>**Default:** en-US | Language code for recommended app localization | No |
#### RedirectResponse

View File

@ -7,6 +7,8 @@ from collections.abc import Sequence
from datetime import datetime
from typing import Any, NamedTuple, Protocol
from constants.languages import languages
_DEFAULT_LANGUAGE = "en-US"
@ -37,6 +39,7 @@ class ExploreBannerQueryService:
if not self._enabled:
return ()
language = language if language in languages else _DEFAULT_LANGUAGE
banners = tuple(self._banners.list_enabled(language))
if banners or language == _DEFAULT_LANGUAGE:
return banners

View File

@ -5,6 +5,8 @@ from typing import NamedTuple, Protocol
from constants.languages import languages
_DEFAULT_LANGUAGE = "en-US"
class RecommendedAppInfoRecord(NamedTuple):
id: str
@ -116,10 +118,9 @@ class RecommendedAppQueryService:
def list_recommended(
self,
*,
requested_language: str | None,
interface_language: str | None,
language: str,
) -> RecommendedAppListResult:
language = self._resolve_language(requested_language, interface_language)
language = language if language in languages else _DEFAULT_LANGUAGE
page = self._catalog.list_recommended(language)
return RecommendedAppListResult(
@ -130,10 +131,9 @@ class RecommendedAppQueryService:
def list_learn_dify(
self,
*,
requested_language: str | None,
interface_language: str | None,
language: str,
) -> LearnDifyAppListResult:
language = self._resolve_language(requested_language, interface_language)
language = language if language in languages else _DEFAULT_LANGUAGE
page = self._catalog.list_learn_dify(language)
return LearnDifyAppListResult(recommended_apps=self._with_trial_status(page.recommended_apps))
@ -176,11 +176,3 @@ class RecommendedAppQueryService:
)
for app in apps
)
@staticmethod
def _resolve_language(requested_language: str | None, interface_language: str | None) -> str:
if requested_language and requested_language in languages:
return requested_language
if interface_language:
return interface_language
return languages[0]

View File

@ -110,7 +110,7 @@ class TestExploreBannerQueryService:
assert service.list_for_language("fr-FR") == (record,)
assert banners.requested_languages == ["fr-FR"]
def test_falls_back_to_en_us(self) -> None:
def test_falls_back_to_en_us_when_requested_translation_is_missing(self) -> None:
record = _record(title="fallback")
banners = FakeExploreBannerQuery({"en-US": (record,)})
service = ExploreBannerQueryService(banners=banners, enabled=True)
@ -118,6 +118,14 @@ class TestExploreBannerQueryService:
assert service.list_for_language("es-ES") == (record,)
assert banners.requested_languages == ["es-ES", "en-US"]
def test_invalid_language_uses_en_us(self) -> None:
record = _record(title="fallback")
banners = FakeExploreBannerQuery({"en-US": (record,)})
service = ExploreBannerQueryService(banners=banners, enabled=True)
assert service.list_for_language("invalid") == (record,)
assert banners.requested_languages == ["en-US"]
def test_does_not_repeat_default_language_query(self) -> None:
banners = FakeExploreBannerQuery()
service = ExploreBannerQueryService(banners=banners, enabled=True)

View File

@ -7,7 +7,7 @@ from flask import Flask
from pydantic import ValidationError
import controllers.console.explore.recommended_app as module
from models import Account
from machinery.context import RequestContext
from models.model import AppMode, IconType
from services.recommended_app_query_service import (
LearnDifyAppListResult,
@ -21,11 +21,13 @@ from services.recommended_app_query_service import (
)
def make_account(interface_language: str | None) -> Account:
account = Account(name="Test User", email="user@example.com")
account.id = "account-1"
account.interface_language = interface_language
return account
def _request_context() -> RequestContext:
return RequestContext(
request_id="request-1",
trace_id="trace-1",
account_id="account-1",
active_workspace_id="workspace-1",
)
class TestRecommendedAppListApi:
@ -44,17 +46,16 @@ class TestRecommendedAppListApi:
return_value=SimpleNamespace(recommended_app_queries=queries),
),
):
result = method(api, module.RecommendedAppsQuery(language="en-US"), make_account("fr-FR"))
result = method(api, module.RecommendedAppsQuery(language="en-US"), _request_context())
queries.list_recommended.assert_called_once_with(
requested_language="en-US",
interface_language="fr-FR",
language="en-US",
)
assert result == {"recommended_apps": [], "categories": []}
class TestLearnDifyAppListApi:
def test_get_with_language_param(self, app: Flask) -> None:
def test_get_uses_default_language(self, app: Flask) -> None:
api = module.LearnDifyAppListApi()
method = unwrap(api.get)
@ -62,18 +63,17 @@ class TestLearnDifyAppListApi:
queries.list_learn_dify.return_value = LearnDifyAppListResult(recommended_apps=())
with (
app.test_request_context("/", query_string={"language": "en-US"}),
app.test_request_context("/"),
patch.object(
module,
"application_services",
return_value=SimpleNamespace(recommended_app_queries=queries),
),
):
result = method(api, module.RecommendedAppsQuery(language="en-US"), make_account("fr-FR"))
result = method(api, module.RecommendedAppsQuery(), _request_context())
queries.list_learn_dify.assert_called_once_with(
requested_language="en-US",
interface_language="fr-FR",
language="en-US",
)
assert result == {"recommended_apps": []}
@ -102,7 +102,7 @@ class TestRecommendedAppApi:
return_value=SimpleNamespace(recommended_app_queries=queries),
),
):
result = method(api, "11111111-1111-1111-1111-111111111111")
result = method(api, _request_context(), "11111111-1111-1111-1111-111111111111")
queries.get_detail.assert_called_once_with("11111111-1111-1111-1111-111111111111")
assert result == {
@ -130,7 +130,7 @@ class TestRecommendedAppApi:
),
):
with pytest.raises(module.RecommendedAppNotFoundError) as exc_info:
method(api, "11111111-1111-1111-1111-111111111111")
method(api, _request_context(), "11111111-1111-1111-1111-111111111111")
assert exc_info.value.data == {
"code": "recommended_app_not_found",

View File

@ -721,14 +721,12 @@ def test_build_application_services_wires_dynamic_recommended_catalog(
)
with patch.object(recommended_app_catalog_gateway.Path, "read_text", return_value=builtin_payload):
result = services.recommended_app_queries.list_recommended(
requested_language="en-US",
interface_language=None,
language="en-US",
)
assert result.recommended_apps
apply_config_overrides(monkeypatch, HOSTED_FETCH_APP_TEMPLATES_MODE="invalid")
with pytest.raises(ValueError, match="invalid fetch recommended apps mode: invalid"):
services.recommended_app_queries.list_recommended(
requested_language="en-US",
interface_language=None,
language="en-US",
)

View File

@ -2,7 +2,6 @@ from unittest.mock import MagicMock
import pytest
from constants.languages import languages
from services.recommended_app_query_service import (
RecommendedAppCatalogPage,
RecommendedAppDetailRecord,
@ -83,17 +82,14 @@ def test_is_previewable_falls_back_to_catalog(expected: bool) -> None:
@pytest.mark.parametrize(
("requested_language", "interface_language", "expected"),
("language", "expected"),
[
("en-US", "fr-FR", "en-US"),
("invalid", "fr-FR", "fr-FR"),
(None, "custom-language", "custom-language"),
(None, None, languages[0]),
("fr-FR", "fr-FR"),
("invalid", "en-US"),
],
)
def test_list_recommended_resolves_language(
requested_language: str | None,
interface_language: str | None,
language: str,
expected: str,
) -> None:
catalog = MagicMock()
@ -101,8 +97,7 @@ def test_list_recommended_resolves_language(
service, _ = _service(catalog=catalog)
service.list_recommended(
requested_language=requested_language,
interface_language=interface_language,
language=language,
)
catalog.list_recommended.assert_called_once_with(expected)
@ -113,7 +108,7 @@ def test_list_recommended_disables_upstream_trial_without_querying_trial_apps()
catalog.list_recommended.return_value = _page("app-1")
service, trial_apps = _service(catalog=catalog)
result = service.list_recommended(requested_language="en-US", interface_language=None)
result = service.list_recommended(language="en-US")
assert result.recommended_apps[0].can_trial is False
trial_apps.existing_ids.assert_not_called()
@ -130,7 +125,7 @@ def test_list_recommended_enriches_trial_status_in_one_bulk_query() -> None:
trial_enabled=True,
)
result = service.list_recommended(requested_language="en-US", interface_language=None)
result = service.list_recommended(language="en-US")
assert [app.can_trial for app in result.recommended_apps] == [True, False]
trial_apps.existing_ids.assert_called_once_with(["app-1", "app-2"])
@ -141,9 +136,9 @@ def test_list_learn_dify_does_not_return_categories() -> None:
catalog.list_learn_dify.return_value = _page(categories=("ignored",))
service, _ = _service(catalog=catalog)
result = service.list_learn_dify(requested_language="invalid", interface_language="fr-FR")
result = service.list_learn_dify(language="invalid")
catalog.list_learn_dify.assert_called_once_with("fr-FR")
catalog.list_learn_dify.assert_called_once_with("en-US")
assert result.recommended_apps == ()
assert not hasattr(result, "categories")

View File

@ -137,7 +137,7 @@ export const zLearnDifyAppListResponseWritable = z.object({
})
export const zGetExploreAppsQuery = z.object({
language: z.string().optional(),
language: z.string().optional().default('en-US'),
})
/**
@ -146,7 +146,7 @@ export const zGetExploreAppsQuery = z.object({
export const zGetExploreAppsResponse = zRecommendedAppListResponse
export const zGetExploreAppsLearnDifyQuery = z.object({
language: z.string().optional(),
language: z.string().optional().default('en-US'),
})
/**