feat(api): expose app mode in webapp site response (#39607)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
林玮 (Jade Lin) 2026-07-27 10:57:18 +08:00 committed by GitHub
parent aeda37db68
commit 7e6ba05464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 83 additions and 3 deletions

View File

@ -24,7 +24,7 @@ from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import RateLimiter, dump_response, extract_remote_ip, to_timestamp
from models.account import TenantStatus
from models.model import App, Site
from models.model import App, AppMode, Site
from repositories.factory import DifyAPIRepositoryFactory
from services.feature_service import FeatureService
from services.human_input_file_upload_service import HumanInputFileUploadService
@ -207,6 +207,7 @@ class HumanInputFormApi(Resource):
site=WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.value_of(app_model.mode),
site=site,
end_user_id=None,
features=features,

View File

@ -14,7 +14,7 @@ from extensions.storage.storage_type import StorageType
from fields.base import ResponseModel
from libs.helper import build_icon_url
from models.account import Tenant, TenantStatus
from models.model import App, EndUser, IconType, Site
from models.model import App, AppMode, EndUser, IconType, Site
from services.feature_service import FeatureModel, FeatureService
from services.file_service import FileService
@ -67,6 +67,7 @@ class WebAppCustomConfigResponse(ResponseModel):
class WebAppSiteResponse(ResponseModel):
app_id: str
mode: AppMode
end_user_id: str | None = None
enable_site: bool
site: WebSiteResponse
@ -83,6 +84,7 @@ class WebAppSiteResponse(ResponseModel):
*,
tenant: Tenant,
app_model: App,
mode: AppMode,
site: Site,
end_user_id: str | None,
features: FeatureModel,
@ -109,6 +111,7 @@ class WebAppSiteResponse(ResponseModel):
return cls(
app_id=app_model.id,
mode=mode,
end_user_id=end_user_id,
enable_site=app_model.enable_site,
site=site_response,
@ -167,6 +170,7 @@ class AppSiteApi(WebApiResource):
return WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.value_of(app_model.mode_compatible_with_agent_with_session(session=db.session())),
site=site,
end_user_id=end_user.id,
features=features,

View File

@ -965,6 +965,12 @@ Returns Server-Sent Events stream.
| ---- | ---- | ----------- | -------- |
| tool_icons | object | Tool icon metadata keyed by tool name | No |
#### AppMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppMode | string | | |
#### AppPermissionQuery
| Name | Type | Description | Required |
@ -1646,6 +1652,7 @@ in form definition, or a variable while the workflow is running.
| custom_config | [WebAppCustomConfigResponse](#webappcustomconfigresponse) | | No |
| enable_site | boolean | | Yes |
| end_user_id | string | | No |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [WebModelConfigResponse](#webmodelconfigresponse) | | No |
| plan | string | | Yes |
| site | [WebSiteResponse](#websiteresponse) | | Yes |

View File

@ -97,6 +97,7 @@ class TestAppSiteApi:
assert result["end_user_id"] == end_user.id
assert result["plan"] == "basic"
assert result["enable_site"] is True
assert result["mode"] == AppMode.CHAT
@patch("controllers.web.site.FileService.get_file_presigned_url")
@patch("controllers.web.site.FeatureService.get_features")
@ -178,6 +179,7 @@ class TestWebAppSiteResponse:
response = WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.CHAT,
site=_site_model(app_id=app_model.id),
end_user_id="eu-1",
features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True),
@ -185,6 +187,7 @@ class TestWebAppSiteResponse:
)
assert response.app_id == app_model.id
assert response.mode == AppMode.CHAT
assert response.end_user_id == "eu-1"
assert response.enable_site is True
assert response.plan == "basic"
@ -209,6 +212,7 @@ class TestWebAppSiteResponse:
response = WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.CHAT,
site=site,
end_user_id=None,
features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True),
@ -236,6 +240,7 @@ class TestWebAppSiteResponse:
response = WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.CHAT,
site=_site_model(app_id=app_model.id),
end_user_id="eu-1",
features=FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True),

View File

@ -163,6 +163,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask, dat
assert body["expiration_time"] == int(expiration_time.timestamp())
assert body["site"] == {
"app_id": app_model.id,
"mode": "chat",
"end_user_id": None,
"enable_site": True,
"site": {
@ -383,6 +384,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
assert body["expiration_time"] == int(expiration_time.timestamp())
assert body["site"] == {
"app_id": app_model.id,
"mode": "chat",
"end_user_id": None,
"enable_site": True,
"site": {

View File

@ -3,7 +3,42 @@ from unittest.mock import MagicMock, patch
from configs import dify_config
from controllers.web import site as site_module
from extensions.storage.storage_type import StorageType
from models.model import IconType, Site
from models.model import AppMode, IconType, Site
from services.feature_service import FeatureModel
def test_app_site_api_returns_legacy_agent_compatible_mode() -> None:
app_model = MagicMock()
app_model.id = "app-id"
app_model.tenant_id = "tenant-id"
app_model.tenant = MagicMock(id="tenant-id", status="normal")
app_model.mode_compatible_with_agent_with_session.return_value = AppMode.AGENT_CHAT
end_user = MagicMock(id="end-user-id")
site = MagicMock(spec=Site)
response = MagicMock()
response.model_dump.return_value = {"mode": AppMode.AGENT_CHAT}
with (
patch.object(site_module, "db") as mock_db,
patch.object(site_module.FeatureService, "get_features", return_value=FeatureModel(can_replace_logo=False)),
patch.object(site_module, "_build_site_icon_url", return_value=None),
patch.object(site_module.WebAppSiteResponse, "from_app_site", return_value=response) as mock_from_app_site,
):
mock_db.session.scalar.return_value = site
result = site_module.AppSiteApi().get(app_model, end_user)
assert result["mode"] == AppMode.AGENT_CHAT
app_model.mode_compatible_with_agent_with_session.assert_called_once_with(session=mock_db.session())
mock_from_app_site.assert_called_once_with(
tenant=app_model.tenant,
app_model=app_model,
mode=AppMode.AGENT_CHAT,
site=site,
end_user_id=end_user.id,
features=FeatureModel(can_replace_logo=False),
can_replace_logo=False,
icon_url=None,
)
def test_build_site_icon_url_uses_s3_presigned_url() -> None:

View File

@ -43,6 +43,16 @@ export type AppMetaResponse = {
}
}
export type AppMode =
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'channel'
| 'chat'
| 'completion'
| 'rag-pipeline'
| 'workflow'
export type AppPermissionQuery = {
appId: string
}
@ -575,6 +585,7 @@ export type WebAppSiteResponse = {
custom_config?: WebAppCustomConfigResponse | null
enable_site: boolean
end_user_id?: string | null
mode: AppMode
model_config?: WebModelConfigResponse | null
plan: string
site: WebSiteResponse

View File

@ -39,6 +39,20 @@ export const zAppMetaResponse = z.object({
tool_icons: z.record(z.string(), z.unknown()).optional(),
})
/**
* AppMode
*/
export const zAppMode = z.enum([
'advanced-chat',
'agent',
'agent-chat',
'channel',
'chat',
'completion',
'rag-pipeline',
'workflow',
])
/**
* AppPermissionQuery
*/
@ -884,6 +898,7 @@ export const zWebAppSiteResponse = z.object({
custom_config: zWebAppCustomConfigResponse.nullish(),
enable_site: z.boolean(),
end_user_id: z.string().nullish(),
mode: zAppMode,
model_config: zWebModelConfigResponse.nullish(),
plan: z.string(),
site: zWebSiteResponse,