mirror of
https://github.com/langgenius/dify.git
synced 2026-07-30 00:39:34 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
from datetime import datetime
|
|
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import and_, exists, or_, select
|
|
from sqlalchemy.orm import Session, scoped_session
|
|
|
|
from libs.helper import escape_like_pattern
|
|
from models import App, AppModelConfig, InstalledApp, Workflow
|
|
from models.model import AppMode
|
|
from services.enterprise.enterprise_service import EnterpriseService
|
|
from services.feature_service import FeatureService
|
|
|
|
|
|
class InstalledAppCursor(BaseModel):
|
|
is_pinned: bool
|
|
last_used_at: datetime | None
|
|
installed_app_id: str
|
|
|
|
|
|
def _published_app_filter():
|
|
"""Return the SQL predicate for installed-app web API availability.
|
|
|
|
The installed-app parameters endpoint reads the published workflow for
|
|
workflow-style apps and the published app model config for easy UI apps.
|
|
Keep the list endpoint aligned in SQL so it does not return entries that
|
|
will immediately fail with app_unavailable when opened.
|
|
"""
|
|
workflow_app_modes = (AppMode.ADVANCED_CHAT, AppMode.WORKFLOW)
|
|
has_published_workflow = exists(select(Workflow.id).where(Workflow.id == App.workflow_id))
|
|
has_published_model_config = exists(select(AppModelConfig.id).where(AppModelConfig.id == App.app_model_config_id))
|
|
|
|
return and_(
|
|
App.mode != AppMode.AGENT,
|
|
or_(
|
|
and_(App.mode.in_(workflow_app_modes), App.workflow_id.isnot(None), has_published_workflow),
|
|
and_(~App.mode.in_(workflow_app_modes), App.app_model_config_id.isnot(None), has_published_model_config),
|
|
),
|
|
)
|
|
|
|
|
|
def _installed_app_cursor_filter(cursor: InstalledAppCursor):
|
|
same_pin_group = InstalledApp.is_pinned == cursor.is_pinned
|
|
if cursor.last_used_at is None:
|
|
later_in_pin_group = and_(
|
|
InstalledApp.last_used_at.is_(None),
|
|
InstalledApp.id > cursor.installed_app_id,
|
|
)
|
|
else:
|
|
later_in_pin_group = or_(
|
|
InstalledApp.last_used_at < cursor.last_used_at,
|
|
InstalledApp.last_used_at.is_(None),
|
|
and_(
|
|
InstalledApp.last_used_at == cursor.last_used_at,
|
|
InstalledApp.id > cursor.installed_app_id,
|
|
),
|
|
)
|
|
|
|
if cursor.is_pinned:
|
|
return or_(
|
|
InstalledApp.is_pinned.is_(False),
|
|
and_(same_pin_group, later_in_pin_group),
|
|
)
|
|
return and_(same_pin_group, later_in_pin_group)
|
|
|
|
|
|
def _installed_app_order_by():
|
|
return (
|
|
InstalledApp.is_pinned.desc(),
|
|
InstalledApp.last_used_at.desc().nulls_last(),
|
|
InstalledApp.id.asc(),
|
|
)
|
|
|
|
|
|
def _filter_rows_by_webapp_auth(
|
|
rows: list[tuple[InstalledApp, App]],
|
|
*,
|
|
user_id: str,
|
|
) -> list[tuple[InstalledApp, App]]:
|
|
if not rows:
|
|
return []
|
|
|
|
app_ids = [app.id for _, app in rows]
|
|
webapp_settings = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids)
|
|
candidates = [
|
|
(installed_app, app)
|
|
for installed_app, app in rows
|
|
if (setting := webapp_settings.get(app.id)) is not None and setting.access_mode != "sso_verified"
|
|
]
|
|
permissions = EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps(
|
|
user_id=user_id,
|
|
app_ids=[app.id for _, app in candidates],
|
|
)
|
|
return [(installed_app, app) for installed_app, app in candidates if permissions.get(app.id)]
|
|
|
|
|
|
class InstalledAppService:
|
|
@classmethod
|
|
def get_visible_page(
|
|
cls,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
cursor: InstalledAppCursor | None,
|
|
limit: int,
|
|
app_id: str | None,
|
|
name: str | None,
|
|
session: Session | scoped_session,
|
|
) -> tuple[list[tuple[InstalledApp, App]], bool, InstalledAppCursor | None]:
|
|
"""Scan ordered candidates until one page of authorized apps is complete."""
|
|
stmt = (
|
|
select(InstalledApp, App)
|
|
.join(App, App.id == InstalledApp.app_id)
|
|
.where(InstalledApp.tenant_id == tenant_id, _published_app_filter())
|
|
)
|
|
if app_id:
|
|
stmt = stmt.where(InstalledApp.app_id == app_id)
|
|
if name and (normalized_name := name.strip()):
|
|
escaped_name = escape_like_pattern(normalized_name)
|
|
stmt = stmt.where(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
|
|
|
webapp_auth_enabled = FeatureService.get_system_features().webapp_auth.enabled
|
|
scan_size = limit * 2 if webapp_auth_enabled else limit + 1
|
|
visible_rows: list[tuple[InstalledApp, App]] = []
|
|
scan_cursor = cursor
|
|
has_more = False
|
|
last_consumed_app: InstalledApp | None = None
|
|
|
|
while True:
|
|
page_stmt = stmt
|
|
if scan_cursor is not None:
|
|
page_stmt = page_stmt.where(_installed_app_cursor_filter(scan_cursor))
|
|
candidate_result = session.execute(page_stmt.order_by(*_installed_app_order_by()).limit(scan_size)).all()
|
|
candidate_rows = [(installed_app, app) for installed_app, app in candidate_result]
|
|
if not candidate_rows:
|
|
break
|
|
|
|
authorized_rows = candidate_rows
|
|
if webapp_auth_enabled:
|
|
authorized_rows = _filter_rows_by_webapp_auth(candidate_rows, user_id=user_id)
|
|
|
|
authorized_installed_app_ids = {installed_app.id for installed_app, _ in authorized_rows}
|
|
for row in candidate_rows:
|
|
installed_app = row[0]
|
|
if installed_app.id not in authorized_installed_app_ids:
|
|
last_consumed_app = installed_app
|
|
continue
|
|
if len(visible_rows) == limit:
|
|
has_more = True
|
|
break
|
|
visible_rows.append(row)
|
|
last_consumed_app = installed_app
|
|
if has_more:
|
|
break
|
|
|
|
if len(candidate_rows) < scan_size:
|
|
break
|
|
last_scanned_app = candidate_rows[-1][0]
|
|
scan_cursor = InstalledAppCursor(
|
|
is_pinned=last_scanned_app.is_pinned,
|
|
last_used_at=last_scanned_app.last_used_at,
|
|
installed_app_id=last_scanned_app.id,
|
|
)
|
|
|
|
next_cursor = (
|
|
InstalledAppCursor(
|
|
is_pinned=last_consumed_app.is_pinned,
|
|
last_used_at=last_consumed_app.last_used_at,
|
|
installed_app_id=last_consumed_app.id,
|
|
)
|
|
if has_more and last_consumed_app
|
|
else None
|
|
)
|
|
return visible_rows, has_more, next_cursor
|
|
|
|
@staticmethod
|
|
def get_published_app(app_id: str, *, session: Session | scoped_session) -> App | None:
|
|
return session.scalar(select(App).where(App.id == app_id, _published_app_filter()).limit(1))
|