The site field returned by HumanInputFormApi is inconsistent with the API docs (vibe-kanban e0fb38c9)

```javascript

Expected structure:

```json
{
    "site": {
        "app_id": "e9823576-d836-4f2b-b46f-bd4df1d82230",
        "end_user_id": "b7aa295d-1560-4d87-a828-77b3f39b30d0",
        "enable_site": true,
        "site": {
            "title": "wf",
            "chat_color_theme": null,
            "chat_color_theme_inverted": false,
            "icon_type": "emoji",
            "icon": "\ud83e\udd16",
            "icon_background": "#FFEAD5",
            "icon_url": null,
            "description": null,
            "copyright": null,
            "privacy_policy": null,
            "custom_disclaimer": "",
            "default_language": "en-US",
            "prompt_public": false,
            "show_workflow_steps": true,
            "use_icon_as_answer_icon": false
        },
        "model_config": null,
        "plan": "basic",
        "can_replace_logo": false,
        "custom_config": null
    },
    // ... other fields
}

```

The current implementation of HumanInputFormApi returns the following structure:

```json

{
    "site": {
        "title": "hitl-chatflow",
        "chat_color_theme": null,
        "chat_color_theme_inverted": false,
        "icon_type": "emoji",
        "icon": "🤖",
        "icon_background": "#FFEAD5",
        "icon_url": null,
        "description": null,
        "copyright": null,
        "privacy_policy": null,
        "custom_disclaimer": "",
        "default_language": "en-US",
        "prompt_public": false,
        "show_workflow_steps": true,
        "use_icon_as_answer_icon": false
    },

    // ... other fields
}

```

\`\`\`
This commit is contained in:
QuantumGhost
2026-01-15 12:26:51 +08:00
parent d87ff9e501
commit c45dd66bd7
3 changed files with 102 additions and 22 deletions

View File

@ -11,7 +11,7 @@ from werkzeug.exceptions import Forbidden
from controllers.web import web_ns
from controllers.web.error import NotFoundError
from controllers.web.site import serialize_site
from controllers.web.site import serialize_app_site_payload
from extensions.ext_database import db
from models.account import TenantStatus
from models.human_input import RecipientType
@ -56,9 +56,9 @@ class HumanInputFormApi(Resource):
if form is None:
raise NotFoundError("Form not found")
site = _get_site_from_form(form)
app_model, site = _get_app_site_from_form(form)
return _jsonify_form_definition(form, site_payload=serialize_site(site))
return _jsonify_form_definition(form, site_payload=serialize_app_site_payload(app_model, site, None))
# def post(self, _app_model: App, _end_user: EndUser, form_token: str):
def post(self, form_token: str):
@ -100,8 +100,8 @@ class HumanInputFormApi(Resource):
return {}, 200
def _get_site_from_form(form: Form) -> Site:
"""Resolve Site for the form's app and validate tenant status."""
def _get_app_site_from_form(form: Form) -> tuple[App, Site]:
"""Resolve App/Site for the form's app and validate tenant status."""
app_model = db.session.query(App).where(App.id == form.app_id).first()
if app_model is None or app_model.tenant_id != form.tenant_id:
raise NotFoundError("Form not found")
@ -113,4 +113,4 @@ def _get_site_from_form(form: Form) -> Site:
if app_model.tenant and app_model.tenant.status == TenantStatus.ARCHIVE:
raise Forbidden()
return site
return app_model, site

View File

@ -7,7 +7,7 @@ from controllers.web.wraps import WebApiResource
from extensions.ext_database import db
from libs.helper import AppIconUrlField
from models.account import TenantStatus
from models.model import Site
from models.model import App, Site
from services.feature_service import FeatureService
@ -104,12 +104,18 @@ class AppSiteInfo:
if tenant.custom_config_dict.get("replace_webapp_logo")
else None
)
self.custom_config = {
"remove_webapp_brand": remove_webapp_brand,
"replace_webapp_logo": replace_webapp_logo,
}
self.custom_config = {
"remove_webapp_brand": remove_webapp_brand,
"replace_webapp_logo": replace_webapp_logo,
}
def serialize_site(site: Site) -> dict:
"""Serialize Site model using the same schema as AppSiteApi."""
return marshal(site, AppSiteApi.site_fields)
def serialize_app_site_payload(app_model: App, site: Site, end_user_id: str | None) -> dict:
can_replace_logo = FeatureService.get_features(app_model.tenant_id).can_replace_logo
app_site_info = AppSiteInfo(app_model.tenant, app_model, site, end_user_id, can_replace_logo)
return marshal(app_site_info, AppSiteApi.app_fields)

View File

@ -12,6 +12,7 @@ from flask import Flask
from werkzeug.exceptions import Forbidden
import controllers.web.human_input_form as human_input_module
import controllers.web.site as site_module
HumanInputFormApi = human_input_module.HumanInputFormApi
RecipientType = human_input_module.RecipientType
@ -71,13 +72,18 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
form = _FakeForm()
tenant = SimpleNamespace(status=TenantStatus.NORMAL)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant)
tenant = SimpleNamespace(
id="tenant-1",
status=TenantStatus.NORMAL,
plan="basic",
custom_config_dict={"remove_webapp_brand": True, "replace_webapp_logo": False},
)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
workflow_run = SimpleNamespace(app_id="app-1")
site_model = SimpleNamespace(
title="My Site",
icon_type="emoji",
icon=None,
icon="robot",
icon_background="#fff",
description="desc",
default_language="en",
@ -100,15 +106,46 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
db_stub = _FakeDB(_FakeSession({"WorkflowRun": workflow_run, "App": app_model, "Site": site_model}))
monkeypatch.setattr(human_input_module, "db", db_stub)
# Patch serialize_site to a predictable value.
monkeypatch.setattr(human_input_module, "serialize_site", lambda site: {"title": site.title})
monkeypatch.setattr(
site_module.FeatureService,
"get_features",
lambda tenant_id: SimpleNamespace(can_replace_logo=True),
)
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
response = HumanInputFormApi().get("token-1")
body = json.loads(response.get_data(as_text=True))
assert body["form_content"] == "hello"
assert body["site"] == {"title": "My Site"}
assert body["site"] == {
"app_id": "app-1",
"end_user_id": None,
"enable_site": True,
"site": {
"title": "My Site",
"chat_color_theme": "light",
"chat_color_theme_inverted": False,
"icon_type": "emoji",
"icon": "robot",
"icon_background": "#fff",
"icon_url": None,
"description": "desc",
"copyright": None,
"privacy_policy": None,
"custom_disclaimer": None,
"default_language": "en",
"prompt_public": False,
"show_workflow_steps": True,
"use_icon_as_answer_icon": False,
},
"model_config": None,
"plan": "basic",
"can_replace_logo": True,
"custom_config": {
"remove_webapp_brand": True,
"replace_webapp_logo": None,
},
}
service_mock.get_form_definition_by_token.assert_called_once_with(
RecipientType.STANDALONE_WEB_APP,
"token-1",
@ -131,13 +168,18 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
return _FakeDefinition()
form = _FakeForm()
tenant = SimpleNamespace(status=TenantStatus.NORMAL)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant)
tenant = SimpleNamespace(
id="tenant-1",
status=TenantStatus.NORMAL,
plan="basic",
custom_config_dict={"remove_webapp_brand": True, "replace_webapp_logo": False},
)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
workflow_run = SimpleNamespace(app_id="app-1")
site_model = SimpleNamespace(
title="My Site",
icon_type="emoji",
icon=None,
icon="robot",
icon_background="#fff",
description="desc",
default_language="en",
@ -158,14 +200,46 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
db_stub = _FakeDB(_FakeSession({"WorkflowRun": workflow_run, "App": app_model, "Site": site_model}))
monkeypatch.setattr(human_input_module, "db", db_stub)
monkeypatch.setattr(human_input_module, "serialize_site", lambda site: {"title": site.title})
monkeypatch.setattr(
site_module.FeatureService,
"get_features",
lambda tenant_id: SimpleNamespace(can_replace_logo=True),
)
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
response = HumanInputFormApi().get("token-1")
body = json.loads(response.get_data(as_text=True))
assert body["form_content"] == "hello"
assert body["site"] == {"title": "My Site"}
assert body["site"] == {
"app_id": "app-1",
"end_user_id": None,
"enable_site": True,
"site": {
"title": "My Site",
"chat_color_theme": "light",
"chat_color_theme_inverted": False,
"icon_type": "emoji",
"icon": "robot",
"icon_background": "#fff",
"icon_url": None,
"description": "desc",
"copyright": None,
"privacy_policy": None,
"custom_disclaimer": None,
"default_language": "en",
"prompt_public": False,
"show_workflow_steps": True,
"use_icon_as_answer_icon": False,
},
"model_config": None,
"plan": "basic",
"can_replace_logo": True,
"custom_config": {
"remove_webapp_brand": True,
"replace_webapp_logo": None,
},
}
assert service_mock.get_form_definition_by_token.call_args_list == [
call(RecipientType.STANDALONE_WEB_APP, "token-1"),
call(RecipientType.BACKSTAGE, "token-1"),