Skip to content

Commit 0135bdd

Browse files
committed
refactor: implement portal up and down functionality for applications
1 parent 5bb516c commit 0135bdd

17 files changed

Lines changed: 313 additions & 22 deletions

File tree

‎apps/application/serializers/application.py‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,6 @@ class ApplicationImportRequest(serializers.Serializer):
719719

720720
class ApplicationEditSerializer(serializers.Serializer):
721721
name = serializers.CharField(required=False, max_length=64, min_length=1, label=_("Application Name"))
722-
is_portal = serializers.BooleanField(required=False, label=_("Whether to publish on portal"))
723722
desc = serializers.CharField(
724723
required=False,
725724
max_length=256,
@@ -1349,6 +1348,38 @@ def publish(self, instance, with_valid=True):
13491348
)
13501349
return self.one(with_valid=False)
13511350

1351+
@transaction.atomic
1352+
def portal_up(self, with_valid=True):
1353+
"""上架到门户:仅已发布的应用可上架,调用方需具备门户上架权限。"""
1354+
if with_valid:
1355+
self.is_valid()
1356+
workspace_id = self.data.get("workspace_id")
1357+
application = (
1358+
QuerySet(Application).filter(id=self.data.get("application_id"), workspace_id=workspace_id).first()
1359+
)
1360+
if not application:
1361+
raise AppApiException(500, _("Application id does not exist"))
1362+
if not application.is_publish:
1363+
raise AppApiException(500, _("Only published applications can be published to Portal"))
1364+
application.is_portal = True
1365+
application.save()
1366+
return self.one(with_valid=False)
1367+
1368+
@transaction.atomic
1369+
def portal_down(self, with_valid=True):
1370+
"""从门户下架,调用方需具备门户下架权限。"""
1371+
if with_valid:
1372+
self.is_valid()
1373+
workspace_id = self.data.get("workspace_id")
1374+
application = (
1375+
QuerySet(Application).filter(id=self.data.get("application_id"), workspace_id=workspace_id).first()
1376+
)
1377+
if not application:
1378+
raise AppApiException(500, _("Application id does not exist"))
1379+
application.is_portal = False
1380+
application.save()
1381+
return self.one(with_valid=False)
1382+
13521383
@staticmethod
13531384
def update_work_flow_model(instance):
13541385
if "nodes" not in instance.get("work_flow"):
@@ -1465,7 +1496,6 @@ def edit(self, instance: Dict, with_valid=True):
14651496
ToolExecutor().validate_mcp_transport(json.dumps(instance.get("mcp_servers")))
14661497
update_keys = [
14671498
"name",
1468-
"is_portal",
14691499
"desc",
14701500
"model_id",
14711501
"multiple_rounds_dialogue",

‎apps/application/urls.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
path('workspace/<str:workspace_id>/application/batch_clean_time', views.ApplicationAPI.BatchCleanTime.as_view()),
1616
path('workspace/<str:workspace_id>/application/<str:application_id>', views.ApplicationAPI.Operate.as_view()),
1717
path('workspace/<str:workspace_id>/application/<str:application_id>/publish', views.ApplicationAPI.Publish.as_view()),
18+
path('workspace/<str:workspace_id>/application/<str:application_id>/portal_up', views.ApplicationAPI.PortalUp.as_view()),
19+
path('workspace/<str:workspace_id>/application/<str:application_id>/portal_down', views.ApplicationAPI.PortalDown.as_view()),
1820
path('workspace/<str:workspace_id>/application/<str:application_id>/move/<str:folder_id>', views.ApplicationAPI.Move.as_view()),
1921
path('workspace/<str:workspace_id>/application/<str:application_id>/application_key', views.ApplicationKey.as_view()),
2022
path('workspace/<str:workspace_id>/application/<str:application_id>/application_stats', views.ApplicationStats.as_view()),

‎apps/application/views/application.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,60 @@ def put(self, request: Request, workspace_id: str, application_id: str):
294294
data={'application_id': application_id, 'user_id': request.user.id,
295295
'workspace_id': workspace_id, }).publish(request.data))
296296

297+
class PortalUp(APIView):
298+
authentication_classes = [TokenAuth]
299+
300+
@extend_schema(
301+
methods=['PUT'],
302+
description=_("Publishing an application to Portal"),
303+
summary=_("Publishing an application to Portal"),
304+
operation_id=_("Publishing an application to Portal"), # type: ignore
305+
parameters=ApplicationOperateAPI.get_parameters(),
306+
request=None,
307+
responses=result.DefaultResultSerializer,
308+
tags=[_('Application')] # type: ignore
309+
)
310+
@has_permissions(PermissionConstants.APPLICATION_PORTAL_UP.get_workspace_application_permission(),
311+
PermissionConstants.APPLICATION_PORTAL_UP.get_workspace_permission_workspace_manage_role(),
312+
ViewPermission([RoleConstants.USER.get_workspace_role()],
313+
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
314+
compare=CompareConstants.AND),
315+
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
316+
@log(menu='Application', operate='Publishing an application to Portal',
317+
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
318+
def put(self, request: Request, workspace_id: str, application_id: str):
319+
return result.success(
320+
ApplicationOperateSerializer(
321+
data={'application_id': application_id, 'user_id': request.user.id,
322+
'workspace_id': workspace_id, }).portal_up())
323+
324+
class PortalDown(APIView):
325+
authentication_classes = [TokenAuth]
326+
327+
@extend_schema(
328+
methods=['PUT'],
329+
description=_("Unpublishing an application from Portal"),
330+
summary=_("Unpublishing an application from Portal"),
331+
operation_id=_("Unpublishing an application from Portal"), # type: ignore
332+
parameters=ApplicationOperateAPI.get_parameters(),
333+
request=None,
334+
responses=result.DefaultResultSerializer,
335+
tags=[_('Application')] # type: ignore
336+
)
337+
@has_permissions(PermissionConstants.APPLICATION_PORTAL_DOWN.get_workspace_application_permission(),
338+
PermissionConstants.APPLICATION_PORTAL_DOWN.get_workspace_permission_workspace_manage_role(),
339+
ViewPermission([RoleConstants.USER.get_workspace_role()],
340+
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
341+
compare=CompareConstants.AND),
342+
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
343+
@log(menu='Application', operate='Unpublishing an application from Portal',
344+
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
345+
def put(self, request: Request, workspace_id: str, application_id: str):
346+
return result.success(
347+
ApplicationOperateSerializer(
348+
data={'application_id': application_id, 'user_id': request.user.id,
349+
'workspace_id': workspace_id, }).portal_down())
350+
297351
class StoreApplication(APIView):
298352
authentication_classes = [TokenAuth]
299353

‎apps/common/auth/constants/operate_constants.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class Operate(Enum):
7676
CHANGE_PASSWORD = ("READ+CREATE", _("Change Password"))
7777
SYSTEM_API_KEY = ("READ+DELETE", _("System API Key"))
7878
PORTAL = ("READ+PORTAL", _("Portal"))
79+
PORTAL_UP = ("READ+PORTAL_UP", _("Publish to Portal"))
80+
PORTAL_DOWN = ("READ+PORTAL_DOWN", _("Unpublish from Portal"))
7981

8082
ANNOTATION_AUTH = ('ANNOTATION', _("Annotation"))
8183
PASSWORD = ("PASSWORD", _("Password verification"))

‎apps/common/auth/constants/permission_constants.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,26 @@ class PermissionConstants(Enum):
14411441
),
14421442
)
14431443

1444+
APPLICATION_PORTAL_UP = (
1445+
Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.PORTAL_UP, bit_index=17),
1446+
PermissionMeta(
1447+
role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
1448+
category=Category.WORKSPACE,
1449+
scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
1450+
resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
1451+
),
1452+
)
1453+
1454+
APPLICATION_PORTAL_DOWN = (
1455+
Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.PORTAL_DOWN, bit_index=18),
1456+
PermissionMeta(
1457+
role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
1458+
category=Category.WORKSPACE,
1459+
scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
1460+
resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
1461+
),
1462+
)
1463+
14441464
# ==================== 应用文件夹 ====================
14451465
APPLICATION_FOLDER_READ = (
14461466
Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.READ, bit_index=0),
@@ -2696,6 +2716,36 @@ class PermissionConstants(Enum):
26962716
),
26972717
)
26982718

2719+
RESOURCE_APPLICATION_PORTAL_UP = (
2720+
Permission(
2721+
group=Group.SYSTEM_RES_APPLICATION,
2722+
sub_group=Group.SYSTEM_RES_APPLICATION,
2723+
operate=Operate.PORTAL_UP,
2724+
bit_index=12,
2725+
),
2726+
PermissionMeta(
2727+
role_list=[RoleConstants.ADMIN],
2728+
category=Category.RESOURCE,
2729+
is_ee=is_ee,
2730+
scope=[PermissionScopeConstants.SYSTEM],
2731+
),
2732+
)
2733+
2734+
RESOURCE_APPLICATION_PORTAL_DOWN = (
2735+
Permission(
2736+
group=Group.SYSTEM_RES_APPLICATION,
2737+
sub_group=Group.SYSTEM_RES_APPLICATION,
2738+
operate=Operate.PORTAL_DOWN,
2739+
bit_index=13,
2740+
),
2741+
PermissionMeta(
2742+
role_list=[RoleConstants.ADMIN],
2743+
category=Category.RESOURCE,
2744+
is_ee=is_ee,
2745+
scope=[PermissionScopeConstants.SYSTEM],
2746+
),
2747+
)
2748+
26992749
# 资源管理 - 应用概览
27002750
RESOURCE_APPLICATION_OVERVIEW_READ = (
27012751
Permission(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# coding=utf-8
2+
3+
from django.test import SimpleTestCase
4+
5+
from common.auth.constants.category_constants import Category
6+
from common.auth.constants.operate_constants import Operate
7+
from common.auth.constants.permission_constants import PermissionConstants
8+
from common.auth.constants.role_constants import RoleConstants
9+
10+
11+
class ApplicationPortalPermissionTests(SimpleTestCase):
12+
"""智能体门户上下架权限已注册且分组/操作位与前端一致。"""
13+
14+
def test_application_portal_up_permission(self):
15+
perm = PermissionConstants.APPLICATION_PORTAL_UP.value
16+
meta = PermissionConstants.APPLICATION_PORTAL_UP.meta
17+
self.assertEqual(perm.group.value, "APPLICATION")
18+
self.assertEqual(perm.operate, Operate.PORTAL_UP)
19+
self.assertEqual(perm.bit_index, 17)
20+
self.assertIn(RoleConstants.WORKSPACE_MANAGE, meta.role_list)
21+
self.assertIn(RoleConstants.USER, meta.role_list)
22+
self.assertEqual(meta.category, Category.WORKSPACE)
23+
24+
def test_application_portal_down_permission(self):
25+
perm = PermissionConstants.APPLICATION_PORTAL_DOWN.value
26+
meta = PermissionConstants.APPLICATION_PORTAL_DOWN.meta
27+
self.assertEqual(perm.group.value, "APPLICATION")
28+
self.assertEqual(perm.operate, Operate.PORTAL_DOWN)
29+
self.assertEqual(perm.bit_index, 18)
30+
self.assertEqual(meta.category, Category.WORKSPACE)
31+
32+
def test_resource_application_portal_up_permission(self):
33+
perm = PermissionConstants.RESOURCE_APPLICATION_PORTAL_UP.value
34+
meta = PermissionConstants.RESOURCE_APPLICATION_PORTAL_UP.meta
35+
self.assertEqual(perm.group.value, "SYSTEM_RESOURCE_APPLICATION")
36+
self.assertEqual(perm.operate, Operate.PORTAL_UP)
37+
self.assertEqual(perm.bit_index, 12)
38+
self.assertIn(RoleConstants.ADMIN, meta.role_list)
39+
self.assertEqual(meta.category, Category.RESOURCE)
40+
41+
def test_resource_application_portal_down_permission(self):
42+
perm = PermissionConstants.RESOURCE_APPLICATION_PORTAL_DOWN.value
43+
meta = PermissionConstants.RESOURCE_APPLICATION_PORTAL_DOWN.meta
44+
self.assertEqual(perm.group.value, "SYSTEM_RESOURCE_APPLICATION")
45+
self.assertEqual(perm.operate, Operate.PORTAL_DOWN)
46+
self.assertEqual(perm.bit_index, 13)
47+
self.assertEqual(meta.category, Category.RESOURCE)
48+
49+
def test_operate_values_match_frontend(self):
50+
self.assertEqual(Operate.PORTAL_UP.value, "READ+PORTAL_UP")
51+
self.assertEqual(Operate.PORTAL_DOWN.value, "READ+PORTAL_DOWN")

‎apps/locales/en_US/LC_MESSAGES/django.po‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9306,6 +9306,21 @@ msgstr ""
93069306
msgid "Publish"
93079307
msgstr ""
93089308

9309+
msgid "Publish to Portal"
9310+
msgstr ""
9311+
9312+
msgid "Unpublish from Portal"
9313+
msgstr ""
9314+
9315+
msgid "Only published applications can be published to Portal"
9316+
msgstr ""
9317+
9318+
msgid "Publishing an application to Portal"
9319+
msgstr ""
9320+
9321+
msgid "Unpublishing an application from Portal"
9322+
msgstr ""
9323+
93099324
msgid "token is required for EVENT triggers"
93109325
msgstr ""
93119326

‎apps/locales/zh_CN/LC_MESSAGES/django.po‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9431,6 +9431,21 @@ msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项"
94319431
msgid "Publish"
94329432
msgstr "发布"
94339433

9434+
msgid "Publish to Portal"
9435+
msgstr "上架到门户"
9436+
9437+
msgid "Unpublish from Portal"
9438+
msgstr "从门户下架"
9439+
9440+
msgid "Only published applications can be published to Portal"
9441+
msgstr "仅已发布的应用可以上架到门户"
9442+
9443+
msgid "Publishing an application to Portal"
9444+
msgstr "将应用上架到门户"
9445+
9446+
msgid "Unpublishing an application from Portal"
9447+
msgstr "将应用从门户下架"
9448+
94349449
msgid "token is required for EVENT triggers"
94359450
msgstr "事件触发器必须设置 token"
94369451

‎apps/locales/zh_Hant/LC_MESSAGES/django.po‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9431,6 +9431,21 @@ msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项"
94319431
msgid "Publish"
94329432
msgstr "發布"
94339433

9434+
msgid "Publish to Portal"
9435+
msgstr "上架到門戶"
9436+
9437+
msgid "Unpublish from Portal"
9438+
msgstr "從門戶下架"
9439+
9440+
msgid "Only published applications can be published to Portal"
9441+
msgstr "僅已發布的應用可以上架到門戶"
9442+
9443+
msgid "Publishing an application to Portal"
9444+
msgstr "將應用上架到門戶"
9445+
9446+
msgid "Unpublishing an application from Portal"
9447+
msgstr "將應用從門戶下架"
9448+
94349449
msgid "token is required for EVENT triggers"
94359450
msgstr "事件觸發器必須設定 token"
94369451

‎ui/src/api/API_README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,10 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外。Syst
390390
- 知识库 Workflow API 记录为 `/<id>/action/<page>/<size>`,筛选 user_name、state;
391391
详情和取消为 GET action/<actionId>、POST action/<actionId>/cancel。
392392
- 智能体复制查询完整 ApplicationDetail,model 映射 model_id 后创建,不使用卡片摘要。
393-
- Workspace 与 System 资源管理的 Application API 通过 `putApplicationPortal(applicationId, isPortal)`
394-
向各自范围的 `application/<applicationId>` 发送 PUT 请求,仅提交 `{ is_portal }`,返回 `ApplicationDetail`。
393+
- Workspace 与 System 资源管理的 Application API 通过 `putApplicationPortalUp(applicationId)` 与
394+
`putApplicationPortalDown(applicationId)` 分别向各自范围的 `application/<applicationId>/portal_up`、
395+
`application/<applicationId>/portal_down` 发送 PUT 请求(上下架接口与通用编辑接口分离),返回 `ApplicationDetail`。
396+
上架仅允许已发布(`is_publish`)的智能体;上下架分别受门户上架/下架权限控制。
395397
`exportApplicationSkills(applicationId, applicationName)` 仍为本地 mock,返回 `{ application_id, name }`,
396398
不发送请求或生成 Skills 文件,真实下载接口待接入。
397399

0 commit comments

Comments
 (0)