mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 22:31:24 +08:00
237 lines
9.2 KiB
Python
237 lines
9.2 KiB
Python
"""Account consumer APIs authenticated by the WebApp login cookie."""
|
|
|
|
from collections.abc import Generator
|
|
from contextlib import contextmanager
|
|
from datetime import datetime
|
|
from uuid import UUID
|
|
|
|
from flask import request
|
|
from flask_restx import Resource
|
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_serializer
|
|
from werkzeug.exceptions import NotFound, ServiceUnavailable
|
|
|
|
from controllers.common.end_user_connections import (
|
|
ConnectionConflictHTTPError,
|
|
ConnectionNotFoundHTTPError,
|
|
raise_connection_error,
|
|
)
|
|
from controllers.common.schema import query_params_from_model, register_response_schema_models
|
|
from controllers.web import web_ns
|
|
from controllers.web.account_admission import web_account_identity as _identity
|
|
from extensions.ext_application_services import application_services
|
|
from fields.base import ResponseModel
|
|
from libs.datetime_utils import to_utc_timestamp
|
|
from libs.external_api import error_responses
|
|
from libs.helper import dump_response
|
|
from services.consumer_service import (
|
|
ConsumerApp,
|
|
ConsumerAppNotFoundError,
|
|
ConsumerAppsQuery,
|
|
ConsumerConnection,
|
|
ConsumerConnectionProvider,
|
|
)
|
|
from services.end_user_connection_labels import mask_account_label
|
|
from services.end_user_connections.errors import ConnectionError
|
|
from services.webapp_access_query_service import WebAppAccessUnavailableError
|
|
|
|
|
|
@contextmanager
|
|
def _errors() -> Generator[None]:
|
|
try:
|
|
yield
|
|
except ValidationError:
|
|
raise ValueError("Invalid consumer request.") from None
|
|
except ConsumerAppNotFoundError:
|
|
raise NotFound("App not found.") from None
|
|
except WebAppAccessUnavailableError:
|
|
raise ServiceUnavailable("App access is temporarily unavailable.") from None
|
|
except ConnectionError as error:
|
|
raise_connection_error(error)
|
|
|
|
|
|
class ConsumerAppResponse(ConsumerApp, ResponseModel):
|
|
@field_serializer("last_used_at")
|
|
def serialize_usage(self, value: datetime | None) -> int | None:
|
|
return to_utc_timestamp(value) if value is not None else None
|
|
|
|
|
|
class ConsumerAppsResponse(ResponseModel):
|
|
data: list[ConsumerAppResponse]
|
|
has_more: bool
|
|
next_cursor: str | None
|
|
|
|
|
|
class ConsumerConnectionResponse(ConsumerConnection, ResponseModel):
|
|
@field_serializer("account_label")
|
|
def serialize_account_label(self, value: str | None) -> str | None:
|
|
"""Mask the persisted provider identity in the Consumer API."""
|
|
return mask_account_label(value)
|
|
|
|
|
|
class ConsumerConnectionsResponse(ResponseModel):
|
|
data: list[ConsumerConnectionResponse]
|
|
has_more: bool
|
|
next_cursor: str | None
|
|
|
|
|
|
class ConsumerConnectionProviderResponse(ConsumerConnectionProvider, ResponseModel):
|
|
pass
|
|
|
|
|
|
class ConsumerConnectionProvidersResponse(ResponseModel):
|
|
data: list[ConsumerConnectionProviderResponse]
|
|
has_more: bool
|
|
next_cursor: str | None
|
|
|
|
|
|
class ConsumerConnectionProvidersQuery(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
cursor: str | None = Field(default=None, min_length=1, max_length=255)
|
|
limit: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
class ConsumerConnectionsQuery(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
cursor: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
limit: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
class ConsumerConnectionAppsQuery(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
cursor: UUID | None = None
|
|
limit: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
register_response_schema_models(
|
|
web_ns,
|
|
ConsumerAppResponse,
|
|
ConsumerAppsResponse,
|
|
ConsumerConnectionResponse,
|
|
ConsumerConnectionsResponse,
|
|
ConsumerConnectionProviderResponse,
|
|
ConsumerConnectionProvidersResponse,
|
|
)
|
|
|
|
|
|
@web_ns.route("/me/apps")
|
|
class ConsumerAppsApi(Resource):
|
|
@web_ns.doc(
|
|
description="List accessible Chat, Completion, legacy Agent, Chatflow, Workflow and Agent WebApps "
|
|
"using the WebApp Account login cookie. "
|
|
"Recent use and favorites are personal WebApp preferences independent of Console stars. "
|
|
"No App Passport or workspace membership is required."
|
|
)
|
|
@web_ns.doc(params=query_params_from_model(ConsumerAppsQuery))
|
|
@web_ns.response(200, "Accessible personal Apps", web_ns.models[ConsumerAppsResponse.__name__])
|
|
def get(self):
|
|
identity = _identity()
|
|
with _errors():
|
|
query = ConsumerAppsQuery.model_validate(request.args.to_dict(flat=True))
|
|
result = application_services().consumer.list_apps(identity, query)
|
|
return dump_response(ConsumerAppsResponse, result)
|
|
|
|
|
|
@web_ns.route("/me/apps/<uuid:app_id>")
|
|
class ConsumerAppApi(Resource):
|
|
@web_ns.doc(
|
|
description="Remove an App from personal recent and favorite lists without deleting its data. "
|
|
"A new accepted execution restores the recent entry. Shared discovery remains available."
|
|
)
|
|
@web_ns.response(204, "App removed from personal lists")
|
|
def delete(self, app_id: UUID):
|
|
identity = _identity()
|
|
with _errors():
|
|
application_services().consumer.remove_app(identity, str(app_id))
|
|
return "", 204
|
|
|
|
|
|
@web_ns.route("/me/apps/<uuid:app_id>/favorite")
|
|
class ConsumerFavoriteApi(Resource):
|
|
@web_ns.response(204, "App favorited")
|
|
def put(self, app_id: UUID):
|
|
identity = _identity()
|
|
with _errors():
|
|
application_services().consumer.set_favorite(identity, str(app_id), enabled=True)
|
|
return "", 204
|
|
|
|
@web_ns.response(204, "App unfavorited")
|
|
def delete(self, app_id: UUID):
|
|
identity = _identity()
|
|
with _errors():
|
|
application_services().consumer.set_favorite(identity, str(app_id), enabled=False)
|
|
return "", 204
|
|
|
|
|
|
@web_ns.route("/me/connections")
|
|
class ConsumerConnectionProvidersApi(Resource):
|
|
@web_ns.doc(
|
|
description="List personal Connection Providers with localized labels, descriptions, public icons and "
|
|
"Connection-group counts across owned tenants. Guests are excluded. Unavailable Providers retain their "
|
|
"counts with a Provider-ID label, an empty description and null icons. No App associations are loaded."
|
|
)
|
|
@web_ns.doc(params=query_params_from_model(ConsumerConnectionProvidersQuery))
|
|
@web_ns.response(200, "Account Connection Providers", web_ns.models[ConsumerConnectionProvidersResponse.__name__])
|
|
def get(self):
|
|
identity = _identity()
|
|
with _errors():
|
|
query = ConsumerConnectionProvidersQuery.model_validate(request.args.to_dict(flat=True))
|
|
result = application_services().consumer.list_connection_providers(
|
|
identity, cursor=query.cursor, limit=query.limit
|
|
)
|
|
return dump_response(ConsumerConnectionProvidersResponse, result)
|
|
|
|
|
|
@web_ns.route("/me/connections/providers/<path:provider>/connections")
|
|
class ConsumerConnectionsApi(Resource):
|
|
@web_ns.doc(
|
|
description="List personal Connections grouped by Provider, account label and Connection type. "
|
|
"Connections without an account label form individual groups. WebApp counts include only accessible Apps."
|
|
)
|
|
@web_ns.doc(params=query_params_from_model(ConsumerConnectionsQuery))
|
|
@web_ns.response(200, "Account Connections", web_ns.models[ConsumerConnectionsResponse.__name__])
|
|
def get(self, provider: str):
|
|
identity = _identity()
|
|
with _errors():
|
|
query = ConsumerConnectionsQuery.model_validate(request.args.to_dict(flat=True))
|
|
result = application_services().consumer.list_connections(
|
|
identity,
|
|
provider=provider,
|
|
cursor=query.cursor,
|
|
limit=query.limit,
|
|
)
|
|
return dump_response(ConsumerConnectionsResponse, result)
|
|
|
|
|
|
@web_ns.route("/me/connections/<uuid:connection_key>/apps")
|
|
class ConsumerConnectionAppsApi(Resource):
|
|
@web_ns.doc(description="List accessible WebApps associated with a Connection group from a Provider's list.")
|
|
@web_ns.doc(params=query_params_from_model(ConsumerConnectionAppsQuery))
|
|
@web_ns.response(200, "Associated WebApps", web_ns.models[ConsumerAppsResponse.__name__])
|
|
@error_responses(ConnectionNotFoundHTTPError)
|
|
def get(self, connection_key: UUID):
|
|
identity = _identity()
|
|
with _errors():
|
|
query = ConsumerConnectionAppsQuery.model_validate(request.args.to_dict(flat=True))
|
|
result = application_services().consumer.list_connection_apps(
|
|
identity, connection_key, cursor=str(query.cursor) if query.cursor else None, limit=query.limit
|
|
)
|
|
return dump_response(ConsumerAppsResponse, result)
|
|
|
|
|
|
@web_ns.route("/me/connections/<uuid:connection_key>")
|
|
class ConsumerConnectionApi(Resource):
|
|
@web_ns.response(204, "Connection group disconnected")
|
|
@web_ns.doc(
|
|
description="Disconnect all owned Connections matching a group key from a Provider's list. "
|
|
"Cookie-authenticated browser mutations require the configured WebApp Origin."
|
|
)
|
|
@error_responses(ConnectionNotFoundHTTPError, ConnectionConflictHTTPError)
|
|
def delete(self, connection_key: UUID):
|
|
identity = _identity()
|
|
with _errors():
|
|
application_services().consumer.delete_connection(identity, connection_key)
|
|
return "", 204
|