diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index 82bfe632d94..e457d2f9413 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -104,6 +104,7 @@ class Group(Enum): KNOWLEDGE_FOLDER = "KNOWLEDGE_FOLDER" TOOL_FOLDER = "TOOL_FOLDER" CHAT_USER_GROUP = "CHAT_USER_GROUP" + PORTAL = "PORTAL" class SystemGroup(Enum): @@ -127,6 +128,7 @@ class SystemGroup(Enum): CHAT_USER = "CHAT_USER" CHAT_USER_GROUP = "CHAT_USER_GROUP" CHAT_USER_AUTH = "CHAT_USER_AUTH" + PORTAL = "PORTAL" OPERATION_LOG = "OPERATION_LOG" APPEARANCE_SETTINGS = "APPEARANCE_SETTINGS" LOGIN_AUTH = "LOGIN_AUTH" @@ -181,7 +183,6 @@ class TopLevelGroup(Enum): OTHER = "OTHER" - class Operate(Enum): """ 一个权限组的操作权限 @@ -487,6 +488,7 @@ def get_workspace_role(self): TopLevelGroup.CHAT_CLIENT.value: _("Chat Client"), SystemGroup.CHAT_USER_GROUP.value: _("Chat User Group"), SystemGroup.CHAT_USER_AUTH.value: _("Chat User Auth"), + SystemGroup.PORTAL.value: _("Portal"), } GROUPS = { TopLevelGroup.IAM: ( @@ -515,6 +517,7 @@ def get_workspace_role(self): SystemGroup.CHAT_USER, SystemGroup.CHAT_USER_GROUP, SystemGroup.CHAT_USER_AUTH, + SystemGroup.PORTAL, WorkspaceGroup.CHAT_USER, WorkspaceGroup.CHAT_USER_GROUP, ), @@ -589,11 +592,11 @@ def __str__(self): def __eq__(self, other): if isinstance(other, Permission): return ( - self.group == other.group - and self.operate == other.operate - and self.resource_path == other.resource_path - and self.parent_group == other.parent_group - and self.is_ee == other.is_ee + self.group == other.group + and self.operate == other.operate + and self.resource_path == other.resource_path + and self.parent_group == other.parent_group + and self.is_ee == other.is_ee ) return str(self) == str(other) @@ -1533,6 +1536,15 @@ class PermissionConstants(Enum): role_list=[RoleConstants.ADMIN], parent_group=[SystemGroup.CHAT_USER_AUTH] ) + PORTAL_READ = Permission(group=Group.PORTAL, operate=Operate.READ, + role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.PORTAL] + ) + PORTAL_EDIT = Permission(group=Group.PORTAL, operate=Operate.EDIT, + role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.PORTAL] + ) + WORKSPACE_CHAT_USER_READ = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN], parent_group=[WorkspaceGroup.CHAT_USER] diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index c257ee61e7f..e9572068d13 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -9450,4 +9450,46 @@ msgid "Edit user group authorization status of resource" msgstr "" msgid "Get user group authorization status of resource by page" +msgstr "" + +msgid "Get portal configuration" +msgstr "" + +msgid "Save portal configuration" +msgstr "" + +msgid "Portal" +msgstr "" + +msgid "Portal configuration does not exist" +msgstr "" + +msgid "portal name" +msgstr "" + +msgid "portal description" +msgstr "" + +msgid "portal logo" +msgstr "" + +msgid "tab logo" +msgstr "" + +msgid "enable public access" +msgstr "" + +msgid "enable api" +msgstr "" + +msgid "enable auth" +msgstr "" + +msgid "auth config" +msgstr "" + +msgid "enable cors" +msgstr "" + +msgid "cors config" msgstr "" \ No newline at end of file diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index eb1d9855c1b..0fbb5b01829 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -9574,4 +9574,46 @@ msgid "Edit user group authorization status of resource" msgstr "编辑用户组对资源的授权状态" msgid "Get user group authorization status of resource by page" -msgstr "分页获取用户组对资源的授权状态" \ No newline at end of file +msgstr "分页获取用户组对资源的授权状态" + +msgid "Get portal configuration" +msgstr "获取门户配置" + +msgid "Save portal configuration" +msgstr "保存门户配置" + +msgid "Portal" +msgstr "门户" + +msgid "Portal configuration does not exist" +msgstr "门户配置不存在" + +msgid "portal name" +msgstr "门户名称" + +msgid "portal description" +msgstr "门户描述" + +msgid "portal logo" +msgstr "门户Logo" + +msgid "tab logo" +msgstr "浏览器Tab Logo" + +msgid "enable public access" +msgstr "是否开启公开访问" + +msgid "enable api" +msgstr "是否开启API服务" + +msgid "enable auth" +msgstr "是否开启身份认证" + +msgid "auth config" +msgstr "身份认证配置" + +msgid "enable cors" +msgstr "是否开启跨域设置" + +msgid "cors config" +msgstr "跨域配置" \ No newline at end of file diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index a9e3af27d45..faab9f85ce2 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -9573,4 +9573,46 @@ msgid "Edit user group authorization status of resource" msgstr "編輯用戶組對資源的授權狀態" msgid "Get user group authorization status of resource by page" -msgstr "分頁獲取用戶組對資源的授權狀態" \ No newline at end of file +msgstr "分頁獲取用戶組對資源的授權狀態" + +msgid "Get portal configuration" +msgstr "獲取門戶配置" + +msgid "Save portal configuration" +msgstr "儲存門戶配置" + +msgid "Portal" +msgstr "門戶" + +msgid "Portal configuration does not exist" +msgstr "門戶配置不存在" + +msgid "portal name" +msgstr "門戶名稱" + +msgid "portal description" +msgstr "門戶描述" + +msgid "portal logo" +msgstr "門戶Logo" + +msgid "tab logo" +msgstr "瀏覽器Tab Logo" + +msgid "enable public access" +msgstr "是否開啟公開訪問" + +msgid "enable api" +msgstr "是否開啟API服務" + +msgid "enable auth" +msgstr "是否開啟身份認證" + +msgid "auth config" +msgstr "身份認證配置" + +msgid "enable cors" +msgstr "是否開啟跨域設置" + +msgid "cors config" +msgstr "跨域配置" \ No newline at end of file diff --git a/apps/maxkb/settings/base/web.py b/apps/maxkb/settings/base/web.py index ec62d4b9902..3899dc0a86b 100644 --- a/apps/maxkb/settings/base/web.py +++ b/apps/maxkb/settings/base/web.py @@ -6,11 +6,13 @@ @date:2025/11/5 14:53 @desc: """ -from pathlib import Path -from ...const import CONFIG, PROJECT_DIR import os +from pathlib import Path + from django.utils.translation import gettext_lazy as _ +from ...const import CONFIG, PROJECT_DIR + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -46,6 +48,7 @@ 'oss', 'trigger', 'django_apscheduler', + "portal" ] MIDDLEWARE = [ diff --git a/apps/maxkb/urls/web.py b/apps/maxkb/urls/web.py index f43983256fb..0c7063a74f3 100644 --- a/apps/maxkb/urls/web.py +++ b/apps/maxkb/urls/web.py @@ -44,6 +44,7 @@ path(admin_api_prefix, include("trigger.urls")), path(admin_api_prefix, include("oss.urls")), path(admin_api_prefix, include("homepage.urls")), + path(admin_api_prefix, include("portal.urls")), path(chat_api_prefix, include("oss.urls")), path(chat_api_prefix, include("chat.urls")), path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')), diff --git a/apps/portal/__init__.py b/apps/portal/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/portal/admin.py b/apps/portal/admin.py new file mode 100644 index 00000000000..8c38f3f3dad --- /dev/null +++ b/apps/portal/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/portal/api/__init__.py b/apps/portal/api/__init__.py new file mode 100644 index 00000000000..2740e7b78b6 --- /dev/null +++ b/apps/portal/api/__init__.py @@ -0,0 +1,2 @@ +# coding=utf-8 +from .portal import * diff --git a/apps/portal/api/portal.py b/apps/portal/api/portal.py new file mode 100644 index 00000000000..e725260e22c --- /dev/null +++ b/apps/portal/api/portal.py @@ -0,0 +1,43 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:MaxKB + @file: portal.py + @date:2026/8/3 + @desc: 门户API文档 +""" +from common.mixins.api_mixin import APIMixin +from common.result import DefaultResultSerializer + + +class PortalAPI(APIMixin): + + class Get(APIMixin): + @staticmethod + def get_response(): + return DefaultResultSerializer + + class Save(APIMixin): + @staticmethod + def get_request(): + return { + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string', 'description': '门户名称'}, + 'description': {'type': 'string', 'description': '门户描述'}, + 'logo': {'type': 'string', 'format': 'binary', 'description': '门户Logo'}, + 'tab_logo': {'type': 'string', 'format': 'binary', 'description': '浏览器Tab Logo'}, + 'enable_public_access': {'type': 'boolean', 'description': '是否开启公开访问'}, + 'enable_api': {'type': 'boolean', 'description': '是否开启API服务'}, + 'enable_auth': {'type': 'boolean', 'description': '是否开启身份认证'}, + 'auth_config': {'type': 'object', 'description': '身份认证配置'}, + 'enable_cors': {'type': 'boolean', 'description': '是否开启跨域设置'}, + 'cors_config': {'type': 'object', 'description': '跨域配置'}, + } + } + } + + @staticmethod + def get_response(): + return DefaultResultSerializer diff --git a/apps/portal/apps.py b/apps/portal/apps.py new file mode 100644 index 00000000000..811caa82b8d --- /dev/null +++ b/apps/portal/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PortalConfig(AppConfig): + name = 'portal' diff --git a/apps/portal/migrations/0001_initial.py b/apps/portal/migrations/0001_initial.py new file mode 100644 index 00000000000..7753a896e3d --- /dev/null +++ b/apps/portal/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.7 on 2026-08-03 06:20 + +import uuid_utils.compat +from django.db import migrations, models + +def create_default_portal(apps, schema_editor): + Portal = apps.get_model("portal", "Portal") + + Portal.objects.create( + name="智能体门户", + description="默认门户", + enable_public_access=True, + enable_api=True, + enable_auth=False, + auth_config={}, + enable_cors=False, + cors_config={}, + ) + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Portal', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), + ('name', models.CharField(default='智能体门户', max_length=64, verbose_name='门户名称')), + ('description', models.TextField(blank=True, max_length=256, null=True, verbose_name='门户描述')), + ('logo', models.CharField(blank=True, max_length=512, null=True, verbose_name='门户Logo地址')), + ('tab_logo', models.CharField(blank=True, max_length=512, null=True, verbose_name='浏览器Tab Logo地址')), + ('enable_public_access', models.BooleanField(default=True, verbose_name='是否开启公开访问')), + ('enable_api', models.BooleanField(default=True, verbose_name='是否开启API服务')), + ('enable_auth', models.BooleanField(default=False, verbose_name='是否开启身份认证')), + ('auth_config', models.JSONField(blank=True, default=dict, verbose_name='身份认证配置')), + ('enable_cors', models.BooleanField(default=False, verbose_name='是否开启跨域设置')), + ('cors_config', models.JSONField(blank=True, default=dict, verbose_name='跨域配置')), + ], + options={ + 'db_table': 'portal', + }, + ), + migrations.RunPython( + create_default_portal, + migrations.RunPython.noop, + ), + ] diff --git a/apps/portal/migrations/__init__.py b/apps/portal/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/portal/models/__init__.py b/apps/portal/models/__init__.py new file mode 100644 index 00000000000..599f93561da --- /dev/null +++ b/apps/portal/models/__init__.py @@ -0,0 +1 @@ +from .portal import * diff --git a/apps/portal/models/portal.py b/apps/portal/models/portal.py new file mode 100644 index 00000000000..2b1a03a6d78 --- /dev/null +++ b/apps/portal/models/portal.py @@ -0,0 +1,75 @@ +# coding=utf-8 + +import uuid_utils.compat as uuid +from common.encoder.encoder import SystemEncoder +from common.mixins.app_model_mixin import AppModelMixin +from django.db import models + + +class Portal(AppModelMixin): + """ + 门户配置 + """ + # 基础信息 + name = models.CharField( + max_length=64, + verbose_name="门户名称", + default="智能体门户" + ) + + description = models.TextField( + null=True, + blank=True, + max_length=256, + verbose_name="门户描述" + ) + + logo = models.CharField( + max_length=512, + null=True, + blank=True, + verbose_name="门户Logo地址" + ) + + tab_logo = models.CharField( + max_length=512, + null=True, + blank=True, + verbose_name="浏览器Tab Logo地址" + ) + + enable_public_access = models.BooleanField( + default=True, + verbose_name="是否开启公开访问" + ) + # API服务配置 + enable_api = models.BooleanField( + default=True, + verbose_name="是否开启API服务" + ) + + # 身份认证配置 + enable_auth = models.BooleanField( + default=False, + verbose_name="是否开启身份认证" + ) + + auth_config = models.JSONField( + default=dict, + blank=True, + verbose_name="身份认证配置" + ) + + enable_cors = models.BooleanField( + default=False, + verbose_name="是否开启跨域设置" + ) + + cors_config = models.JSONField( + default=dict, + blank=True, + verbose_name="跨域配置" + ) + + class Meta: + db_table = "portal" \ No newline at end of file diff --git a/apps/portal/serializers/__init__.py b/apps/portal/serializers/__init__.py new file mode 100644 index 00000000000..2740e7b78b6 --- /dev/null +++ b/apps/portal/serializers/__init__.py @@ -0,0 +1,2 @@ +# coding=utf-8 +from .portal import * diff --git a/apps/portal/serializers/portal.py b/apps/portal/serializers/portal.py new file mode 100644 index 00000000000..d01d1eec9a5 --- /dev/null +++ b/apps/portal/serializers/portal.py @@ -0,0 +1,84 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:MaxKB + @file: portal.py + @date:2026/8/3 + @desc: 门户配置序列化器 +""" +import uuid_utils.compat as uuid +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.exception.app_exception import AppApiException +from knowledge.models import File, FileSourceType +from portal.models import Portal + + +class PortalSerializer(serializers.Serializer): + name = serializers.CharField(required=False, label=_('portal name'), help_text=_('portal name')) + description = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_('portal description'), help_text=_('portal description')) + logo = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_('portal logo'), help_text=_('portal logo')) + tab_logo = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_('tab logo'), help_text=_('tab logo')) + enable_public_access = serializers.BooleanField(required=False, label=_('enable public access'), + help_text=_('enable public access')) + enable_api = serializers.BooleanField(required=False, label=_('enable api'), help_text=_('enable api')) + enable_auth = serializers.BooleanField(required=False, label=_('enable auth'), help_text=_('enable auth')) + auth_config = serializers.JSONField(required=False, label=_('auth config'), help_text=_('auth config')) + enable_cors = serializers.BooleanField(required=False, label=_('enable cors'), help_text=_('enable cors')) + cors_config = serializers.JSONField(required=False, label=_('cors config'), help_text=_('cors config')) + + class Model(serializers.ModelSerializer): + class Meta: + model = Portal + fields = '__all__' + + def one(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + portal = Portal.objects.first() + if portal is None: + raise AppApiException(500, _("Portal configuration does not exist")) + return PortalSerializer.Model(portal).data + + def _upload_file(self, file_obj): + file_id = uuid.uuid7() + file = File( + id=file_id, + file_name=file_obj.name, + source_type=FileSourceType.SYSTEM, + meta={"debug": False}, + ) + file.save(file_obj.read()) + return f"./oss/file/{file_id}" + + def _handle_file_field(self, portal, field_name, value): + if hasattr(value, 'read'): + old_url = getattr(portal, field_name) + if old_url: + old_file_id = old_url.split("/")[-1] + QuerySet(File).filter(id=old_file_id).delete() + new_url = self._upload_file(value) + setattr(portal, field_name, new_url) + else: + setattr(portal, field_name, value) + + def edit(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + portal = Portal.objects.first() + if portal is None: + raise AppApiException(500, _("Portal configuration does not exist")) + file_fields = ['logo', 'tab_logo', 'id', 'create_time', 'update_time'] + for field, value in instance.items(): + if hasattr(portal, field) and field not in file_fields: + setattr(portal, field, value) + for field_name in ['logo', 'tab_logo']: + if field_name in instance: + self._handle_file_field(portal, field_name, instance.get(field_name)) + portal.save() + return PortalSerializer.Model(portal).data diff --git a/apps/portal/tests.py b/apps/portal/tests.py new file mode 100644 index 00000000000..7ce503c2dd9 --- /dev/null +++ b/apps/portal/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/portal/urls.py b/apps/portal/urls.py new file mode 100644 index 00000000000..de6451ed57e --- /dev/null +++ b/apps/portal/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +app_name = 'portal' + +urlpatterns = [ + path('portal', views.PortalView.as_view()), +] diff --git a/apps/portal/views/__init__.py b/apps/portal/views/__init__.py new file mode 100644 index 00000000000..2740e7b78b6 --- /dev/null +++ b/apps/portal/views/__init__.py @@ -0,0 +1,2 @@ +# coding=utf-8 +from .portal import * diff --git a/apps/portal/views/portal.py b/apps/portal/views/portal.py new file mode 100644 index 00000000000..6c2c3ea4b51 --- /dev/null +++ b/apps/portal/views/portal.py @@ -0,0 +1,54 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:MaxKB + @file: portal.py + @date:2026/8/3 + @desc: 门户配置视图 +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.parsers import MultiPartParser, JSONParser +from rest_framework.request import Request +from rest_framework.views import APIView + +from common import result +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants, RoleConstants +from common.log.log import log +from portal.api.portal import PortalAPI +from portal.serializers.portal import PortalSerializer + + +class PortalView(APIView): + authentication_classes = [TokenAuth] + parser_classes = [JSONParser, MultiPartParser] + + @extend_schema( + methods=['GET'], + description=_('Get portal configuration'), + summary=_('Get portal configuration'), + operation_id=_('Get portal configuration'), + responses=PortalAPI.Get.get_response(), + tags=[_('Portal')] + ) + @has_permissions( + PermissionConstants.PORTAL_EDIT, RoleConstants.ADMIN) + def get(self, request: Request): + return result.success(PortalSerializer().one()) + + @extend_schema( + methods=['PUT'], + description=_('Save portal configuration'), + summary=_('Save portal configuration'), + operation_id=_('Save portal configuration'), + request=PortalAPI.Save.get_request(), + responses=PortalAPI.Save.get_response(), + tags=[_('Portal')] + ) + @log(menu='Portal', operate='Save portal configuration') + @has_permissions( + PermissionConstants.PORTAL_EDIT, RoleConstants.ADMIN) + def put(self, request: Request): + return result.success(PortalSerializer().edit(request.data))