refactor(api): merge ping and version endpoints into system (#40058)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Byron.wang
2026-08-10 16:50:40 +08:00
committed by GitHub
parent 96655436f4
commit 976d061f9b
22 changed files with 196 additions and 352 deletions
+2 -4
View File
@@ -41,10 +41,9 @@ from . import (
knowledge_fs_proxy,
notification,
onboarding,
ping,
setup,
spec,
version,
system,
workflow_run_archive,
)
from .agent import composer as agent_composer
@@ -213,7 +212,6 @@ __all__ = [
"onboarding",
"ops_trace",
"parameter",
"ping",
"plugin",
"rag_pipeline",
"rag_pipeline_datasets",
@@ -231,11 +229,11 @@ __all__ = [
"socketio_workflow",
"spec",
"statistic",
"system",
"tags",
"tool_providers",
"trial",
"trigger_providers",
"version",
"website",
"workflow",
"workflow_app_log",
-17
View File
@@ -1,17 +0,0 @@
from pydantic import BaseModel, Field
from controllers.fastopenapi import console_router
class PingResponse(BaseModel):
result: str = Field(description="Health check result", examples=["pong"])
@console_router.get(
"/ping",
response_model=PingResponse,
tags=["console"],
)
def ping() -> PingResponse:
"""Health check endpoint for connection testing."""
return PingResponse(result="pong")
@@ -10,21 +10,27 @@ from controllers.fastopenapi import console_router
logger = logging.getLogger(__name__)
class PingResponse(BaseModel):
result: str = Field(description="Health check result", examples=["pong"])
class VersionQuery(BaseModel):
current_version: str = Field(..., description="Current application version")
class VersionFeatures(BaseModel):
can_replace_logo: bool = Field(description="Whether logo replacement is supported")
model_load_balancing_enabled: bool = Field(description="Whether model load balancing is enabled")
class VersionResponse(BaseModel):
version: str = Field(description="Latest version number")
release_date: str = Field(description="Release date of latest version")
release_notes: str = Field(description="Release notes for latest version")
can_auto_update: bool = Field(description="Whether auto-update is supported")
features: VersionFeatures = Field(description="Feature flags and capabilities")
@console_router.get(
"/ping",
response_model=PingResponse,
tags=["console"],
)
def ping() -> PingResponse:
"""Health check endpoint for connection testing."""
return PingResponse(result="pong")
@console_router.get(
@@ -38,13 +44,7 @@ def check_version_update(query: VersionQuery) -> VersionResponse:
result = VersionResponse(
version=dify_config.project.version,
release_date="",
release_notes="",
can_auto_update=False,
features=VersionFeatures(
can_replace_logo=dify_config.CAN_REPLACE_LOGO,
model_load_balancing_enabled=dify_config.MODEL_LB_ENABLED,
),
)
if not check_update_url:
@@ -62,11 +62,9 @@ def check_version_update(query: VersionQuery) -> VersionResponse:
result.version = query.current_version
return result
latest_version = content.get("version", result.version)
if _has_new_version(latest_version=latest_version, current_version=f"{query.current_version}"):
if _has_new_version(latest_version=latest_version, current_version=query.current_version):
result.version = latest_version
result.release_date = content.get("releaseDate", "")
result.release_notes = content.get("releaseNotes", "")
result.can_auto_update = content.get("canAutoUpdate", False)
return result
@@ -75,7 +73,6 @@ def _has_new_version(*, latest_version: str, current_version: str) -> bool:
latest = version.parse(latest_version)
current = version.parse(current_version)
# Compare versions
return latest > current
except version.InvalidVersion:
logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version)
+2 -2
View File
@@ -34,11 +34,11 @@ def init_app(app: DifyApp) -> None:
# Ensure route decorators are evaluated.
import controllers.console.init_validate as init_validate_module
import controllers.console.ping as ping_module
import controllers.console.system as system_module
from controllers.console import remote_files, setup
_ = init_validate_module
_ = ping_module
_ = system_module
_ = remote_files
_ = setup
-10
View File
@@ -24795,19 +24795,9 @@ FastOpenAPI proof of concept for Dify API
| setup_at | string | Setup completion time (ISO format) | No |
| step | string, <br>**Available values:** "finished", "not_started" | Setup step status<br>*Enum:* `"finished"`, `"not_started"` | Yes |
###### VersionFeatures
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| can_replace_logo | boolean | Whether logo replacement is supported | Yes |
| model_load_balancing_enabled | boolean | Whether model load balancing is enabled | Yes |
###### VersionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| can_auto_update | boolean | Whether auto-update is supported | Yes |
| features | [VersionFeatures](#versionfeatures) | Feature flags and capabilities | Yes |
| release_date | string | Release date of latest version | Yes |
| release_notes | string | Release notes for latest version | Yes |
| version | string | Latest version number | Yes |
@@ -1,27 +0,0 @@
import builtins
import pytest
from flask import Flask
from flask.views import MethodView
from extensions import ext_fastopenapi
if not hasattr(builtins, "MethodView"):
builtins.MethodView = MethodView # type: ignore[attr-defined]
@pytest.fixture
def app() -> Flask:
app = Flask(__name__)
app.config["TESTING"] = True
return app
def test_console_ping_fastopenapi_returns_pong(app: Flask):
ext_fastopenapi.init_app(app)
client = app.test_client()
response = client.get("/console/api/ping")
assert response.status_code == 200
assert response.get_json() == {"result": "pong"}
@@ -0,0 +1,41 @@
import builtins
from unittest.mock import patch
import pytest
from flask.views import MethodView
from configs import dify_config
from dify_app import DifyApp
from extensions import ext_fastopenapi
if not hasattr(builtins, "MethodView"):
builtins.MethodView = MethodView # type: ignore[attr-defined]
@pytest.fixture
def app() -> DifyApp:
app = DifyApp(__name__)
app.config["TESTING"] = True
return app
def test_console_ping_fastopenapi_returns_pong(app: DifyApp) -> None:
ext_fastopenapi.init_app(app)
response = app.test_client().get("/console/api/ping")
assert response.status_code == 200
assert response.get_json() == {"result": "pong"}
def test_console_version_fastopenapi_returns_current_version(app: DifyApp) -> None:
ext_fastopenapi.init_app(app)
with patch("controllers.console.system.dify_config.CHECK_UPDATE_URL", None):
response = app.test_client().get("/console/api/version", query_string={"current_version": "0.0.0"})
assert response.status_code == 200
assert response.get_json() == {
"version": dify_config.project.version,
"release_notes": "",
}
@@ -1,35 +0,0 @@
import builtins
from unittest.mock import patch
import pytest
from flask import Flask
from flask.views import MethodView
from configs import dify_config
from extensions import ext_fastopenapi
if not hasattr(builtins, "MethodView"):
builtins.MethodView = MethodView # type: ignore[attr-defined]
@pytest.fixture
def app() -> Flask:
app = Flask(__name__)
app.config["TESTING"] = True
return app
def test_console_version_fastopenapi_returns_current_version(app: Flask):
ext_fastopenapi.init_app(app)
with patch("controllers.console.version.dify_config.CHECK_UPDATE_URL", None):
client = app.test_client()
response = client.get("/console/api/version", query_string={"current_version": "0.0.0"})
assert response.status_code == 200
data = response.get_json()
assert data["version"] == dify_config.project.version
assert data["release_date"] == ""
assert data["release_notes"] == ""
assert data["can_auto_update"] is False
assert "features" in data
@@ -0,0 +1,134 @@
import logging
from unittest.mock import MagicMock, patch
import pytest
import controllers.console.system as system_module
class TestHasNewVersion:
def test_has_new_version_true(self) -> None:
result = system_module._has_new_version(
latest_version="1.2.0",
current_version="1.1.0",
)
assert result is True
def test_has_new_version_false(self) -> None:
result = system_module._has_new_version(
latest_version="1.0.0",
current_version="1.1.0",
)
assert result is False
def test_has_new_version_invalid_version(self, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger="controllers.console.system"):
result = system_module._has_new_version(
latest_version="invalid",
current_version="1.0.0",
)
assert result is False
assert "Invalid version format" in caplog.text
class TestCheckVersionUpdate:
def test_no_check_update_url(self) -> None:
query = system_module.VersionQuery(current_version="1.0.0")
with (
patch.object(
system_module.dify_config,
"CHECK_UPDATE_URL",
"",
),
patch.object(
system_module.dify_config.project,
"version",
"1.0.0",
),
):
result = system_module.check_version_update(query)
assert result == system_module.VersionResponse(version="1.0.0", release_notes="")
def test_http_error_fallback(self, caplog: pytest.LogCaptureFixture) -> None:
query = system_module.VersionQuery(current_version="1.0.0")
with (
patch.object(
system_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
system_module.httpx,
"get",
side_effect=Exception("boom"),
),
caplog.at_level(logging.WARNING, logger="controllers.console.system"),
):
result = system_module.check_version_update(query)
assert result.version == "1.0.0"
assert "Check update version error" in caplog.text
def test_new_version_available(self) -> None:
query = system_module.VersionQuery(current_version="1.0.0")
response = MagicMock()
response.json.return_value = {
"version": "1.2.0",
"releaseNotes": "New features",
}
with (
patch.object(
system_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
system_module.httpx,
"get",
return_value=response,
),
patch.object(
system_module.dify_config.project,
"version",
"1.0.0",
),
):
result = system_module.check_version_update(query)
assert result.version == "1.2.0"
assert result.release_notes == "New features"
def test_no_new_version(self) -> None:
query = system_module.VersionQuery(current_version="1.2.0")
response = MagicMock()
response.json.return_value = {
"version": "1.1.0",
}
with (
patch.object(
system_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
system_module.httpx,
"get",
return_value=response,
),
patch.object(
system_module.dify_config.project,
"version",
"1.2.0",
),
):
result = system_module.check_version_update(query)
assert result == system_module.VersionResponse(version="1.2.0", release_notes="")
@@ -1,162 +0,0 @@
import logging
from unittest.mock import MagicMock, patch
import pytest
import controllers.console.version as version_module
class TestHasNewVersion:
def test_has_new_version_true(self):
result = version_module._has_new_version(
latest_version="1.2.0",
current_version="1.1.0",
)
assert result is True
def test_has_new_version_false(self):
result = version_module._has_new_version(
latest_version="1.0.0",
current_version="1.1.0",
)
assert result is False
def test_has_new_version_invalid_version(self, caplog: pytest.LogCaptureFixture):
with caplog.at_level(logging.WARNING, logger="controllers.console.version"):
result = version_module._has_new_version(
latest_version="invalid",
current_version="1.0.0",
)
assert result is False
assert "Invalid version format" in caplog.text
class TestCheckVersionUpdate:
def test_no_check_update_url(self):
query = version_module.VersionQuery(current_version="1.0.0")
with (
patch.object(
version_module.dify_config,
"CHECK_UPDATE_URL",
"",
),
patch.object(
version_module.dify_config.project,
"version",
"1.0.0",
),
patch.object(
version_module.dify_config,
"CAN_REPLACE_LOGO",
True,
),
patch.object(
version_module.dify_config,
"MODEL_LB_ENABLED",
False,
),
):
result = version_module.check_version_update(query)
assert result.version == "1.0.0"
assert result.can_auto_update is False
assert result.features.can_replace_logo is True
assert result.features.model_load_balancing_enabled is False
def test_http_error_fallback(self, caplog: pytest.LogCaptureFixture):
query = version_module.VersionQuery(current_version="1.0.0")
with (
patch.object(
version_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
version_module.httpx,
"get",
side_effect=Exception("boom"),
),
caplog.at_level(logging.WARNING, logger="controllers.console.version"),
):
result = version_module.check_version_update(query)
assert result.version == "1.0.0"
assert "Check update version error" in caplog.text
def test_new_version_available(self):
query = version_module.VersionQuery(current_version="1.0.0")
response = MagicMock()
response.json.return_value = {
"version": "1.2.0",
"releaseDate": "2024-01-01",
"releaseNotes": "New features",
"canAutoUpdate": True,
}
with (
patch.object(
version_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
version_module.httpx,
"get",
return_value=response,
),
patch.object(
version_module.dify_config.project,
"version",
"1.0.0",
),
patch.object(
version_module.dify_config,
"CAN_REPLACE_LOGO",
False,
),
patch.object(
version_module.dify_config,
"MODEL_LB_ENABLED",
True,
),
):
result = version_module.check_version_update(query)
assert result.version == "1.2.0"
assert result.release_date == "2024-01-01"
assert result.release_notes == "New features"
assert result.can_auto_update is True
def test_no_new_version(self):
query = version_module.VersionQuery(current_version="1.2.0")
response = MagicMock()
response.json.return_value = {
"version": "1.1.0",
}
with (
patch.object(
version_module.dify_config,
"CHECK_UPDATE_URL",
"http://example.com",
),
patch.object(
version_module.httpx,
"get",
return_value=response,
),
patch.object(
version_module.dify_config.project,
"version",
"1.2.0",
),
):
result = version_module.check_version_update(query)
assert result.version == "1.2.0"
assert result.can_auto_update is False
@@ -1,6 +1,6 @@
import pytest
from controllers.console.version import _has_new_version
from controllers.console.system import _has_new_version
@pytest.mark.parametrize(
@@ -20,5 +20,5 @@ from controllers.console.version import _has_new_version
("1.0.0", "1.0.0-dev", True),
],
)
def test_has_new_version(latest_version, current_version, expected):
def test_has_new_version(latest_version: str, current_version: str, expected: bool) -> None:
assert _has_new_version(latest_version=latest_version, current_version=current_version) == expected
@@ -5,18 +5,10 @@ export type ClientOptions = {
}
export type VersionResponse = {
can_auto_update: boolean
features: VersionFeatures
release_date: string
release_notes: string
version: string
}
export type VersionFeatures = {
can_replace_logo: boolean
model_load_balancing_enabled: boolean
}
export type GetVersionData = {
body?: never
path?: never
@@ -2,21 +2,10 @@
import * as z from 'zod'
/**
* VersionFeatures
*/
export const zVersionFeatures = z.object({
can_replace_logo: z.boolean(),
model_load_balancing_enabled: z.boolean(),
})
/**
* VersionResponse
*/
export const zVersionResponse = z.object({
can_auto_update: z.boolean(),
features: zVersionFeatures,
release_date: z.string(),
release_notes: z.string(),
version: z.string(),
})
@@ -167,10 +167,8 @@ const baseConsoleState: ConsoleStateFixture = {
current_env: 'testing',
current_version: '0.1.0',
latest_version: '0.1.0',
release_date: '',
release_notes: '',
version: '0.1.0',
can_auto_update: false,
},
isLoadingCurrentWorkspace: false,
workspacePermissionKeys: [
@@ -509,10 +509,8 @@ const consoleState: ConsoleStateFixture = {
current_env: 'testing',
current_version: '1.0.0',
latest_version: '1.0.0',
release_date: '',
release_notes: '',
version: '1.0.0',
can_auto_update: false,
},
isLoadingCurrentWorkspace: false,
isLoadingWorkspacePermissionKeys: false,
@@ -11,10 +11,8 @@ vi.mock('@/context/permission-state', async () => {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
},
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
@@ -26,10 +24,8 @@ vi.mock('@/context/version-state', async () => {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
},
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
@@ -58,20 +58,16 @@ const mockLangGeniusVersionInfo = vi.fn(() => ({
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}))
const createLangGeniusVersionInfo = (currentVersion: string) => ({
current_env: '',
current_version: currentVersion,
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
})
vi.mock('@/context/version-state', async () => {
@@ -19,14 +19,8 @@ const defaultLangGeniusVersionInfo: LangGeniusVersionInfo = {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
features: {
can_replace_logo: false,
model_load_balancing_enabled: false,
},
can_auto_update: false,
}
type ConsoleStateFixture = Omit<BaseConsoleStateFixture, 'langGeniusVersionInfo'> & {
@@ -84,23 +84,11 @@ const mockSystemFeaturesState = vi.hoisted(() => ({
const mockLangGeniusVersionState = vi.hoisted(() => ({
data: {
version: '1.0.1',
release_date: '',
release_notes: '',
features: {
can_replace_logo: false,
model_load_balancing_enabled: false,
},
can_auto_update: false,
} as
| {
version: string
release_date: string
release_notes: string
features: {
can_replace_logo: boolean
model_load_balancing_enabled: boolean
}
can_auto_update: boolean
}
| undefined,
}))
@@ -380,13 +368,7 @@ describe('Console bootstrap', () => {
})
mockLangGeniusVersionState.data = {
version: '1.0.1',
release_date: '',
release_notes: '',
features: {
can_replace_logo: false,
model_load_balancing_enabled: false,
},
can_auto_update: false,
}
mockGetRequest.mockImplementation((url: string) => {
if (url === '/version') return Promise.resolve(mockLangGeniusVersionState.data)
-6
View File
@@ -5,14 +5,8 @@ export const initialLangGeniusVersionInfo: LangGeniusVersionInfo = {
current_env: '',
current_version: '',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
features: {
can_replace_logo: false,
model_load_balancing_enabled: false,
},
can_auto_update: false,
}
export const initialWorkspaceSummary: GetWorkspacesCurrentSummaryResponse = {
@@ -55,9 +55,7 @@ vi.mock('@/context/account-state', async () => {
current_version: '',
latest_version: '',
version: '',
release_date: '',
release_notes: '',
can_auto_update: false,
},
}))
})
@@ -72,9 +70,7 @@ vi.mock('@/context/workspace-state', async () => {
current_version: '',
latest_version: '',
version: '',
release_date: '',
release_notes: '',
can_auto_update: false,
},
}))
})
@@ -89,9 +85,7 @@ vi.mock('@/context/permission-state', async () => {
current_version: '',
latest_version: '',
version: '',
release_date: '',
release_notes: '',
can_auto_update: false,
},
}))
})
@@ -106,9 +100,7 @@ vi.mock('@/context/version-state', async () => {
current_version: '',
latest_version: '',
version: '',
release_date: '',
release_notes: '',
can_auto_update: false,
},
}))
})
-6
View File
@@ -56,13 +56,7 @@ const defaultLangGeniusVersionInfo = {
current_version: '',
latest_version: '',
version: '',
release_date: '',
release_notes: '',
features: {
can_replace_logo: false,
model_load_balancing_enabled: false,
},
can_auto_update: false,
} satisfies LangGeniusVersionInfo
const userProfileAtom = atom(defaultUserProfile)