diff --git a/api/README_CN.md b/api/README_CN.md new file mode 100644 index 0000000000..ee8b9a34ff --- /dev/null +++ b/api/README_CN.md @@ -0,0 +1,91 @@ +# Dify 后端 API + +## 使用方法 + +> [重要事项] +> +> 在 v1.3.0 版本中,`poetry` 已被 +> [ `uv` ](https://docs.astral.sh/uv/) 替代,作为 Dify API 后端服务的包管理器。 + +1. 启动 docker-compose 栈 + + 后端需要一些中间件,包括 PostgreSQL、Redis 和 Weaviate,可以使用 `docker-compose` 一起启动。 + + ```bash + cd ../docker + cp middleware.env.example middleware.env + # 如果不使用 weaviate,请将配置文件更改为其他向量数据库 + docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d + cd ../api + ``` + +2. 将 `.env.example` 复制为 `.env` + + ```cli + cp .env.example .env + ``` +3. 在 `.env` 文件中生成一个 `SECRET_KEY`。 + + Linux 系统的 bash 命令 + ```bash for Linux + sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env + ``` + Mac 系统的 bash 命令 + ```bash for Mac + secret_key=$(openssl rand -base64 42) + sed -i '' "/^SECRET_KEY=/c\\ + SECRET_KEY=${secret_key}" .env + ``` + +4. 创建环境。 + + Dify API 服务使用 [UV](https://docs.astral.sh/uv/) 来管理依赖项。 + 首先,如果还没有安装 uv 包管理器,需要先安装它。 + + ```bash + pip install uv + # 或者在 macOS 上 + brew install uv + ``` + +5. 安装依赖项 + + ```bash + uv sync --dev + ``` + +6. 运行迁移 + + 在首次启动之前,将数据库迁移到最新版本。 + + ```bash + uv run flask db upgrade + ``` + +7. 启动后端 + + ```bash + uv run flask run --host 0.0.0.0 --port=5001 --debug + ``` + +8. 启动 Dify [web](../web) 服务。 +9. 通过访问 `http://localhost:3000` 来设置你的应用程序。 +10. 如果你需要处理和调试异步任务(例如数据集导入和文档索引),请启动工作进程服务。 + + ```bash + uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion + ``` + +## 测试 + +1. 为后端和测试环境安装依赖项 + + ```bash + uv sync --dev + ``` + +2. 使用 `pyproject.toml` 文件中 `tool.pytest_env` 部分模拟的系统环境变量在本地运行测试 + + ```bash + uv run -P api bash dev/pytest/pytest_all_tests.sh + ``` \ No newline at end of file diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 3e908b76a7..2a6874221c 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,4 +1,5 @@ import uuid +import json from typing import cast from flask_login import current_user # type: ignore @@ -139,6 +140,8 @@ class AppApi(Resource): parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") + parser.add_argument("permission", type=str, location="json") + parser.add_argument("partial_member_list", type=list, location="json") args = parser.parse_args() app_service = AppService() diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index f42364f110..ea11bfd09f 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -98,6 +98,8 @@ app_partial_fields = { "updated_by": fields.String, "updated_at": TimestampField, "tags": fields.List(fields.Nested(tag_fields)), + "permission": fields.String, + "permission_account_ids": fields.List(fields.String), } diff --git a/api/models/model.py b/api/models/model.py index 901e92284a..2675c3896a 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -101,6 +101,7 @@ class App(Base): updated_by = db.Column(StringUUID, nullable=True) updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + permission = db.Column(db.String(255)) @property def desc_or_prompt(self): @@ -295,6 +296,23 @@ class App(Base): return tags or [] +class AppPermission(Base): + __tablename__ = "app_permissions" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="app_permission_pkey"), + db.Index("idx_app_permissions_app_id", "app_id"), + db.Index("idx_app_permissions_account_id", "account_id"), + db.Index("idx_app_permissions_tenant_id", "tenant_id"), + ) + + id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = db.Column(StringUUID, nullable=False) + account_id = db.Column(StringUUID, nullable=False) + has_permission = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) + created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + tenant_id = db.Column(StringUUID, nullable=False) + + class AppModelConfig(Base): __tablename__ = "app_model_configs" __table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id")) diff --git a/api/services/app_permission_service.py b/api/services/app_permission_service.py new file mode 100644 index 0000000000..44a89b88b6 --- /dev/null +++ b/api/services/app_permission_service.py @@ -0,0 +1,191 @@ +from sqlalchemy.orm import Session + +from extensions.ext_database import db +from models.model import AppPermission + + +class AppPermissionService: + @classmethod + def get_app_permissions_by_app_id(cls, app_id: str): + """ + Get a list of account IDs that have permission for an app. + + Args: + app_id (str): The ID of the app. + + Returns: + list: A list of account IDs with permissions for the app. + """ + with Session(db.engine) as session: + permissions = session.query(AppPermission).filter( + AppPermission.app_id == app_id, + AppPermission.has_permission == True + ).all() + + return [permission.account_id for permission in permissions] + + @classmethod + def get_app_permissions_by_account_id(cls, account_id: str): + """ + Get a list of app IDs that an account has permission for. + + Args: + account_id (str): The ID of the account. + + Returns: + list: A list of app IDs the account has permission for. + """ + with Session(db.engine) as session: + permissions = session.query(AppPermission).filter( + AppPermission.account_id == account_id, + AppPermission.has_permission == True + ).all() + + return [permission.app_id for permission in permissions] + + @classmethod + def update_app_permissions(cls, tenant_id: str, app_id: str, account_ids: list): + """ + Update the permissions for an app by replacing all existing permissions. + + Args: + tenant_id (str): The ID of the tenant. + app_id (str): The ID of the app. + account_ids (list): A list of account IDs to grant permission to. + + Returns: + bool: True if the operation succeeds. + """ + try: + with Session(db.engine) as session: + # Delete existing permissions for the app + session.query(AppPermission).filter( + AppPermission.app_id == app_id + ).delete() + + # Create new permissions + permissions = [] + for account_id in account_ids: + permission = AppPermission( + tenant_id=tenant_id, + app_id=app_id, + account_id=account_id, + has_permission=True + ) + permissions.append(permission) + + session.add_all(permissions) + session.commit() + + return True + except Exception as e: + db.session.rollback() + raise e + + @classmethod + def add_app_permission(cls, tenant_id: str, app_id: str, account_id: str): + """ + Add permission for an account to access an app. + + Args: + tenant_id (str): The ID of the tenant. + app_id (str): The ID of the app. + account_id (str): The ID of the account to grant permission to. + + Returns: + AppPermission: The created permission object. + """ + with Session(db.engine) as session: + # Check if permission already exists + existing_permission = session.query(AppPermission).filter( + AppPermission.app_id == app_id, + AppPermission.account_id == account_id + ).first() + + if existing_permission: + existing_permission.has_permission = True + session.commit() + return existing_permission + + # Create new permission + permission = AppPermission( + tenant_id=tenant_id, + app_id=app_id, + account_id=account_id, + has_permission=True + ) + + session.add(permission) + session.commit() + + return permission + + @classmethod + def remove_app_permission(cls, app_id: str, account_id: str): + """ + Remove permission for an account to access an app. + + Args: + app_id (str): The ID of the app. + account_id (str): The ID of the account to remove permission from. + + Returns: + bool: True if the permission was removed. + """ + with Session(db.engine) as session: + permission = session.query(AppPermission).filter( + AppPermission.app_id == app_id, + AppPermission.account_id == account_id + ).first() + + if permission: + session.delete(permission) + session.commit() + return True + + return False + + @classmethod + def check_app_permission(cls, app_id: str, account_id: str): + """ + Check if an account has permission to access an app. + + Args: + app_id (str): The ID of the app. + account_id (str): The ID of the account. + + Returns: + bool: True if the account has permission. + """ + with Session(db.engine) as session: + permission = session.query(AppPermission).filter( + AppPermission.app_id == app_id, + AppPermission.account_id == account_id, + AppPermission.has_permission == True + ).first() + + return permission is not None + + @classmethod + def clear_app_permissions(cls, app_id: str): + """ + Clear all permissions for an app. + + Args: + app_id (str): The ID of the app. + + Returns: + bool: True if the operation succeeds. + """ + try: + with Session(db.engine) as session: + session.query(AppPermission).filter( + AppPermission.app_id == app_id + ).delete() + + session.commit() + + return True + except Exception as e: + db.session.rollback() + raise e \ No newline at end of file diff --git a/api/services/app_service.py b/api/services/app_service.py index e87a1c7931..49f8f13bc0 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -18,8 +18,9 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_was_created from extensions.ext_database import db from models.account import Account -from models.model import App, AppMode, AppModelConfig +from models.model import App, AppMode, AppModelConfig, AppPermission from models.tools import ApiToolProvider +from services.app_permission_service import AppPermissionService from services.tag_service import TagService from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task @@ -59,6 +60,23 @@ class AppService: filters.append(App.id.in_(target_ids)) else: return None + # Add permission-based filtering + from sqlalchemy import or_ + permission_filters = [ + # All team members can see apps with permission "all_team_members" or null + or_(App.permission == "all_team_members", App.permission == None), + # Creator can see their own apps + App.created_by == user_id, + # Users with explicit permission in app_permissions table + App.id.in_( + db.session.query(AppPermission.app_id) + .filter( + AppPermission.account_id == user_id, + AppPermission.has_permission == True + ) + ) + ] + filters.append(or_(*permission_filters)) app_models = db.paginate( db.select(App).where(*filters).order_by(App.created_at.desc()), @@ -66,7 +84,8 @@ class AppService: per_page=args["limit"], error_out=False, ) - + for app in app_models.items: + app.permission_account_ids = AppPermissionService.get_app_permissions_by_app_id(app.id) return app_models def create_app(self, tenant_id: str, args: dict, account: Account) -> App: @@ -231,7 +250,16 @@ class AppService: app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) app.updated_by = current_user.id app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.permission = args.get("permission") db.session.commit() + tenant_id = current_user.current_tenant_id + # Handle app permissions + if args.get("permission") == "partial_members" and args.get("partial_member_list"): + account_ids = args.get("partial_member_list") + AppPermissionService.update_app_permissions(tenant_id, app.id, account_ids) + # Clear permissions if permission is not partial_members + elif args.get("permission") != "partial_members": + AppPermissionService.clear_app_permissions(app.id) return app @@ -304,6 +332,9 @@ class AppService: Delete app :param app: App instance """ + # Clear app permissions + AppPermissionService.clear_app_permissions(app.id) + db.session.delete(app) db.session.commit() @@ -373,3 +404,47 @@ class AppService: meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"} return meta + + def get_app_permission_member_list(self, app_id: str) -> list: + """ + Get the list of account IDs that have permission to access the app + + Args: + app_id (str): The ID of the app + + Returns: + list: List of account IDs with permission to access the app + """ + return AppPermissionService.get_app_permissions_by_app_id(app_id) + + def check_app_permission(self, app: App, user: Account) -> bool: + """ + Check if a user has permission to access an app + + Args: + app (App): The app to check permission for + user (Account): The user account to check + + Returns: + bool: True if the user has permission, False otherwise + """ + # App owner always has permission + if app.created_by == user.id: + return True + + # Tenant owner/admin always has permission + if user.is_owner or user.is_admin: + return True + + # Check app permission setting + if not app.permission or app.permission == "all_team_members": + # All team members have permission + return True + elif app.permission == "only_me": + # Only the app creator has permission + return app.created_by == user.id + elif app.permission == "partial_members": + # Check if the user is in the partial member list + return AppPermissionService.check_app_permission(app.id, user.id) + + return False diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 3f8c180c1a..49ac1aec15 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -81,6 +81,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { icon_background, description, use_icon_as_answer_icon, + permission, + partial_member_list }) => { try { await updateAppInfo({ @@ -91,6 +93,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { icon_background, description, use_icon_as_answer_icon, + permission, + partial_member_list }) setShowEditModal(false) notify({ @@ -381,6 +385,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { show={showEditModal} onConfirm={onEdit} onHide={() => setShowEditModal(false)} + appPermission={app.permission} + appSelectedMemberIDs={app.permission_account_ids} /> )} {showDuplicateModal && ( diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index f30b286786..9aac571809 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -1,8 +1,12 @@ 'use client' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' +import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine, RiArrowDownSLine } from '@remixicon/react' import { useDebounceFn, useKeyPress } from 'ahooks' +import { RiCloseLine as RemixIconCloseLine } from '@remixicon/react' +import cn from 'classnames' +import { Users01, UsersPlus } from '@/app/components/base/icons/src/vender/solid/users' +import { Check } from '@/app/components/base/icons/src/vender/line/general' import AppIconPicker from '../../base/app-icon-picker' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' @@ -11,10 +15,15 @@ import Textarea from '@/app/components/base/textarea' import Switch from '@/app/components/base/switch' import Toast from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' +import Avatar from '@/app/components/base/avatar' +import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' +import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import type { AppIconType } from '@/types/app' +import { fetchMembers } from '@/service/common' import { noop } from 'lodash-es' +import type { Member } from '@/models/common' export type CreateAppModalProps = { show: boolean @@ -27,6 +36,8 @@ export type CreateAppModalProps = { appIconUrl?: string | null appMode?: string appUseIconAsAnswerIcon?: boolean + appPermission?: string + appSelectedMemberIDs?: string[] onConfirm: (info: { name: string icon_type: AppIconType @@ -34,6 +45,8 @@ export type CreateAppModalProps = { icon_background?: string description: string use_icon_as_answer_icon?: boolean + permission: 'only_me' | 'all_team_members' | 'partial_members' + partial_member_list?: string[] }) => Promise confirmDisabled?: boolean onHide: () => void @@ -50,6 +63,8 @@ const CreateAppModal = ({ appDescription, appMode, appUseIconAsAnswerIcon, + appPermission = 'only_me', + appSelectedMemberIDs, onConfirm, confirmDisabled, onHide, @@ -65,7 +80,15 @@ const CreateAppModal = ({ const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [description, setDescription] = useState(appDescription || '') const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false) + const [permission, setPermission] = useState<'only_me' | 'all_team_members' | 'partial_members'>(appPermission) + const [selectedMemberIDs, setSelectedMemberIDs] = useState(appSelectedMemberIDs || []) + const [isPermissionSelectorOpen, setIsPermissionSelectorOpen] = useState(false) + const [keywords, setKeywords] = useState('') + const [searchKeywords, setSearchKeywords] = useState('') + const [memberList, setMemberList] = useState([]) + + const { userProfile, currentWorkspace } = useAppContext() const { plan, enableBilling } = useProviderContext() const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) @@ -81,9 +104,56 @@ const CreateAppModal = ({ icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined, description, use_icon_as_answer_icon: useIconAsAnswerIcon, + permission, + partial_member_list: permission === 'partial_members' ? selectedMemberIDs : [], }) onHide() - }, [name, appIcon, description, useIconAsAnswerIcon, onConfirm, onHide, t]) + }, [name, appIcon, description, useIconAsAnswerIcon, permission, selectedMemberIDs, onConfirm, onHide, t]) + + const { run: handleSearch } = useDebounceFn(() => { + setSearchKeywords(keywords) + }, { wait: 500 }) + + const handleKeywordsChange = (value: string) => { + setKeywords(value) + handleSearch() + } + + const selectMember = (member: Member) => { + if (selectedMemberIDs.includes(member.id)) + setSelectedMemberIDs(selectedMemberIDs.filter(v => v !== member.id)) + else + setSelectedMemberIDs([...selectedMemberIDs, member.id]) + } + + const selectedMembersDisplay = React.useMemo(() => { + return [ + userProfile, + ...memberList.filter(member => member.id !== userProfile.id).filter(member => selectedMemberIDs.includes(member.id)), + ].map(member => member.name).join(', ') + }, [userProfile, selectedMemberIDs, memberList]) + + const showMe = React.useMemo(() => { + return userProfile.name.includes(searchKeywords) || userProfile.email.includes(searchKeywords) + }, [searchKeywords, userProfile]) + + const filteredMemberList = React.useMemo(() => { + return memberList.filter(member => (member.name.includes(searchKeywords) || member.email.includes(searchKeywords)) && member.id !== userProfile.id) + }, [memberList, searchKeywords, userProfile]) + + useEffect(() => { + if (isPermissionSelectorOpen && permission === 'partial_members') { + (async () => { + try { + const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} }) + setMemberList(accounts || []) + } catch (e) { + console.error('Failed to fetch members', e) + setMemberList([]) + } + })() + } + }, [isPermissionSelectorOpen, permission]) const { run: handleSubmit } = useDebounceFn(submit, { wait: 300 }) @@ -158,6 +228,119 @@ const CreateAppModal = ({

{t('app.answerIcon.descriptionInExplore')}

)} + {/* permissions */} + {isEditModal && ( +
+
{t('app.permissions')}
+ + setIsPermissionSelectorOpen(v => !v)} + className='block' + > +
+ {permission === 'only_me' && ( + <> + +
{t('app.permissionsOnlyMe')}
+ + )} + {permission === 'all_team_members' && ( + <> +
+ +
+
{t('app.permissionsAllMember')}
+ + )} + {permission === 'partial_members' && ( + <> +
+ +
+
{selectedMembersDisplay}
+ + )} + +
+
+ +
+
+
{ setPermission('only_me'); setIsPermissionSelectorOpen(false); }}> +
+ +
{t('app.permissionsOnlyMe')}
+ {permission === 'only_me' && } +
+
+
{ setPermission('all_team_members'); setIsPermissionSelectorOpen(false); }}> +
+
+ +
+
{t('app.permissionsAllMember')}
+ {permission === 'all_team_members' && } +
+
+
{ setPermission('partial_members'); setSelectedMemberIDs([userProfile.id]); }}> +
+
+ +
+
{t('app.permissionsInvitedMembers')}
+ {permission === 'partial_members' && } +
+
+
+ {permission === 'partial_members' && ( +
+
+ handleKeywordsChange(e.target.value)} + onClear={() => handleKeywordsChange('')} + placeholder={t('common.operation.search') || ''} + /> +
+ {showMe && ( +
+ +
+
+ {userProfile.name} + {` (${t('datasetSettings.form.me')})`} +
+
{userProfile.email}
+
+ +
+ )} + {filteredMemberList.map(member => ( +
selectMember(member)}> + +
+
{member.name}
+
{member.email}
+
+ {selectedMemberIDs.includes(member.id) && } +
+ ))} +
+ )} +
+
+
+
+ )} {!isEditModal && isAppsFull && }
@@ -192,4 +375,4 @@ const CreateAppModal = ({ ) } -export default CreateAppModal +export default CreateAppModal \ No newline at end of file diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index 25fddf7a91..0a34f09d06 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -167,6 +167,10 @@ const translation = { title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖', description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll', }, + permissions: 'Sichtbarkeit', + permissionsOnlyMe: 'Nur ich', + permissionsAllMember: 'Alle Teammitglieder', + permissionsInvitedMembers: 'Teilnehmer', importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen', duplicate: 'Duplikat', importFromDSL: 'Import von DSL', diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index c57d6c25b5..591a666bba 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -116,6 +116,10 @@ const translation = { description: 'Whether to use the WebApp icon to replace 🤖 in the shared application', descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore', }, + permissions: 'Visibility', + permissionsOnlyMe: 'Only me', + permissionsAllMember: 'All Team Members', + permissionsInvitedMembers: 'Invited Members', switch: 'Switch to Workflow Orchestrate', switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', switchTip: 'not allow', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index f6cc9d1735..d92d18493f 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -165,6 +165,10 @@ const translation = { descriptionInExplore: 'Si se debe usar el icono de la aplicación web para reemplazarlo 🤖 en Explore', description: 'Si se va a usar el icono de la aplicación web para reemplazarlo 🤖 en la aplicación compartida', }, + permissions: 'Visibilidad', + permissionsOnlyMe: 'Solo yo', + permissionsAllMember: 'Todos los miembros del equipo', + permissionsInvitedMembers: 'Miembros invitados', importFromDSLUrl: 'URL de origen', importFromDSLUrlPlaceholder: 'Pegar enlace DSL aquí', importFromDSL: 'Importar desde DSL', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 70e7801810..b446f5c970 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -169,6 +169,10 @@ const translation = { description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر', title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید', }, + permissions: 'قابلیت دسترسی', + permissionsOnlyMe: 'فقط من', + permissionsAllMember: 'همه اعضای تیم', + permissionsInvitedMembers: 'اعضای دعوت شده', mermaid: { handDrawn: 'دست کشیده شده', classic: 'کلاسیک', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index 005418108a..fb6052a60e 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -165,6 +165,10 @@ const translation = { title: 'Utiliser l’icône WebApp pour remplacer 🤖', descriptionInExplore: 'Utilisation de l’icône WebApp pour remplacer 🤖 dans Explore', }, + permissions: 'Visibilité', + permissionsOnlyMe: 'Seul moi', + permissionsAllMember: 'Tous les membres du team', + permissionsInvitedMembers: 'Membres invités', importFromDSLUrlPlaceholder: 'Collez le lien DSL ici', importFromDSL: 'Importation à partir d’une DSL', importFromDSLUrl: 'À partir de l’URL', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index c9b035568b..1347a6bd6b 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -165,6 +165,10 @@ const translation = { descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं', description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं', }, + permissions: 'दृश्यता', + permissionsOnlyMe: 'केवल मैं', + permissionsAllMember: 'सभी टीम सदस्य', + permissionsInvitedMembers: 'आमंत्रित सदस्य', importFromDSLFile: 'डीएसएल फ़ाइल से', importFromDSLUrl: 'यूआरएल से', importFromDSL: 'DSL से आयात करें', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index a33dd5c571..e0d0ee3bc7 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -177,6 +177,10 @@ const translation = { title: 'Usa l\'icona WebApp per sostituire 🤖', descriptionInExplore: 'Se utilizzare l\'icona WebApp per sostituirla 🤖 in Esplora', }, + permissions: 'Visibilità', + permissionsOnlyMe: 'Solo io', + permissionsAllMember: 'Tutti i membri del team', + permissionsInvitedMembers: 'Membri invitati', importFromDSLUrl: 'Dall\'URL', importFromDSLFile: 'Da file DSL', importFromDSL: 'Importazione da DSL', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index ae57a8f801..5413a1f57a 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -170,6 +170,10 @@ const translation = { description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか', descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか', }, + permissions: '可視性', + permissionsOnlyMe: '私だけ', + permissionsAllMember: 'チームのすべてのメンバー', + permissionsInvitedMembers: '招待されたメンバー', mermaid: { handDrawn: '手描き', classic: 'クラシック', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 89cd274647..a8e469cb09 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -161,6 +161,10 @@ const translation = { title: 'WebApp 아이콘을 사용하여 🤖', descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부', }, + permissions: '가시성', + permissionsOnlyMe: '나만', + permissionsAllMember: '모든 팀 멤버', + permissionsInvitedMembers: '초대된 멤버', importFromDSL: 'DSL에서 가져오기', importFromDSLFile: 'DSL 파일에서', importFromDSLUrl: 'URL에서', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 562962bf38..2b21bc4fac 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -172,6 +172,10 @@ const translation = { title: 'Użyj ikony WebApp, aby zastąpić 🤖', descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj', }, + permissions: 'Widoczność', + permissionsOnlyMe: 'Tylko ja', + permissionsAllMember: 'Wszystkie członkowie zespołu', + permissionsInvitedMembers: 'Zaproszone członkowie', importFromDSL: 'Importowanie z DSL', importFromDSLUrl: 'Z adresu URL', importFromDSLFile: 'Z pliku DSL', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 8f920e4280..32c9f69546 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -165,6 +165,10 @@ const translation = { description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado', title: 'Use o ícone do WebApp para substituir 🤖', }, + permissions: 'Visibilidade', + permissionsOnlyMe: 'Apenas eu', + permissionsAllMember: 'Todos os membros do time', + permissionsInvitedMembers: 'Membros convidados', importFromDSLUrlPlaceholder: 'Cole o link DSL aqui', importFromDSLUrl: 'Do URL', importFromDSLFile: 'Do arquivo DSL', diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 3f288c1396..877c1f8e04 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -165,6 +165,10 @@ const translation = { description: 'Dacă se utilizează pictograma WebApp pentru a înlocui 🤖 în aplicația partajată', title: 'Utilizați pictograma WebApp pentru a înlocui 🤖', }, + permissions: 'Vizibilitate', + permissionsOnlyMe: 'Doar mine', + permissionsAllMember: 'Toți membrii echipei', + permissionsInvitedMembers: 'Membri invitați', importFromDSL: 'Import din DSL', importFromDSLUrl: 'De la URL', importFromDSLUrlPlaceholder: 'Lipiți linkul DSL aici', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index a5b543c867..f994b13296 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -169,6 +169,10 @@ const translation = { description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении', descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"', }, + permissions: 'Видимость', + permissionsOnlyMe: 'Только я', + permissionsAllMember: 'Все члены команды', + permissionsInvitedMembers: 'Приглашенные члены', mermaid: { handDrawn: 'Рисованный', classic: 'Классический', diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index e4ba29094b..c807627b37 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -113,6 +113,10 @@ const translation = { description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji', descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči', }, + permissions: 'Vidnost', + permissionsOnlyMe: 'Samo jaz', + permissionsAllMember: 'Vsi člani skupine', + permissionsInvitedMembers: 'Pozvani člani', switch: 'Preklopi na Workflow Orchestrate', switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ', switchTip: 'dovolila', diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 061e3a8076..2771a77af4 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -109,6 +109,10 @@ const translation = { description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่', descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่', }, + permissions: 'ความสามารถเข้าถึง', + permissionsOnlyMe: 'ฉันเท่านั้น', + permissionsAllMember: 'ทุกสมาชิกของทีม', + permissionsInvitedMembers: 'สมาชิกที่เชิญ', switch: 'เปลี่ยนไปใช้ Workflow Orchestrate', switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration', switchTip: 'ไม่อนุญาต', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index f205bd8ae4..dcb5e7780e 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -165,6 +165,10 @@ const translation = { title: 'Değiştirmek 🤖 için WebApp simgesini kullanın', description: 'Paylaşılan uygulamada değiştirmek 🤖 için WebApp simgesinin kullanılıp kullanılmayacağı', }, + permissions: 'Görünürlük', + permissionsOnlyMe: 'Sadece ben', + permissionsAllMember: 'Tüm Takım Üyeleri', + permissionsInvitedMembers: 'Davetli Üyeler', mermaid: { handDrawn: 'Elle çizilmiş', classic: 'Klasik', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 2a9c03eace..d24fb13411 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -165,6 +165,10 @@ const translation = { description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі', descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді', }, + permissions: 'Відкритість', + permissionsOnlyMe: 'Тільки я', + permissionsAllMember: 'Всі члени команди', + permissionsInvitedMembers: 'Призначені члени', importFromDSLUrl: 'З URL', importFromDSL: 'Імпорт з DSL', importFromDSLUrlPlaceholder: 'Вставте посилання на DSL тут', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index fd0a66ad03..9a8661e150 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -165,6 +165,10 @@ const translation = { descriptionInExplore: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong Khám phá hay không', title: 'Sử dụng biểu tượng WebApp để thay thế 🤖', }, + permissions: 'Tính năng truy cập', + permissionsOnlyMe: 'Chỉ tôi', + permissionsAllMember: 'Tất cả thành viên nhóm', + permissionsInvitedMembers: 'Thành viên được mời', importFromDSLFile: 'Từ tệp DSL', importFromDSL: 'Nhập từ DSL', importFromDSLUrlPlaceholder: 'Dán liên kết DSL vào đây', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 7ef8c1b514..07a56568dd 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -117,6 +117,10 @@ const translation = { description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖', descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖', }, + permissions: '可见权限', + permissionsOnlyMe: '只有我', + permissionsAllMember: '所有团队成员', + permissionsInvitedMembers: '部分团队成员', switch: '迁移为工作流编排', switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', switchTip: '不能够', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index 44412bfcca..df92a2ad8a 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -164,6 +164,10 @@ const translation = { title: '使用 WebApp 圖示取代 🤖', description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖', }, + permissions: '可見權限', + permissionsOnlyMe: '只有我', + permissionsAllMember: '所有團隊成員', + permissionsInvitedMembers: '部分團隊成員', importFromDSLUrl: '寄件者 URL', importFromDSL: '從 DSL 導入', importFromDSLFile: '從 DSL 檔', diff --git a/web/service/apps.ts b/web/service/apps.ts index 3f7ec7b548..c41a5ef8f5 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -28,8 +28,8 @@ export const createApp: Fetcher('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) } -export const updateAppInfo: Fetcher = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon }) => { - return put(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon } }) +export const updateAppInfo: Fetcher = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list }) => { + return put(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list } }) } export const copyApp: Fetcher = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { diff --git a/web/types/app.ts b/web/types/app.ts index 39f011dcaa..12acff2e06 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -359,6 +359,10 @@ export type App = { updated_at: number updated_by?: string } + /** Permission */ + permission: string + /** Permission Account IDs */ + permission_account_ids: string[] } export type AppSSO = {