Merge remote-tracking branch 'dev/main_form_dev'

pull/19011/head
安泽芃 1 year ago
commit dd19bdce67

@ -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
```

@ -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()

@ -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),
}

@ -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"))

@ -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

@ -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

@ -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 && (

@ -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<void>
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<string[]>(appSelectedMemberIDs || [])
const [isPermissionSelectorOpen, setIsPermissionSelectorOpen] = useState(false)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const [memberList, setMemberList] = useState<Member[]>([])
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 = ({
<p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.descriptionInExplore')}</p>
</div>
)}
{/* permissions */}
{isEditModal && (
<div className='pt-2'>
<div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.permissions')}</div>
<PortalToFollowElem
open={isPermissionSelectorOpen}
onOpenChange={setIsPermissionSelectorOpen}
placement='bottom-start'
offset={4}
>
<PortalToFollowElemTrigger
onClick={() => setIsPermissionSelectorOpen(v => !v)}
className='block'
>
<div className={cn('flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-[6px] hover:bg-state-base-hover-alt',
isPermissionSelectorOpen && 'bg-state-base-hover-alt',
)}>
{permission === 'only_me' && (
<>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('app.permissionsOnlyMe')}</div>
</>
)}
{permission === 'all_team_members' && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('app.permissionsAllMember')}</div>
</>
)}
{permission === 'partial_members' && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div title={selectedMembersDisplay} className='mr-2 grow truncate text-sm leading-5 text-components-input-text-filled'>{selectedMembersDisplay}</div>
</>
)}
<RiArrowDownSLine className={cn('h-4 w-4 shrink-0 text-text-secondary')} />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='w-[480px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('only_me'); setIsPermissionSelectorOpen(false); }}>
<div className='flex items-center gap-2'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsOnlyMe')}</div>
{permission === 'only_me' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('all_team_members'); setIsPermissionSelectorOpen(false); }}>
<div className='flex items-center gap-2'>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsAllMember')}</div>
{permission === 'all_team_members' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('partial_members'); setSelectedMemberIDs([userProfile.id]); }}>
<div className='flex items-center gap-2'>
<div className={cn('mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#FFF6ED]', permission === 'partial_members' && '!bg-[#EEF4FF]')}>
<UsersPlus className={cn('h-3.5 w-3.5 text-[#FB6514]', permission === 'partial_members' && '!text-[#444CE7]')} />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsInvitedMembers')}</div>
{permission === 'partial_members' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
</div>
{permission === 'partial_members' && (
<div className='max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular pb-1 pl-1 pr-1'>
<div className='sticky left-0 top-0 z-10 bg-white p-2 pb-1'>
<Input
showLeftIcon
showClearIcon
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
placeholder={t('common.operation.search') || ''}
/>
</div>
{showMe && (
<div className='flex items-center gap-2 rounded-lg py-1 pl-3 pr-[10px]'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>
{userProfile.name}
<span className='text-xs font-normal text-text-tertiary'>{` (${t('datasetSettings.form.me')})`}</span>
</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{userProfile.email}</div>
</div>
<Check className='h-4 w-4 shrink-0 text-text-accent opacity-30' />
</div>
)}
{filteredMemberList.map(member => (
<div key={member.id} className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-3 pr-[10px] hover:bg-state-base-hover' onClick={() => selectMember(member)}>
<Avatar avatar={member.avatar_url} name={member.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>{member.name}</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{member.email}</div>
</div>
{selectedMemberIDs.includes(member.id) && <Check className='h-4 w-4 shrink-0 text-text-accent' />}
</div>
))}
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)}
{!isEditModal && isAppsFull && <AppsFull className='mt-4' loc='app-explore-create' />}
</div>
<div className='flex flex-row-reverse'>

@ -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',

@ -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',

@ -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',

@ -169,6 +169,10 @@ const translation = {
description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر',
title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید',
},
permissions: 'قابلیت دسترسی',
permissionsOnlyMe: 'فقط من',
permissionsAllMember: 'همه اعضای تیم',
permissionsInvitedMembers: 'اعضای دعوت شده',
mermaid: {
handDrawn: 'دست کشیده شده',
classic: 'کلاسیک',

@ -165,6 +165,10 @@ const translation = {
title: 'Utiliser licône WebApp pour remplacer 🤖',
descriptionInExplore: 'Utilisation de licô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 dune DSL',
importFromDSLUrl: 'À partir de lURL',

@ -165,6 +165,10 @@ const translation = {
descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं',
description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं',
},
permissions: 'दृश्यता',
permissionsOnlyMe: 'केवल मैं',
permissionsAllMember: 'सभी टीम सदस्य',
permissionsInvitedMembers: 'आमंत्रित सदस्य',
importFromDSLFile: 'डीएसएल फ़ाइल से',
importFromDSLUrl: 'यूआरएल से',
importFromDSL: 'DSL से आयात करें',

@ -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',

@ -170,6 +170,10 @@ const translation = {
description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか',
descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか',
},
permissions: '可視性',
permissionsOnlyMe: '私だけ',
permissionsAllMember: 'チームのすべてのメンバー',
permissionsInvitedMembers: '招待されたメンバー',
mermaid: {
handDrawn: '手描き',
classic: 'クラシック',

@ -161,6 +161,10 @@ const translation = {
title: 'WebApp 아이콘을 사용하여 🤖',
descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부',
},
permissions: '가시성',
permissionsOnlyMe: '나만',
permissionsAllMember: '모든 팀 멤버',
permissionsInvitedMembers: '초대된 멤버',
importFromDSL: 'DSL에서 가져오기',
importFromDSLFile: 'DSL 파일에서',
importFromDSLUrl: 'URL에서',

@ -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',

@ -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',

@ -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',

@ -169,6 +169,10 @@ const translation = {
description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении',
descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"',
},
permissions: 'Видимость',
permissionsOnlyMe: 'Только я',
permissionsAllMember: 'Все члены команды',
permissionsInvitedMembers: 'Приглашенные члены',
mermaid: {
handDrawn: 'Рисованный',
classic: 'Классический',

@ -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',

@ -109,6 +109,10 @@ const translation = {
description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่',
descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่',
},
permissions: 'ความสามารถเข้าถึง',
permissionsOnlyMe: 'ฉันเท่านั้น',
permissionsAllMember: 'ทุกสมาชิกของทีม',
permissionsInvitedMembers: 'สมาชิกที่เชิญ',
switch: 'เปลี่ยนไปใช้ Workflow Orchestrate',
switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration',
switchTip: 'ไม่อนุญาต',

@ -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',

@ -165,6 +165,10 @@ const translation = {
description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі',
descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді',
},
permissions: 'Відкритість',
permissionsOnlyMe: 'Тільки я',
permissionsAllMember: 'Всі члени команди',
permissionsInvitedMembers: 'Призначені члени',
importFromDSLUrl: 'З URL',
importFromDSL: 'Імпорт з DSL',
importFromDSLUrlPlaceholder: 'Вставте посилання на DSL тут',

@ -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',

@ -117,6 +117,10 @@ const translation = {
description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖',
descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖',
},
permissions: '可见权限',
permissionsOnlyMe: '只有我',
permissionsAllMember: '所有团队成员',
permissionsInvitedMembers: '部分团队成员',
switch: '迁移为工作流编排',
switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
switchTip: '不能够',

@ -164,6 +164,10 @@ const translation = {
title: '使用 WebApp 圖示取代 🤖',
description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖',
},
permissions: '可見權限',
permissionsOnlyMe: '只有我',
permissionsAllMember: '所有團隊成員',
permissionsInvitedMembers: '部分團隊成員',
importFromDSLUrl: '寄件者 URL',
importFromDSL: '從 DSL 導入',
importFromDSLFile: '從 DSL 檔',

@ -28,8 +28,8 @@ export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: A
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
}
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon }) => {
return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon } })
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; permission?: string; partial_member_list?: string[] }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list }) => {
return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list } })
}
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {

@ -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 = {

Loading…
Cancel
Save